1.Vue—关于响应式(二、源码异步更新队列原理分析)
2.学透Vue源码~nextTick原理
3.Vue3之事件循环、源码nextTick与源码解析
4.Vue异步更新机制以及$nextTick原理
5.Vue.nextTick 的源码原理和用途
6.vue2.x中的数据异步更新和nextTick方法解析
Vue—关于响应式(二、异步更新队列原理分析)
本节学习要点:Event Loop、源码Promise
关于Event Loop的源码介绍,可以参考阮一峰老师的源码sys源码文章。
关于Promise,源码请访问:developer.mozilla.org/z...
上一节介绍了Vue通过Object.defineProperty拦截数据变化的源码响应式原理,数据变化后会触发notify方法来通知变更。源码这一节将继续分析,源码收到通知后Vue会开启一个异步更新队列。源码
以下是源码两个问题:
一、异步更新队列
首先看一段代码演示。源码
将上一节的源码代码拿过来,假设我们现在不仅依赖x,源码还有y、z,分别将x、y、z输出到页面上。我们现在依赖了x、y、z三个变量,那么我们应该把onXChange函数名改为watch,表示它可以监听变化,而不仅仅是监听一个x的变化。
可以看到这三个值都被打印在页面上。
现在我们对x、y、z的value进行修改。
查看页面,结果没有问题,每个数据的变化都被监听到并且进行了响应。
既然结果是对的,那我们的问题是什么?
这个问题是:每次数据变化都进行了响应,每次都渲染了模板,如果数据变化了一百次、一千次呢?难道要重复渲染一百遍、一千遍吗?
我们都知道频繁操作DOM会影响网页性能,涉及重排和重绘的知识感兴趣请阅读阮一峰老师的文章:ruanyifeng.com/blog/...
因此,既要保证所有的依赖都准确更新,又要保证不能频繁渲染成为了首要问题。现在我们修改x.value、y.value、z.value都是宁波跨境溯源码同步通知依赖进行更新的,有没有一种机制可以等到我修改这些值之后再执行更新任务呢?
这个答案是——异步。
异步任务会等到同步任务清空后执行,借助这个特点和我们前面的分析,我们需要:
按照步骤,我们创建如下代码:
接着我们需要修改一下notify的代码,监听到数据变化后不立即调用依赖进行更新,而是将依赖添加到队列中。
回到页面,我们发现页面上还是重复渲染了三次模板。
那么我们写的这段代码有什么用呢?异步又体现在哪里呢?接着往下看。
二、nextTick原理分析
上面的代码中,虽然我们开启了一个队列,并且成功将任务推入队列中进行执行,但本质上还是同步推入和执行的。我们要让它变成异步队列。
于是到了Promise发挥作用的时候了。关于宏任务和微任务的介绍请参考:zhuanlan.zhihu.com/p/...
我们创建nextTick函数,nextTick接收一个回调函数,返回一个状态为fulfilled的Promise,并将回调函数传给then方法。
然后只需要在添加任务时调用nextTick,将执行任务的flushJobs函数传给nextTick即可。
回到页面。
虽然修改了x、y、z三个变量的value,最后页面上只渲染了一次。
再来总结一下这段代码的执行过程:
这也正是Vue采用的解决方案——异步更新队列,官方文档描述得很清楚。
文档地址:cn.vuejs.org/v2/guide/r...
三、结合Vue源码来看nextTick
在Vue中,我们可以通过两种方式来调用nextTick:
(至于什么时候使用nextTick,如果你不偷懒看了官方文档的话,都能找到答案哈哈)
以下源码节选自vue2.6.版本,这两个API分别在initGlobalAPI函数和renderMixin函数中挂载,它们都引用了nextTick函数。
nextTick源码如下:
在内部,它访问了外部的callbacks,这个callbacks就是前面提到的队列,nextTick一调用就给队列push一个回调函数,然后判断pending(pending的作用就是控制同一时间内只执行一次timerFunc),调用timerFunc(),最后返回了一个Promise(使用过nextTick的北京同创源码骗局应该都知道吧)。
我们来看一下callbacks、pending、timerFunc是如何定义的。
可以看到timerFunc函数只是调用了p.then方法并将flushCallbacks函数推入了微任务队列,而p是一个fulfilled状态的Promise,与我们自己的nextTick功能一致。
这个flushCallbacks函数又干了什么呢?
flushCallbacks中重新将pending置为初始值,复制callbacks队列中的任务后将队列清空,然后依次执行复制的任务,与我们自己的flushJobs函数功能一致。
看完上面的源码,可以总结出Vue是这么做的,又到了小学语文之——提炼中心思想的时候了。
对比一下我们自己写的代码,你学会了吗?
以上演示代码已上传github:github.com/Mr-Jemp/VueS...
后面要学习的内容在这里:
Vue—关于响应式(三、Diff Patch原理分析)
Vue—关于响应式(四、深入学习Vue响应式源码)
本文由博客一文多发平台OpenWrite发布!
学透Vue源码~nextTick原理
nextTick的官方解释:在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM。
例如:我们有如下代码:
第一次输出结果为hello world,第二次结果为更新后的Hello World。
即我们在update方法中第一行对message的更新,并不是马上同步到span中,而是在完成span的更新之后回调了我们传入nextTick的函数。
Vue中数据的更新不会同步触发dom元素的更新,也就是说dom更新是异步执行的,并且在更新之后调用了我们传入nextTick的函数。
那么问题来了,Vue为什么需要nextTick呢?nextTick又是如何实现的呢?
为了理解nextTick的设计意图和实现原理,我们需要理解Vue的响应式原理,包括数据劫持、依赖收集和数据代理等概念。我们需要实现一个简易版的Vue,用于创建Vue对象,处理参数el和data,并使用Object.defineProperty()方法实现数据劫持。
接下来,我们实现Observe类用于监听数据变化,通过get方法收集依赖并存储到Dep类中。Dep类保存依赖,并在数据变更时调用Watcher类,Watcher类观察数据变化,触发依赖收集并在数据变更后执行更新。
通过以上的山东版武松源码代码,我们就实现了一个简易版的Vue,用于模拟dom变更。
为什么要使用nextTick?当我们对数据进行频繁更新时,可能会导致严重的性能问题。Vue使用nextTick来优化这个问题,避免频繁的DOM更新操作,只在合适的时机执行一次DOM更新。
为了实现异步更新,Vue使用事件循环机制。每次事件循环期间,Vue将数据变更缓存起来,只在最后一次视图渲染时执行一次DOM更新操作。
Vue中nextTick的实现涉及异步更新队列的概念。Vue为每个要观察的数据创建Watcher对象,当数据变更时,会触发Watcher对象的update方法,但不再立即执行更新操作,而是将变更的Watcher对象保存到待更新的队列中。在微任务中,Vue执行更新队列中的更新操作。
Vue实现nextTick的核心原理包括依赖收集、数据劫持、事件循环机制和异步更新队列。通过这些原理,Vue能够在确保数据响应式的同时,优化性能,减少无效的DOM更新操作。
Vue3之事件循环、nextTick与源码解析
事件循环是JavaScript单线程执行的核心机制,确保了同步任务与异步任务能有序执行。同步任务按顺序执行,而异步任务则分为宏任务和微任务。宏任务包括setTimeout、setInterval、整体代码、ajax、postMessage、交互事件等,微任务则包括Promise.then、catch、finally、MutationObserver、process.nextTick(Node环境下)。
事件循环机制确保了同步任务先执行,红鸟白鹭源码宏任务和微任务则交替执行,形成事件循环的周期。此过程确保了JavaScript代码的流畅执行,避免了因耗时任务阻塞主线程导致的卡顿。
在Vue3中,nextTick功能用于处理异步更新DOM问题。它允许开发者在DOM更新之前执行异步代码,确保DOM的正确渲染。有以下两种使用方式:一种是直接传入回调函数,另一种是通过async和await实现。当对数据进行操作后,如果观察到DOM没有更新,原因在于Vue3中数据响应式是同步的,而DOM更新是异步的。
为解决此问题,可以使用nextTick将同步代码转化为异步代码,确保在浏览器的下一次事件循环中执行DOM更新。在Vue3源代码中,nextTick通过将同步代码包装为Promise,从而转化为异步任务来实现这一功能。
Vue3将DOM更新设置为异步,旨在优化性能。考虑到大量数据变化时,频繁的DOM更新可能导致性能开销过大,异步更新策略降低了这种浪费,提高了应用的响应性和性能效率。
Vue异步更新机制以及$nextTick原理
对于Vue的内部工作机制,很多人可能会对异步更新机制和$nextTick的原理感到好奇。今天,我们将深入探讨这两个关键点。
Vue的更新策略是异步的:一旦数据变化,Vue会将这些改变放入一个队列,只有在下一个事件循环(也称为tick)中才会执行实际的更新操作。这样做可以避免不必要的重复计算和DOM操作,提高了性能。例如,即使一个watcher多次被触发,它只会被添加到队列一次。
当你在数据变化后立即尝试获取DOM,可能会发现获取的是旧数据,这是因为DOM更新是异步的,直到下一个事件循环结束,Vue才会真正渲染出更新后的视图。这就是$nextTick的作用,它会在下一次事件循环中执行,确保数据已经更新完毕。
Vue的DOM更新实际上是批量进行的,这有助于优化性能。例如,即使同时更新多个数据,DOM也会在一次渲染中完成。在实际操作中,$nextTick确保了更新过程的同步性,尤其是在处理宏任务和微任务时。
事件循环机制是理解这一切的关键,JavaScript的任务分为同步和异步,Vue利用微任务的特性,确保DOM在更新完成后立即可见。在Vue源码中,$nextTick实际是通过promise.then或MutationObserver等机制实现异步回调。
总结来说,Vue的异步更新机制通过队列缓存数据变化,直到事件循环的下一次渲染,以提高性能。$nextTick则是这个机制的一个工具,用于确保在用户代码执行完毕后,DOM能够快速响应最新的数据变化。
Vue.nextTick 的原理和用途
一、原理
Vue 实现响应式的策略是按一定的策略进行 DOM 的更新,而非数据变化立即导致 DOM 变化。Vue 在修改数据后,并不会立即更新视图,而是等到同一事件循环中的所有数据变化完成之后,再统一进行视图更新。这样的策略确保了 DOM 的更新与数据变化之间的一致性。
二、 Vue.nextTick 的机制
1、为什么用Vue.nextTick()
Vue.nextTick() 方法是 Vue 的核心方法之一,它允许你在 DOM 更新循环结束之后执行延迟回调,确保你获取到的是更新后的 DOM。通过 Vue.nextTick(),开发者可以在修改数据后立即调用它,从而在 DOM 更新后立即访问最新的 DOM 结构。
2、什么是Vue.nextTick()?
Vue.nextTick() 方法用于在下次 DOM 更新循环结束之后执行延迟回调。这样,当修改数据后立即调用 Vue.nextTick(),你可以在回调中访问到更新后的 DOM。
MutationObserver
MutationObserver 是 HTML5 中的一个 API,用于监视 DOM 的变化。调用 MutationObserver 需要先给它绑定回调函数,并得到 MutationObserver 实例。回调会在 MutationObserver 实例监听到 DOM 变动时触发。在这里,回调是在 microtask 中执行的。
源码浅析
Vue.nextTick() 的实现位于 src/core/util/next-tick.js 文件中,主要分为两部分:能力检测和根据能力检测选择执行回调队列的方式。
能力检测确保优先使用微任务执行,如果浏览器不支持微任务,则使用宏任务。Vue.nextTick() 的执行顺序依次为:Promise、MutationObserver、setImmediate、setTimeout。
对外暴露的 nextTick 函数在每次调用时执行回调,但不直接执行回调函数,而是确保同一 tick 内多次调用 nextTick 的回调都集中在同一个异步任务中,在下一个 tick 执行完毕。
附加
no 的定义如下。
三、怎么用
Vue.nextTick([callback, context])
参数说明:
Vue 实例方法 vm.$nextTick 做了封装,将 context 参数设置为当前 Vue 实例。
四、小结
Vue.nextTick() 的使用是为了获取更新后的 DOM。触发时机是在同一事件循环中的数据变化后,DOM 更新完成时立即执行回调。
同一事件循环中的代码执行完毕 -> DOM 更新 -> nextTick callback 触发
应用场景:
版本分析
在 Vue 2.6 版本中,优先使用 microtask 作为异步延迟包装器,方法实现相对简单。而在 Vue 2.5 版本中,nextTick 的实现是通过 microTimerFunc 和 macroTimerFunc 组合实现的,延迟调用优先级为:Promise > setImmediate > MessageChannel > setTimeout。具体的源码实现细节在相关版本中有所不同。
在 Vue 2.5 版本中,存在一些问题,如在重绘之前状态改变时的不一致(如 issue #)以及在事件处理程序中使用 macrotask 导致的不可预知行为(如 issue # 和 issue #)。
尽管 microtask 在某些情况下也可能存在问题,如在顺序事件(如 issue # 和 issue #)之间或在同一事件的冒泡过程中触发(issue #),但 Vue.nextTick() 依然提供了在 DOM 更新后访问最新 DOM 结构的便利。
vue2.x中的数据异步更新和nextTick方法解析
前言
众所周知,vue中的更新时异步的,比如this.msg=xxx,你看起来他是立马更新了,其实并没有。它会异步执行,接下来就来看看怎么实现的吧。
先上图首先从数据改动开始说起调用this.msg=xxx数据发生变更
在数据初始化阶段已经收集了依赖的watcher到dep中,执行dep.notify通知watcehr变更
notify方法遍历调用所有以来的watcher的update方法,把当前watcher实例放入queueWatcher函数中执行,接下来就是异步更新的关键了,看代码
queueWatcher函数代码在src\core\observer\scheduler.js主要作用:把当前watcher实例添加到一个queue中
exportfunctionqueueWatcher(watcher:Watcher){ //拿到watcher的唯一标识constid=watcher.id//无论有多少数据更新,相同的watcher只被压入一次//我理解这就是为什么在一次操作中,多次更改了变量的值,但是只进行了一次页面更新的原因,//同一变量依赖它的watcher是一定的,所以已经存在了就不再放进watcher队列中了,也不会走后面的逻辑if(has[id]==null){ //缓存当前的watcher的标识,用于判断是否重复has[id]=true//如果当前不是刷新状态,直接入队if(!flushing){ queue.push(watcher)}else{ //ifalreadyflushing,splicethewatcherbasedonitsid//ifalreadypastitsid,itwillberunnextimmediately.//此处能走到这儿,说明flushSchedulerQueue函数被执行了watcher队列已经正在开始被更新了,//并且在执行某个watcher.run方法的时候又触发的数据响应式更新,重新触发了queueWatcher//因为在执行的时候回有一个给watcher排序的操作,所以,当watcher正在更新时已经是排好顺序了的,此时需要插入到特定的位置,保持watcher队列依然是保持顺序的leti=queue.length-1while(i>index&&queue[i].id>watcher.id){ i--}queue.splice(i+1,0,watcher)}//queuetheflush//waiting表示当前的flushSchedulerQueue还没有被执行,因为还没有重置状态,waiting仍然为true//所以waiting的意义就是表明是否执行了flushSchedulerQueue,if(!waiting){ waiting=true//直接同步刷新队列if(process.env.NODE_ENV!=='production'&&!config.async){ //同步执行flushSchedulerQueue()return}//把更新队列函数放到异步队列中nextTick(flushSchedulerQueue)}}}flushSchedulerQueue代码在相同目录下//主要作用:遍历执行每一个watcher的run方法,进而实现数据和视图的更新,并在执行完所有的方法之后,重置状态,表示正在刷新队列的flushing,表示watcher是否存在的has,表示是否需要执行nexttick的waiting
functionflushSchedulerQueue(){ //当方法被执行时,设置为正在刷新状态,以示可以继续执行nextTick方法flushing=true//把队列中的watcher排个序,/***排序的作用:(此句照搬照抄而来)*1.保证父组件的watcher比子组件的watcher先更新,因为父组件总是先被创建,子组件后被创建*2.组件用户的watcher在其渲染watcher之前执行。*3.如果一个组件在其父组件执行期间被销毁了,会跳过该子组件。*/queue.sort((a,b)=>a.id-b.id)//中间略去若干代码...//遍历queue中存的所有的watcher,执行run方法更新for(index=0;index<queue.length;index++){ watcher=queue[index]watcher.run()}//因为queue是在一个闭包中,所以当遍历执行完毕了,就把队列清空queue.length=0;//has是判断当前watcher是否重复,作为是否把watcher放进queue的依据//此时已经执行完了queue中的所有watcher了,之前已经执行过的watcher如果发生了变更,可以重新加入了has={ }//waiting是判断是否执行nextTick的标识,当前的刷新队列已经执行完毕了,说以,可以设置为false了,执行下一轮的的添加异步事件队列的方法//flushing是判断是否当前异步事件正在执行的标志,当前更新完毕,作为判断watcher入队的形式waiting=flushing=false}nextTick方法源码src\core\util\next-tick.js
exportfunctionnextTick(cb?:Function,ctx?:Object){ let_resolve//把执行更新操作之后的回调函数添加到队列里//用trycatch包装一下传进来的函数,避免使用$nextTick时,传入的回调函数出错能够及时的捕获到//只要执行了nextTick函数,就把回调函数添加到回调列表里//这里的cb回调函数就是flushSchedulerQueue函数,里面执行了queue中存放的所有的watcher.run方法callbacks.push(()=>{ if(cb){ try{ cb.call(ctx)}catch(e){ handleError(e,ctx,'nextTick')}}elseif(_resolve){ _resolve(ctx)}})//通过pending来判断是否需要向任务队列中添加任务//如果上一个清空回调列表的当flushCallbacks函数还在任务队列中,就不往任务队列中添加//第一次执行时,就默认就添加一个进任务队列,一旦添加进任务队列,就表明暂时不在需要往任务队列中添加flush函数//当执行了上一个flushCallbacks函数的时候,pending修改为false,表明可以重新添加一个清空回调列表的flush函数到任务队列了if(!pending){ pending=true//这里是调用清空callbacks数组中方法,并执行的函数,timerFunc()}//$flow-disable-line//判断当前环境是否支持promise,如果支持的话,可以返回一个期约对象,if(!cb&&typeofPromise!=='undefined'){ returnnewPromise(resolve=>{ _resolve=resolve})}}timerFunc()方法,主要是做一些降级操作,实现异步的关键
timerFunc=()=>{ Promise.resolve().then(flushCallbacks)}//如果当前环境不支持的话,会进行一定的降级操作,直到最后,用宏任务settimeout来处理看看flushCallbacks,任务就是执行了所有的callbacks函数
functionflushCallbacks(){ //如果开始执行了flushCallbacks说明,当前的异步任务已经为空了,如果此时再nextTick方法会添加新的任务进去了pending=false//拷贝一份callbacks中的所有回调函数,用于执行constcopies=callbacks.slice(0)//随即删除所有callbackscallbacks.length=0//当微任务队列中的flushCallbacks添加到执行栈中了,就执行callbacks中的所有的函数//也就是调用执行每一个flushSchedulerQueue函数,然后遍历执行每一个函数for(leti=0;i<copies.length;i++){ copies[i]()}}基本关键变量的作用waiting:变量,作为是否执行nextTick,添加flushSchedulerQueue方法的关键,标志着callbacks中是否有flushSchedulerQueue方法,比如同一个变量的改变,可能会影响多个watcher,因为执行flushSchedulerQueue是异步的,遍历dep.update先把所有的watcher都放入到queue中,也才只执行了一次nextTick,callbacks中也只有一个方法。虽然当第一次方如watcher时就会执行nexttick把flushSchedulerQueue方法放入callbacks中,看起来好像已经要执行了,但是因为queue是闭包变量,所以,后续的变量仍然可以添加queue中,
flushing::表示是否正在执行flushSchedulerQueue方法,如果是正在执行更新方法的话,对向已经排好序的watcher队列中添加新的watcher,需要把新watcher插入到排好序的指定的位置,这也就是为什么遍历watdher那块儿会直接使用queue.length的原因,这个长度会发生变化。
pending::pending是决定是否把更新callbacks数组的方法放入异步队列的关键,保证了异步队列中只有一个清空callbacks的任务,也就解释了,连续手动执行多个$nextTick方法不会立即执行,也还是会把他们的回调放入callbacks中,然后等到任务都执行完毕了,一下把所有的回调函数都执行掉。
参考
vue源码
/post/
Vue.nextTick原理分析
了解 Vue 的 nextTick 是如何实现的,首先需要回顾一下 JavaScript 的运行机制。
JavaScript 是单线程执行的,通过事件循环机制进行任务调度。具体流程如下:当执行栈为空时,检查微任务队列,执行全部微任务,之后执行宏任务队列中的事件。
在 JavaScript 中,常见的宏任务包括 setTimeout, MessageChannel, postMessage 和 setImmediate,而微任务则包括 MutationObserver 和 Promise.then。
Vue 的 nextTick 实现细节在 2.6. 版本后被单独封装在 src/core/util/next-tick.js 文件中,源码简洁,约 多行。其核心原理在于,同一 tick 内添加的任务会在下一个 tick 执行,避免创建多个异步任务。
当调用 nextTick 时不传入回调函数,会返回一个 Promise 化的调用,当内部 resolve 函数执行,即进入 then 的逻辑。
总结,nextTick 的实现和使用方法在源码分析后变得清晰。在 Vue 开发中,通过 nextTick 可以确保在数据响应变化后获取最新的 DOM 更新,避免了同步操作导致的渲染冲突问题。