当前位置: 首页 > Web前端 > vue.js

Vue-nextTick原理

时间:2023-04-01 12:00:57 vue.js

VUE-nextTick原理1.JS事件循环在介绍Vue的nextTick之前,先简单介绍一下JS的运行机制:JS执行是单线程的,是基于事件循环的。对于事件循环的理解,阮老师写了一篇文章说的很清楚,大致分为以下几个步骤:(1)所有的同步任务都在主线程上执行,形成一个执行上下文栈。(2)除了主线程之外,还有一个“任务队列”。只要异步任务有运行结果,就会在“任务队列”中放入一个事件。(3)一旦“执行栈”中的所有同步任务都执行完毕,系统就会去读取“任务队列”,看看里面有什么事件。那些对应的异步任务结束等待状态,进入执行栈,开始执行。(4)主线程不断重复上面的第三步。主线程的执行过程是一个tick,所有的异步结果都是通过“任务队列”来调度的。每个任务都存储在消息队列中。规范规定任务分为宏任务和微任务两类,每个宏任务结束后,必须清除所有微任务。在浏览器环境下,常见的宏任务有setTimeout、MessageChannel、postMessage、setImmediate;常见的微任务包括MutationObsever和Promise.then。2.Vue的nextTickVue的nextTick顾名思义就是下一个tick。Vue在内部实现了nextTick并将其作为全局API公开。支持传入回调函数,保证回调函数在下一个tick执行。官网文档介绍了Vue.nextTick的使用场景:用法:推迟下一个DOM更新周期后执行的回调。更改一些数据后立即使用它以等待DOM更新。使用:在下一个DOM更新周期结束后执行延时回调,修改数据后立即使用该方法获取更新后的DOM。在Vue.js中,数据驱动的视图发生了变化。由于JS执行是单线程的,一个tick过程中可能会多次修改数据,但是Vue.js不会傻到每次修改数据都去驱动。一旦视图发生变化,它会将所有的数据变化推送到一个队列中,然后在内部调用nextTick来更新视图,所以数据到DOM视图的变化需要在下一个tick完成。3、源码1。/*@flow*/2./*globalsMessageChannel*/4.import{noop}from'shared/util'5.import{handleError}from'./error'6.import{isIOS,isNative}from'./env'8.constcallbacks=[]9.letpending=false11.functionflushCallbacks(){12.pending=false13.constcopies=callbacks.slice(0)14.callbacks.length=015.for(leti=0;i{39.setImmediate(flushCallbacks)40.}41.}elseif(typeofMessageChannel!=='undefined'&&(42.isNative(MessageChannel)||43.//PhantomJS44.MessageChannel.toString()==='[objectMessageChannelConstructor]'45.)){46.constchannel=newMessageChannel()47.constport=channel.port248.channel.port1.onmessage=flushCallbacks49.macroTimerFunc=()=>{50.port.postMessage(1)51.}52.}else{53./*istanbul忽略下一个*/54.macroTimerFunc=()=>{55.setTimeout(flushCallbacks,0)56.}57.}59.//确定MicroTask延迟实现。60./*istanbulignorenext,$flow-disable-line*/61.if(typeofPromise!=='undefined'&&isNative(Promise)){62.constp=Promise.resolve()63.microTimerFunc=()=>{64.p.then(flushCallbacks)65.//在有问题的UIWebViews中,Promise.then并没有完全中断,但是66.//它可能会陷入一种奇怪的状态,回调被推入67.//微任务队列,但队列不会被刷新,直到浏览器68.//需要做一些其他工作,例如处理一个定时器。因此我们可以69.//通过添加一个空计时器来“强制”刷新微任务队列。70.if(isIOS)setTimeout(noop)71.}72.}else{73.//回退到宏74.microTimerFunc=macroTimerFunc75.}77./**78.*包装一个函数,这样如果里面有任何代码触发状态更改,79。*使用任务而不是MicroTask对更改进行排队。80.*/81.导出函数withMacroTask(fn:Function):Function{82.returnfn._withTask||(fn._withTask=function(){83.useMacroTask=true84.constres=fn.apply(null,arguments)85.useMacroTask=false86.returnres87.})88.}90.exportfunctionnextTick(cb?:函数,ctx?:对象){91.让_resolve92.回调。push(()=>{93.if(cb){94.try{95.cb.call(ctx)96.}catch(e){97.handleError(e,ctx,'nextTick')98.}99.}elseif(_resolve){100._resolve(ctx)101.}102.})103.if(!pending){104.pending=true105.if(useMacroTask){106.macroTimerFunc()107.}else{108.microTimerFunc()109.}110.}111.//$flow-disable-line112.if(!cb&&typeofPromise!=='undefined'){113.returnnewPromise(resolve=>{114._resolve=resolve115.})116.}117.}这个源码中的next-tick.js文件里面有一个重要的注释,这里翻译一下:在vue2.5之前,nextTick基本都是基于micro实现的任务,但在某些情况下微任务的优先级太高,并且可能在连续的顺序事件之间(例如#4521,#6690)甚至在冒泡过程之间的同一事件触发(#6566)但是如果全部改成macrotask,对一些有重绘和动画的场景也会有性能影响,比如issue#6813。vue2.5以后的版本提供的解决方案是默认使用微任务,但在需要的时候强制使用宏任务(比如在v-on附带的事件处理器中)。这个强制是指Vue.js在绑定DOM事件的时候,默认会对回调handler函数调用withMacroTask方法handler=withMacroTask(handler)做一层包装,保证在整个回调函数执行过程中,任何当数据状态发生变化时,这些变化会被推送到宏任务中。对于宏任务的执行,Vue.js首先检查是否支持原生的setImmediate,这是只有高版本的IE和Edge才支持的特性。如果不支持,会检查是否支持原生的MessageChannel。如果不支持,则会降级为setTimeout0。4.一个小例子{{name}}更改名称

执行结果为:同步方法:杨123456setter之前:杨123456setter之后:名称改为Promise方法:名称改为setTimeout方法:名称改为parsing同步方法:修改data中的name后,此时会触发th的setter中的dep.notify通知依赖此数据的渲染观察者的名称update,update会将flushSchedulerQueue函数传递给nextTick,renderwatcher会在flushSchedulerQueue函数运行时重新渲染re-renderview。;所以当我们直接修改name打印出来的时候,此时异步的变化还没有patch到view上,所以获取到的view上的DOM元素还是setter之前的原始内容:为什么之前打印的是原始内容二传手?是因为nextTick在调用的时候会将callbacks一个一个的压入到callbacks数组中,执行的时候通过一个for循环一个一个的执行,所以类似于队列的概念,先进先出;修改名称后,触发填充renderwatcher的schedulerQueue队列,并将其执行函数flushSchedulerQueue传递给nextTick。这个时候callbacks队列中已经有pre-setterfunction了,因为这个cb是在pre-setterfunction之后被push到callbacks队列中的,然后先进先出执行callbacks的时候,回调函数在先执行setter之前,此时并没有执行renderwatcher的watcher.run,所以打印出来的DOM元素还是原来的内容。setter之后:setter之后,此时flushSchedulerQueue已经执行完毕,renderwatcher已经对view的变化进行了patch,所以此时获取到的DOM就是修改后的内容。Promise方法:当DOM发生变化时,以与Promise.then相同的方式执行此函数。setTimeout方法:最后执行宏任务的任务,此时DOM已经发生变化。注意在执行pre-setter函数的异步任务之前,同步代码已经执行完毕,异步任务还没有执行完,所有的$nextTick函数也都执行完了,所有的callbacks都已经push到callbacks队列中执行了,所以当执行setter之前的函数时,回调队列是这样的:【setter之前的函数,flushSchedulerQueue,setter之后的函数,Promise模式的函数】,是一个微任务队列,执行之后执行宏任务和setTimeout,所以打印得到上面的结果。另外,如果浏览器的宏任务队列中有setImmediate、MessageChannel、setTimeout/setInterval等各种类型的任务,它们会按上述顺序加入事件循环的顺序执行,所以如果浏览器支持MessageChannel,nextTick会执行macroTimerFunc,如果macrotask队列中有nextTick添加的任务和用户添加的setTimeout类型的任务,则nextTick中的任务会先执行,因为MessageChannel的优先级高于setTimeout,setImmediate也是如此。说明一下,以上部分内容的出处和本人查阅期间的网络搜索也主要用于个人学习,相当于一个记事本的存在,链接的文章暂不列出。如果作者看到了,可以联系我,贴出原文链接。