【Vue源码】16.响应式原理-派发更新

响应式原理-派发更新

在上一节 15.响应式原理-依赖收集 中我们了解了依赖收集的过程,收集的目的就是当我们修改数据时可以对相关依赖派发更新,本节将了解派发更新的过程。

先回顾一下 setter 部分逻辑:

/**
 * Define a reactive property on an Object.
 */
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep();

  const property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return;
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get;
  const setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // ...
    set: function reactiveSetter(newVal) {
      const 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();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    },
  });
}
复制代码

setter 主要看两个点:

  1. childOb = !shallow && observe(newVal),如果 shallowfalse 的情况,会对新设置的值变成一个响应式对象
  2. set 中通过执行 dep.notify() 进行派发更新,也就是调用 Dep.notify

过程分析

Dep.notifyDep 的一个实例方法,定义在 src/core/observer/dep.js 中:

class Dep {
  // ...
  notify() {
    // stabilize the subscriber list first
    const subs = this.subs.slice();
    // 遍历 dep 中存储的 watcher,执行 watcher.update()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  }
}
复制代码

这里主要是遍历 subs ,也就是 Watcher 的实例数组,然后调用 watcherupdate 方法:

class Watcher {
  // ...
  update() {
    /* istanbul ignore else */
    if (this.computed) {
      // 懒执行时走这里,比如 computed
      // 将 dirty 置为 true,就可以让 computedGetter 执行时重新计算 computed 回调函数的执行结果

      // A computed property watcher has two modes: lazy and activated.
      // It initializes as lazy by default, and only becomes activated when
      // it is depended on by at least one subscriber, which is typically
      // another computed property or a component's render function.
      if (this.dep.subs.length === 0) {
        // In lazy mode, we don't want to perform computations until necessary,
        // so we simply mark the watcher as dirty. The actual computation is
        // performed just-in-time in this.evaluate() when the computed property
        // is accessed.
        this.dirty = true;
      } else {
        // In activated mode, we want to proactively perform the computation
        // but only notify our subscribers when the value has indeed changed.
        this.getAndInvoke(() => {
          this.dep.notify();
        });
      }
    } else if (this.sync) {
      // 在使用 $watch 或者 watch 选项时,可以传入一个 sync 选项,标识 watcher 需要同步更新
      this.run();
    } else {
      // 一般的 watcher 更新都是异步队列,将 watcher 放入到更新对象队列
      queueWatcher(this);
    }
  }
}
复制代码

这里对于 Watcher 的不同状态执行不同逻辑,computedsync 稍后再看,在一般的组件数据更新的场景中,会执行 queueWatcher(this)

queueWatcher 定义在 src/core/observer/scheduler.js 中:

const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let waiting = false;
let flushing = false;
/**
 * Push a watcher into the watcher queue.
 * Jobs with duplicate IDs will be skipped unless it's
 * pushed when the queue is being flushed.
 */
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.

      // 如果已经刷新,则根据它的id进行拼接;
      // 如果已经超过了它的id,则它将立即运行。
      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;
      nextTick(flushSchedulerQueue);
    }
  }
}
复制代码

在这里引入了队列的概念 ,在 Vue 中并不会每次数据变化都会触发 Watcher 回调,而是把这些 Watcher 先添加到队列里面,然后在 nextTick 后执行 flushSchedulerQueue

queueWatcher 的逻辑主要注意这几点:

  1. 首先用 has 保证只添加一次 Watcher
  2. 满足 !flushing 条件,把 watcher 推入到队列中,否则说明正在执行队列,就会从后往前找,找到第一个待插入 watcherid 比当前队列中 watcherid 大的位置。把 watcher 按照 id 的插入到队列中,因此 queue 的长度发生了变化 【标记点 1】
  3. 通过 waiting 来保证 nextTick(flushSchedulerQueue) 逻辑只执行一次, nextTick 暂时不管,可以理解为是在异步的去执行 flushSchedulerQueue

flushSchedulerQueue 定义在 src/core/observer/scheduler.js 中:

let flushing = false;
let index = 0;
/**
 * Flush both queues and run the watchers.
 */
function flushSchedulerQueue() {
  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.
  // 1.组件的更新由父到子:因为父组件的创建过程是先于子组件的。所以 watcher 的创建也是先子后父,执行顺序也是先子后父
  // 2.用户自定义的 watcher 要优于 渲染 watcher 执行:因为用户自定义的 watcher 是在渲染 watcher 之前创建的
  // 3.如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行可以被跳过
  queue.sort((a, b) => a.id - b.id);

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  // 每次遍历都会求一次 queue 的长度,因为在 watcher.run() 时,有可能用户会添加新的 watcher
  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;
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice();
  const updatedQueue = queue.slice();
  // 状态恢复
  resetSchedulerState();

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue);
  callUpdatedHooks(updatedQueue);

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit("flush");
  }
}
复制代码

flushSchedulerQueue 主要做了下面几件事:

  1. 队列排序:queue.sort((a, b) => a.id - b.id)
  • 组件的更新由父到子:因为父组件的创建过程是先于子组件的。所以 watcher 的创建也是先子后父,执行顺序也是先子后父
  • 用户自定义的 watcher 要优于 渲染 watcher 执行:因为用户自定义的 watcher 是在渲染 watcher 之前创建的
  • 如果一个组件在父组件的 watcher 执行期间被销毁,那么它对应的 watcher 执行可以被跳过
  1. 队列遍历
for (index = 0; index < queue.length; index++) {
  // ...
  watcher.run();
  // ...
}
复制代码

拿到 watcher 之后执行它的 run 方法。在这里要注意每次遍历都会求一次 queue 的长度,因为在 watcher.run() 时,有可能用户会添加新的 watcher ,这时就会走到我们讲到的 queueWatcher 方法内,执行 【标记点 1】 的逻辑。

  1. 状态恢复:resetSchedulerState()
const queue: Array<Watcher> = [];
let has: { [key: number]: ?true } = {};
let circular: { [key: number]: number } = {};
let waiting = false;
let flushing = false;
let index = 0;
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState() {
  index = queue.length = activatedChildren.length = 0;
  has = {};
  if (process.env.NODE_ENV !== "production") {
    circular = {};
  }
  waiting = flushing = false;
}
复制代码

目的就是把控制流程状态的一些变量恢复到初始值,把 watcher 队列清空。

接下来分析 watcher.run() 方法,定义在 src/core/observer/watcher.js 中:

class Watcher {
  /**
   * Scheduler job interface.
   * Will be called by the scheduler.
   */
  run() {
    if (this.active) {
      this.getAndInvoke(this.cb);
    }
  }

  getAndInvoke(cb: Function) {
    const value = this.get();
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value;
      this.value = value;
      this.dirty = false;
      if (this.user) {
        try {
          // 传入的两个参数就是 `value` 和 `oldValue` ,这就是我们使用自定义 `watcher` 时能在回调函数的参数中拿到 `newVal` 、 `oldVal` 的原因
          cb.call(this.vm, value, oldValue);
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`);
        }
      } else {
        cb.call(this.vm, value, oldValue);
      }
    }
  }
}
复制代码

run 方法就是执行 getAndInvoke 方法,并传入 watcher 的回调函数,主要做了下面两件事:

  1. const value = this.get()
  2. 满足新旧值不相等、新值为对象类型、 deep 模式其一,就执行 watcher 的回调。在回调函数执行的时候传入的两个参数就是 valueoldValue ,这就是我们使用自定义 watcher 时能在回调函数的参数中拿到 newValoldVal 的原因。

对于渲染 watcher 而言,它在执行 this.get() 方法求值的时候,会执行 getter 方法:

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

以上就是当我们去修改数据时,会触发组件重新渲染的原因。

总结

16.响应式原理-派发更新.png
依赖派发的实质就是当数据变化时,触发 setter 逻辑,把在依赖收集过程中订阅的所有观察者也就是 watcher,都触发她们的 update 的过程,并且这个过程利用了队列做进一步优化,在 nextTick 后执行所有 watcher 的 run,最后执行它们的回调函数。

源码分析 GitHub 地址

参考:Vue.js 技术揭秘

© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享