vue的异步更新

195 阅读2分钟

入口

在实现响应式时,我们给每个属性重写的set方法里面的dep.notify()来遍历每个watcher实现节点更新。而入口就是在notify方法。

notify

notify方法主要为遍历当前属性的dep收集的所有wacther,然后调用每个watcher的update方法。

notify () {
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update() //通知所有依赖来调用自己的update方法
    }
  }
}

watcher的update方法

update方法主要执行updateWater(),this为watcher实例,sync以及lazy都为该watcher实例的属性。update方面有三个分支

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
  • 懒执行:例如computed就会进行该分支,该分支主要为将dirty设置为true,在组件更新之后,当响应式数据再次被更新时,执行computed的getter,重新执行computed回调函数,计算新值,然后缓存到watcher.value
  • 同步执行(this.$watch()或者watch选项):传递一个sync配置,比如{sync:true}
  • 将当前watcher放入watcher队列,一般走这个分支

queueWatcher

queueWatcher()主要做了把当前watcher根据id大小然后找到一个watcher的id比当前watcher的id小的插入,然后把watcher实例添加到queue数组中,然后调用nextTick()把flushSchedulerQueue()方法添加进去。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) { // watcher只有第一次加入队列,后面会被忽略
    has[id] = true//缓存一下,表示已经入队
    if (!flushing) {  // flushing为false说明当前watcher队列没有再被刷新,watcher直接进队
      queue.push(watcher)
    } else {//flushing为true说明队列已经被刷新了,这时候watcher需要进行排序操作,从而保证新入队的watcher后,刷新队列依然有序
      // 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
      // 跳过执行过的watcher和所有比我大的
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      // 最后放在队列中,一个比我老的 watcher 后面
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {  //为false,表示当前浏览器的异步队列任务中没有flushSchedulerQueue函数的时候
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        //如果当前为开发环境,配置传入了async值 为true的时候
        //直接进行同步执行,直接去刷新wathcer队列,性能大打折扣
        flushSchedulerQueue()
        return
      }
      //nextTick,this.$nextTick调用的方法
      nextTick(flushSchedulerQueue) // 注册宏微任务
    }
  }
}
  • nextTick nextTick的原理:try/catch包裹方法添加进callbacks数组->timerFun(使用微任务),执行flushCallbacks->flushCallbacks为遍历callback数组执行每个方法
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  //callbacks为一个全局数组,把我们传入的nextTick的回调函数做了一层try/catch包装,然后将包装后的函数放在callbacks数组中,所以callbacks数组存储就是我们传入的nextTick的回调函数
  callbacks.push(() => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  if (!pending) {//锁,防止重复执行timerFunc
    pending = true
    timerFunc()//把flushCllbacks放在new Promise.resolve().then(flushCllbacks)中
  }
  // $flow-disable-line 
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve  
    })
  }
}
  • flushCallbacks函数是:
    • 将pending再次设置为false,表示下一个flushCallbacks函数可以进入浏览器的微任务队列
    • 清空callbacks数组
    • 执行callbacks数组中的所有函数
      • flushSchedulerQueue
      • 用户自己调用this.$nextTick传递的回调函数
function flushCallbacks () {
  pending = false//确保同一时刻在微任务任务队列中只有一个flushCallbacks
  const copies = callbacks.slice(0)  //将callbacks中的数据复制给copies,并将callbacks清空
  callbacks.length = 0 
  for (let i = 0; i < copies.length; i++) {  // 执行回调函数
    copies[i]()
  }
}
function flushSchedulerQueue () {
  currentFlushTimestamp = getNow()
  flushing = true//flushing为true表示watcher队列正在被刷新,不会进行新watcher添加
  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.

  // watcher.id 越大,表示这个 watcher 越年轻,实例是越后面生成的
  // 本着先更新父组件再更新子组件的原则
  // 当 父组件传给子组件的数据变化的时候,父组件需要把 变化后的数据 传给 子组件,子组件才能知道数据变了那么 子组件才能更新组件内使用 props 的地方所以,父组件必须先更新,把最新数据传给 子组件,子组件再更新,此时才能获取最新的数据不然你子组件更新了,父组件再传数据过来,那就不会子组件就不会显示最新的数据了
  // 一个组件在父组件更新期间被销毁了,它及它的子组件将会被跳过 
 queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  //循环遍历wathcer队列,一次执行watcher的run方法
  for (index = 0; index < queue.length; index++) {
    //拿出当前索引的watcher
    watcher = queue[index]
    //首先执行before钩子----渲染的时候会有一个beforeUpate钩子
    if (watcher.before) {
      watcher.before()
    }
    //清空缓存,表示当前watcher已经被执行,当该watcher再次入队时就可以进来了
    id = watcher.id
    has[id] = null
    //执行watcher的run方法
    watcher.run()//执行了updateComponent方法,该方法主要为把对应的虚拟节点机型patch操作从而更新dom
    // 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()
  // reset state
  resetSchedulerState()

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

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}
  • 为什么需要排队呢?要先更新父组件再更新子组件呢? 因为父组件会给子组件通过props传递一些数据,而这些props也是一个watcher,而在flushSchedulerQueue该调度队列方法里面,会对watcher的run方法调用从而更新节点。因此先更新那个watcher的数据要有先后顺序,否则子组件先父组件更新,拿到的props的数据就会是旧的数据,从而不能达到更新的目的。
  • run方法
    • 执行实例化watcher传递的第二个参数,updateComponnet或者获取this.xx的一个函数(parsepath返回的函数)

    • 更新旧值为新值

    • 执行实例化watcher时传递的第三个参数,比如用户watcher的回调函数

更新过程图示

image.png

总结

  • 异步更新: Vue的异步更新机制的核心是利用了浏览器的异步任务队列实现的,首选微任务队列,宏任务队列次之。
    当响应式数据更新之后,会调用dep.notify方法,通知dep中收集的watcher去执行update方法。
    然后通过nextTick方法将一个刷新watcher队列的方法(flushScheduleQueue)放入一个全局的callbacks数组中。
    如果此时浏览器的异步队列中没有一个叫flushCallbacks的函数,则执行timeFunc函数,将flushCallbacks函数放入异步任务队列中。如果异步任务队列中已经存在flushCallbacks函数,等待其执行完成后再放入下一个flushCallbacks函数。
    flushCallbacks函数负责执行callbacks数组中的所有flushSchedulerQueue函数。
    flushSchedulerQueue函数负责刷新watcher队列,即执行queue数组每一个watcher的run方法,从而进行更新阶段,比如执行组件更新函数或者执行用户watch的回调函数。

  • nexxTick如何实现 Vue.nextTick实现原理:

  • 将 传递的回调函数用try/catch包裹放入callbacks数组

  • 执行timeFunc函数,在浏览器的异步任务队列放入一个刷新callbacks数组的函数flushCallbacks(执行存储的为用户使用nextTick回调的callbacks数组中的每个函数)。因为flushCallbacks在执行callbacks数组的时候,callbacks数组第一个方法就是flushSchedulerQueue,而flushSchedulerQueue这个调度器主要是执行每个watcher进行大小排序,本着先更新父组件后更新子组件的原则,然后执行排序好的watcher的run方法从而更新节点。执行完这个flushSchedulerQueue方法之后,节点已经实现更新了。然后再执行callbacks后的方法,这些方法就是我们自己使用nextTick添加的方法,因此我们咋nextTick添加的方法,因此我们咋nextTick可以获取更新后的节点。 例如:flushSchedulerQueue方法主要为排序watcher数组,并且执行watcher的run方法,进行调用pacth方法来更新真实dom。

mounted:{
  this.counter = 1
  this.counter = 2
  this.counter = 3
  this.nextTick(() => {
    console.log('111')
  }) 
}
//callbacks数组第一个就是flushSchedulerQueue
//后面跟的就是用户nextTick的回调方法
此时callbcks的数组值为:[flushSchedulerQueue, () => {
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  }]

nextTick更多理解
因为vue是异步更新DOM的,一旦观察到数据变化,Vue会开启一个队列,然后把同一个事件循环当中观察到的数据变化的watcher推送到这个队列中。如果这个watcher被触发多次,只会被推送到队列一次。这种缓冲罐行为可以有效的去掉重复数据造成的不必要的计算和DOM操作。而在下一个事件循环时,Vue会清空队列,并进行必要的DOM更新。
宏任务:setTimeout ,setInterval, setImmediate,requestAnimationFrame, I/O ,UI渲染
微任务:Promise, process.nextTick, Object.observe, MutationObserver

参考