Vue2源码分析☞ 5 ☞ 异步更新机制

347 阅读7分钟

雨露同行风雨同舟.jpg

活着,最有意义的事情,就是不遗余力地提升自己的认知,拓展自己的认知边界。

引言

关于Vue中的数据更新,是通过watcher来完成的,那么:

  • 不同的数据更新顺序是什么样的?
  • 不同类型的watcher之间的执行顺序又是怎样的?
  • watcher的调度原理是怎样的?
  • Vue是如何基于Promise实现异步更新的? 读完下面的内容,希望能对你有所帮助!

测试案例

一个简单的案例

<!DOCTYPE html>
<html>

<head>
    <title>Vue源码剖析</title>
    <script src="../../dist/vue.js"></script>
</head>

<body>
    <div id="demo">
        <h1>行装</h1>
        <div>背心:{{vest}}</div>
        <div>裤子:{{trousers}}</div>
    </div>
    <script>
        // 创建实例
        const app = new Vue({
            el: '#demo',
            data: { 
              vest: 'broad',
              trousers: 'broad'
            },
            beforeUpdate(){
              console.log('keep in shape')
            },
            mounted() {
                setTimeout(() => {
                  this.vest = 'thin'
                }, 1000)
            }
        });
    </script>
</body>

</html>

宏任务与微任务

eventloop.png

事件循环(Event Loop):浏览器为了协调事件处理、脚本执行、网络请求和渲染等任务而制定的工作机制。

宏任务Task:代表一个个离散的、独立的工作单元。浏览器完成一个宏任务,在下一个宏任务执行开始前,会对页面进行重新渲染。主要包括创建文档对象、解析HTML、执行主线JS代码以及各种事件如页面重载、输入、网络事件和定时器等。

微任务:微任务是更小的任务,是在当前宏任务执行结束后立即执行的任务。如果存在微任务,浏览器会清空微任务之后再重新渲染。微任务的例子有Promise回调函数、DOM变化等。

源码解析

切入点

从上述简单案例中入手,在beforeUpdate钩子中打印日志,并在打印日志的地方打一个断点,刷新页面,下面看执行到断点处时的调用堆栈:

setTimeout.png 按照执行顺序依次为:

  • mounted: 页面完成初始渲染和挂载后,执行mounted钩子函数;

  • (匿名):用户传给setTimeout的匿名函数;

  • proxySetter:在匿名函数中,通过this访问了data选项中的属性,因此触发了代理修改器(在初始化过程中,对data选项中的属性进行了代理处理,详细过程可参考Vue2源码解析☞ 2 ☞ 初始化

  • reactiveGetter:在proxyGetter函数中,修改了vm._data中的属性,因此触发了reactiveGetter函数(关于data选项中属性定义数据劫持,可参考Vue2源码解析☞ 3 ☞ 响应式机制

  • notify:在reactiveGetter函数中,通过dep调用notify函数,在notify函数中,分别执行dep.subswatcher实例的update方法(其中,watcher可能是三种watcher(详细介绍可参考:Vue2源码解析☞ 4 ☞ 搞懂Vue中的三种Watcher),此处是render watcher

  • update:在此函数中,Vue提供了三种处理策略:

    • 1、watcher实例的lazy属性为true时,将dirty属性置为true,适用于computed watcher,既没有调用watcherrun方法,也没有放入异步更新队列(关于计算属性的值是何时更新的,computedGetter是如何被触发的,可参考:Vue2源码解析☞ 4 ☞ 搞懂Vue中的三种Watcher);
    • 2、watcher实例的sync属性为true时,会立即执行watcher实例的run方法;(只有watch watchersync属性适合设置为true,个人观点仅供参考);
    • 3、其余场景,执行queueWatcher函数
  • 至于queueWatchernextTicktimerFuncflushCallbacksflushSchedulerQueue等函数具体做了什么,下面将通过源码来分析;

  • before:此函数是创建render watcher实例时,以options参数形式传给Watcher构造器的,并挂载到watcher实例上的。(before函数本质上就是调用beforeUpdate钩子);

queueWatcher的逻辑

/**
 * 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) {//防止重复执行watcher更新
    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
      //略
      nextTick(flushSchedulerQueue)
    }
  }
}
  • 如果flushingfalse,则直接将watcher追加到queue中(此时并没有进行排序)。
  • 如果flushingtrue,则执行以下逻辑:
    • 获取watcher队列的最大索引,并赋值给i
    • index是全局变量,是watcher队列中当前正在处理的watcher实例的索引。在flushSchedulerQueue函数中,会根据watcherid属性进行升序排列,并依次处理每一个watcher,并实时更新index的值。
    • 如果i > index && queue[i].id > watcher.id,表明queue[i]还没有处理,且可以将当前的watcher插入到queue[i]前边。
    • 如果i > index && queue[i].id < watcher.id,表明虽然queue[i]还没有处理,但当前watcher的id属性比queue[i]的id属性值大,所以会排在第i+1项。
    • 如果i < index,表明queue[i]已经处理了,只能将当前watcher放在第i+1项。
  • 如果waitingfalse,则将flushSchedulerQueue传递给nextTick,在下一个时钟执行,waiting的值是通过resetSchedulerState函数来恢复。

那么,nextTick到底做了什么,继续往下看。

nextTick

export function nextTick (cb?: Function, ctx?: Object) {//注意:两个参数都是可选参数
  let _resolve
  callbacks.push(() => {//callbacks用来收集需要异步执行的代码,异步更新队列
    if (cb) {
      try {
        cb.call(ctx) //允许我们给回调函数绑定上下文,也就是this,通常情况下是vm,特殊情况下可以考虑
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx) //当没有传入回调函数时,
    }
  })
  if (!pending) {//执行flushCallbacks时,在开始执行异步队列中的代码前,将pending置为false
    pending = true
    timerFunc()
  }
  // $flow-disable-line
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

下面我们从实际的发生场景来解读上面的代码:

  • 场景一:既有回调函数,又有上下文 将回调函数封装到箭头函数代码块中,追加到异步更新队列中,并给回调函数绑定传入的上下文。 如果pendingfalse,则执行timerFunc

  • 场景二:只有回调函数,没有上下文 与场景一的唯一区别,就是没有修改回调函数的上下文。

  • 场景三:没有回调函数 此时,压入异步更新队列的本质上就是resolve函数。

例如:

this.$nextTick(null, obj).then((data) => {
    //略
)

其中,this.$nextTick()返回的是Promise实例,当执行flushCallbacks时,callbacks中某一项的resolve函数执行后,此时会触发then函数的回调,将obj赋值给data

timerFunc

let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  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)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  //略
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  //略
} else {
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
  • Vue采用的降级策略:Promise ——> MutationObserver ——> setImmediate ——> setTimeout。由于目前主流浏览器都已支持Promise API,所以着重研究Promise的场景即可。

  • 对于IOS平台,需要添加一个空的计时器,来强制清空微任务队列。

flushCallbacks

function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)//备份异步更新队列
  callbacks.length = 0 //清空异步更新队列
  for (let i = 0; i < copies.length; i++) {
    copies[i]() //执行异步回调
  }
}
  • 上述代码,保证在一个微任务中,将pending置为false,同时备份异步更新队列,并置空异步更新队列。

flushSchedulerQueue

queueWatcher函数中,将flushSchedulerQueue传给了nextTick函数,因此flushSchedulerQueue是异步更新队列的一个回调函数。

callbacks中,除了可能是flushSchedulerQueue外,还可能是用户编写的回调。

flushSchedulerQueue函数中,也会涉及到一个队列,不同的是调度队列中都是watcher实例,

Vue是如何调度这些watcher实例的呢?

继续看源码:

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)
  // 这里的user watcher其实就是我们所说的watch watcher,因为初始化watch选项是在initData阶段,
  // render watcher是在beforeCreate之后。
  // 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()  //这里的watcher可以是watch watcher和render watcher
    }
    id = watcher.id
    has[id] = null
    watcher.run()
  }

  // keep copies of post queues before resetting state
  // 在重置状态前,保存缓存组件队列的备份和watcher队列的备份
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue) //本质上是执行updated钩子
}
  • watcher队列进行排序,为了保证先创建的watcher先更新。
  • 依次执行watcher实例的run方法(对于watch watcher就是执行其handler,对于render watcher就是重新渲染挂载)。
  • 重置watcher调度相关的状态。
  • 执行缓存组件的相应钩子(缓存组件后续有专题介绍),执行updated钩子。

总结

1、watcher的执行顺序

Vue中,三种watcher中只有render watcherwatch watcher会异步执行,并且通过watcher实例的id值的大小来排序。

在一个Vue实例中,先执行initWatch,然后创建render watcher,也就是说在vue实例内部,watch watcher的执行顺序优先与render watcher

  • 对于有父子关系的两个vue实例,他们的钩子执行顺序如下:
  • 父组件的beforeCreate
  • 父组件的created
  • 父组件的beforeMount
    • 子组件的beforeCreate
    • 子组件的created
    • 子组件的beforeMount
    • 子组件的mounted
  • 父组件的mounted

那么这两个vue实例的render watcher的创建顺序是怎样的呢?

先看一个调用堆栈图:

comp.png

从上图可以知道,先创建vue实例的render watcher实例,然后执行组件的渲染挂载,如果存在子组件,则会执行createComponent,进一步执行子组件的初始化。

由此可知,父组件render watcher的执行顺序优先与子组件。

2、异步更新机制

data的属性值变化后,会触发代理修改器proxySetter,进而触发reactiveSetter函数,通过dep发送通知,执行subs中的所有watcher实例的update方法。

如果需要异步执行,则会执行queueWatcherwatcher实例被压入queue队列。

watcher的调度函数flushSchedulerQueue,会被当做回调函数压入callbacks队列中,通过Promise异步完成各回调函数的执行。

结束语

山水.png

千山万水何惧怕,拨开云雾见红霞

—— 祝君好梦 ——