Cosen95/blog

Vue源码探秘(派发更新)

Opened this issue · 0 comments

引言

在上一节,我们分析了响应式数据中依赖收集的过程,而收集依赖的目的就是在数据更新时会遍历订阅者并派发更新。这一节,让我们一起来看下派发更新的过程。

setter

当数据被修改时会触发setter,回顾setter函数:

// src/core/observer/index.js
/**
 * 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,
    get: function reactiveGetter() {
      // ...
    },
    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();
      }
      // #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();
    },
  });
}

这里setter做了两件事情:修改属性值和派发更新。

首先,setter会通过getter拿到修改前的值value和修改后的值newVal做比较,如果两者相同直接return(表示值没有发生变化)。

这里的newVal !== newVal && value !== value 表示的是 valuenewVal 都是 NaN ,因为 NaN 不等于自身。

接着调用了customSetter函数。

再接下来的这部分就是关键逻辑了:首先修改属性值,如果之前定义了setter则直接调用setter,如果没有则做一个赋值操作。

接下来的这句:

childOb = !shallow && observe(newVal);

其实在调用defineReactive函数时,他已经执行一遍了,这里再次执行是考虑到数据可能原来没有被观察,而现在被修改为数组或者纯对象,那就需要调用 observe 将它转换成为一个响应式对象。

最后的dep.notify()就是派发更新了,也正是本节要分析的重点。

派发更新

先来看 notify 函数的定义:

// src/core/observer/dep.js

export default class Dep {
  //...
  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();
    }
  }
}

这里首先拿到了subs的副本。

这里采用了Array.slice实现了数组的拷贝,其实还有很多方法可以实现。具体参考这里

之后的if判断中的config.async是一个全局配置,默认值为true,用来表示观察者是异步执行还是同步执行。

如果是同步执行,则需要在这里给 subs 中的 Watcherid 从小到大的顺序进行排序。这里为什么要有一个排序操作呢?先留个疑问,我们接着往下看。

接下来就是遍历 subs 中的 Watcher 并执行它们的 update 方法。我们来看 update 方法的定义:

// /src/core/observer/watcher.js

export default class Watcher {
  //...
  /**
   * Subscriber interface.
   * Will be called when a dependency changes.
   */
  update() {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  }
  // ...
}

这里的三条条件语句分别对应Watcher的不同状态。前两种分别对应计算属性同步Watcher,会在后面详细介绍。当前会走else逻辑执行queueWatcher函数。queueWatcher函数定义在src/core/observer/scheduler.js中:

// src/core/observer/scheduler.js

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

queueWatcher 函数会把传入的 Watcher 推入到一个异步的观察者队列 queue 中。

为什么要引入队列呢,实际上这是一个优化手段。它并不会每次数据更新时,就直接遍历 subs 执行 Watcher 的回调,而是将这些 Watcher 放入一个异步队列,调用 nextTick 来异步执行 flushSchedulerQueue

函数首先定义了一个对象 has 用于记录传入 Watcherid ,它的作用是避免同一个 Watcher 被重复推入队列。

这种情况什么时候会出现呢,比如我们同时修改了被同一个 Watcher 订阅的多个数据,那么会触发多个 setter ,也就会调用 queueWatcher 函数多次。

这里定义 flushfalse ,所以会走 if 逻辑将 Watcher push 到 queue 中。flush 的作用以及 else 的具体逻辑现在还不清楚,需要我们往下分析才知道。

最后的 if 语句中的 waiting 和上面的类似,也是为了保证 if 里面的逻辑只执行一次。这里再次遇到之前的同步异步的判断,如果是同步则直接执行 flushSchedulerQueue 函数,否则执行 nextTick(flushSchedulerQueue)

nextTick 方法的具体实现会在下一节详细介绍。接下来我们就来看 flushSchedulerQueue 函数的定义:

// src/core/observer/scheduler.js

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++) {
    // ...
  }

  // ...
}

flushSchedulerQueue 函数一开始会将 flushing 置为 true ,然后对 queue 做一次按 id 从小到大的排列(也就是按从父到子的顺序排序)。注释说明了做这个排序的原因:

1、组件的更新顺序是从父到子,因为组件的创建顺序是从父到子。

2、组件的 user Watcher 会优先于渲染 Watcher 执行。(user Watcher 什么情况下出现呢,比如我们在组件对象中使用 watch 属性或者调用 $watch 方法时就会创建一个 user Watcher)。

3、如果一个组件在它的父组件的 Watcher 执行的时候被销毁,那这个组件对应的 Watcher 的执行可以被跳过。

回顾前面的 dep.notify() ,我们在那里提出了一个问题:为什么同步执行的 Watcher 要在 dep.notify() 就先排好顺序?

现在可以解开疑惑了:由于异步 WatcherflushSchedulerQueue 函数会在所有 Watcher 入列后才执行,所以可以正确排序,而同步 WatcherflushSchedulerQueue 函数是同步执行的,不会等所有 Watcher 入列后才执行,所以无法保证 queue 能正确排序,所以干脆在 dep 中就先把 subs 排好。

queue 排好序之后就要遍历 queue 中的 Watcher 。注意在 for 循环前的一段注释:

// do not cache length because more watchers might be pushed
// as we run existing watchers

注释的意思是不要缓存length(也就是不要在for循环前先获取queue.length)。这么做的原因是什么呢?

因为在执行 Watcher.run() 时可能有新的 Watcher 加入进来导致 queue 长度发生变化。而新加入的 Watcher 就又会执行到 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);
    }
    // ...
  }
}

这个时候 flushing 已经被修改为 true 了,所以会执行 else 逻辑。

其实在上面的分析中,我们只看了if逻辑:也就是一开始Watcher入队时。

当所有 Watcher 入队后会遍历执行回调。而在执行 Watcher 过程中可能会有新 Watcher 入队,这个时机就对应 else 逻辑。

else 中不能简单地将 Watcher push 到 queue 中,因为 queue 在遍历执行前已经排好了顺序,这里必须保证新插入的 Watcher 不会打乱顺序,所以需要计算出正确的插入位置。while 的判断逻辑是这样的:

i > index && queue[i].id > watcher.id;

i取值queue.length - 1,也就是queue的最后一位。所以 i > index 的意思是在 [index, i] 的区间从后往前找,然后一直找到 queue[i].id 小于(不可能等于) watcher.id 的位置,找到后将 Watcher 插入。

看完这块,我们回到flushSchedulerQueue函数,接着往下看下for循环的代码:

// src/core/observer/scheduler.js

// 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) {
    // ...
  }
}

这里的逻辑是如果 Watcher 定义了 before 就先执行 before ,接着将这个 Watcherhas 中对应的值置为null,然后调用 watcher.run 。我们来看 watcher.run 的定义:

// src/core/observer/watcher.js

export default class Watcher {
  // ...
  run() {
    if (this.active) {
      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;
        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.get() 方法拿到 value ,在上一节我们有介绍过 get 函数,它会手动调用数据的 getter 拿到属性值返回。接着如果满足以下任一条件,就执行回调 this.cb ,同时把新旧值都传入:

  • value(新值)和this.value(旧值)不相等
  • value(新值)是一个对象
  • Watcher 是一个 deep Watcher

回到刚刚的for循环,watcher.run() 执行完后,还有一段 if 判断:

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;
  }
}

这段逻辑是Vue针对死循环抛出的警告,并终止运行。

我们通过一个例子来说明这种情况:

// App.vue

<template>
  <div id="app">
    {{ msg }}
    <button @click="change">change</button>
  </div>
</template>

<script>
  export default {
    data() {
      return {
        msg: 1
      }
    },
    methods: {
      change() {
        this.msg++;
      }
    },
    watch: {
      msg() {
        this.msg++;
      }
    }
  }
</script>

在这个例子中,我通过 watch 属性来观察 msg ,并且在触发的回调又修改了 msg ,这样就造成了死循环,此时 Vue 会抛出一个警告:

这段警告也正是 if 逻辑中的那一段警告。这样 for 循环所有逻辑都分析完了,回到 flushSchedulerQueue 函数,在 for 循环执行完后,还剩下这一段:

// 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");
}

这里的 activatedChildrencallActivatedHooks 函数是和 keep-alive 相关的,所以会放在后面 keep-alive 的章节再来介绍。updatedQueue 则是拿到 queue 的一个副本,然后执行 callUpdatedHooks(updatedQueue) 。我们来看 callUpdatedHooks 的定义:

// src/core/observer/scheduler.js
function callUpdatedHooks(queue) {
  let i = queue.length;
  while (i--) {
    const watcher = queue[i];
    const vm = watcher.vm;
    if (vm._watcher === watcher && vm._isMounted && !vm._isDestroyed) {
      callHook(vm, "updated");
    }
  }
}

可以看到,callUpdatedHooks 会遍历 queue 中的 Watcher ,如果是一个渲染 Watcher 并且实例已经 mounted 且未 destroyed ,则调用生命周期钩子函数 updated

回到 flushSchedulerQueue 函数,这里还调用了 resetSchedulerState 函数:

// src/core/observer/scheduler.js
/**
 * Reset the scheduler's state.
 */
function resetSchedulerState() {
  index = queue.length = activatedChildren.length = 0;
  has = {};
  if (process.env.NODE_ENV !== "production") {
    circular = {};
  }
  waiting = flushing = false;
}

resetSchedulerState 函数其实就是把相关变量恢复到初始值,同时将队列清空。

这样派发更新的整个流程就基本分析完了。

总结

到这里,我们已经把数据变化是怎么引起页面重新渲染的的流程分析完了,总结一下大概就是:

  • 派发更新,实际上就是当数据发生变化的时候,会触发 setter ,然后遍历在收集依赖过程收集到的观察者 Watcher,然后触发它们的 update方法
  • Vue 并不是简单的每次触发 setter 就遍历 Watcher ,而是会先把 Watcher 推入到队列中,然后在下一个 tick 执行 flush 方法。

下一节我们来看下nextTick的实现,这部分也是面试常会被问到的。