vue的异步更新及 nexttick,vue的nexttick实现原理
我最近在学习一些基础知识,所以我想做一系列的尝试来谈谈这些复杂而重要的知识点。下面这篇文章主要介绍了关于Vue异步更新机制的信息和$ TERM $nextTick的原理,供大家参考
目录
前言:Vue的异步更新DOM更新就是异步DOM更新或者批处理事件循环执行过程。源代码深入异步更新队列nextTick$nextTick摘要一般来说,更新DOM是同步的。既然更新DOM是同步过程,为什么Vue需要借用$nextTick来处理?为什么优先考虑微任务?摘要
前言
相信很多人会好奇Vue的内部更新机制,或者日常工作中遇到的一些奇怪的问题需要用$nextTick来解决。今天我们来说说Vue中的异步更新机制以及$nextTick的原理。
Vue的异步更新
如果您没有注意到,Vue异步执行DOM更新。每当观察到数据变化时,Vue将打开一个队列并缓冲在同一事件周期中发生的所有数据变化。如果同一个观察器被多次触发,它只会被推入队列一次。这种在缓冲期间删除重复数据的方法对于避免不必要的计算和DOM操作非常重要。然后,在下一个事件循环“滴答”中,Vue刷新队列并执行实际的(已消除重复的)工作。
DOM更新是异步的
当我们在更新完数据后立即获取DOM中的内容时,会发现获取的内容还是旧的。
模板
div class=next_tick
div ref= title class= title { name } }/div
/div
/模板
脚本
导出默认值{
data() {
返回{
姓名:“钱端九难”
}
},
已安装(){
this.name=前端
console.log(sync ,this。$refs.title.innerText)
这个。$nextTick(()={
console.log(nextTick ,this。$refs.title.innerText)
})
}
}
/脚本
从图中可以发现,当数据发生变化时,dom元素中的内容是旧数据,而更新的数据是在nextTick中获取的。为什么?
实际上,这里你使用微任务或宏任务来获取dom元素中的内容,这也是更新后的数据。我们可以试试:
已安装(){
this.name=前端
console.log(sync ,this。$refs.title.innerText)
Promise.resolve()。然后(()={
Console.log(微任务,这个。$refs.title.innerText)
})
setTimeout(()={
Console.log(宏任务,this。$refs.title.innerText)
}, 0)
这个。$nextTick(()={
console.log(nextTick ,this。$refs.title.innerText)
})
}
你不觉得有点奇怪吗?其实也没什么奇怪的。在vue源代码中,其实现原理是微任务和宏任务。慢慢往下看,后面会解释。
DOM更新还是批量的
没错,vue中的DOM更新仍然是批量处理的。这样做的好处是可以最大程度的优化性能。好的,这里也有可看的。别担心。
Vue同时更新了多个数据。你认为dom更新了多次还是一次?让我们试试。
模板
div class=next_tick
div ref= title class= title { name } }/div
div class= verse { verse } }/div
/div
/模板
脚本
导出默认值{
名称: nextTick ,
data() {
返回{
名称:南九前端,
诗句:‘东山若能东山再起,大鹏展翅冲天’,
计数:0
}
},
已安装(){
this.name=前端
这个. verse=世间万物皆空,名利如风
//console.log(sync ,this。$refs.title.innerText)
//Promise.resolve()。然后(()={
//console.log(微任务,这个。$refs.title.innerText)
//})
//setTimeout(()={
//console.log(宏任务,这个。$refs.title.innerText)
//}, 0)
//这个。$nextTick(()={
//console.log(nextTick ,这个。$refs.title.innerText)
//})
},
已更新(){
这个.计数
console.log(update:,this.count)
}
}
/脚本
style lang=less 。诗句{
font-size:(20/@ rem);
}
/风格
我们可以看到更新的钩子只执行了一次,表明我们是同时更新了多个数据,DOM只会更新一次
我们再来看另一种情况,同步和异步混合。DOM会更新多少次?
已安装(){
this.name=前端
这个. verse=世间万物皆空,名利如风
Promise.resolve()。然后(()={
this.name=研究…
})
setTimeout(()={
“半冷半热,流年一杯浊酒”
})
//console.log(sync ,this。$refs.title.innerText)
//Promise.resolve()。然后(()={
//console.log(微任务,这个。$refs.title.innerText)
//})
//setTimeout(()={
//console.log(宏任务,这个。$refs.title.innerText)
//}, 0)
//这个。$nextTick(()={
//console.log(nextTick ,这个。$refs.title.innerText)
//})
},
已更新(){
这个.计数
console.log(update:,this.count)
}
从图中可以发现,DOM会渲染三次,分别是一次同步(两次同步一起更新),一次微任务,一次宏任务。而当用setTimeout更新数据时,页面数据变化的过程会很明显。(这句话是重点,记得小本子。)这也是为什么nextTick源代码中的setTimeout作为最后手段,优先考虑微任务的原因。
事件循环
是的,和事件循环有很大关系。这里稍微提一下。更多细节,我们可以看看探索JavaScript执行机制。
因为JavaScript是单线程的,所以它的任务不可能只有同步任务。那些耗时较长的任务,如果也按照同步任务执行,会造成页面阻塞。所以JavaScript任务一般分为同步任务和异步任务两类,异步任务又分为宏观任务和微观任务。
宏任务:脚本(整体代码),setTimeout,setInterval,setImmediate,I/O,UI渲染
微任务:承诺
执行过程
同步任务直接放入主线程执行,而异步任务(点击事件、定时器、ajax等。)挂在后台执行,等待I/O事件完成或者行为事件触发。系统在后台执行异步任务。如果异步任务事件(或行为事件)被触发,该任务将被添加到任务队列中,每个任务将由一个回调函数处理。这里,异步任务分为宏观任务和微观任务。宏任务进入宏任务队列,微任务进入微任务队列。执行队列中的任务在执行堆栈中完成。当主线程中的所有任务完成后,读取微任务队列。如果有什么微任务,就全部执行,然后读取宏任务队列。以上过程会不断重复,也就是我们常说的「事件循环(Event-Loop)」。总的来说,在事件循环中,微任务会先于宏任务执行。而在微任务执行完后会进入浏览器更新渲染阶段,所以在更新渲染前使用微任务会比宏任务快一些,一次循环就是一次tick 。
在一个事件循环中,微任务在这个循环中保持提取,直到微任务队列被清空,而宏任务在一个循环中保持提取一次。
如果在事件周期的执行过程中添加了一个异步任务,如果是一个宏任务,它将被放在宏任务的末尾,等待下一个周期的执行。如果是微任务,就放在这个事件循环中微任务任务的末尾继续执行。直到微任务队列为空。
源码深入
异步更新队列
在Vue中,DOM更新一定是数据变化引起的,所以我们可以快速找到更新DOM的入口,也就是在set期间通过dep.notify通知观察者更新的时候。
//观察者. js
//当依赖关系改变时,触发更新
update() {
如果(this.lazy) {
//惰性执行将在这里进行,如computed
this.dirty=true
}else if(this.sync) {
//同步执行会到这里,比如这个。$watch()或watch选项,并传递一个同步配置{sync: true}
this.run()
}否则{
//将当前观察器放入观察器队列,通常是这样。
队列观察器(this)
}
}
从这里我们可以发现vue默认是异步更新机制,会实现一个队列来缓存当前需要更新的watcher。
//scheduler.js
/*将一个观察者对象推入观察者队列。如果队列中已经存在相同的id,将会跳过观察者对象,除非在刷新队列时推送该对象*/
导出函数queueWatcher (watcher: Watcher) {
/*获取观察者的id*/
const id=watcher.id
/*检查id是否存在。如果已经存在,直接跳过。如果不存在,将在has中进行标记,以便下次检查*/
if(有[id]==null) {
has[id]=true
//如果flushing为false,则表示当前watcher队列没有被刷新,那么watcher直接进入队列。
如果(!冲洗){
queue.push(观察器)
}否则{
//如果观察器队列已经被刷新,则需要特殊处理来插入新的观察器。
//确保新的观察器刷新仍然正常。
设i=queue.length - 1
while (i=0 queue[i]。id watcher.id) {
异
}
queue.splice(Math.max(i,index) 1,0,watcher)
}
//将刷新排队
如果(!等待){
//watching为false,表示当前浏览器的异步任务队列中没有flushSchedulerQueue函数。
等待=真
//这是我们共同的这个。$nextTick
nextTick(flushSchedulerQueue)
}
}
}
好了,从这里我们可以发现,vue并没有在数据发生变化后立即更新视图,而是维护了一个watcher队列,id重复的watcher只会推送队列一次,因为我们只关心最终的数据,而不是更新了多少次。等到下一个滴答,这些观察者将被从队列中取出,视图将被更新。
nextTick
nextTick的目的是生成一个回调函数添加到task或microtask中。在当前栈执行完之后(之前可能还有其他函数),回调函数会被调用,这个回调函数会被异步触发(也就是在下一个tick)。
//next-tick.js
常量回调=[]
让待定=假
//批处理
函数flushCallbacks () {
待定=假
const copies=callbacks.slice(0)
回调长度=0
//依次执行nextTick的方法
for(设I=0;一份,长度;i ) {
份数[i]()
}
}
导出函数nextTick (cb,ctx) {
让_解决
callbacks.push(()={
if (cb) {
尝试{
cb .呼叫(ctx)
} catch (e) {
handleError(e,ctx, nextTick )
}
} else if (_resolve) {
_解析(ctx)
}
})
//因为nextTick会在内部调整,所以用户也会调整nextTick,但是异步只需要一次。
如果(!待定){
待定=真
定时器函数()
}
//执行后会返回一个promise实例,这也是$nextTick可以调用then方法的原因。
如果(!cb类型的承诺!==未定义){
返回新承诺(resolve={
_resolve=解决
})
}
}
兼容性处理,优先使用promise.then优雅降级(兼容性处理是一个不断试错的过程,谁用谁会。
E Vu内部尝试使用原生Promise.then,对异步队列使用MutationObserver和setImmediate,如果执行环境不支持,它将改为使用setTimeout(fn,0)。
//定时器函数
//promise . then-MutationObserver-set immediate-setTimeout
Vue3中不再做兼容性处理,而是直接使用promise.then。
如果(类型的承诺!==undefined 是Native(Promise)) {
const p=Promise.resolve()
timerFunc=()={
p.then(刷新回调)
if (isIOS)设置超时(noop)
}
isUsingMicroTask=true
} else if(!这是一种变异观察者!==未定义 (
isNative(变异观察器)
//PhantomJS和iOS 7.x
mutationobserver . tostring()===[object mutationobserver constructor]
)) {
让计数器=1
const observer=new mutation observer(flush callbacks)//可以监控DOM变化,监控后会异步更新。
//但是我这里不是想用它来做DOM监控,而是想利用它的微任务。
const textNode=document . create textNode(String(counter))
observer.observe(textNode,{
characterData: true
})
timerFunc=()={
计数器=(计数器1) % 2
textNode.data=String(计数器)
}
isUsingMicroTask=true
} else if (typeof setImmediate!==undefined 是Native(setImmediate)) {
timerFunc=()={
setImmediate(flushCallbacks)
}
}否则{
//回退到setTimeout。
timerFunc=()={
setTimeout(flushCallbacks,0)
}
}
$nextTick
我们平时调用的$nextTick其实就是上面的方法,只是为了方便我们在源代码renderMixin中附加了vue的原型。
导出函数renderMixin (Vue) {
//安装运行时便利助手
installRenderHelpers(vue . prototype)
vue . prototype . $ next tick=function(fn){
返回nextTick(fn,this)
}
vue . prototype . _ render=function(){
//.
}
//.
}
总结
一般更新DOM是同步的
说了这么多,相信你已经初步了解了Vue的异步更新机制和$nextTick的原理。在每个事件周期结束时,页面会被渲染一次,从上面我们知道渲染过程也是一个宏任务。这里可能有一个误区,就是DOM树的修改是同步的,只有渲染过程是异步的,也就是说我们修改完DOM就可以马上得到更新后的DOM。如果你不相信我,我们可以试试:
!声明文档类型
html lang=en
头
meta charset=UTF-8
meta http-equiv= X-UA-Compatible content= IE=edge
meta name= viewport content= width=device-width,initial-scale=1.0
标题文档/标题
/头
身体
Div id=title 欲试人间烟火,怎能奢求人间沧桑/div
脚本
Title.innerText=万卷诗书无用,半旧志留疏狂
console.log(已更新,标题)
/脚本
/body
/html
既然更新DOM是个同步的过程,那为什么Vue却需要借用$nextTick来处理呢?
答案很明显,因为Vue考虑的是性能,Vue会缓存用户多次同步修改的数据。当同步代码完成后,就意味着这一次的数据修改结束了,然后它会更新相应的DOM。一方面可以省去不必要的DOM操作,比如同时多次修改一个数据,只需要关心最后一次。另一方面可以聚合DOM操作,提高渲染性能。
看下图应该更容易理解。
为什么优先使用微任务?
这个应该不用多说,因为微任务必须在宏任务之前执行。如果nexttick是一个微任务,那么它会在当前同步任务执行完之后立即执行所有的微任务,即修改DOM的操作也会在当前tick内执行。当执行完本轮所有tick任务后,将开始UI渲染。如果nexttick是一个宏任务,则被推入宏任务队列,在当前一轮tick执行后的某一轮执行。注意不一定是下一轮,因为你不确定在它之前的宏任务队列里有几个宏任务在等着。所以为了尽快更新DOM,在Vue中首选微任务,在Vue3中,不具备兼容性判断。promise.then直接用微任务,不再考虑宏任务。
总结
关于Vue异步更新机制和$nextTick原理的这篇文章到此为止。关于Vue异步更新和$nextTick原理的更多信息,请搜索我们之前的文章或者继续浏览下面的相关文章。希望大家以后能多多支持我们!
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。