乐趣区

关于vue.js:Vue3nextTick调度分析

nextTick

  • 定义:将回调推延到下一个 DOM 更新周期之后执行,在更改了一些数据以期待 DOM 更新后立刻应用它
  • 在理论中应用这个办法个别是用于组件更新,你须要获取更新后的数据,所以应用 nextTick 期待 DOM 更新

    import {createApp, nextTick} from 'vue'
    const app = createApp({setup() {const message = ref('Hello!')
        const changeMessage = async newMessage => {
          message.value = newMessage
          // 这里的 value 是旧值
          await nextTick()
          // nextTick 后获取的就是 DOM 更新后的 value
          console.log('Now DOM is updated')
        }
      }
    })
  • 这个 api 应用时相当简略,而且相当容易了解,然而为了知其意,还是要翻一下源码理解它的执行机制

    export function nextTick(
    this: ComponentPublicInstance | void,
    fn?: () => void): Promise<void> {
    const p = currentFlushPromise || resolvedPromise
    return fn ? p.then(this ? fn.bind(this) : fn) : p
    }
  • 下面是 vue 源码中 nextTick 的实现,为了搞清楚实现逻辑,就必须搞懂 currentFlushPromise 这个变量的含意,所以要从工作的调度机制开始剖析

任务调度

首先这个调度机制的性能在 runtime-corescheduler文件

  • API

    // 这个文件会抛出以下几个 API 函数
    nextTick(){} // 将函数在工作队列清空后执行
    queueJob(){} // 增加工作并开始执行工作队列
    invalidateJob(){} // 删除工作
    queuePreFlushCb(){} // 增加前置回调函数并开始执行工作队列
    queuePostFlushCb(){} // 增加后置回调函数并开始执行工作队列
    flushPreFlushCbs(){} // 执行前置回调函数
    flushPostFlushCbs(){} // 执行后置回调函数
  • 咱们首先要晓得几个要害变量

    let isFlushing = false // 是否正在清空工作队列
    let isFlushPending = false // 清队工作已创立,期待清空状态
    const queue: SchedulerJob[] = [] // 工作队列
    let flushIndex = 0 // 以后正在执行的工作在工作队列中的索引
  • 而后咱们从 queueJob 这个函数开始

    /* 
    这个函数次要是将一个工作进行入队操作
    而后在满足条件的状况下启动 queueFlush
     */
    export function queueJob(job: SchedulerJob) {
      /**
       * 工作可入队逻辑
       * 1. 工作队列为空
       * 2. 待入队工作不能存在于工作队列中(按状况剖析)
       */
      if (
        (!queue.length ||
          !queue.includes(
            job,
            /* 
              在正在清空队列且以后待入队工作是能够递归时,阐明当前任务肯定和以后正在执行工作是同一工作,所以 +1,就是为了保障待入队工作和正在执行工作雷同,但不能和前面待执行工作雷同
             */
            isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
          )) &&
        job !== currentPreFlushParentJob
      ) {
        // 二分查找工作在队列中的地位
        const pos = findInsertionIndex(job)
        if (pos > -1) {queue.splice(pos, 0, job)
        } else {queue.push(job)
        }
        queueFlush()}
    }
  • queueFlush

    function queueFlush() {
      /**
        清队工作创立后禁止再次创立更多的清队工作
        因为在入队操作实现后,flushJobs 会在一次递归中将工作队列全副清空,所以只须要一次清队工作即可
       */
      if (!isFlushing && !isFlushPending) {
        isFlushPending = true
        /* 
          清队工作创立胜利,并记录下以后清队工作,这个标记能够用于 nextTick 创立自定义函数,阐明 nextTick 的执行机会是在清队工作后的,其实从这个中央就能够了解 nextTick 的执行原理了
        */
        currentFlushPromise = resolvedPromise.then(flushJobs)
      }
    }
  • flushJobs

    // 清空工作队列
    function flushJobs(seen?: CountMap) {
      isFlushPending = false // 敞开清队工作期待状态
      isFlushing = true // 开启正在清空队列状态
      if (__DEV__) {seen = seen || new Map()
      }
    
      // 清空前置回调工作队列
      flushPreFlushCbs(seen)
    
      /* 
        工作队列中的工作依据 ID 进行排序的起因
          1. 因为组件更新是从父组件到子组件的,而工作更新是在数据源更新时触发的,所以为了更新工作的程序就须要进行排序
          2. 如果在父组件更新期间曾经卸载了组件,那么子组件的更新工作就能够跳过
      */
      queue.sort((a, b) => getId(a) - getId(b))
    
      try {
        // 遍历工作队列
        for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex]
          if (job && job.active !== false) {if (__DEV__ && checkRecursiveUpdates(seen!, job)) {continue}
            // 执行当前任务
            callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
          }
        }
      } finally {
        // 重置当前任务索引
        flushIndex = 0
        // 清空工作队列
        queue.length = 0
    
        // 执行后置回调工作队列
        flushPostFlushCbs(seen)
          // 重置清队工作的状态
        isFlushing = false
        currentFlushPromise = null
        /* 
          因为清队工作执行期间也会有工作入队,所以为了清队执行实现
          就须要判断各工作队列的长度,而后递归执行
        */
        if (
          queue.length ||
          pendingPreFlushCbs.length ||
          pendingPostFlushCbs.length
        ) {flushJobs(seen)
        }
      }
    }

总结

  • nextTick 的执行机会是在工作队列 (前置、主工作、后置) 革除后的,currentFlushPromise是清队工作的 promise 标记
  • 工作队列执行程序:执行前置回调工作队列 -> 执行主工作队列 -> 执行后置回调工作队列
退出移动版