新人看Vue2源码(三) - 异步更新队列与nextTick

416 阅读5分钟

前言

距离上一篇过去了一个多月,抱歉咕咕咕了这么久。上个月在公司疯狂加班加点赶版本进度(领导说这个很重要拍),在测试进度都快80%了,领导一句话先别上了,有个更重要的页面重构让我先去整,直接把我整麻了,合着我加班了一个月等于做了个寂寞。。然后又要去铲前人的屎💩山代码(其实就是我自己拉一坨好看点的💩山上去,顺便写点注释)。无论如何,最难熬的时刻快结束了,终于有时间可以写文章了!

Vue的批量、异步更新队列

大家以前或多或少都可能对 Vue 的更新,还有 nextTick 有些疑问吧,就比如最经典的通过 v-if 将某个组件从 false 改为 true,然后立刻获取焦点,你会发现写成同步的方式是行不通的,至于为什么可能也没太过去在意,所以我们就一起去了解下吧。

前置知识

大家都知道这涉及到了event loop的知识,我们稍微快速的过一下。

任务的话有两类,一个是同步任务,一个是异步任务,而异步任务又可以分为宏任务微任务。在每个宏任务执行之后会清空掉微任务队列,再执行下一个宏任务。

举个简单的例子

console.log("Start");
setTimeout(() => {console.log("setTimeout")}, 0);
new Promise((resolve)=>{
      console.log("Promise0")
      resolve();
    })
    .then(() => {console.log("Promise1")})
    .then(()=>{console.log("Promise2")})
console.log("End")

结果

Start
Promise0
End
Promise1
Promise2
setTimeout

分析

宏任务队列: [run script, setTimeout]
同步任务: [Start, Promise0, End]
微任务队列: [Promise1, Promise2]

调用顺序 [run script [Start, Promise0, End [Promise1, Promise2]],setTimeout]

调用过程(没兴趣的同学可直接看小总结)

回想一下,我们每个属性都做了数据拦截,每次 set 的时候都会去触发 Dep 然后告诉 Watcher 更新,那么我们只要追踪一下这个过程就知道大概的一个顺序。

src/core/observer/index.js

  • dep.notify();
export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  //  创建key和dep一一对应的关系
  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() {
      const value = getter ? getter.call(obj) : val;
      ...
      return value;
    },
    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();
    },
  });
}

src/core/observer/dep.js

  • subs[i].update() => Watcher.update()
  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)
    }
    
    //  循环管理内部的watcher实例,执行他们的update方法
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }

src/core/observer/watcher.js

  • queueWatcher()
 update() {
    /* istanbul ignore else */

    //  lazy指computed
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      //  sync 是 watcher 的时候可以传入配置{sync: true}
      this.run();
    } else {
      //  正常情况下是这步
      //  watcher入队
      queueWatcher(this);
    }
  }

src/core/observer/scheduler.js

  • nextTick()
  • flushSchedulerQueue
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.
      
      // 翻译: 如果已经在冲刷,会根据这个watcher的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

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }

      //  异步启动任务队列冲刷任务
      //  此处的nextTick就是我们用的那个
      //  启动一个异步任务,在未来的某个时刻执行flushSchedulerQueue
      nextTick(flushSchedulerQueue)
    }
  }
}

src/core/util/next-tick.js

  • nextTick()
  • timerFunc()
  • flushCallbacks() // 作用: 遍历执行[flushSchedulerQueue]
const callbacks = []
let pending = false

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  //  这里的callbacks存放的是下面包装过的flushSchedulerQueue
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}

//  声明
let timerFunc

//  一堆的降级处理,
//  可以简单的理解为有Promise用Promise, 没有就用MutationObserver,setImmediate,setTimeout
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else {...}

export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve

  //  封装一个能够处理错误的高阶函数
  //  并将它存入callbacks的数组中
  callbacks.push(() => {
    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(resolve => {
      _resolve = resolve
    })
  }
}

src/core/observer/scheduler.js

  • flushSchedulerQueue()
  • watcher.run()
//  遍历执行所有的watchers,执行他们的run函数
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();
    ...
  }
    ...
}

src/core/observer/watcher.js

  • this.get()
  • this.getter.call(vm, vm) -> 调用mountComponent
  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) {
          const info = `callback for watcher "${this.expression}"`;
          invokeWithErrorHandling(
            this.cb,
            this.vm,
            [value, oldValue],
            this.vm,
            info
          );
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  }
  
  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  get() {
    pushTarget(this);
    let value;
    const 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 {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value;
  }
  
  
  constructor(
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    ...
    // parse expression for getter
    //  回想之前的$mount -> mountComponent -> updateComponent  + new Watcher(vm, updateComponent)
    //  如果参数2是函数,表示它是组件的更新函数
    if (typeof expOrFn === "function") {
      this.getter = expOrFn;
    } else {
      ...
    }
    this.value = this.lazy ? undefined : this.get();
  }

src/core/instance/lifecycle.js

  • updateComponent()
  • render()
  • update(vnode)
  • patch(oldVnode, vnode)
export function mountComponent(
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el;
  if (!vm.$options.render) {
    ...
  }
  callHook(vm, "beforeMount");

  //  组件更新函数声明
  let updateComponent;
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== "production" && config.performance && mark) {
    ...
  } else {
    updateComponent = () => {
      //  首先执行render -> vdom
      //  然后_update将vdom转为dom
      vm._update(vm._render(), hydrating);
    };
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  
  //  重点,updateComponent 就是 watcher里的第二个参数 expOrFn
  new Watcher(
    vm,
    updateComponent,
    noop,
    {
      before() {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, "beforeUpdate");
        }
      },
    },
    true /* isRenderWatcher */
  );
  hydrating = false;

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true;
    callHook(vm, "mounted");
  }
  return vm;
}

小总结

  • obj.set()
  • dep.notify() // 遍历调用watcher的update
  • watcher.update()
  • queueWatcher(this) // watcher入队 queue: Watcher[],并且会对watcher进行去重
  • nextTick(flushSchedulerQueue)
  • timerFunc() // callbacks: flushSchedulerQueue[] ---async---
  • flushCallbacks() // 遍历调用callbacks的flushSchedulerQueue
  • flushSchedulerQueue() // 遍历调用queue的watcher.run
  • watcher.run() // 真正的更新函数
  • watcher.getter() -> updateComponent
  • render()
  • update(vnode)
  • patch(oldvnode, vnode)

关于nextTick

下面通过几个例子来解释一下 nextTick 的一些原理,以及 和 Promise 的一些区别。

思考1: 为什么在nextTick才能拿到dom的最新值

    <div id="app">
      <h2>初始化</h2>
      <div>
        <p class="ppp">count --- {{count}}</p>
      </div>
    </div>
const app = new Vue({
      el: "#app",
      data: {
        count: "asd",
      },
      mounted() {
        let el = document.querySelector(".ppp");
        console.log("start0", el.textContent, this.count);
        this.count = 1;
        console.log("start1", el.textContent, this.count);
        this.count = 2;
        console.log("start2", el.textContent, this.count);
        this.$nextTick(() => {
          console.log("next--", el.textContent, this.count);
        });
      },
    });

大家不妨先猜一下结果,再来康康对不对。

image.png

可以看到,我们每次修改完count之后直接去访问el的真实值会发现没有变化,而是需要在nextTick中才能拿到真实值。到这里大家可能会觉得nextTick是异步的嘛,肯定在这些set函数之后执行,如果你有这样的一个想法,那么请你再看下面的例子再下判断。

思考2:如果nextTick放到前面有用吗

const app = new Vue({
      el: "#app",
      data: {
        count: "asd",
      },
      mounted() {
        let el = document.querySelector(".ppp");

        this.$nextTick(() => {
          console.log("next1--", el.textContent, this.count);
        });

        console.log("start0", el.textContent, this.count);
        this.count = 1;
        console.log("start1", el.textContent, this.count);
        this.$nextTick(() => {
          console.log("next2--", el.textContent, this.count);
        });
        this.count = 2;
        console.log("start2", el.textContent, this.count);
      },
    });

image.png

注意到了吗,next1打印的el.textContent的值没有变化,穿插在start1和start2之间的next2能拿到最新值。我们结合前面的异步更新过程来看下这里有什么秘密。

image.png

解释起来很简单,我们手动调用的nextTick会往callbacks里塞进一个回调函数,我们的this.count = 1也会往里面塞入一个回调函数。那么我们只要在flushSchedulerQueue后面的回调函数中访问,就能获得dom的最新修改值。

思考3:Promise和nextTick哪个先执行

大家想一想,Promise和nextTick都是微任务,那么按照event loops来理解的话,是不是谁先写在前面谁就先执行?我们一起来验证下。

    const app = new Vue({
      el: "#app",
      data: {
        count: "asd",
      },
      mounted() {
        let el = document.querySelector(".ppp");

        console.log("start0", el.textContent, this.count);
        this.count = 1;
        console.log("start1", el.textContent, this.count);

        this.count = 2;
        console.log("start2", el.textContent, this.count);

        Promise.resolve().then(() => {
          console.log("Promise--", el.textContent, this.count);
        });

        this.$nextTick(() => {
          console.log("next--", el.textContent, this.count);
        });
      },
    });

image.png

嗯?是不是跟你想的有点不一样。但是如果你看了上面的调用过程,有一个特别重要的东西如果你留意到了就知道为什么会这样。


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

let timerFunc;

const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    // In problematic UIWebViews, Promise.then doesn't completely break, but
    // it can get stuck in a weird state where callbacks are pushed into the
    // microtask queue but the queue isn't being flushed, until the browser
    // needs to do some other work, e.g. handle a timer. Therefore we can
    // "force" the microtask queue to be flushed by adding an empty timer.
    if (isIOS) setTimeout(noop)
  }

再看一下小总结的图

image.png

这下清楚了吧,Promise 从设计上来讲优先级没有nextTick高,所以它不会nextTick先执行。

小总结

nextTick的理解

  • 概念: vue批量异步更新策略的实际执行者,组件更新的时候他不会立即执行,而是通过nextTick异步起动
  • 作用:nextTick(cb),当数据变化需要访问dom最新的值时候
  • 如何工作:数据变化的时候,会触发dep.notify,让watcher入队,然后nextTick会异步的冲刷队列,最后调用watcher.run()更新dom

总结

  • 异步:只要监听到数据变化,Vue 就会开启一个队列,并缓冲在同一个事件循环中发生的所有数据变更。
  • 批量:如果同一个 watcher 被多次触发,只会被推入到队列中一次。去重对于避免不必要的计算和dom操作是十分必要的。然后在下一个事件循环的 tick 中,Vue 会刷新队列实际工作。
  • 异步策略:Vue 在内部对异步队列会尝试使用 Promise.then 进行异步处理,如果浏览器不支持就会尝试 MutationObserversetImmediatesetTimeout进行异步操作。