2021-07-13 系统装修,补充gitee仓库 2021-02-14 补充 2019-11-26 初稿

之前尝试研究 Vue2.x 的数据响应式原理,上次更新还是2019-11-26,最近又有兴趣了,尝试补充和完善。

我建立了一个仓库 https://gitee.com/xiaoxfa/source-vue2

本次向尝试总结的是下面的大纲:

Vue2 数据响应式 - 图1掌握了大纲,回头去看vue3 的响应式能触类旁通。

前置知识

Object.defineProperty

对一个对象里的key做拦截,手动设置 getter setter方法。有了这个特性,我们操作数据时候就能拦截到,配合后面的依赖搜集机制,能完成最基本的数据响应式变化。

上一段简单的代码辅助说明:

  1. const obj = {}
  2. /**
  3. * 响应式
  4. * @param {object} obj
  5. * @param {string} key
  6. * @param {any} val
  7. */
  8. function defineReactive(obj, key, val) {
  9. Object.defineProperty(obj, key, {
  10. get() {
  11. console.log(`你正在读取${key}:${val}`)
  12. return val
  13. },
  14. set(newVal) {
  15. if (newVal !== val) {
  16. val = newVal
  17. console.log(`setter: old:${val}, new:${newVal}`)
  18. // do sth.
  19. document.querySelector('#app').innerHTML = newVal
  20. }
  21. },
  22. })
  23. }
  24. defineReactive(obj, 'time', '')
  25. setInterval(() => {
  26. obj.time = +new Date()
  27. }, 1000)

观察控制台:

  1. $ obj.time
  2. > 你正在读取time:
  3. $ obj.time=3
  4. > setter: old:3, new:3

通过这个简单函数,我们修改对应的key属性,就能自动更新数据状态。当然了,这里只是一个简单的实现。

数据劫持

还是思考刚才的案例,这里我们没有考虑深层嵌套的情况,因此需要完善细节。

  1. 如果一个值是object类型,就需要进行劫持,伪代码 typeof val===object && val!==null 见代码 @/state.js#initData
  2. 封装对象的处理方法, class Observer{} ,定义 this.walk() 方法
  3. 对于一个 object,需要 Object.keys 循环得到每一项key
  4. keys.forEach — defineReactive — defineProperty
  5. 处理嵌套,深度优先,先处理递归。由此可见如果处理复杂数据vue会有性能隐患,后续vue3的懒递归proxy能解决
  6. 此时赋值操作有两种 a={b:1}a.b=1 ,因此value是对象还要继续递归。新属性无法被拦截
  7. 对象中的数组同样可以代理,但是实际情况中很少对索引取值和赋值,不必要,因此数组不必循环代理
  8. 数组中有7种方法会修改原始数组 push pop shift unshit splice sort reverse 但没有原生的拦截方法,因此需要hack,也就是重写数组方法,而 forEach map 不会改变原始数组可以忽略
  9. 思路是切面编程,继承原始数组方法,先调用原始方法,再执行自定义方法。继承的思路是es5里的 Object.create,也可以使用 object.setPrototype
  10. 考虑 数组中的对象,对象中的数组,因此需要判断是 普通的object还是 array 来执行不同的方法
  11. 数组改变数据,如果是新增的内容(insert)同样需要设置响应式, push unshift 可以直接拿到赋值,splice第三个参数如果有值也意味着是补充数据
  12. insert内容也需要被观测,这里引入 __ob__ 属性表示是否被观测过,值是observer的实例this,为了避免 __ob__ 也被观测,设置 enumberable configureablefalse , value 设置为 this
  13. 这样在数组中可以通过 __ob__ 来拿到 实例方法,可以对insert的数据进行代理
  14. 数据简化代理 this._data.xx ,简化为 this.xx ,思路也是 defineProperty

代码部分,见仓库。

响应式

实现了数据劫持是不够的,我们希望数据驱动,数据发生了变化就更新数据,过程是自动的。

这里就引入了新的概念,以下是简化版。

  1. 编译阶段,挂载到页面时候, new Watcher(vm, updateComponent, () => {}, true);
  2. 这个 class Watcher 会搜集 第二个参数,也就是更新节点的方法
  3. exprOrFn里面会访问劫持的数据get方法,也即是取值。取值时候会触发 defineReactvie 中的get

    1. let id = 0
    2. class Wacher {
    3. constructor(vm, exprOrFn, ...args){
    4. this.exprOrFn = exprOrFn
    5. this.getter = exprOrFn
    6. this.id = id++
    7. this.get()
    8. }
    9. get(){
    10. Dep.target = this
    11. this.getter()
    12. Dep.target = null
    13. }
    14. update(){this.get}
    15. }
    16. // 这意味着,每次 newWatcher会立即更新一次,后续watcher实例调用 get方法会继续更新
  4. 在get方法中添加 new dep=new Dep() 准备管理依赖

  1. function defineReactive(data,key,value){
  2. let dep = new Dep();
  3. Object.defineProperty(data,key,{
  4. get(){
  5. if(Dep.target){ // 如果取值时有watcher
  6. dep.depend(); // 让watcher保存dep,并且让dep 保存watcher
  7. }
  8. },
  9. set(newValue){
  10. dep.notify(); // 通知渲染watcher去更新
  11. }
  12. })
  13. }
  1. class Dep 用来管理页面中的watcher
  2. 一个组件是一个watcher,如果用户用到了 watch会增加新的watcher>一个组件会有多个属性,每个属性出现一次就是一个 dep实例。因此 dep和watcher是多对多的关系,dep里管理多个watcher,一个watcher里有多个dep,后续如果依赖项
  1. class Dep{
  2. constructor(){
  3. this.subs=[]
  4. }
  5. depned(){
  6. this.subs.push(Dep.target)
  7. }
  8. notify(){
  9. this.subs.forEach(w=>w.update())
  10. }
  11. }
  12. Dep.target=null

重新梳理三者关系:

  • new Watcher 创建watcher实例,包含了要更新的代码,同时把当前watcher实例放入 Dep.target
  • 更新代码中会取值,触发 observe 的get方法,能够取得 watcher实例,通知dep.subs 塞入watcher
  • 后续触发修改值set方法,通知dep.notify 这时候调用subs所有watcher实例update方法,也就是重新执行渲染

后续还有数组的部分。

这里还有一个问题,就是多次修改值,会触发多次,不太好,引入了异步更新。

异步更新

思路大致如下:

  1. 多次调用更新
  2. 准备更新任务队列queue=[], 队列去重 has={},是否排队 pending=false
  3. 获得当前watcher.id 准备放入has中,如果第一次,防 queue队列中
  4. 设定定时器统一处理queue更新方法,并设置flag
  5. 操作完成重置 queue,has和pending

在vue3中已经不考虑异步更新的兼容问题了,在vue2中 promise.then > MutationObserver > setImmediate > setTimeout

这块不是很感兴趣,就看了看。