1202年了,学习Vue3.0原理之前,一起回顾一下Vue2.x版本的响应式源码

862 阅读8分钟

打个广告🤭 上一篇 体验Vue3.0, 仿一个网易云音乐客户端

为什么要在这个时候写这样的一篇文章?

转眼2021年了,Vue3.0正式发布也已经过去了几个月。但框架仅仅做到会用是不够的,要深入去了解其相关原理,需要知道一下它与之前版本到底有啥区别,优势在哪里。所以今天就一起重新品2.x版本的响应式源码!

以下每个阶段可能有一些方法或者对象实例暂时无法理解的,没关系,当你看完文章完整的流程后,手敲一遍,就会恍然大悟的🤭

Observer、Dep、Watcher作用与关系

用过Vue的同学或多或少都知道尤大是使用数据劫持与订阅-发布模式来实现响应式的,其离不开以下三个对象。

Observer

Observer监听器是给需要响应式的对象进行数据劫持的,即添加gettersetter,同时也是Dep与Watcher对数据进行依赖收集与数据更新的中间站。其相关源码与解析如下:


// 在vue初始化的时候会调用
function initData(vm) {
    let data = vm.$options.data
    // 这里假设data就是个返回了对象的函数,实际这里做了很多判断
    data = vm._data = data.call(vm, vm)
    const keys = Object.keys(data);
    // 将 _data 的数据代理到 this 上
    for (let i = 0; i < keys.length; i++) {
        proxy(vm, '_data', keys[i]);
    }
    // 正常做个会有while循环, 其实是后面获取了option中的props与methods 做了重名的判断,这里假设不会有这种情况

    observe(data, true)
}

function observe(value, asRootData) {
    // 如果不是个对象或者是个虚拟dom节点的 无需观测
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    let obj
    // 如果已经被监测了,则无需再实例化
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        obj = value.__ob__
    } else {
        obj = new Observer(value)
    }

    return obj
}

// 属性代理方法
function def(obj, key, val, enumerable) {
    Object.defineProperty(obj, key, {
        value: val,
        enumerable: !!enumerable,
        writable: true,
        configurable: true
    })
}

class Observer {
    constructor(value) {
        this.value = value
        // 这里的dep是给数组依赖收集的
        this.dep = new Dep()

        // 这里的def代理的__ob__很重要,后面数组就会用到这代理的observer对象
        def(value, '__ob__', this)
        // 如果值是数组的话需要额外处理,因为object.defineProperty 无法监听数组的任何改变数组长度的操作,以及原型上的方法。
        // 这也是为什么尤大不对数组进行处理的原因,而是通过$set与代理变异方法来实现
        if (Array.isArray(value)) {
            // 代理数组的变异方法,重新赋值原型
            value.__proto__ = arrayMethods
            this.observeArray(value)
        } else {
            // 否则就遍历对象,为对象的属性进行劫持
            this.walk(value)
        }
    }
    // 监听对象
    walk(obj) {
        const keys = Object.keys(obj)
        for (let i = 0; i < keys.length; i++) {
            defineReactive(obj, keys[i])
        }
    }
    // 监听数组的每一项,即arr[i].xxxx = xxx的时候可以监听到,就是因为这里,但不会监听到arr[i] = xxx
    observeArray(array) {
        for (let i = 0; i < array.length; i++) {
            observe(array[i])
        }
    }
}




根据注释解析我们大致可以了解到,observe方法里面的value应该就是我们的data或者其子对象,然后通过实例化Obverver,分别对值是对象与数组进行了相应的处理。

对象劫持

对象的话走walk方法,遍历对象的key,将每个key与原obj传入了defineReactive方法,而这个方法就是调用了Object.defineProperty。其方法分析如下:

/**
  每个key对应一个dep
  obj是我们需要劫持的对象,例如最开始的data,以及后面data里面的对象
  val是手动set的时候传值,默认情况下是不需要的
**/
function defineReactive(obj, key, val) {
    const dep = new Dep()

    // 获取对象属性的描述对象
    const property = Object.getOwnPropertyDescriptor(obj, key)
    // 如果对象的值之前被设置过了,且设置为不可更改值,即无需监听
    if (property && property.configurable === false) {
        return
    }

    // 如果原本的对象的属性定义了对应的get与set,应该与其保持一致
    const getter = property && property.get
    const setter = property && property.set
    // 非set的情况下,没有定义get,即这时候val会是undefined,需要赋予其原本的值
    if ((!getter || setter) && arguments.length === 2) {
        val = obj[key]
    }
    // 递归进行依赖收集
    let ob = observe(val)
    Object.defineProperty(obj, key, {
        // 可遍历,可修改
        enumerable: true,
        configurable: true,
        get: function reactiveGetter() {
            const value = getter ? getter.call(obj) : val
            // 这里的Dep是依赖收集器,后面会介绍到,现在只要知道这里的 Dep.target 为一个Watcher实例
            if (Dep.target) {
                // 依赖收集
                dep.depend()
                // 如果val是对象或者是数组的时候,ob会是observe对象,否则就是undefined
                // 如果是数组的话,通过ob.dep.depend来进行数组方法的依赖收集
                if (ob) {
                    ob.dep.depend()
                    if (Array.isArray(value)) {
                        // 这里为什么需要判断其是否是数组呢,因为前面的ob.dep.depend只是对最外层的数组的方法进行的依赖收集
                        // 但是考虑到值如果也是数组的情况,即多维数组的依赖的收集
                        dependArray(value)
                    }
                }
            }
            return value
        },
        set: function reactiveSetter(newVal) {
            const value = getter ? getter.call(obj) : val
            if (newVal === value) return
            // 如果对象属性是不能被设置的
            if (getter && !setter) return
            if (setter) {
                setter.call(obj, newVal)
            } else {
                val = newVal
            }
            // 新数据递归进行响应式处理
            ob = observe(newVal)
            // 通知数据更新
            dep.notify()
        }
    })
}

// 多维数组变异方法的依赖收集
function dependArray(array) {
    for (let i = 0; i < array.length; i++) {
        const element = array[i];
        // 这里的__obj__就是前面def发挥的作用
        element && element.__ob__ && element.__ob__.dep.depend()
        if (Array.isArray(element)) {
            dependArray(element)
        }
    }
}

数组劫持

数组的话,情况比较特殊,不会像对象一样每一个值使用Object.defineProperty进行处理,这也就是为什么this.a[0] = xxx无效的原因。但对数组的每一个值都会去调用observe方法对其进行数据监听,这就是为什么this.a[0].xxx = xxx会有效的原因。而对数组的增删改查操作都是通过劫持其原型上的变异方法,调用方法进行依赖收集来实现的。其实现分析如下:

const arrayMethods = Object.create(Array.prototype)
// 数组的变异方法
const methodsToPatch = [
    'push',
    'pop',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
]

// 这里是变异方法进行劫持
methodsToPatch.forEach((method) => {
    // 数组的原方法
    const original = arrayMethods[method]
    def(arrayMethods, method, function (...args) {
        const result = original.apply(this, args)
        // 这里的ob 刚刚前面讲Observer的时候已经说过了,def将当前的obsever实例代理到value,而这对于数组来说,value就是数组本身
        const ob = this.__ob__
        // push/unshift/splice的值
        let inserted
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args
                break
            case 'splice':
                inserted = args.slice(2)
                break
        }
        // 需要对新增的值进行响应式处理
        if (inserted) ob.observeArray(inserted)
        // 变化通知
        ob.dep.notify()
        return result
    })
})

小结

看到这里,抛去里面dep相关的代码,相信你应该对Observer如何对数据进行劫持有了一些了解。不知道你有没有发现,无论是数组还是对象,在Observer.observeArraydefineReactive方法里面都会对其子对象或者值调用observe方法,而observe方法又会对传入的值进行判断,是对象又会进行实例化一个Observer实例。正是通过这种‘递归’方式,对我们传入的data值及其子对象进行了数据监听!

希望你能继续看下去,当你看完WatcherDep相关源码,在回过头看这里,会有不一样的收获!

Dep

依赖收集器,负责收集响应式对象的依赖关系,从上面可以知道每个响应式对象包括子对象都会实例化一个Dep实例,对对象来说是在defineReactive中实例化,对数组来说,前面也说到,是对其变异方法进行劫持,其方法对应的dep是在Observer类里面实例化的,而每个Dep实例里面subsWatcher实例数组,当数据有变更时,会通过dep.notify通知各个watcher

let depId = 0
class Dep {
    constructor() {
        this.id = depId++
        this.subs = []
    }
    removeSub(sub) {
        // 这里的sub是watcher
        remove(this.subs, sub)
    }
    addSub(sub) {
    	// 这里的sub是watcher
        this.subs.push(sub)
    }
    // watcher实例互相依赖收集
    depend() {
        if (Dep.target) {
            Dep.target.addDep(this)
        }
    }

    notify() {
        const subs = this.subs.slice()
        for (let i = 0, l = subs.length; i < l; i++) {
        // 更新通知
            subs[i].update()
        }
    }
}


Dep.target = null

function remove(arr, item) {
    if (arr.length) {
        var index = arr.indexOf(item);
        if (index > -1) {
            return arr.splice(index, 1)
        }
    }
}

Watcher

订阅者,或者称为观察者,负责我们的相关更新操作。有三类的Watcher,负责组件渲染的Watcher(renderWatcher)、计算属性的Watcher(computedWatcher)、watch属性的Watcher(userWatcher),当响应式对象进行更新操作的时候,会触发其setter方法,进而调用其对应的依赖收集器实例的dep.notify,调用收集的watcher数组的update方法,进行对应的更新处理。

class Watcher {
    dirty
    getter
    cb
    value
    // vm 为vue实例对象
    // expOrFn为更新的方法, renderWatcher中为我们的updateComponent更新视图, userWatcher/computedWatcher为重新求值方法
    // cb 为回调,主要在userWatcher中及我们的watch属性才需要用到
    constructor(vm, expOrFn, cb, options, isRenderWatcher) {
        this.vm = vm
        if (isRenderWatcher) {
            vm._watcher = this
        }
        vm._watchers.push(this)
        this.id = ++wId
        this.cb = cb
        // 记录上一次求值的依赖
        this.deps = []
        // 记录当前求值的依赖
        this.newDeps = []
        this.depIds = new Set()
        this.newDepIds = new Set()
        // options lazy是computedWatcher的参数, user则是userWatcher的参数
        if (options) {
            this.deep = !!options.deep
            this.user = !!options.user
            this.lazy = !!options.lazy
            this.sync = !!options.sync
        } else {
            this.deep = this.user = this.lazy = this.sync = false
        }
        this.dirty = this.lazy
        if (typeof expOrFn === 'function') {
            this.getter = expOrFn
        } else {
            // 如果是watch
            this.getter = parsePath(expOrFn)
        }

        // renderWatcher、userWatcher new watcher的时候,get方法会调用
        // 如果是computeWatcher this.lazy为true, get方法不会被调用 
        this.value = this.lazy ?
            undefined :
            this.get()
    }

    get() {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
            // 这里的getter 对于renderWatcher是updateComponent方法,在watch就是其对应的key, computed是对应的方法
            value = this.getter.call(vm, vm)
        } catch (error) {

        } finally {
            // 这个是为watch的deep服务的
            if (this.deep) {
                traverse(value)
            }
            popTarget()
            this.cleanupDeps()

        }
        return value
    }

    // 将自己添加到dep里面
    addDep(dep) {
        const id = dep.id
        // 这里两个dep作用是为了防止重复收集依赖
        if (!this.newDepIds.has(id)) {
            this.newDepIds.add(id)
            this.newDeps.push(dep)
            if (!this.depIds.has(id)) {
                dep.addSub(this)
            }
        }
    }
    // 将现有的所有依赖加入
    depend() {
        let i = this.deps.length
        while (i--) {
            this.deps[i].depend()
        }
    }
    update() {
        // 对于computed数据来说,初始化 computedWatcher lazy是为true,且一直是true
        // 所以 computedWatcher 不会执行run ,而是依靠其计算方法中的data的属性值改变的时触发其computedWatcher,将dirty置为true。
        // 在我们对计算属性进行取值操作的时候,会因为dirty为true,从而调用evaluate获得最新的值。(这是一个很巧妙的设计,等到用到的才会最终去计算取值)
        if (this.lazy) {
            this.dirty = true
        } else if (this.sync) {
            this.run()
        } else {
            queueWatcher(this)
        }
    }
    // 重新计算值,计算属性才会调用到的方法
    evaluate() {
    	// 对于计算属性来说,get方法会去调用其在vue实例声明的那个computed对象方法
        // 例如 computed: { name(){ return this.first + thi.second } },这里的get就会去求name返回的这两个data对象的属性的值的计算结果。
        // 在此之前,会将本身这个watcher给加到frist/second两个响应式数据的dep的subs数组里面,当这两个值发生改变的时候,也会触发computed属性发生改变。
        // 并且,等会你会看到,更新操作里面会对watcher的id进行排序,因为computedWatcher会比renderWatcher(这个是在挂载的时候才实例化)先实例化,所以其值更改会在渲染之前
        // 这也就是为什么页面看到的computed属性的值也是更新了的。
        this.value = this.get()
        this.dirty = false
    }

    run() {
        const value = this.get()
        if (value !== this.value || isObject(value) || this.deep) {
            const oldValue = this.value
            this.value = value
            this.cb.call(this.vm, value, oldValue)
        }
    }

    // 清除无用的依赖
    cleanupDeps() {
        let i = this.deps.length
        while (i--) {
            const dep = this.deps[i]
            // 如果当前的新的依赖列表里面没有,旧的依赖有的情况,就需要解除依赖收集
            if (!this.newDepIds.has(dep.id)) {
                dep.removeSub(this)
            }
        }

        // 当前的依赖列表作为上一次依赖列表,然后重置当前的依赖列表
        let tmp = this.depIds
        this.depIds = this.newDepIds
        this.newDepIds = tmp
        this.newDepIds.clear()
        tmp = this.deps
        this.deps = this.newDeps
        this.newDeps = tmp
        this.newDeps.length = 0
    }
}

const targetStack = []

// 依赖采集数组
// Dep.target都是一个Watcher
function pushTarget(target) {
    targetStack.push(target)
    Dep.target = target
}

function popTarget() {
    targetStack.pop()
    Dep.target = targetStack[targetStack.length - 1]
}

几个联系上文的点如下

  • 依赖收集

    前面一直在说的一个概念,到这里可以知道dep收集的就是watcher,如何收集watcher呢?,从Observer分析可以知道对象在取值操作的时候会判断当前是否有Dep.target,从上面Watcher分析知道了这个要有值的情况是得Watcher被实例化的时候即调用new Watcher。而Wacther在被实例化的时候,会根据watcher的类型是否调用get方法,get方法则会将当前的watcher实例赋值给Dep.target,从而完成依赖的收集。接下来我们分析何时会进行Watcher的实例化。

  • 何时会调用new Watcher

    • renderWatcher: 负责data数据到视图的更新的工作,需要做到每个数据发生改变的时候会更新试图,它必须是在数据与计算属性,以及watch的初始化之后,同时还得对data的每个属性进行取值getter操作才会触发依赖收集(渲染视图的时候就会进行取值操作),所以它实例化的地方就是在组件挂载的时候。
      // 这里大致实现挂载与更新的方法,其实际远不止这些
      
      const vm = new Vue({
          data() {
              return {
                  name: 'cn',
                  age: 24,
                  wife: {
                      name: 'csf',
                      age: 23
                  }
              }
          },
          computed: {
              husband() {
                  return this.name + this.wife.name
              }
          },
          watch: {
              wife: {
                  handler(val, oldVal) {
                      console.log('watch--->',val.name, oldVal.name)
                  },
                  deep: true,
                  immediate: true
              }
          },
          render() {
              return `
                  <h3>normal key</h3>
                  <p>${this.name}</p>
                  <p>${JSON.stringify(this.wife)}</p>
                  <h3>computed key</h3>
                  <p>${this.husband}</p>
              `;
          }
      }).$mount(null, document.getElementById('root'))
      
      function mountComponent(vm, el, hydrating) {
        vm.$el = el;
        const _updateComponent = function (vm) {
            vm._update(vm._render(), hydrating)
        }
        // 没错就是在这里,_updateComponent就是我们的更新视图的方法
        // 因为这里因为没设置lazy,所以实例化watcher的时候,会直接调用
        // _updateComponent -> _render -> 渲染页面 
        new Watcher(vm, _updateComponent, noop, {}, true)
        return vm
      }
      Vue.prototype.$mount = function (
          el,
          hydrating
          ) {
              return mountComponent(this, el, hydrating)
          };
    
      Vue.prototype._update = function (node, hydrating) {
          hydrating.innerHTML = node
      }
    
    • computedWatcher: 负责计算属性的变化。在一开始初始化Vue实例就进行了实例化,发生在renderWatcher之前,原因前面注释也有说明,不在累赘。下面看其如何实例化的。
    // initState在Vue.prototype._init里面调用,_init会在Vue被实例化的时候调用
    function initState(vm) {
      // destory的时候用
      vm._watchers = [];
      // 这里的参数就是我们的data、computed、watch、render.....
      const opts = vm.$options
      if (opts.data) {
      // 这里就是初始化data数据了,也就是对数据进行响应式处理,等下会说到
          initData(vm)
      }
      // 这里就是initComputed了
      if (opts.computed) initComputed(vm, opts.computed)
      if (opts.watch) {
          // 这里是init我们的watch
          initWatch(vm, opts.watch)
      }
    }
    
    function initComputed(vm, computed) {
      // 计算属性的wacher对象
      const watchers = vm._computedWatchers = Object.create(null)
    
      for (const key in computed) {
          const userDef = computed[key]
          // 因为compouted有函数形式或者 set/get方式的
          const getter = typeof userDef === 'function' ? userDef : userDef.get
          // 在这里就实例化了
          watchers[key] = new Watcher(
              vm,
              getter || noop,
              noop,
              // 注意这里,lazy初始化是true
              { lazy: true }
          )
          if (!(key in vm)) {
              defineComputed(vm, key, userDef)
          }
      }
    }
    
    function defineComputed(target, key, userDef) {
      // 暂时默认不是服务端渲染
      // const shouldCache = !isServerRendering()
      if (typeof userDef === 'function') {
          sharedPropertyDefinition.get = createComputedGetter(key)
          sharedPropertyDefinition.set = noop
      } else {
          sharedPropertyDefinition.get = createComputedGetter(key)
          sharedPropertyDefinition.set = userDef.set || noop
      }
      
      // 进行代理,当我们this.computedxxx时候,就会触发下面的computedGetter 进行取值操作
      Object.defineProperty(target, key, sharedPropertyDefinition)
    }
    
    
    function createComputedGetter(key) {
        return function computedGetter() {
            const watcher = this._computedWatchers && this._computedWatchers[key]
            if (watcher) {
                // 这里一开始new的时候 dirty 是为true的
                // 只要不取值,第一次,computed 值因为 dirty 是true所以是 undefined
                // 没必要一开始就算出其值(浪费),只有取值了才会调用 evaluate 重新计算值。
                if (watcher.dirty) {
                 // 调用evaluate计算值,计算完后会置为false
                 // 除非依赖的数据发生改变,会将 dirty 置为true 否则都无需取计算
                    watcher.evaluate()
                }
                if (Dep.target) {
                    watcher.depend()
                }
                return watcher.value
            } 
        }
    }
    
    • userWatcher: watch监听的变化。在一开始初始化Vue实例就进行了实例化,发生在renderWatcher之前,这里可能有点绕,需要多看几遍,多调试几遍,下面看其如何实例化的。
      stateMixin(Vue)
      function stateMixin(Vue) {
        //  expOrFn是我们要监听的值的key cb是我们传的回调
        Vue.prototype.$watch = function(expOrFn, cb, options) {
            const vm = this
            options = options || {}
            // watcher 里面的user就是给watch用的
            options.user = true
            // 这里也是会立马调用到watcher的get方法
            const watcher = new Watcher(vm, expOrFn, cb, options)
            // 如果watch配置了立马获取值的话
            if (options.immediate) {
                try {
                    cb.call(vm, watcher.value, null)
                } catch (error) {
                }
             }
           }
      }
       
    
      function initWatch(vm, watch) {
           for (const key in watch) {
                // 这是user定义的回调
                const handler = watch[key]
                createWatcher(vm, key, handler)
            }
      }
      function createWatcher(vm, expOrFn, handler, options) {
          // 针对watch值是对象的情况
          if(isPlainObject(handler)) {
              options = handler
              handler = handler.handler
          }
          return vm.$watch(expOrFn, handler, options)
      }
      
    
      // 下面回顾上面内容
      class Watcher {
        dirty
        getter
        cb
        value
        constructor(vm, expOrFn, cb, options, isRenderWatcher) {
            if (options) {
                this.user = !!options.user
            } else {
                this.deep = this.user = this.lazy = this.sync = false
            }
            if (typeof expOrFn === 'function') {
                this.getter = expOrFn
            } else {
                // 如果是watch的话是走这里
                this.getter = parsePath(expOrFn)
            }
            // new watcher的时候,get方法会调用
            this.value = this.lazy ?
                undefined :
                this.get()
        }
    
        get() {
            pushTarget(this)
            let value
            const vm = this.vm
            try {
                // 
                value = this.getter.call(vm, vm)
            } catch (error) {
    
            } finally {
                // 这个是为watch的deep服务的
                // 如果deep的话需要递归遍历其watch的data属性的其下的所有子属性
                // 将当前的userWatcher加入到它们的dep中,这样才能深度改变
                if (this.deep) {
                    traverse(value)
                }
                popTarget()
                this.cleanupDeps()
    
            }
            return value
        }
        
        run() {
            const value = this.get()
            if (value !== this.value || isObject(value) || this.deep) {
                const oldValue = this.value
                this.value = value
                // cb就是我们的回调
                this.cb.call(this.vm, value, oldValue)
            }
          }
      }
      
      function parsePath(path) {
          // 解析watch 的a.b.c的情况
          const segments = path.split('.');
          // 这里的obj会被赋予vm,所以返回的就是watch的data中key的值
          return function (obj) {
            for (let i = 0; i < segments.length; i++) {
              if (!obj) { return }
              obj = obj[segments[i]];
            }
            return obj
          }
      }
    
      // 为了防止重复依赖收集
      const seenObjects = new Set()
    
      // 递归遍历watch的需要deep的响应式对象的值,进行依赖收集,这样才能实现deep
      function traverse(val) {
          _traverse(val,seenObjects)
          seenObjects.clear()
      }
    
      function _traverse (val, seen) {
          let i, keys
          const isA = Array.isArray(val)
          if ((!isA && !isObject(val))) {
            return
          }
          if (val.__ob__) {
            const depId = val.__ob__.dep.id
            if (seen.has(depId)) {
              return
            }
            seen.add(depId)
          }
          if (isA) {
            i = val.length
            while (i--) _traverse(val[i], seen)
          } else {
            keys = Object.keys(val)
            i = keys.length
            while (i--) _traverse(val[keys[i]], seen)
          }
      }
    
  • 总结一下

    通过上面完整的Watcher对象,以及几种watcher实例的分析,可以大致了解了watcher在什么时候,如何与我们的响应式数据进行一个绑定,从而完成发布->订阅,现在再次引用官网的图,是不是有点明白了。

如果你完整读完前面的内容的话,应该对整个流程有个大致了解,总的来说如下

  • data数据劫持:遍历data选项的属性,属性值不是数组的话,利用Object.defineProperty为属性添加gettersetter,是数组的则是劫持其变异方法,再递归遍历其子对象、遍历数组,重复操作。
  • watcher实例化:不同的watcher在不同的阶段实例化,对data数据进行取值操作,进行依赖收集。
  • 修改响应式数据:触发setter,调用dep.notify,遍历其对应的dep上的subs数组,调用watcher.update

更新策略

前面的基本都讲了,其实还差一个就是我们的更新操作,即watcher.update会发生什么。

当我们数据发生变化的时候,走到watcher.update,并不是立马就去调用watcher.run方法进行更新操作,而是异步更新,可以想一下,如果不是异步更新,那么每次调用this.xxx = xxx的时候,就引起了视图更新,如果一次性更新的数据很多,或者我们只是想要最后的那个结果,中间的变换都是无效的,是不是就会出现很多弊端。

所以尤大是这样做的,watcher观察到数据发生了变化,会开启一个队列queue,缓冲在同一事件循环中发生的数据改变。如果一个watcher多次被触发,则只会被推入队列中一次(去除重复的数据,无用的数组,取最后一次)。然后真正的更新是在下一次事件循环的Tick中触发。具体详情可以参考vue异步更新机制

let waiting = false
let has = {}
// 缓存watcher数组
const queue = []

// 缓存在一次更新中的watcher
function queueWatcher(watcher) {
    const id = watcher.id
    if (!has[id]) {
        has[id] = true
        queue.push(watcher)
        if (!waiting) {
            waiting = true
            nextTick(flushSchedulerQueue)
        }
    }
}

let index = 0
// flushScheduleQueue函数的作用主要是执行更新的操作
// 它会把queue中所有的watcher取出来并执行相应的更新     
function flushSchedulerQueue() {
    flushing = true
    let watcher, id
    // watcher 按先后排序
    queue.sort((a, b) => a.id - b.id)
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index]
        if (watcher.before) {
            watcher.before()
        }
        id = watcher.id
        has[id] = null
        watcher.run()
    }
    resetSchedulerState()
}


// 重置
function resetSchedulerState() {
    index = queue.length = 0
    has = {}
    waiting = flushing = false
}


// 一般callback都是只有flushSchedulerQueue
// 当我们自己定义了$nextTick的时候也会加入到这里
const callbacks = []
let pending = false
function nextTick(cb, ctx) {
    callbacks.push(() => {
        if (cb) {
            try {
                cb.call(ctx)
            } catch (e) {
            }
        }
    })
    // 防止重复执行
    if (!pending) {
        pending = true
        timerFunc()
    }
}

const p = Promise.resolve()
// 这里我们默认浏览器支持promise,其源码判断很多种情况
let timerFunc = () => {
    p.then(flushCallbacks)
}

function flushCallbacks() {
    pending = false
    const copies = callbacks.slice(0)
    callbacks.length = 0
    for (let i = 0; i < copies.length; i++) {
        copies[i]()
    }
}

最后

内容讲起来也是挺多挺绕的,一开始可能会比较懵,但是将整个流程相关的都捋一遍后,其实会发生其设计的妙处。个人感觉注释算是比较齐全了,空讲代码是比较难完全理解的,所以建议拉下下面我手敲的demo例子,对比着一步一步的调试。里面也带上了官方源码,上面的基本都在src/core文件夹下,看完全的话可以找dist/vue.js。整理不易,如果有什么说错的地方可以在评论区指出,如果觉得对你有帮助的话,希望能给个三连🤭

demo地址