vue3、vue2中nextTick源码解析

2024-04-15 10:12

本文主要是介绍vue3、vue2中nextTick源码解析,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

nexttick是啥

nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致我们对数据的修改不会更新,如果此时想要获取更新后的Dom,就需要使用这个方法.
vue的异步更新策略意思是如果数据变化,vue不会立刻更新dom,而是开启一个队列,把组件更新函数保存在队列里,在统一事件循环中发生的所有数据变更会异步的批量更新,这一策略导致我们对数据的修改不会立即体现在dom上,此时如果想要获取更新后的dom状态,就要使用nexttick
nextTick所指定的回调会在浏览器更新DOM完毕之后再执行。即在一次事件循环中,更新了数据,把更新Dom的操作放入队列中,使用了nextTick,则把nextTick里的回调放入队列中,执行完所有的同步代码后,去执行微任务,即依次调用队列里的函数。

函数签名:

nextTick<T = void, R = void>(this: T, fn?: (this: T) => R): Promise<Awaited<R>>:

this:不是参数,是ts中的一个语法,给 this 定义类型。给用于绑定回调函数中的 this 上下文,可以省略。
fn:要异步执行的回调函数,是一个函数,可以省略。
函数返回一个 Promise,Promise 的泛型为 Awaited,表示回调函数执行后的返回值。

使用场景:

  • created中想要获取dom时
  • 响应式数据变化后获取dom更新后的状态,比如希望获取列表更新后的高度
vm.name = 'changed'
vm.$nextTick(()=>{ // 要在更新数据的后面使用console.log(app.innerHTML)
})

vue2、vue3中nexttick源码

简单来讲就是nexttick回调函数使用promise.then方式放入了异步,在所有dom都更新完成后才调用
先上一个小例子,(来自https://b23.tv/AmCLtgx),

async increment() {this.count++;//dom还未更新console.log(document.getElementById('counter').textContent)//0await nextTick();//dom已经更新conosle.log(document.getElementById('counter').textContent)//1

我们先看,响应式数据count改变之后,会发生什么. (具体代码讲解放在代码注释里了,以下为vue3源码)

  1. 让与响应式数据相关连的函数去排队,调用queueJob()
    源码位置:(太长了,只放一部分)
    https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts
// 为组件创建一个响应式效果,以便在组件的依赖项发生变化时触发重新渲染,换句话说,就是一个响应式数据改变后, 与它相关联的函数用什么方式去执行
// create reactive effect for renderingconst effect = (instance.effect = new ReactiveEffect(componentUpdateFn,//与响应式数据相关联的函数NOOP,//空函数,,表示没有特定的调度逻辑() => queueJob(update),//响应式数据改变后,不会立刻执行componentUpdateFn,而是让componentUpdateFn去排队,在未来某一时刻执行、instance.scope, // track it in component's effect scope))
  1. 排队函数具体内容
    源码位置:
    https://github.com/vuejs/core/blob/main/packages/runtime-core/src/scheduler.ts

queueJob方法: 把上一步中输入的参数update(也就是job)按特定规则推到queue任务队列里, 调用queueFlush()

export function queueJob(job: SchedulerJob) {// the dedupe search uses the startIndex argument of Array.includes()// by default the search index includes the current job that is being run// so it cannot recursively trigger itself again.// if the job is a watch() callback, the search will start with a +1 index to// allow it recursively trigger itself - it is the user's responsibility to// ensure it doesn't end up in an infinite loop.//先检查queue数组是否为空,或者job是否已经包含在queue数组中。这个检查确保相同的job不会被多次排队。if (!queue.length ||!queue.includes(job,isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex,)) {if (job.id == null) {//如果job无id属性,把job插入到queue数组末尾queue.push(job)} else {//如果job有id属性,就按照id把job插入到queue数组中特定位置queue.splice(findInsertionIndex(job.id), 0, job)}queueFlush()//刷新队列}
}

queueFlush方法: 把flushJobs(真正刷新队列的函数)放在promise.then里,等同步任务都完成后才真的刷新队列,再执行里面的更新函数

function queueFlush() {//刷新队列方法if (!isFlushing && !isFlushPending) {//当前没有正在进行的刷新操作,并且没有待处理(被挂起)的刷新操作isFlushPending = true//表示有一个刷新操作待处理(被挂起)currentFlushPromise = resolvedPromise.then(flushJobs)}//注意这里是promise,属于微任务,所以会在未来某一时刻异步的执行,因此,当flushjob这个方法真正执行时,其实其他所有的同步代码都已经执行完了
}

flushjob方法:按特定顺序(从父组件更新到子组件遍历并执行任务队列queue中的任务, 并处理执行过程中的任何错误。

function flushJobs(seen?: CountMap) {//循环遍历所有的组件更新函数,去更新所有的组件isFlushPending = falseisFlushing = trueif (__DEV__) {seen = seen || new Map()}// Sort queue before flush.// This ensures that:// 1. Components are updated from parent to child. (because parent is always//    created before the child so its render effect will have smaller//    priority number)// 2. If a component is unmounted during a parent component's update,//    its update can be skipped.queue.sort(comparator)//排序,确保组件从父组件更新到子组件,并允许跳过已卸载组件的更新。// conditional usage of checkRecursiveUpdate must be determined out of// try ... catch block since Rollup by default de-optimizes treeshaking// inside try-catch. This can leave all warning code unshaked. Although// they would get eventually shaken by a minifier like terser, some minifiers// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)const check = __DEV__? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job): NOOPtry {//遍历作业队列。for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {const job = queue[flushIndex]if (job && job.active !== false) {if (__DEV__ && check(job)) {continue}callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)}}} finally {flushIndex = 0queue.length = 0flushPostFlushCbs(seen)isFlushing = falsecurrentFlushPromise = null// some postFlushCb queued jobs!// keep flushing until it drains.if (queue.length || pendingPostFlushCbs.length) {//若queue仍然有作业,或有待处理的后续刷新回调,则递归调用flushjob,直到队列为空且所有回调都执行完毕flushJobs(seen)}}
}

nextTick: 获取queueFlush中用到的resolvedPromise, 用then方法执行nexttick的回调函数,

export function nextTick<T = void, R = void>(this: T,//确保回调函数fn在执行时具有正确的上下文fn?: (this: T) => R,
): Promise<Awaited<R>> {const p = currentFlushPromise || resolvedPromisereturn fn ? p.then(this ? fn.bind(this) : fn) : p
}//如果提供了回调函数 fn,则在 p 的 promise 对象上调用 then 方法。如果有可用的 this 上下文,则使用 bind 方法将 fn 函数绑定到 this 上下文。否则,直接使用 fn。
//如果未提供回调函数(fn 为假值),则直接返回 p 的 promise 对象。

回看一开始的例子,

async increment() {this.count++;//导致组件更新函数入队//dom还未更新console.log(document.getElementById('counter').textContent)//0await nextTick();//导致下面所有代码封装成一个匿名函数,并放到刚才的组件更新函数后面,//因此清空队列的时候,会先把所有的组件全清空后,才会执行nexttick后的延迟的语句或回调函数//dom已经更新conosle.log(document.getElementById('counter').textContent)//1

vue2中netxtick源码位置:
https://github.com/vuejs/vue/blob/main/src/core/util/next-tick.ts
里面用到了优雅降级,可以看看,不多写了

/* globals MutationObserver */import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'export let isUsingMicroTask = falseconst callbacks: Array<Function> = []
let pending = falsefunction flushCallbacks() {pending = falseconst copies = callbacks.slice(0)callbacks.length = 0for (let i = 0; i < copies.length; i++) {copies[i]()}
}// Here we have async deferring wrappers using microtasks.
// In 2.5 we used (macro) tasks (in combination with microtasks).
// However, it has subtle problems when state is changed right before repaint
// (e.g. #6813, out-in transitions).
// Also, using (macro) tasks in event handler would cause some weird behaviors
// that cannot be circumvented (e.g. #7109, #7153, #7546, #7834, #8109).
// So we now use microtasks everywhere, again.
// A major drawback of this tradeoff is that there are some scenarios
// where microtasks have too high a priority and fire in between supposedly
// sequential events (e.g. #4521, #6690, which have workarounds)
// or even between bubbling of the same event (#6566).
let timerFunc
//优雅降级
// The nextTick behavior leverages the microtask queue, which can be accessed
// via either native Promise.then or MutationObserver.
// MutationObserver has wider support, however it is seriously bugged in
// UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It
// completely stops working after triggering a few times... so, if native
// Promise is available, we will use it:
/* istanbul ignore next, $flow-disable-line */
if (typeof Promise !== 'undefined' && isNative(Promise)) {//首先,判断是否原生支持Promiseconst p = Promise.resolve()timerFunc = () => {p.then(flushCallbacks)// In problematic UIWebViews, Promise.then doesn't completely break, but// it can get stuck in a weird state where callbacks are pushed into the// microtask queue but the queue isn't being flushed, until the browser// needs to do some other work, e.g. handle a timer. Therefore we can// "force" the microtask queue to be flushed by adding an empty timer.if (isIOS) setTimeout(noop)}isUsingMicroTask = true
} else if (//其次 判断是否原生支持MutationObserver!isIE &&typeof MutationObserver !== 'undefined' &&(isNative(MutationObserver) ||// PhantomJS and iOS 7.xMutationObserver.toString() === '[object MutationObserverConstructor]')
) {// Use MutationObserver where native Promise is not available,// e.g. PhantomJS, iOS7, Android 4.4// (#6466 MutationObserver is unreliable in IE11)let counter = 1const observer = new MutationObserver(flushCallbacks)const textNode = document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc = () => {counter = (counter + 1) % 2textNode.data = String(counter)}isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {// 再次 判断是否原生支持setImmediate // Fallback to setImmediate.// Technically it leverages the (macro) task queue,// but it is still a better choice than setTimeout.timerFunc = () => {setImmediate(flushCallbacks)}
} else {// 最后 都不支持的情况下 则使用setTimeout来兜底// Fallback to setTimeout.timerFunc = () => {setTimeout(flushCallbacks, 0)}
}// 将回调函数cb包装成一个箭头函数push到事件队列callbacks中
export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/*** @internal*/
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {let _resolvecallbacks.push(() => {if (cb) {try {cb.call(ctx)} catch (e: any) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = truetimerFunc()}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
}

这篇关于vue3、vue2中nextTick源码解析的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/905529

相关文章

部署Vue项目到服务器后404错误的原因及解决方案

《部署Vue项目到服务器后404错误的原因及解决方案》文章介绍了Vue项目部署步骤以及404错误的解决方案,部署步骤包括构建项目、上传文件、配置Web服务器、重启Nginx和访问域名,404错误通常是... 目录一、vue项目部署步骤二、404错误原因及解决方案错误场景原因分析解决方案一、Vue项目部署步骤

前端原生js实现拖拽排课效果实例

《前端原生js实现拖拽排课效果实例》:本文主要介绍如何实现一个简单的课程表拖拽功能,通过HTML、CSS和JavaScript的配合,我们实现了课程项的拖拽、放置和显示功能,文中通过实例代码介绍的... 目录1. 效果展示2. 效果分析2.1 关键点2.2 实现方法3. 代码实现3.1 html部分3.2

CSS弹性布局常用设置方式

《CSS弹性布局常用设置方式》文章总结了CSS布局与样式的常用属性和技巧,包括视口单位、弹性盒子布局、浮动元素、背景和边框样式、文本和阴影效果、溢出隐藏、定位以及背景渐变等,通过这些技巧,可以实现复杂... 一、单位元素vm 1vm 为视口的1%vh 视口高的1%vmin 参照长边vmax 参照长边re

CSS3中使用flex和grid实现等高元素布局的示例代码

《CSS3中使用flex和grid实现等高元素布局的示例代码》:本文主要介绍了使用CSS3中的Flexbox和Grid布局实现等高元素布局的方法,通过简单的两列实现、每行放置3列以及全部代码的展示,展示了这两种布局方式的实现细节和效果,详细内容请阅读本文,希望能对你有所帮助... 过往的实现方法是使用浮动加

css渐变色背景|<gradient示例详解

《css渐变色背景|<gradient示例详解》CSS渐变是一种从一种颜色平滑过渡到另一种颜色的效果,可以作为元素的背景,它包括线性渐变、径向渐变和锥形渐变,本文介绍css渐变色背景|<gradien... 使用渐变色作为背景可以直接将渐China编程变色用作元素的背景,可以看做是一种特殊的背景图片。(是作为背

C语言中自动与强制转换全解析

《C语言中自动与强制转换全解析》在编写C程序时,类型转换是确保数据正确性和一致性的关键环节,无论是隐式转换还是显式转换,都各有特点和应用场景,本文将详细探讨C语言中的类型转换机制,帮助您更好地理解并在... 目录类型转换的重要性自动类型转换(隐式转换)强制类型转换(显式转换)常见错误与注意事项总结与建议类型

MySQL 缓存机制与架构解析(最新推荐)

《MySQL缓存机制与架构解析(最新推荐)》本文详细介绍了MySQL的缓存机制和整体架构,包括一级缓存(InnoDBBufferPool)和二级缓存(QueryCache),文章还探讨了SQL... 目录一、mysql缓存机制概述二、MySQL整体架构三、SQL查询执行全流程四、MySQL 8.0为何移除查

在Rust中要用Struct和Enum组织数据的原因解析

《在Rust中要用Struct和Enum组织数据的原因解析》在Rust中,Struct和Enum是组织数据的核心工具,Struct用于将相关字段封装为单一实体,便于管理和扩展,Enum用于明确定义所有... 目录为什么在Rust中要用Struct和Enum组织数据?一、使用struct组织数据:将相关字段绑

使用Java实现一个解析CURL脚本小工具

《使用Java实现一个解析CURL脚本小工具》文章介绍了如何使用Java实现一个解析CURL脚本的工具,该工具可以将CURL脚本中的Header解析为KVMap结构,获取URL路径、请求类型,解析UR... 目录使用示例实现原理具体实现CurlParserUtilCurlEntityICurlHandler

CSS自定义浏览器滚动条样式完整代码

《CSS自定义浏览器滚动条样式完整代码》:本文主要介绍了如何使用CSS自定义浏览器滚动条的样式,包括隐藏滚动条的角落、设置滚动条的基本样式、轨道样式和滑块样式,并提供了完整的CSS代码示例,通过这些技巧,你可以为你的网站添加个性化的滚动条样式,从而提升用户体验,详细内容请阅读本文,希望能对你有所帮助...