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

vue源码中nextTick是如何实现的_0

时间:2023-03-31 15:03:49 vue.js

1.Vue.nextTick的内部逻辑在初始化Vue全局API的initGlobalAPI(Vue)实现中,Vue.nextTick是这样定义的。functioninitGlobalAPI(Vue){//...Vue.nextTick=nextTick;}可见直接将nextTick函数赋给Vue.nextTick非常简单。2.vm.$nextTickVue.prototype.$nextTick=function(fn){returnnextTick(fn,this)}的内部逻辑;可见vm.$nextTick内部也调用了nextTick函数。3.前置知识nextTick函数的作用可以理解为传入函数的异步执行。这里先介绍一下什么是异步执行,从JS的运行机制说起。1、JS运行机制JS的执行是单线程的。所谓单线程就是事件任务必须排队执行。下一个任务只有在上一个任务完成后才会执行。这是一个同步任务。为了避免上一个任务执行的时间还没有结束,下一个任务就无法执行,引入了异步任务的概念。JS运行机制可以简单的按照以下步骤进行。所有的同步任务都在主线程上执行,形成一个执行上下文栈。除了主线程之外,还有一个任务队列(taskqueue)。只要异步任务有运行结果,它的回调函数就会作为任务加入到任务队列中。一旦执行栈中的所有同步任务都执行完毕,就会读取任务队列,查看其中有哪些任务,添加到执行栈中,并执行。主线程不断重复上面的第三步。它通常被称为事件循环(EventLoop)。2.异步任务类型nextTick函数异步执行传入函数,属于异步任务。有两种类型的异步任务。主线程的执行过程是一个tick,所有的异步任务都是通过任务队列一个一个执行的。每个任务都存储在任务队列中。规范规定任务分为宏任务和微任务两类,每个宏任务结束后必须清除所有微任务。用一段代码直观地介绍任务的执行顺序。对于(macroTaskQueue的macroTask){handleMacroTask();for(microTaskQueue的microTask){handleMicroTask(microTask);}}在浏览器环境下,常用的宏任务创建方法有setTimeout、setInterval、postMessage、MessageChannel(队列优先于setTimeiout执行)网络请求IO页面交互:DOM、鼠标、键盘、滚动事件页面渲染常用创建方法微任务Promise.thenMutationObserveprocess.nexttick中的nextTick函数使用这些方法来处理函数通过参数cb传入的异步任务。3.nextTick函数varcallbacks=[];varpending=false;函数nextTick(cb,ctx){var_resolve;callbacks.push(function(){if(cb){try{cb.call(ctx);}catch(e){handleError(e,ctx,'nextTick');}}elseif(_resolve){_resolve(ctx);}});如果(!pending){pending=true;定时器功能();}if(!cb&&typeofPromise!=='undefined'){returnnewPromise(function(resolve){_resolve=resolve;})}}可以看到在nextTick函数中,函数通过参数cb传入被打包然后推入回调数组。然后使用变量pending来确保timerFunc()在一个事件循环中只执行一次。最后如果执行(!cb&&typeofPromise!=='undefined'),判断参数cb不存在,浏览器支持Promise,则返回一个Promise类实例化对象。比如nextTick().then(()=>{}),当_resolve函数执行时,then逻辑就会执行。让我们看一下timerFunc函数的定义。首先,我们将只使用Promise创建一个异步执行的ztimerFunc函数。Vue源码相关视频讲解:进入学习vartimerFunc;if(typeofPromise!=='undefined'&&isNative(Promise)){varp=Promise.resolve();timerFunc=function(){p.then(flushCallbacks);如果(isIOS){setTimeout(noop);}};}发现timerFunc函数是用各种异步执行方式调用flushCallbacks函数。看一下flushCallbacks函数varcallbacks=[];varpending=false;functionflushCallbacks(){pending=false;var副本=callbacks.slice(0);回调.length=0;对于(vari=0;i//块1ExpandisTrue//元素1

//block2ExpandisFalse//element2
按正常逻辑点击element1时,expand会置为false,block1不会displayed,会显示block2,点击block2时,expand会设置为false,然后会显示block1。当时的实际情况是点击element1只会显示block1,这就是为什么,是什么导致了这个BUG。Vue官方说明点击事件是一个宏任务,上的点击事件触发nextTick(微任务)上的第一次更新。在事件冒泡到外部div之前处理微任务。更新期间,将在外部div中添加一个点击监听器。因为DOM结构相同,所以外部div和内部元素都被重用。事件最终命中外层div,触发第一次更新添加的监听器,进而触发第二次更新。要解决此问题,您可以简单地为两个外部div提供不同的键,以强制它们在更新期间被替换。这样可以防止接收到冒泡事件。当然当时官方还是给出了解决方案,改用timerFunc来实现创建宏任务的方法,顺序是setImmediate,MessageChannel,setTimeout,所以nextTick是一个宏任务。点击事件是一个宏任务。当点击事件执行时,nextTick(宏任务)上的更新只会在下一个事件循环中执行,这样事件冒泡就已经执行完了。不会出现BUG的情况。但是过了一段时间,timerFunc的实现顺序变成了Promise、MutationObserver、setImmediate、setTimeout。在任何地方使用宏任务都会导致一些奇妙的问题。代表发行号#6813,代码打出来。你可以在这里看到它。这里有两个关键的控制媒体查询。当页面宽度大于1000px时,li显示类型为行内框,小于1000px时,显示类型为块级元素。监听页面缩放,当页面宽度小于1000px时,ul控件用v-show="showList"隐藏。初始状态:快速拖动网页边框缩小页面宽度时,会先显示下方第一张图片,然后快速隐藏,而不是直接隐藏。要想出现这种BUG,首先要明白一个概念,UIRender(UI渲染)的执行时机,如下:macro拿了一个macrotask。micro清除微任务队列。判断当前帧是否值得更新,否则重新进入步骤1,在绘制帧前执行requestAnimationFrame队列任务。UI更新,执行UIRender。如果宏任务队列不为空,则更容易理解重新进入步骤的过程。监控窗口的缩放以前是一个宏任务。当窗口尺寸小于1000px时,showList会变成flase,会触发nextTick执行,是一个宏。任务。在这两个宏任务之间,将执行UIRender。这时li的内联框架设置就失效了,显示为块级框架。执行nextTick宏任务后,再次进行UIRender时,会再次显示ul的显示值。切换到无,列表被隐藏。所以Vue认为用microtasks创建的nextTick的可控性还不错,不像用macrotasks创建的nextTick会有不可控的场景。在2.6+版本中,使用时间戳来解决#6566BUG。设置一个变量attachedTimestamp。在执行nextTick函数传入的flushSchedulerQueue函数时,执行currentFlushTimestamp=getNow()得到一个时间戳,赋值给变量currentFlushTimestamp,然后在监听DOM上的事件之前做一个劫持。它是在add函数中实现的。functionadd(name,handler,capture,passive){if(useMicrotaskFix){varattachedTimestamp=currentFlushTimestamp;var原始=处理程序;handler=original._wrapper=function(e){if(e.target===e.currentTarget||e.timeStamp>=attachedTimestamp||e.timeStamp<=0||e.target.ownerDocument!==文档){returnoriginal.apply(this,arguments)}};}target.addEventListener(name,handler,supportsPassive?{capture:capture,passive:passive}:capture);}Executeif(useMicrotaskFix),useMicrotaskFix在创建带有microtasks的异步执行函数时设置为true。执行varattachedTimestamp=currentFlushTimestamp将执行nextTick回调函数时的时间戳赋值给变量attachedTimestamp,然后执行if(e.timeStamp>=attachedTimestamp),其中e.timeStampDOM上的事件触发时的时间戳大于attachedTimestamp,这个事件会被执行。为什么,回到#6566BUG。由于微任务的执行优先级很高,比#6566BUG中的事件冒泡要快,所以才会出现这个BUG。当点击i标签时,冒泡事件的触发早于nextTick的执行,此时e.timeStamp小于attachedTimestamp。如果执行冒泡事件,会导致#6566BUG,所以只有触发冒泡事件晚于执行nextTick才能避免这个bug,所以e.timeStamp大于attachedTimestamp才能执行冒泡事件.