vue3、vue2中nextTick源码解析
nexttick是啥
nextTick是Vue提供的一个全局API,由于Vue的异步更新策略导致我们对数据的修改不会更新,如果此时想要获取更新后的Dom,就需要使用这个方法.
vue的异步更新策略意思是如果数据变化,vue不会立刻更新dom,而是开启一个队列,把组件更新函数保存在队列里,在统一事件循环中发生的所有数据变更会异步的批量更新,这一策略导致我们对数据的修改不会立即体现在dom上,此时如果想要获取更新后的dom状态,就要使用nexttick
nextTick所指定的回调会在浏览器更新DOM完毕之后再执行。即在一次事件循环中,更新了数据,把更新Dom的操作放入队列中,使用了nextTick,则把nextTick里的回调放入队列中,执行完所有的同步代码后,去执行微任务,即依次调用队列里的函数。
函数签名:
nextTick(this: T, fn?: (this: T) => R): Promise>:
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)//0 await nextTick(); //dom已经更新 conosle.log(document.getElementById('counter').textContent)//1
我们先看,响应式数据count改变之后,会发生什么. (具体代码讲解放在代码注释里了,以下为vue3源码)
- 让与响应式数据相关连的函数去排队,调用queueJob()
源码位置:(太长了,只放一部分)
https://github.com/vuejs/core/blob/main/packages/runtime-core/src/renderer.ts
// 为组件创建一个响应式效果,以便在组件的依赖项发生变化时触发重新渲染,换句话说,就是一个响应式数据改变后, 与它相关联的函数用什么方式去执行 // create reactive effect for rendering const effect = (instance.effect = new ReactiveEffect( componentUpdateFn,//与响应式数据相关联的函数 NOOP,//空函数,,表示没有特定的调度逻辑 () => queueJob(update),//响应式数据改变后,不会立刻执行componentUpdateFn,而是让componentUpdateFn去排队,在未来某一时刻执行、 instance.scope, // track it in component's effect scope ))
- 排队函数具体内容
源码位置:
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 = false isFlushing = true if (__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) : NOOP try {//遍历作业队列。 for (flushIndex = 0; flushIndex
nextTick: 获取queueFlush中用到的resolvedPromise, 用then方法执行nexttick的回调函数,
export function nextTick( this: T,//确保回调函数fn在执行时具有正确的上下文 fn?: (this: T) => R, ): Promise { const p = currentFlushPromise || resolvedPromise return 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)//0 await 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 = false const callbacks: Array = [] let pending = false function flushCallbacks() { pending = false const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i = 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)) {//首先,判断是否原生支持Promise const 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.x MutationObserver.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 = 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 // 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 export function nextTick(this: T, cb: (this: T, ...args: any[]) => any): void export function nextTick(cb: (this: T, ...args: any[]) => any, ctx: T): void /** * @internal */ export function nextTick(cb?: (...args: any[]) => any, ctx?: object) { let _resolve callbacks.push(() => { if (cb) { try { cb.call(ctx) } catch (e: any) { handleError(e, ctx, 'nextTick') } } else if (_resolve) { _resolve(ctx) } }) if (!pending) { pending = true timerFunc() } // $flow-disable-line if (!cb && typeof Promise !== 'undefined') { return new Promise(resolve => { _resolve = resolve }) } }
- 让与响应式数据相关连的函数去排队,调用queueJob()
还没有评论,来说两句吧...