首页 前端 Vue.js 正文

浅曦Vue源码-42-patch 阶段-$nextTick & 异步队列更新


一、前情回顾 & 背景

上一篇小作文作为 patch 阶段的第一篇主要做了以下工作:

  1. 重新修改 test.html 加入了可以修改响应式数据的 button#btn 元素,以及绑定点击事件修改 data.forProp.a

  2. 重新梳理了完整的响应式流程,包含依赖收集、修改数据、派发更新的过程;并且明确了 WatcherDep以及响应式数据间的依赖和被依赖关系以及三者协作过程;

  3. 通过修改 this.forProp.a 进入到了 dep.notify(),接着看到了作为计算属性lazy watcher普通 watcherwatcher.update() 方法中的不同处理方式;

因为作为就算属性的 lazy watcher 要等到用到的时候才会求值,所以放到后面再说,本篇小作文的接着讲把要更新的 watcher 作为参数传递给 queueWatcher 方法后的事情;

二、queueWatcher

方法位置:src/core/observer/scheduler.js -> function queueWatcher

方法参数:watcher,待更新的 Watcher 实例

方法作用:将 watcher 推入 watcher 队列,id 相同的 watcher 将会被忽略,但是当队列正在被刷新时例外,具体如下:

  1. 获取 watcher.idwatcher.id 是一个自增的数字,数字越小标识这个 watcher 的创建的顺序越靠前

  2. 判重,如果不存在该 id 再处理,并且缓存该 watcher.id

  • 2.1 如果队列未处于正在刷新状态,即 flushing 不为 true,则将该 watcher 推入队列

  • 2.2 否则,从队列末尾向前遍历找到比当前 watcher.id 小的那个,把当前 watcher 插入id较小的那个后面;

  • 判断 waiting 标识符,第一次执行 queueWatcherwaitingfalse,但是执行过一次 queueWatcher 后就被置为 true 了。这么做确保本次事件循环中只会在下一个循环中添加一个 flushSchedulerQueue 任务;这也是常说的 Vue 会合并更新,然后在下个事件循环中全量更新。在当前循环中收集要更新的 watcher 放入队列,而不是立刻执行这个 watcher

  • 经过第一个 watcher.update 调用 queueWatcher 的三步骤后,全局变量 waiting 变为 false,如果 dep.notify 中还有 watcher 需要 update,那么仍然会调用 queueWatcher,那这个时候咋办呢?

    因为 dep.notifyfor 循环这种同步代码,连续调用 subs[i].update() ,对于 queueWatcher 来说,浏览器的下一个事件循环中已经有刷新队列的任务了 —— flushSchedulerQueue;只管向队列中添加 watcher 就好了,当下一个事件循环开始的时候就会消耗这个队列;

    export function queueWatcher (watcher: Watcher) {
      const id = watcher.id
    
      // 如果 watcher 已经存在,就不处理,保证不会重复进入队列
      if (has[id] == null) {
        // 缓存 watcher.id,用于判断 watcher 是否已经进入队列
        has[id] = true
        if (!flushing) {
          // flushing 标识当前队列是否正在被刷新
          // 当前没处在刷新队列状态, watcher 直接进入队列 queue
          queue.push(watcher)
        } else {
          // 如果已经在刷新队列正处于被刷新的状态,
          // 从 queue 末尾开始遍历,根据当前 watcher.id,找到id 比它小的 watcher 位置,
          // 然后将自己插入到这 小 id 的 watcher 的下一个位置
          // 即将当前 watcher 放入到已经排序的队列 queue 中
          // 至于为啥是队列,后面会解释的
          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 就是 Vue.nextTick 或者 this.$nextTick
          // 其主要作用有两点:
          // 1.就是把刷新 queue 队列的 flushSchdulerQueue 放入 callbacks 列表
          // 2. 通过 pending 控制浏览器中只有一个刷新 callbacks 的 flushCallbacks 任务
          nextTick(flushSchedulerQueue)
        }
      }
    }

    2.1 flushSchedulerQueue

    方法位置:src/core/observer/scheduler.js

    方法参数:无

    方法作用:

    1. 维护 flushingtrue

    2. queue 里面的 watcher 进行排序,排序的意义在于:

    • 2.1 确保组件更新顺序从父级到子级,因为父组件总是在子组件之前被创建

    • 2.2  一个组件的用户 watcher(你自己写在代码里面的 watch 叫做用户watcher)在渲染 watcher 之前被执行,因为用户 watcher 渲染 watcher 创建

    • 2.3 如果一个组件在其父组件watcher 执行期间被销毁,则它的 watcher 会被跳过

  • 遍历 queue,逐个调用 queue 中的每个 watcher.before(如有),然后调用 watcher.run 重新求值;

  • 调用 resetSchedulerState 重置 watingflushing 标识符;

  • 触发 activatedupdated 组件的生命周期钩子

    function flushSchedulerQueue () {
    currentFlushTimestamp = getNow()
    flushing = true
    let watcher, id

  • // 刷新队列之前先给队列排序(升序),可以保证: // 1. 组件更新顺序从父级到子级,因为父组件总是在子组件之前被创建 // 2. 一个组件的用户 watcher(你自己写在代码里面的 watcher 叫做用户watcher) //    在渲染 watcher 之前被执行,因为用户 watcher 先于渲染 watcher 创建 // 3. 如果一个组件在其父组件的 watcher 执行期间被销毁,则它的 watcher 会被跳过 //    排序以后再刷新队列期间新进来的 watcher 也会按顺序放入队列的合适位置

    queue.sort((a, b) => a.id - b.id)

    // 不要缓存 queue.length // 简介利用了 数组长度是个动态更新的值,这有啥好处呢? // 因为在执行当前 watcher 时, // 队列中可能会被 push 进来更多 watcher for (index = 0; index < queue.length; index++) { watcher = queue[index] // 执行 before 钩子,在使用 vm.$watch  // 或者 watch 选项时可以选配 options.before 传递 if (watcher.before) { watcher.before() } // 将缓存的 watcher 清除 id = watcher.id has[id] = null // 执行 watcher.run(),最终触发更新函数, // 比如渲染 watcher 的 updateComponent watcher.run() }

    // 在重置状态(flushing/wating)前复制保存激活的子列表 const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice()

    resetSchedulerState() // 这里会把 waiting 重置为 false

    // 调用组件的 updated 和 activated 钩子 callActivatedHooks(activatedQueue) callUpdatedHooks(updatedQueue) }

    #### 2.1.1 wathcer.before
    上面的 `flushSecheduleQueue` 中调用了 `watcher.before`,下面就是一个创建`渲染 watcher` 时传递的 `before` 选项;
    ```js
    export function mountComponent (): Component {
      let updateComponent
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
          vm._update(vm._render(), hydrating)
        }
      }
    
    
      // 这个玩意儿就是渲染 watcher
      new Watcher(vm, updateComponent, noop, {
        before () {
          // 这个 before 方法将会称为 watcher.before 
          // 在响应式更新后 watcher 被重新求值前调用
          if (vm._isMounted && !vm._isDestroyed) {
            callHook(vm, 'beforeUpdate')
          }
        }
      }, true)
    
    }

    2.1.2 Watcher.prototype.run

    这个方法就是被上面 flushSchedulerQueue 调用的 watcher.run,其主要作用就是调用创建 watcher 时传递的回调函数:

    1. 对于渲染 watcher 就是 updateComponent 方法;

    2. 对于用户 watcher 就是监听到值变化时要执行的回调函数,所谓用户 watcher 就是我们在 Vue 组件中传递的 watch 选项例如,{ watch: { someVal (newVal, oldVal) { ....} } }

    export default class Watcher {
      constructor () {
        this.before = options.before
    
        this.getter = expOrFn
    
        this.value = this.lazy
          ? undefined
          : this.get()
      }
    
      get () {
        pushTarget(this)
        let value
        const vm = this.vm
        try {
          // 执行回调函数 updateComponent,进入 patch 阶段
          value = this.getter.call(vm, vm)
        } catch (e) {
    
        } finally {
        }
        return value
      }
    
      update () {
        queueWatcher(this)
      }
    
      run () {
        if (this.active) {
          // 调用 this.get 方法对 watcher 重新求值
          const value = this.get()
          if (
            value !== this.value ||
            // deep wathcer 和 Object/Array 的 watcher 即便是同一个值也要触发重新计算
            // 因为有可能其中的key value 已经发生了变化
            isObject(value) ||
            this.deep
          ) {
            // set new value
            // 缓存旧值为之前的 value
            const oldValue = this.value
    
            // 更新 value 为最新求得的 value
            this.value = value
            if (this.user) {
              // 如果是用户 watcher,则执行用户传递的第三个参数——回调函数,
              // 参数为 val 和 oldVal
              const info = `callback for watcher "${this.expression}"`
              invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info)
            } else {
              // 更新一个渲染 watcher 时,
              // 也就是说这个 run 方法是由渲染 watcher.run 调用,
              // 其 cb 是调用了 updateComponent 方法
              this.cb.call(this.vm, value, oldValue)
            }
          }
        }
      }
    
    }

    2.2 nextTick

    方法位置:src/core/util/next-tick.js -> function nextTick

    方法参数:

    1. cb,下一个 tick 需要调用的回调函数,经过包装放到 callbacks 列表中;

    2. ctxcb 触发时指定的上下文对象

    方法作用:

    1. 包装 cb 函数,放入 callbacks 队列中,这队列将会由 flushCallbacks 消耗,在我们目前 patch 阶段中的 cbflushQueueWatcher 方法,这个方法被放到 callbacks 队列中,当触发时执行 watcher.run 方法对 watcher 重新求值;

    2. 维护 pending,前面说了 nextTick 需要保证浏览器在下个事件环的任务队列中只有 flushCallback;保证方法也很简单,第一次执行置标识符 pendingtrue,后面再执行的时候判断 pendingtrue 就不添加了。当 flushCallbacks 执行后再将 pending 置为 false 就可以了。

    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
      callbacks.push(() => {
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      if (!pending) {
        // 维护 pending 为 true,
        // 确保这个下个事件循环中只有一个 flushCallbacks
        pending = true
    
        // timerFunc 负责把 flushCallbacks 放入到下个事件循环中
        timerFunc()
      }
      // $flow-disable-line
      if (!cb && typeof Promise !== 'undefined') {
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }

    为啥叫 nextTick 呢? tick 是个事件循环的概念,表示的浏览器从收到通知后从任务队列中取出一个任务,然后执行它这个全套过程叫做一个 tick。所以 next tick 顾名思义,放到下一次 tick 执行;

    有很多人估计看到过一个经典面试题:说说 $nextTick 的原理。估计很多人都知道 $nextTick 中关于如何把回调函数放到下一个 tick 中的降级过程,优先使用 Promise.then,如果没有 Promise 则使用 MutationObserver,如果前两个都没有尝试 setImmediate,如果前面都没有就用 setTimeout

    那么这些逻辑都是在哪里处理的呢?没错 timerFunc 方法~

    2.1.1 timerFunc

    方法位置:src/core/util/next-tick.js -> let timerFunc

    方法参数:无

    方法作用:通过 js 的异步任务,将 flushCallbacks 放到下一个事件循环。在处理这个问题的时候是存在优先级的,优先使用微任务,实在不行再使用宏任务,优先级按顺序如下:

    1. 原生的 Promise.then 优先级最高,将 flushCallbacks 放到下一个事件循环开始前的微任务队列;

    2. 如果原生 Promise 不被支持,则降级到 MuatationObserver

    3. 前面两个微任务都不被支持,看下 setImmedaite 这个宏任务是否支持,若支持则使用;

    4. 最后用 setTimeout 作为兜底选项使用;

    let timerFunc
    
    // nextTick 行为充分利用微任务对队列,
    // 通过原生 Promise 或者 MutationObserver 实现
    // MutationObserver 虽然被广泛支持,
    // 但是在 ios>= 9.3.3 的UIWebView 仍然存在严重问题
    
    if (typeof Promise !== 'undefined' && isNative(Promise)) {
      const p = Promise.resolve()
      timerFunc = () => {
        // 在微任务队列中放入 flushCallbacks
        p.then(flushCallbacks)
    
        // 在有问题的 UIWebViews 中,Promise.then 不会完全退出,而是会陷入怪异状态,
        // 在这种状态下,回调被推入微任务队列,但是队列没有被刷新,
        // 直至浏览器需要执行其他工作时才会刷新,比如处理定时器,
        // 因此我们可以通过添加空的定时器来强制刷新微任务队列
        if (isIOS) setTimeout(noop)
      }
      isUsingMicroTask = true
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
      isNative(MutationObserver) ||
      // PhantomJS and iOS 7.x
      MutationObserver.toString() === '[object MutationObserverConstructor]'
    )) {
      // 在原生的 Promise 不可用的时候,MutationsObserver 次之
      // 比如 PhantomJS, ios 7, android 4.4
      // IE11 仍不可用
      let counter = 1
      const observer = new MutationObserver(flushCallbacks)
      const textNode = document.createTextNode(String(counter))
      observer.observe(textNode, {
        characterData: true
      })
      timerFunc = () => {
        counter = (counter + 1) % 2
        textNode.data = String(counter)
      }
      isUsingMicroTask = true
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
      // 再次之是 setImmediate 
      // 虽然是一个宏任务了,但仍比 setTimeout 要好
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    } else {
      // 最后用 setTimeout 兜底
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }

    2.2.2 flushCallbacks

    方法位置:src/core/util/next-tick.js

    方法参数:无

    方法作用:消耗 callbacks 队列,赋值 callback 中的函数,然后清空 callbacks 队列;

    function flushCallbacks () {
      pending = false
      const copies = callbacks.slice(0)
      callbacks.length = 0
      // 遍历 copies 数组,
      // 数组中存储的是 flushSchedulerQueue 包装函数
      for (let i = 0; i < copies.length; i++) {
        copies[i]()
      }
    }

    callbacks 存放就是上面 2.1flushSchedulerQueue 函数,这么说其实并不准确,它存放的是一个被包装过的函数,这个包装过程发生在 nextTick 中:

    export function nextTick (cb?: Function, ctx?: Object) {
      // ...
      // cb 就是 flushSchedulerQueue 方法
      callbacks.push(() => {
        // 就是这个包装函数,主要是处理 cb 执行时的错误
        if (cb) {
          try {
            cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
      // ....
    }

    三、总结

    本篇小作文讲述了 Vue 如何组织队列更新的,主要依托于下面几个方法:

    1. Watcher.prototype.update,当响应式数据发生变化,其对应的 dep.notify 执行,watcher.update 会调用 queueWatcher

    2. queueWatcher 负责把 watcher 实例加入到待求值的 watcher 队列 queue 中,添加到队列需要根据当前队列是否处于刷新状态做不同的处理;

    3. queueWatcher 还会调用 nextTick 方法,传入消耗 queue 队列的 flushSchedulerQueue 方法;

    4. nextTick 会把 flushSchedulerQueue 包装然后放到 callbacks 队列,nextTick 另一个重要任务就是把消耗 callbacks 队列的 flushCallback 放入到下一个事件循环(或者下一个事件循环的开头,即微任务);

    原文:https://juejin.cn/post/7101656752818487304
    打赏
    海报

    本文转载自互联网,旨在分享有价值的内容,文章如有侵权请联系删除,部分文章如未署名作者来源请联系我们及时备注,感谢您的支持。

    转载请注明本文地址:https://www.shouxicto.com/article/5497.html

    相关推荐

    Vue3 新特性

    Vue3 新特性

       一、前情回顾 & 背景    上一篇小作文作为 patch 阶段的第一篇主要做了以下工作:    ...

    Vue.js 2022.06.01 0 728

    50+Vue经典面试题源码级详解(24)

       Vue 3.0的设计目标是什么?做了哪些优化?    分析    还是问新特性,陈述典型新特性,分析其给你带来的变化即可。    思路    从以...

    Vue.js 2022.06.01 0 712

    发布评论

    ainiaobaibaibaibaobaobeishangbishibizuichiguachijingchongjingdahaqiandaliandangaodw_dogedw_erhadw_miaodw_tuzidw_xiongmaodw_zhutouganbeigeiliguiguolaiguzhanghahahahashoushihaixiuhanheixianhenghorse2huaixiaohuatonghuaxinhufenjiayoujiyankeaikeliankouzhaokukuloukunkuxiaolandelinileimuliwulxhainiolxhlikelxhqiuguanzhulxhtouxiaolxhwahahalxhzanningwennonuokpinganqianqiaoqinqinquantouruoshayanshengbingshiwangshuaishuijiaosikaostar0star2star3taikaixintanshoutianpingtouxiaotuwabiweifengweiquweiwuweixiaowenhaowoshouwuxiangjixianhuaxiaoerbuyuxiaokuxiaoxinxinxinxinsuixixixuyeyinxianyinyueyouhenghengyuebingyueliangyunzanzhajizhongguozanzhoumazhuakuangzuohenghengzuoyi
    支付宝
    微信
    赞助本站