当前位置: 首页 > Web前端 > HTML

每日一题Vue的异步更新实现原理是什么?

时间:2023-03-28 10:57:24 HTML

最近面试总会被问到这样一个问题:使用vue时,将for循环中声明的变量i从1增加到100,然后在页面上显示i,页面上的i是从1跳转到100,还是什么?答案当然是只会显示100个,不会有跳转过程。如何通过setTimeout或Promise.then等方法模拟页面从1到100显示的过程。讲道理,如果没有vue单独运行这个程序,输出肯定是从1到100,但是为什么在vue中不一样呢?for(leti=1;i<=100;i++){console.log(i);}这个涉及到Vue底层的异步更新原理,还有nextTick的实现。不过在说nextTick之前,有必要先介绍一下JS的事件运行机制。JS运行机制众所周知,JS是一种基于事件循环的单线程语言。执行步骤大致是:代码执行时,所有同步任务都在主线程执行,形成执行栈;主线程外还有一个任务队列,只要异步任务有运行结果,就会往任务队列中放入一个事件;一旦执行栈中的所有同步任务都执行完毕(主线程代码执行完毕),此时主线程不会空闲,而是去读取任务队列。至此,异步任务结束等待状态,开始执行。主线程不断重复上述步骤。我们把主线程执行一次的过程称为一个tick,所以nextTick就是下一个tick的意思,也就是说使用nextTick的场景就是我们要在下一个tick做某事的时候。所有异步任务结果都通过任务队列进行调度。任务分为两大类:宏任务(macrotask)和微任务(microtask)。它们之间的执行规则是每个宏任务结束后清空所有微任务。常见的宏任务有setTimeout/MessageChannel/postMessage/setImmediate,微任务有MutationObsever/Promise.then。nextTick的原理是分发更新。大家都知道Vue的响应式是靠收集和分发更新来实现的。修改数据后,dispatchupdate过程会触发setter的逻辑,执行dep.notify()://src/core/observer/watcher.jsclassDep{notify(){//subs是Watcher的实例数组constsubs=this.subs.slice()for(leti=0,l=subs.length;i=[]lethas:{[key:number]:?true}={}letwaiting=falseletflushing=falseletindex=0exportfunctionqueueWatcher(watcher:Watcher){constid=watcher.id//根据id是否重复进行优化if(有[id]==null){has[id]=trueif(!flushing){queue.push(watcher)}else{leti=queue.length-1while(i>index&&queue[i].id>watcher.id){i--}queue.splice(i+1,0,watcher)}if(!waiting){waiting=true//flushSchedulerQueue函数:刷新两个队列并运行观察者nextTick(flushSchedulerQueue)}}}这里在pushwatcher的时候根据id和flush优化队列。并不是每次数据变化都会触发watcher的回调,而是先将这些watcher添加到一个队列中,然后在nextTick之后执行flushSchedulerQueueflushSchedulerQueue函数,是保存更新事件的队列的一些处理,这样更新就可以满足Vue更新的生命周期。这也就解释了为什么for循环不能导致页面更新,因为for是主线程的代码,一开始执行数据变化的时候会push到队列中,i的值已经变化了执行完for中的代码后到100,这时候vue只到了nextTick(flushSchedulerQueue)这一步。nextTick源码详细答案参考前端高级面试题然后打开vue2.x的源码,目录core/util/next-tick.js,代码量很小,而且是注释只有110行,比较容易理解。constcallbacks=[]letpending=falseexportfunctionnextTick(cb?:Function,ctx?:Object){let_resolvecallbacks.push(()=>{if(cb){try{cb.call(ctx)}catch(e){handleError(e,ctx,'nextTick')}}elseif(_resolve){_resolve(ctx)}})if(!pending){pending=truetimerFunc()}先传入回调函数cb(上一节的flushSchedulerQueue)被压入callbacks数组,最后通过timerFunc函数一次解决。让timerFuncif(typeofPromise!=='undefined'&&isNative(Promise)){constp=Promise.resolve()timerFunc=()=>{p.then(flushCallbacks)if(isIOS)setTimeout(noop)}isUsingMicroTask=true}elseif(!isIE&&typeofMutationObserver!=='undefined'&&(isNative(MutationObserver)||//PhantomJS和iOS7.xMutationObserver.toString()==='[objectMutationObserverConstructor]')){让counter=1constobserver=newMutationObserver(flushCallbacks)consttextNode=document.createTextNode(String(counter))observer.observe(textNode,{characterData:true})timerFunc=()=>{计数器=(counter+1)%2textNode.data=String(counter)}isUsingMicroTask=true}elseif(typeofsetImmediate!=='undefined'&&isNative(setImmediate)){timerFunc=()=>{setImmediate(flushCallbacks)}}else{timerFunc=()=>{setTimeout(flushCallbacks,0)}}timerFunc下面一大段ifelse用来判断在不同的设备和情况下使用哪个特性来实现异步任务:先检查原生是否支持Promise,如果不支持再检查是否支持MutationObserver。如果不行,只能尝试宏任务实现,第一个是setImmediate,这是只有高版本的IE和Edge才支持的特性。如果不支持,则会降级为setTimeout0。这里使用回调,而不是直接在nextTick中执行回调函数,原因是保证nextTick在同一个tick中执行多次,多个异步任务会不启动,这些异步任务会被压缩成一个同步任务,在下一个tick执行。nextTick使用nextTick不仅是vue的源码文件,也是vue的全局API。让我们看看如何使用它。当设置vm.someData='newvalue'时,组件不会立即重新渲染。刷新队列时,组件将在下一个事件循环滴答时更新。大多数时候我们不需要关心这个过程,但是如果你想根据更新的DOM状态做一些事情,它可能会很棘手。虽然Vue.js通常鼓励开发人员以数据驱动的方式思考并避免直接接触DOM,但有时我们不得不这样做。要等待Vue在数据更改后完成更新DOM,请在数据更改后立即使用Vue.nextTick(callback)。这样回调函数就会在DOM更新完成后被调用。官网用例:{{message}}

varvm=newVue({el:'#example',data:{message:'123'}})vm.message='newmessage'//改变数据vm.$el.textContent==='newmessage'//falseVue.nextTick(function(){vm.$el.textContent==='newmessage'//true})又因为$nextTick()返回的是Promise对象,所以也可以使用async/await语法来处理事件,非常方便。