Vue3nextTick调度分析

数字镂月人
• 阅读 1148

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标记
  • 任务队列执行顺序:执行前置回调任务队列 -> 执行主任务队列 -> 执行后置回调任务队列
点赞
收藏
评论区
推荐文章
Oracle 分组与拼接字符串同时使用
SELECTT.,ROWNUMIDFROM(SELECTT.EMPLID,T.NAME,T.BU,T.REALDEPART,T.FORMATDATE,SUM(T.S0)S0,MAX(UPDATETIME)CREATETIME,LISTAGG(TOCHAR(
Wesley13 Wesley13
4年前
MySQL部分从库上面因为大量的临时表tmp_table造成慢查询
背景描述Time:20190124T00:08:14.70572408:00User@Host:@Id:Schema:sentrymetaLast_errno:0Killed:0Query_time:0.315758Lock_
Alex799 Alex799
4年前
Vue面试题
1、Vue的生命周期?beforeCreate、created、beforeMount、mounted、beforeUpdate、updated、beforeDestroy、destroyed(创建、挂载、更新、卸载)挂载中可以操作DOM,创建中不能操作DOM;常用挂载或者创建生命周期就行了2、methods和computed的区别?
Easter79 Easter79
4年前
Vue diff 算法
一、虚拟DOM(virtualdom)  diff算法首先要明确一个概念就是diff的对象是虚拟DOM(virtualdom),更新真实DOM是diff算法的结果。  注:virtualdom 可以看作是一个使用JavaScript模拟了DOM结构的树形结构,这个树结构包含
九旬 九旬
4年前
nextTick原理解析
nextTick是什么\$nextTick:根据官方文档的解释,它可以在DOM更新完毕之后执行一个回调函数,并返回一个Promise(如果支持的话)js//修改数据vm.msg"Hello";//DOM还没有更新Vue.nextTick(function()//DOM更新了);这块理解EventLoop的应该一看就懂,其实就是
Wesley13 Wesley13
4年前
树莓派 安装mysql
1.    首先更新我们树莓派的软件sudoaptget update2.    等待更新完毕后安装mysql服务sudoaptgetinstallmysqlserver3.安装过程中需要输入两次mysql中root的登录密码          安装成功后使用以下命令登录mysql
Wesley13 Wesley13
4年前
mysql设置时区
mysql设置时区mysql\_query("SETtime\_zone'8:00'")ordie('时区设置失败,请联系管理员!');中国在东8区所以加8方法二:selectcount(user\_id)asdevice,CONVERT\_TZ(FROM\_UNIXTIME(reg\_time),'08:00','0
Stella981 Stella981
4年前
Linux日志安全分析技巧
0x00前言我正在整理一个项目,收集和汇总了一些应急响应案例(不断更新中)。GitHub地址:https://github.com/Bypass007/EmergencyResponseNotes本文主要介绍Linux日志分析的技巧,更多详细信息请访问Github地址,欢迎Star。0x01日志简介Lin
解析$nextTick魔力,为啥大家都爱它?
1.为什么需要使用$nextTick?首先我们来看看官方对于$nextTick的定义:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。由于vue的试图渲染是异步的,生命周期的created()钩子函数进行的DO
小万哥 小万哥
1年前
DOM(文档对象模型):理解网页结构与内容操作的关键技术
DOM(文档对象模型)定义了一种访问和操作文档的标准。它是一个平台和语言无关的接口,允许程序和脚本动态访问和更新文档的内容、结构和样式。HTMLDOM用于操作HTML文档,而XMLDOM用于操作XML文档。HTMLDOM示例通过ID获取并修改HTML元素的
解析$nextTick魔力,为啥大家都爱它?
作者:京东保险卓雅倩1.为什么需要使用\$nextTick?首先我们来看看官方对于\$nextTick的定义:在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。由于vue的试图渲染是异步的,生命周期的create