⼀次弄懂Vue2和Vue3的nextTick实现原理
都会⽤ nextTick,也都知道 nextTick 作⽤是在下次 DOM 更新循环结束之后,执⾏延迟回调,就可以拿到更新后的 DOM 相关信息
那么它到底是怎么实现的呢,在 Vue2 和 Vue3 中⼜有什么区别呢?本⽂将结合案例介绍执⾏原理再深⼊源码,全部注释,包你⼀看就会在进⼊ nextTick 实现原理之前先稍微回顾⼀下 JS 的执⾏机制,因为这与 nextTick 的实现息息相关
JS 执⾏机制
我们都知道 JS 是单线程的,⼀次只能⼲⼀件事,即同步,就是说所有的任务都需要排队,后⾯的任务需要等前⾯的任务执⾏完才能执⾏,如果前⾯的任务耗时过长,后⾯的任务就需要⼀直等,这是⾮常影响⽤户体验的,所以才出现了异步的概念
同步任务:指排队在主线程上依次执⾏的任务
异步任务:不进⼊主线程,⽽进⼊任务队列的任务,⼜分为宏任务和微任务
宏任务: 渲染事件、请求、script、setTimeout、setInterval、Node中的setImmediate 等
微任务: Promise.then、MutationObserver(监听DOM)、Node 中的 Tick等
当执⾏栈中的同步任务执⾏完后,就会去任务队列中拿⼀个宏任务放到执⾏栈中执⾏,执⾏完该宏任务中的所有微任务,再到任务队列中拿宏任务,即⼀个宏任务、所有微任务、渲染、⼀个宏任务、所有微任务、渲染…(不是所有微任务之后都会执⾏渲染),如此形成循环,即事件循环(EventLoop)
nextTick 就是创建⼀个异步任务,那么它⾃然要等到同步任务执⾏完成后才执⾏
我们先结合例⼦弄懂执⾏原理,再深⼊源码
Vue2
nextTick ⽤法
看例⼦,⽐如当 DOM 内容改变后,我们需要获取最新的⾼度
<template>
<div>{{ name }}</div>
</template>
<script>
export default{
data(){
return{
name:""
}
},
mounted(){
console.log(this.$el.clientHeight)// 0
this.name ="沐华"
console.log(this.$el.clientHeight)// 0
this.$nextTick(()=>{
console.log(this.$el.clientHeight)// 18
});
}
};
</script>
为什么在 nextTick ⾥就能拿到最新的 DOM 相关信息?是怎么拿到的,我们来分析⼀下原理
原理分析
在执⾏ this.name = '沐华' 的时候,就会触发 Watcher 更新,watcher 会把⾃⼰放到⼀个队列
⽤队列的原因是⽐如多个数据变更就更新视图多次的话,性能上就不好了,所以对视图更新做⼀个异步更新的队列,避免重复计算和不必要的DOM操作,在下⼀轮事件循环的时候刷新队列,并执⾏已去重的任务(nextTick的回调函数),更新视图
然后调⽤ nextTick(),响应式派发更新的源码在这⼀块是这样的,地址:src/core/observer/scheduler.js - 164⾏
export function queueWatcher(watcher: Watcher){
...
// 因为每次派发更新都会引起渲染,所以把所有 watcher 都放到 nextTick ⾥调⽤
nextTick(flushSchedulerQueue)
}
这⾥参数 flushSchedulerQueue ⽅法就会被放⼊事件循环,主线程任务的⾏完后就会执⾏这个函数,对 watcher 队列排序、遍历、执⾏watcher 对应的 run ⽅法,然后 render,更新视图
也就是说 this.name = '沐华' 的时候,任务队列可以简单理解成这样 [flushSchedulerQueue]
然后下⼀⾏ console.log(...),由于会更新视图的任务 flushSchedulerQueue 在任务队列⾥没有执⾏,所以⽆法拿到更新后的视图
然后执⾏到 this.$nextTick(fn) 的时候,添加⼀个异步任务,这时的任务队列可以简单理解成这样 [flushSchedulerQueue, fn]
然后同步任务就执⾏完了,接着按顺序执⾏任务队列⾥的任务,第⼀个任务执⾏就会更新视图,后⾯⾃然能得到更新后的视图了nextTick 源码剖析
源码版本:2.6.14,源码地址:src/core/util/next-tick.js
这⾥整个源码分为两部分,⼀是判断当前环境能使⽤的最合适的 API 并保存异步函数,⼆是调⽤异步函数 执⾏回调队列
环境判断
主要是判断⽤哪个宏任务或微任务,因为宏任务耗费的时间是⼤于微任务的,所以成先使⽤微任务,判断顺序如下
Promise
MutationObserver
setImmediate
setTimeout
export let isUsingMicroTask =false// 是否启⽤微任务开关
const callbacks =[]// 回调队列
let pending =false// 异步控制开关,标记是否正在执⾏回调函数
// 该⽅法负责执⾏队列中的全部回调
function flushCallbacks(){
// 重置异步开关
pending =false
// 防⽌nextTick⾥有nextTick出现的问题
// 所以执⾏之前先备份并清空回调队列
const copies = callbacks.slice(0)
callbacks.length =0
// 执⾏任务队列
for(let i =0; i < copies.length; i++){
copies[i]()
}
}
原生js和js的区别let timerFunc // ⽤来保存调⽤异步任务⽅法
// 判断当前环境是否⽀持原⽣ Promise
if(typeof Promise !=='undefined'&&isNative(Promise)){
// 保存⼀个异步任务
const p = solve()
timerFunc=()=>{
// 执⾏回调函数
p.then(flushCallbacks)
// ios 中可能会出现⼀个回调被推⼊微任务队列,但是队列没有刷新的情况// 所以⽤⼀个空的计时器来强制刷新任务队列
if(isIOS)setTimeout(noop)
}
isUsingMicroTask =true
}else if(!isIE &&typeof MutationObserver !=='undefined'&&(
isNative(MutationObserver)||
// 不⽀持 Promise 的话,在⽀持MutationObserver的⾮ IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
let counter =1
const observer =new MutationObserver(flushCallbacks)
const textNode = ateTextNode(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,虽然也是宏任务,但是⽐setTimeout更好timerFunc=()=>{
setImmediate(flushCallbacks)
}
}else{
// 以上都不⽀持的情况下,使⽤ setTimeout
timerFunc=()=>{
setTimeout(flushCallbacks,0)
}
}
环境判断结束就会得到⼀个延迟回调函数 timerFunc
然后进⼊核⼼的 nextTick
nextTick()
我们⽤ Tick() 或者 this.$nextTick() 都是调⽤ nextTick() 这个⽅法
这⾥代码不多,主要逻辑就是:
把传⼊的回调函数放进回调队列 callbacks
执⾏保存的异步任务 timeFunc,就会遍历 callbacks 执⾏相应的回调函数了
export function nextTick(cb?: Function, ctx?: Object){
let _resolve
// 把回调函数放⼊回调队列
callbacks.push(()=>{
if(cb){
try{
cb.call(ctx)
}catch(e){
handleError(e, ctx,'nextTick')
}
}else if(_resolve){
_resolve(ctx)
}
})
if(!pending){
// 如果异步开关是开的,就关上,表⽰正在执⾏回调函数,然后执⾏回调函数
pending =true
timerFunc()
}
// 如果没有提供回调,并且⽀持 Promise,就返回⼀个 Promise
if(!cb &&typeof Promise !=='undefined'){
return new Promise(resolve =>{
_resolve = resolve
})
}
}
可以看到最后有返回⼀个 Promise 是可以让我们在不传参的时候⽤的,如下
this.$nextTick().then(()=>{...})
Vue3
nextTick ⽤法
先看个例⼦,点击按钮更新 DOM 内容,并获取最新的 DOM 内容
<template>
<div ref="test">{{name}}</div>
<el-button @click="handleClick">按钮</el-button>
</template>
<script setup>
import{ ref, nextTick }from'vue'
const name =ref("沐华")
const test =ref(null)
async function handleClick(){
name.value ='掘⾦'
console.log(test.value.innerText)// 沐华
await nextTick()
console.log(test.value.innerText)// 掘⾦
}
return{ name, test, handleClick }
</script>
Vue3 ⾥这⼀块有⼤改,不过事件循环的原理还是⼀样,只是加了⼏个专门维护队列的⽅法,以及关联到 effect,不过好在这⾥源码的代码不多,所以不如直接看源码会更容易理解
nextTick 源码剖析
源码版本:3.2.11,源码地址:packages/runtime-core/src/sheduler.ts
const resolvedPromise: Promise<any>= solve()
let currentFlushPromise: Promise<void>|null=null
export function nextTick<T=void>(this:T, fn?:(this:T)=>void): Promise<void>{
const p = currentFlushPromise || resolvedPromise
return fn ? p.then(this?fn.bind(this): fn): p
}
就⼀个 Promise,没了
就这
好吧,认真点
可以看出 nextTick 接受⼀个函数为参数,同时会创建⼀个微任务
在我们页⾯调⽤ nextTick 的时候,会执⾏该函数,把我们的参数 fn 赋值给 p.then(fn),在队列的任务完成后,fn 就执⾏了
由于加了⼏个维护队列的⽅法,所以执⾏顺序是这样的:
queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn
现在不知道都是⼲嘛的不要紧,⼏分钟后你就会清楚了
我们按顺序来,先看⼀下⼊⼝函数 queueJob 是在哪⾥调⽤的,看代码
// packages/runtime-core/src/renderer.ts - 1555⾏
function baseCreateRenderer(){
const setupRenderEffect:SetupRenderEffectFn=(...)=>{
const effect =new ReactiveEffect(
componentUpdateFn,
()=>queueJob(instance.update),// 当作参数传⼊
instance.scope
)
}
}
在 ReactiveEffect 这边接收过来的形参就是 scheduler,最终被⽤到了下⾯这⾥,看过响应式源码的这⾥就熟悉了,就是派发更新的地⽅

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。