「Vue源码」派发更新

Vue 通过 Object.defineProperty 重写了 get 方法,实现了「依赖收集」,又通过 set 方法来实现了「派发更新」。

Object.defineProperty(obj, key, {  enumerable: true,  configurable: true,  set: function reactiveSetter (newVal) {    var value = getter ? getter.call(obj) : val;    /* eslint-disable no-self-compare */    if (newVal === value || (newVal !== newVal && value !== value)) {      return    }    /* eslint-enable no-self-compare */    if (process.env.NODE_ENV !== \\\'production\\\' && customSetter) {      customSetter();    }    // #7981: for accessor properties without setter    if (getter && !setter) { return }    if (setter) {      setter.call(obj, newVal);    } else {      val = newVal;    }    childOb = !shallow && observe(newVal);    dep.notify();  }});

在 Vue 项目中,访问 data 值时就会触发 get 方法,修改 data 值时,就会触发 set 方法,例如在 Vue 中:
    changeTextFn(){  this.text = Math.random();}
    由于重写了 set 方法,所以会走重写后的逻辑判断,先要获取到点击之前的「旧值」,再判断「新值」「旧值」是否相同,相同会直接返回当前值并退出。
    如果新旧值不同,并且「传入的值」还是一个对象就会去执行 observe(newVal) ,这个方法之前也介绍过,添加私有属性 __ob__ 并去执行每个属性的监听,说白了就去执行传入对象的 get 方法,收集依赖项。
    再然后就要去执行重要的一步:
    dep.notify();
    通过 dep.notify 去实现监听数据的更新,notify 方法定义在:
    src/core/observer/dep.js // 文件路径
    // notify 方法notify () { // stabilize the subscriber list first const subs = this.subs.slice() if (process.env.NODE_ENV !== \\\'production\\\' && !config.async) { // subs aren\\\'t sorted in scheduler if not running async // we need to sort them now to make sure they fire in correct // order subs.sort((a, b) => a.id - b.id) } for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() }}
    this.subs 下面收集都是 Watcher ,先浅拷贝赋值给 subs ,然后通过 id 去排序。排完序后,就依次执行 update 方法:
    src/core/observer/watcher.js // 文件定义位置
    // update 方法update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { this.run() } else { queueWatcher(this) }}
    update 方法里,判断是否是同步、异步。接着执行 queueWatcher 方法:
      src/core/observer/scheduler.js // 文件定义位置
      // queueWatcher 方法export function queueWatcher (watcher: Watcher) { const id = watcher.id if (has[id] == null) { has[id] = true if (!flushing) { queue.push(watcher) } else { // if already flushing, splice the watcher based on its id // if already past its id, it will be run next immediately. let i = queue.length - 1 while (i > index && queue[i].id > watcher.id) { i-- } queue.splice(i + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true
      if (process.env.NODE_ENV !== \\\'production\\\' && !config.async) { flushSchedulerQueue() return } nextTick(flushSchedulerQueue) } }}
      获取到 Watcher 的 id 存入 queue 数组里面去,然后执行到 nextTick ,nextTick 之前讲过,会在下一个 tick 里面去执行任务,所以,我们来看 flushSchedulerQueue 执行的是什么:
        function flushSchedulerQueue () {  currentFlushTimestamp = getNow()  flushing = true  let watcher, id
        // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component\\\'s user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component\\\'s watcher run, // its watchers can be skipped. queue.sort((a, b) => a.id - b.id)
        // do not cache length because more watchers might be pushed // as we run existing watchers for (index = 0; index < queue.length; index++) { watcher = queue[index] if (watcher.before) { watcher.before() } id = watcher.id has[id] = null watcher.run() // in dev build, check and stop circular updates. if (process.env.NODE_ENV !== \\\'production\\\' && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (circular[id] > MAX_UPDATE_COUNT) { warn( \\\'You may have an infinite update loop \\\' + ( watcher.user ? `in watcher with expression \\\"${watcher.expression}\\\"` : `in a component render function.` ), watcher.vm ) break } } }}
        我们拿到了需要更新的 queue 这个数组,需要去执行下排序,我们在收集的时候是按父到子组件收集的 Watcher,所以执行的时候需要先执行父组件的更新,再执行子组件的更新。如果我们在页面写了手动监听的 watch 那就会先执行手动的,再执行渲染 watcher 。
        queue.sort((a, b) => a.id - b.id)
        执行完排序会去执行 before,before 对应着 watcher.before() 这个函数,函数里面会去执行钩子函数 beforeUpdate,为什么 before 执行的是 beforeUpdate,我把代码放在了下面。
        继续执行 watcher.run() 方法。
        run 方法里面执行 this.get() 方法,get 方法是 Watcher 原型方法,会执行:
        value = this.getter.call(vm, vm)
        getter 求值会触发 updateComponent 更新视图,这个 getter 是 Watcher 传入的 updateComponent 方法,这块有点乱,好好理一下:
          new Watcher(vm, updateComponent, noop, {  before: function before () {    if (vm._isMounted && !vm._isDestroyed) {      callHook(vm, \\\'beforeUpdate\\\');    }  }}, true /* isRenderWatcher */);
          // Watcher 方法export default class Watcher{ constructor ( vm: Component, expOrFn: string | Function, cb: Function, options?: ?Object, isRenderWatcher?: boolean  )}
          new Watcher 传入的参数 updateComponent 参数就是 watcher.js 里面的 expOrFn ,在 Watcher 中又做了重新赋值:
          this.getter = expOrFn
          所以,this.getter 执行的是 updateComponent 方法,在 watcher 的 get 方法里又执行了 this.getter ,所以会调用一次 _update 和 _render 方法,执行 Demo 的渲染。
            updateComponent = function () {  vm._update(vm._render(), hydrating);};
            而上面我们提到过的 beforeUpdate 钩子函数,是在 new Watcher 中传入的 before 方法。

            图片授权基于 www.pixabay.com 相关协议

            推荐阅读

            如何用 vue-cli 调试源码?
            如何调试 Vue 源码?

            Vue 在挂载数据前都经历了什么?


            原创文章,作者:小道研究,如若转载,请注明出处:https://www.sudun.com/ask/34554.html

            (0)
            小道研究's avatar小道研究
            上一篇 2024年4月16日 下午4:49
            下一篇 2024年4月16日 下午4:51

            相关推荐

            发表回复

            您的邮箱地址不会被公开。 必填项已用 * 标注