• nextTick源码解读


    📝个人主页爱吃炫迈
    💌系列专栏:Vue
    🧑‍💻座右铭:道阻且长,行则将至💗


    nextTick原理

    nextTick

    export let isUsingMicroTask = false // 标记 nextTick 最终是否以微任务执行
    /*存放异步执行的回调*/
    const callbacks = [] 
    /*一个标记位,如果已经有timerFunc被推送到任务队列中去则不需要重复推送*/
    let pending = false
    /*一个函数指针,指向函数将被推送到任务队列中,等到主线程任务执行完时,任务队列中的timerFunc被调用*/
    let timerFunc
    
    /*
      推送到队列中下一个tick时执行
      cb 回调函数
      ctx 上下文
    */
    export function nextTick (cb?: Function, ctx?: Object) {
      let _resolve
       // 第一步 传入的cb会被push进callbacks中存放起来
      callbacks.push(() => {
        if (cb) {      
            try {
                cb.call(ctx)
          } catch (e) {
            handleError(e, ctx, 'nextTick')
          }
        } else if (_resolve) {
          _resolve(ctx)
        }
      })
    
      // 第二步:判断用什么方法
      // 检查上一个异步任务队列(即名为callbacks的任务数组)是否派发和执行完毕了。pending此处相当于一个锁
      if (!pending) {
      // 若上一个异步任务队列已经执行完毕,则将pending设定为true(把锁锁上)
        pending = true
        // 调用判断Promise,MutationObserver,setTimeout的优先级
        timerFunc()
      }
    
      // 第三步:nextTick 函数会返回一个Promise对象。该Promise对象在异步任务执行完毕后会resolve,可以让用户在异步任务执行完毕后进行处理。
      if (!cb && typeof Promise !== 'undefined') {   
        return new Promise(resolve => {
          _resolve = resolve
        })
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44

    解释

    第二步pending 的作用就是一个锁,防止后续的 nextTick 重复执行 timerFunc(换句话说:当在同一轮事件循环中多次调用 nextTick 时 ,timerFunc 只会执行一次)。timerFunc 内部创建会一个微任务或宏任务,等待所有的 nextTick 同步执行完成后,再去执行 callbacks 内的回调。

    timerFunc

    💡 timerFunc函数,主要通过一些兼容判断来创建合适的 timerFunc,最优先肯定是微任务,其次再到宏任务。 优先级为 promise.then > MutationObserver > setImmediate > setTimeout

    // 判断当前环境是否原生支持 promise
    if (typeof Promise !== 'undefined' && isNative(Promise)) { // 支持 promise
      const p = Promise.resolve()
      timerFunc = () => {
        // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
        p.then(flushCallbacks)
        if (isIOS) setTimeout(noop)
      }
      // 标记当前 nextTick 使用的微任务
      isUsingMicroTask = true
    
      // 如果不支持 promise,就判断是否支持 MutationObserver
    } else if (!isIE && typeof MutationObserver !== 'undefined' && (
        isNative(MutationObserver) ||
        MutationObserver.toString() === '[object MutationObserverConstructor]'
      )) {
      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 // 标记当前 nextTick 使用的微任务
    
       // 判断当前环境是否原生支持 setImmediate
    } else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
    
      timerFunc = () => {
        setImmediate(flushCallbacks)
      }
    
         // 以上三种都不支持就选择 setTimeout
    } else {
      timerFunc = () => {
        setTimeout(flushCallbacks, 0)
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41

    我们发现无论那种timerFunc 最终都会执行flushCallbacks 函数

    flushCallbacks

    💡 flushCallbacks 里做的事情很简单,它就负责执行 callbacks 里的回调。

    // flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
    function flushCallbacks() {
      pending = false
      const copies = callbacks.slice(0) // 拷贝一份 callbacks
      callbacks.length = 0 // 清空 callbacks
      for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
        copies[i]()
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9

    image-20230929174246795

    异步更新流程

    Vue 使用异步更新,等待所有数据同步修改完成后,再去执行更新逻辑。

    update

    💡
    触发某个数据的setter方法后,它的setter函数会通知闭包中的Dep,Dep则会调用它管理的所有Watch对象。触发Watch对象的update实现。

    /*调度者接口,当依赖发生改变的时候进行回调 */
      update () {
        /* istanbul ignore else */
        if (this.lazy) {
          this.dirty = true
        } else if (this.sync) {
          /*同步则执行run直接渲染视图*/
          this.run()
        } else {
          /*异步推送到观察者队列中,下一个tick时调用。*/
          queueWatcher(this) // this为当前实例watcher
        }
      }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    queueWatcher

    💡 将一个观察者对象push进观察者队列,在队列中已经存在相同的id则该观察者对象将被跳过,除非它是在队列被刷新时推送

    export function queueWatcher (watcher: Watcher) {
      /*获取watcher的id*/
      const id = watcher.id
      /*检验id是否存在,已经存在则直接跳过,不存在则标记哈希表has,用于下次检验*/
      if (has[id] == null) {
        has[id] = true
    
        // 不是刷新
        if (!flushing) {
          queue.push(watcher)  // 将多个渲染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 >= 0 && queue[i].id > watcher.id) {
            i--
          }
          queue.splice(Math.max(i, index) + 1, 0, watcher)
        }
    
        // 是刷新
        if (!waiting) {
          waiting = true
          nextTick(flushSchedulerQueue) //这里会产生一个nextTick,队列刷新函数(flushSchedulerQueue)
        }
      }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27

    从queueWatcher代码中看出Watch对象并不是立即更新视图,而是被push进了一个队列queue,此时状态处于waiting的状态,这时候会继续会有Watch对象被push进这个队列queue,等到下一个tick运行时将这个队列queue全部拿出来run一遍,这些Watch对象才会被遍历取出,更新视图。同时,id重复的Watcher不会被多次加入到queue中去。这也解释了同一个watcher被多次触发,只会被推入到队列中一次。

    flushSchedulerQueue

    💡 flushSchedulerQueue 内将刚刚加入 queuewatcher 逐个 run 更新。

    function flushSchedulerQueue () {
      currentFlushTimestamp = getNow()
      flushing = true
      let watcher, id
    
      // 在刷新之前对队列进行排序。
      // 这确保了:
      // 1. 组件从父级更新到子级。(因为父母总是在子进程之前创建)
      // 2. 组件的用户观察程序在其渲染观察程序之前运行(因为用户观察者是在渲染观察者之前创建的)
      // 3. 如果组件在父组件的观察程序运行期间被销毁,可以跳过它的观察者。
      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()
      }
    
      // 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)
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34

    resetSchedulerState

    💡 resetSchedulerState 重置状态,等待下一轮的异步更新。

    function resetSchedulerState () {
      index = queue.length = activatedChildren.length = 0
      has = {}
      if (process.env.NODE_ENV !== 'production') {
        circular = {}
      }
      waiting = flushing = false
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    要注意此时 flushSchedulerQueue 还未执行,它只是作为回调传入而已。因为用户可能也会调用 nextTick 方法。这种情况下,callbacks 里的内容为 [“flushSchedulerQueue”, “用户的nextTick回调”],当所有同步任务执行完成,才开始执行 callbacks 里面的回调。

    由此可见,最先执行的是页面更新的逻辑,其次再到用户的 nextTick 回调执行。这也是为什么我们能在 nextTick 中获取到更新后DOM的原因。

    参考文章:

    Vue你不得不知道的异步更新机制和nextTick原理 - 掘金

    通俗易懂的Vue异步更新策略及 nextTick 原理 - 掘金

  • 相关阅读:
    线性PEG磷脂甲氧基-聚乙二醇-双肉豆蔻磷脂酰乙醇胺 mPEG-DMPE
    02-数组(Array)应用分析
    Linux升级gcc到最新版本gcc-11.2.0
    Java进阶(十一)缓冲流
    c语言11周(16~20)
    pdf怎么加密?
    2023年【施工升降机司机(建筑特殊工种)】最新解析及施工升降机司机(建筑特殊工种)考试资料
    JavaScript实现冒泡排序
    趣谈 Python 设计模式(1)观察者模式
    Java 进阶 01 —— 5 分钟回顾一下 Java 基础知识
  • 原文地址:https://blog.csdn.net/ZhangYu_010228/article/details/133419296