阅读 53

Vue2.x(含组件)主流程源码笔记(八):update 阶段(上)

前面七章分析了 vue demo 的整个初始化过程,本文开始分析当数据(model)发生变化时,Vue 的处理过程。

我们以点击 dom 触发 plus 执行 this.info.age++ 为例,分析 Vueupdate 阶段。

proxyGetter && proxySetter

此时读取 this.info,则触发在 ininState->initData->proxy(vm, "_data", key) 监听的 proxyGetter:

function proxy(target, sourceKey, key) {
  sharedPropertyDefinition.get = function proxyGetter() {
    return this[sourceKey][key];
  };

  sharedPropertyDefinition.set = function proxySetter(val) {
    this[sourceKey][key] = val;
  };

  Object.defineProperty(target, key, sharedPropertyDefinition);
}
复制代码

其中,thisVue 实例,sourceKey_data,即返回 _data 对象里的 key。在读取 key 时,又触发了在 ininState->initData->observe->walk->defineReactive?1 里的 reactiveGetter

reactiveGetter && reactiveSetter

Object.defineProperty(obj, key, {
  enumerable: true,
  configurable: true,
  get: function reactiveGetter() {
    var value = getter ? getter.call(obj) : val;

    if (Dep.target) {
      dep.depend();

      if (childOb) {
        childOb.dep.depend();

        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }

    return value;
  },
  set: function reactiveSetter(newVal) {
    var value = getter ? getter.call(obj) : val;

    if (newVal === value || (newVal !== newVal && value !== value)) {
      return;
    }

    if (true && customSetter) {
      customSetter();
    }

    if (getter && !setter) {
      return;
    }

    if (setter) {
      setter.call(obj, newVal);
    } else {
      val = newVal;
    }

    childOb = !shallow && observe(newVal);
    dep.notify();
  },
});
复制代码

因为此时并非在执行 watcherupdate 操作, 所以 Dep.targetwatcher 不存在,直接返回 value

然后读取 this.info.age,直接触发 reactiveGetter 返回 value

读取阶段结束后,触发 reactiveSetter 来设置 this.info.age 的新值,设置后执行:

childOb = !shallow && observe(newVal);
dep.notify();
复制代码

对新值进行了监听后,然后通知该变量对应的订阅中心 Dep

Dep.prototype.notify

Dep.prototype.notify = function notify() {
  var subs = this.subs.slice();

  if (true && !config.async) {
    subs.sort(function (a, b) {
      return a.id - b.id;
    });
  }

  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
复制代码

其中,subs 为变量的订阅列表。在本例中,变量 this.info.age 有 3 个订阅者 watcher,分别来自 watch watcher渲染 watcher计算 watcher

notify 里,如果配置了同步,则对订阅列表 subsid 大小进行排序。然后依次触发每项(watcher)的 update 方法。

Watcher.prototype.update

Watcher.prototype.update = function update() {
  if (this.lazy) {
    this.dirty = true; //设置 计算 watcher 的标识位
  } else if (this.sync) {
    this.run(); // 如果设置同步,则立即执行
  } else {
    queueWatcher(this); //以当前实例为参数
  }
};
复制代码

Watcher.prototype.update 主要针对配置的一些处理,然后将当前 watcher 传入 queueWatcher

其中 计算 watcher 为懒加载(即 this.lazytrue),所以不会执行 queueWatcher,但会将标识位 this.dirty 置为 true。这样计算属性在后面 computedGetter 里取值时,就会通过 Watcher.prototype.evaluate 重新计算而不是直接取缓存值 value,并且还会将标识位 this.dirty 置为 false,这样在后续如果没有计算属性对应的表达式里的被监听的其他值更新触发计算属性 watcher 更新的情况下,就可以直接取 value 值了。

queueWatcher

function queueWatcher(watcher) {
  var id = watcher.id;

  if (has[id] == null) {
    has[id] = true; // 防止同一时刻同一 watcher 的多次 push

    if (!flushing) {
      queue.push(watcher); //是否在执行 flushSchedulerQueue
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      var 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; // 防止多次 nextTick,即防止多次注册新的微任务队列

      if (true && !config.async) {
        flushSchedulerQueue();
        return;
      }

      nextTick(flushSchedulerQueue); //将 flushSchedulerQueue 函数传入 nextTick
    }
  }
}
复制代码
  • has 为一个对象,用于过滤同一个 watcher 的同时间多次调用 update
  • queue 为一个数组,用于存放等待更新的 watcher 队列
  • flushing 为一个布尔值,用于标识队列是否正在执行更新,直到 queue 执行完后才重置为 false
  • flushSchedulerQueue 为更新队列的执行函数
  • waiting 为一个布尔值,用于标识已经把 flushSchedulerQueue 注册到下一次微循环的任务队列中,直到 queue 执行完后才重置为 false

queueWatcher 里主要将当前实例 Watcher pushqueuewatcher 队列里,然后将 flushSchedulerQueue 传入 nextTick 方法。

nextTick

function nextTick(cb, ctx) {
  var _resolve;
  // callbacks 保存异步执行的任务队列
  callbacks.push(function () {
    if (cb) {
      try {
        cb.call(ctx);
      } catch (e) {
        handleError(e, ctx, 'nextTick');
      }
    } else if (_resolve) {
      _resolve(ctx);
    }
  });

  if (!pending) {
    pending = true;
    timerFunc();
  } // $flow-disable-line

  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(function (resolve) {
      _resolve = resolve;
    });
  }
}
复制代码

nextTick 里对异步执行的任务队列 cb 统一收集到 callbacks 数组( flushSchedulerQueue 为其中一项),然后执行 timerFunc

timerFunc 里通过异步执行 flushCallbacks。异步方法的选用优先级:Promise.resolve().then>MutationObserver>setImmediate>setTimeout,前两个属于 microTask 微任务队列,后两个属于 macroTask 宏任务队列。

执行 nextTick(flushSchedulerQueue)后,进入异步等待,此时回到 Dep.prototype.notify 里执行下一个 watcherupdatewatcher pushqueue 里。

待到异步调用后,执行 flushCallbacks,该方法里遍历 callbacks 并执行。

为什么要这么做

如果没有异步队列,则每一次的数据变化时,就会直接触发 update->patch 去比较 vnode 进行更新 dom,如果在同一时间有大量修改数据(watcher 被多次触发),则会反复频繁更新视图。而有了这个队列,在当前栈可以先判断 watcher 的重复性,过滤掉重复更新。保证更新视图操作 DOM 的动作是在当前栈执行完以后的下一个事件循环 tick 的时候调用,大大优化了性能。

flushSchedulerQueue

function flushSchedulerQueue() {
  currentFlushTimestamp = getNow();
  flushing = true;
  var watcher, id;

  queue.sort(function (a, b) {
    return 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(); // in dev build, check and stop circular updates.

    if (true && 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;
      }
    }
  } // keep copies of post queues before resetting state

  var activatedQueue = activatedChildren.slice();
  var updatedQueue = queue.slice();
  resetSchedulerState(); // call component updated and activated hooks

  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue); // devtool hook

  if (devtools && config.devtools) {
    devtools.emit('flush');
  }
}
复制代码

对于本例,queue 里目前有两个 watcherwatch watcher渲染 watcherflushSchedulerQueue 里先对 queue 排序:

  • 组件的更新是由父到子的(因为父组件的创建在子组件之前),所以 watcher 的创建和执顺序行也应该是先父后子
  • 用户自定义 watcher 应该在 渲染 watcher 之前执行(因为用户自定义 watcher 的创建在 渲染 watcher 之前)
  • 如果一个组件在父组件的 watcher 执行期间被销毁,那么这个子组件的 watcher 都可以被跳过(this.active 标识)。

然后执行 queue 队列中的每一个 watcherwatcher.before 触发生命周期 beforeUpdate 钩子。然后执行 watcher.run(),下文分析。

执行结束后,调用 resetSchedulerState 重置状态,调用 callActivatedHooks 改变组件为 activated 状态并触发生命周期 activated 钩子,调用 callUpdatedHooks 触发生命周期 update 钩子,最后触发工具的 flush 事件,整个流程执行结束。

Watcher.prototype.run

Watcher.prototype.run = function run() {
  if (this.active) {
    var value = this.get();

    if (value !== this.value || isObject(value) || this.deep) {
      // set new value
      var oldValue = this.value;
      this.value = value;

      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, 'callback for watcher "' + this.expression + '"');
        }
      } else {
        this.cb.call(this.vm, value, oldValue);
      }
    }
  }
};
复制代码

this.active 用于标识该 watcher 是否已经被卸载。在 Watcher.prototype.teardown 里设置为 false(已卸载)。

Watcher.prototype.run 执行 this.get 得到 value 赋给 this.value。然后更新 watchervalue 值。如果 this.user 为真这表示为用户定义的 watch,则执行 cb 即用户定义的回调方法,watch watcher 就会触发该回调。

Watcher.prototype.get

Watcher.prototype.get = function get() {
  pushTarget(this);
  var value;
  var vm = this.vm;

  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, 'getter for watcher "' + this.expression + '"');
    } else {
      throw e;
    }
  } finally {
    if (this.deep) {
      traverse(value);
    }

    popTarget();
    this.cleanupDeps();
  }

  return value;
};
复制代码

Watcher.prototype.get 里主要是通过 this.getter 得到 value

先将本 watcher 推入全局变量 Dep.target 下,在添加订阅结束后,移除在 Dep.target 下的 watcher

如果设置了 deeptrue,那么执行 traverse 里递归执行 _traverse 读取对象里的每一项子属性给他们添加本 watcher 订阅。

  • 其中对于 watch watchergetter 为:

    //...
    return function (obj) {
      for (var i = 0; i < segments.length; i++) {
        if (!obj) {
          return;
        }
    
        obj = obj[segments[i]];
      }
    
      return obj;
    };
    //...
    复制代码

    在读取 obj[segments[i]] 时,会触发属性 infoproxyGetter -> reactiveGetter ,在得到最新值的同时,将该 watch watcher 添加到对应的订阅列表,并且因为 info 是对象,所以还添加到 info.__ob__ 上。

  • 其中对于 渲染 watchergetter 为:

    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
    复制代码

    vm._render 会根据 render 函数得到 vnode。在执行 render 的过程中,读取到的每一个变量都会触发其对应的 proxyGetter->reactiveGetter 取得最新值和该 渲染 watcher 订阅到对应变量的订阅列表里。得到 vnode 后,然后执行 vm._update 更新视图,下一章分析。

最后执行 cleanupDeps 方法通过对比新旧 depIds 来删除无效的订阅 sub ,最后返回 value,回到 flushSchedulerQueue 方法里。

本章小结

  1. 本章以一个数据更新为始,分析了对数据的添加订阅和触发订阅的相关逻辑。
  2. 一共涉及到 3 个 watcher:分别是 watch watcher渲染 watcher计算 watcher,对不同 watcher 进行了不同的处理。
  3. 触发订阅涉及到了异步队列优化,优化了不必要的视图更新,大大提升了性能。