一次弄懂Vue2和Vue3的nextTick实现原理

更新时间:2023-07-17 14:15:07 阅读: 评论:0

⼀次弄懂Vue2和Vue3的nextTick实现原理
都会⽤ nextTick,也都知道 nextTick 作⽤是在下次 DOM 更新循环结束之后,执⾏延迟回调,就可以拿到更新后的 DOM 相关信息
那么它到底是怎么实现的呢,在 Vue2 和 Vue3 中⼜有什么区别呢?本⽂将结合案例介绍执⾏原理再深⼊源码,全部注释,包你⼀看就会在进⼊ nextTick 实现原理之前先稍微回顾⼀下 JS 的执⾏机制,因为这与 nextTick 的实现息息相关
JS 执⾏机制
我们都知道 JS 是单线程的,⼀次只能⼲⼀件事,即同步,就是说所有的任务都需要排队,后⾯的任务需要等前⾯的任务执⾏完才能执⾏,如果前⾯的任务耗时过长,后⾯的任务就需要⼀直等,这是⾮常影响⽤户体验的,所以才出现了异步的概念
同步任务:指排队在主线程上依次执⾏的任务
异步任务:不进⼊主线程,⽽进⼊任务队列的任务,⼜分为宏任务和微任务
宏任务: 渲染事件、请求、script、tTimeout、tInterval、Node中的tImmediate 等
微任务: Promi.then、MutationObrver(监听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/obrver/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 并保存异步函数,⼆是调⽤异步函数 执⾏回调队列
环境判断
主要是判断⽤哪个宏任务或微任务,因为宏任务耗费的时间是⼤于微任务的,所以成先使⽤微任务,判断顺序如下
Promi
MutationObrver
tImmediate
tTimeout
export let isUsingMicroTask =fal// 是否启⽤微任务开关
const callbacks =[]// 回调队列
let pending =fal// 异步控制开关,标记是否正在执⾏回调函数
// 该⽅法负责执⾏队列中的全部回调
function flushCallbacks(){最好整容医院
// 重置异步开关
pending =fal
// 防⽌nextTick⾥有nextTick出现的问题
// 所以执⾏之前先备份并清空回调队列
const copies = callbacks.slice(0)
callbacks.length =0
// 执⾏任务队列
for(let i =0; i < copies.length; i++){
copies[i]()
}
}
let timerFunc // ⽤来保存调⽤异步任务⽅法
// 判断当前环境是否⽀持原⽣ Promi
if(typeof Promi !=='undefined'&&isNative(Promi)){
// 保存⼀个异步任务
const p = solve()
timerFunc=()=>{
// 执⾏回调函数
p.then(flushCallbacks)
// ios 中可能会出现⼀个回调被推⼊微任务队列,但是队列没有刷新的情况// 所以⽤⼀个空的计时器来强制刷新任务队列
if(isIOS)tTimeout(noop)
}
isUsingMicroTask =true
}el if(!isIE &&typeof MutationObrver !=='undefined'&&(
isNative(MutationObrver)||
// 不⽀持 Promi 的话,在⽀持MutationObrver的⾮ IE 环境下
// 如 PhantomJS, iOS7, Android 4.4
let counter =1
const obrver =new MutationObrver(flushCallbacks)
const textNode = ateTextNode(String(counter))
obrver.obrve(textNode,{
characterData:true
})
timerFunc=()=>{
counter =(counter +1)%2
textNode.data =String(counter)
}
isUsingMicroTask =true
}el if(typeof tImmediate !=='undefined'&&isNative(tImmediate)){ // 使⽤tImmediate,虽然也是宏任务,但是⽐tTimeout更好timerFunc=()=>{
tImmediate(flushCallbacks)
}
}el{
// 以上都不⽀持的情况下,使⽤ tTimeout
timerFunc=()=>{
tTimeout(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')
}
}el if(_resolve){
_resolve(ctx)
}
})
if(!pending){
// 如果异步开关是开的,就关上,表⽰正在执⾏回调函数,然后执⾏回调函数
pending =true
timerFunc()
}
// 如果没有提供回调,并且⽀持 Promi,就返回⼀个 Promi
if(!cb &&typeof Promi !=='undefined'){
return new Promi(resolve =>{
_resolve = resolve
})
}
}
可以看到最后有返回⼀个 Promi 是可以让我们在不传参的时候⽤的,如下
this.$nextTick().then(()=>{...})
Vue3
nextTick ⽤法
先看个例⼦,点击按钮更新 DOM 内容,并获取最新的 DOM 内容
聚焦课堂
<template>
<div ref="test">{{name}}</div>
<el-button @click="handleClick">按钮</el-button>
</template>
<script tup>描写小猫的一段话
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 resolvedPromi: Promi<any>= solve()
let currentFlushPromi: Promi<void>|null=null
export function nextTick<T=void>(this:T, fn?:(this:T)=>void): Promi<void>{
const p = currentFlushPromi || resolvedPromi
return fn ? p.then(this?fn.bind(this): fn): p
}
就⼀个 Promi,没了
就这
好吧,认真点
可以看出 nextTick 接受⼀个函数为参数,同时会创建⼀个微任务
在我们页⾯调⽤ nextTick 的时候,会执⾏该函数,把我们的参数 fn 赋值给 p.then(fn),在队列的任务完成后,fn 就执⾏了
由于加了⼏个维护队列的⽅法,所以执⾏顺序是这样的:
queueJob -> queueFlush -> flushJobs -> nextTick参数的 fn
现在不知道都是⼲嘛的不要紧,⼏分钟后你就会清楚了
我们按顺序来,先看⼀下⼊⼝函数 queueJob 是在哪⾥调⽤的,看代码
// packages/runtime-core/src/renderer.ts - 1555⾏
function baCreateRenderer(){
const tupRenderEffect:SetupRenderEffectFn=(...)=>{
const effect =new ReactiveEffect(
componentUpdateFn,
()=>queueJob(instance.update),// 当作参数传⼊
instance.scope
)
}上古神兽有哪些
}
在 ReactiveEffect 这边接收过来的形参就是 scheduler,最终被⽤到了下⾯这⾥,看过响应式源码的这⾥就熟悉了,就是派发更新的地⽅

本文发布于:2023-07-17 14:15:07,感谢您对本站的认可!

本文链接:https://www.wtabcd.cn/fanwen/fan/82/1101458.html

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

标签:任务   队列   回调   源码   函数   原理   循环
相关文章
留言与评论(共有 0 条评论)
   
验证码:
推荐文章
排行榜
Copyright ©2019-2022 Comsenz Inc.Powered by © 专利检索| 网站地图