乐趣区

关于vue.js:Vue3响应式原理

对于 Vue3

话说,Vue3 曾经进行到 rc4 版本了,4 月份 beta 公布的时候前端圈红红火火,不晓得大家开始学了没

整顿了一些资源,当初开始学习应该还不算晚 [狗头]

  • vue-next 仓库
  • 20200723 Vue3 官网公布的 beta 文档
  • Vue3 Roadmap & FAQ
  • Vue3 仓库曾经合并的 780 多个 PR
  • 尤大在 Vue Mastery 的 Vue3 课:Vue 3 Deep Dive with Evan You
  • 202007 尤大在前端会客厅节目对于 Vue3 的访谈
  • 202005 The process: Making Vue 3
  • 202004 尤大 – 聊聊 Vue.js 3.0 Beta 官网直播
  • 2018 VueConf 杭州 尤大对于 Vue3 的演讲视频

vue2 响应式原理回顾

  • 对象响应化:遍历每个 key,通过 Object.defineProperty API 定义 getter,setter
// 伪代码
function observe(){if(typeof obj !='object' || obj == null){return}
    if(Array.isArray(obj)){Object.setPrototypeOf(obj,arrayProto)
    }else{const keys = Object.keys()
    for(let i=0;i<keys.length;i++){const key = keys[i]
      defineReactive(obj,key,obj[key])
    }
    }
}
function defineReactive(target, key, val){observe(val)
  Object.defineProperty(obj, key, {get(){
      // 依赖收集
      dep.depend()
      return val
    },
    set(newVal){if(newVal !== val){observe(newVal)
        val = newVal
        // 告诉更新
        dep.notify()}
    }
  })
}
  • 数组响应化:笼罩数组的原型办法,减少告诉变更的逻辑
// 伪代码
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push','pop','shift','unshift','splice','reverse','sort'].forEach(key=>{arrayProto[key] = function(){originalProto[key].apply(this.arguments)
        notifyUpdate()}
})

vue2 响应式痛点

  • 递归,耗费大
  • 新增 / 删除属性,须要额定实现独自的 API
  • 数组,须要额定实现
  • Map Set Class 等数据类型,无奈响应式
  • 批改语法有限度

vue3 响应式计划

应用 ES6 的 Proxy 进行数据响应化,解决上述 Vue2 所有痛点

Proxy 能够在指标对象上加一层拦挡 / 代理,外界对指标对象的操作,都会通过这层拦挡

相比 Object.defineProperty,Proxy 反对的对象操作非常全面:get、set、has、deleteProperty、ownKeys、defineProperty…… 等等

// reactive 伪代码
function reactice(obj){
  return new Proxy(obj,{get(target, key, receiver){const ret = Reflect.get(target, key, receiver)
      return isObject(ret) ? reactice(ret) : ret
    },
    set(target, key, val, receiver){const ret = Reflect.set(target, key, val, receiver)
      return ret
    },
    deleteProperty(target, key){const ret = Reflect.deleteProperty(target, key)
      return ret
    },
  })
}

响应式原理

  • 通过 effect 申明依赖响应式数据的函数 cb (例如视图渲染函数 render 函数),并执行 cb 函数,执行过程中,会触发响应式数据 getter
  • 在响应式数据 getter 中进行 track 依赖收集:建设 数据 &cb 的映射关系存储于 targetMap
  • 当变更响应式数据时,触发 trigger 依据 targetMap 找到关联的 cb 执行
  • 映射关系 targetMap 构造:
targetMap: WeakMap{ 
    target:Map{key: Set[cb1,cb2...] 
    }
}

手写 vue3 响应式

大抵构造

// mini-vue3.js

/* 建设响应式数据 */
function reactice(obj){}

/* 申明响应函数 cb(依赖响应式数据) */
function effect(cb){}

/* 依赖收集:建设 数据 &cb 映射关系 */
function track(target,key){}

/* 触发更新:依据映射关系,执行 cb */
function trigger(target,key){}

reactive

/* 建设响应式数据 */
function reactive(obj){
  // Proxy:http://es6.ruanyifeng.com/#docs/proxy
  // Proxy 相当于在对象外层加拦挡
  // Proxy 递归是惰性的, 须要增加递归的逻辑
  
  // Reflect:http://es6.ruanyifeng.com/#docs/reflect
  // Reflect: 用于执行对象默认操作,更标准、更敌对, 能够了解成操作对象的合集
  // Proxy 和 Object 的办法 Reflect 都有对应
  if(!isObject(obj)) return obj
  const observed = new Proxy(obj,{get(target, key, receiver){const ret = Reflect.get(target, key, receiver)
      console.log('getter'+ret)
      // 跟踪 收集依赖
      track(target, key)
      return reactive(ret)
    },
    set(target, key, val, receiver){const ret = Reflect.set(target, key, val, receiver)
      console.log('setter'+key+':'+val + '=>' + ret)
      // 触发更新
      trigger(target, key)
      return ret
    },
    deleteProperty(target, key){const ret = Reflect.deleteProperty(target, key)
      console.log('delete'+key+':'+ret)
      // 触发更新
      trigger(target, key)
      return ret
    },
  })
  return observed
}

effect

/* 申明响应函数 cb */
const effectStack = []
function effect(cb){

  // 对函数进行高阶封装
  const rxEffect = function(){
    // 1. 捕捉异样
    // 2.fn 出栈入栈
    // 3. 执行 fn
    try{effectStack.push(rxEffect)
      return cb()}finally{effectStack.pop()
    }
  }

  // 最后要执行一次, 进行最后的依赖收集
  rxEffect()

  return rxEffect
}

track

/* 依赖收集:建设 数据 &cb 映射关系 */
const targetMap = new WeakMap()
function track(target,key){
  // 存入映射关系
  const effectFn = effectStack[effectStack.length - 1]  // 拿出栈顶函数
  if(effectFn){let depsMap = targetMap.get(target)
    if(!depsMap){depsMap = new Map()
      targetMap.set(target, depsMap)
    }
    let deps = depsMap.get(key)
    if(!deps){deps = new Set()
      depsMap.set(key, deps)
    }
    deps.add(effectFn)
  }
}

trigger

/* 触发更新:依据映射关系,执行 cb */
function trigger(target, key){const depsMap = targetMap.get(target)
  if(depsMap){const deps = depsMap.get(key)
    if(deps){deps.forEach(effect=>effect())
    }
  }
}

测试 demo

<!-- test.html -->
<div id="app">
 {{msg}}
</div>

<script src="./mini-vue3.js"></script>

<script>
  // 定义一个响应式数据
  const state = reactive({msg:'message'})

  // 定义一个应用到响应式数据的 dom 更新函数
    function updateDom(){document.getElementById('app').innerText = state.msg
    }

    // 用 effect 申明更新函数
  effect(updateDom)

  // 定时变更响应式数据
  setInterval(()=>{state.msg = 'message' + Math.random()
  },1000)
</script>

成果:

如果想获取上述代码,放在了这个仓库:mini-vue3-reactive

退出移动版