1.Vue2剥丝抽茧-响应式系统之nextTick
2.Vue3之事件循环、源码nextTick与源码解析
3.Vue2.0源码阅读(2) —vue.nextTicket()
4.学透Vue源码~nextTick原理
5.Vue.nextTick 的理解原理和用途
6.Vue—关于响应式(二、异步更新队列原理分析)
Vue2剥丝抽茧-响应式系统之nextTick
在 Vue2 的源码源码解析系列文章中,探讨响应式系统的理解核心概念——nextTick。了解 dom 更新机制,源码认识到浏览器中的理解小区车辆管理系统c源码 js 引擎线程与 GUI 渲染线程交替执行,且 dom 更新发生在 js 线程,源码但需要渲染线程绘制后,理解用户才能观察到。源码因此,理解若 js 线程执行过长,源码可能无法及时将更新的理解 dom 显示给用户。
以极端情况为例,源码即使在js 线程中修改了 dom,理解如果 js 线程忙于执行其他任务,源码导致渲染线程未能及时绘制更新的 dom,用户将无法看到这些修改。引入 bundle.js 的情况下,即便修改了 dom,如果 js 线程陷入死循环,用户界面仍会保持空白状态,尽管 dom 已经更新。
值得注意的是,在 js 线程中连续修改同一 dom 元素,最终渲染时,显示的 dom 结果是最后一次修改后的状态。即使在 js 线程中进行了多次修改,最终呈现给用户的 dom 结果只会是最后一次修改的结果。
讨论到 js 线程中的宏任务与微任务队列,简单阐述宏任务与微任务的概念,以及它们如何影响事件循环的执行顺序。然而,这里不深入探讨具体实现细节,读者可进一步研究相关资料以获取更多深入理解。
Vue3之事件循环、nextTick与源码解析
事件循环是JavaScript单线程执行的核心机制,确保了同步任务与异步任务能有序执行。同步任务按顺序执行,而异步任务则分为宏任务和微任务。宏任务包括setTimeout、setInterval、整体代码、ajax、postMessage、交互事件等,微任务则包括Promise.then、catch、finally、MutationObserver、process.nextTick(Node环境下)。swap源码html
事件循环机制确保了同步任务先执行,宏任务和微任务则交替执行,形成事件循环的周期。此过程确保了JavaScript代码的流畅执行,避免了因耗时任务阻塞主线程导致的卡顿。
在Vue3中,nextTick功能用于处理异步更新DOM问题。它允许开发者在DOM更新之前执行异步代码,确保DOM的正确渲染。有以下两种使用方式:一种是直接传入回调函数,另一种是通过async和await实现。当对数据进行操作后,如果观察到DOM没有更新,原因在于Vue3中数据响应式是同步的,而DOM更新是异步的。
为解决此问题,可以使用nextTick将同步代码转化为异步代码,确保在浏览器的下一次事件循环中执行DOM更新。在Vue3源代码中,nextTick通过将同步代码包装为Promise,从而转化为异步任务来实现这一功能。
Vue3将DOM更新设置为异步,旨在优化性能。考虑到大量数据变化时,频繁的DOM更新可能导致性能开销过大,异步更新策略降低了这种浪费,提高了应用的响应性和性能效率。
Vue2.0源码阅读(2) —vue.nextTicket()
揭开Vue.nextTick之谜
在vue圈子中,有一句广为流传的“都市传说”:“遇事不决,问nextTick。”这句话背后的nextTick究竟是何物?根据官方文档的解释,nextTick()是在下次DOM更新循环结束之后执行延迟回调。其核心功能是在数据更新后自动调用回调函数,获取更新后的DOM。接下来,我们将深入源码,一探nextTick的真谛。
将nextTick定义至Vue原型链的代码位于src/core/instance/render.js,具体实现则在src/core/util/next-tick.js。nextTick接受两个参数:函数cd(实际使用场景中,为延迟执行的函数)与this上下文。内部定义了一个回调函数数组callbacks,当cb存在时将其添加至数组,同时将回调函数的上下文指向组件的this;若cb不存在,则将resolve函数添加至数组。接着判断pending值,其用于控制状态。当pending值为false,表示无回调函数正在执行,SoftwareSerial源码分析进而执行timerFunc函数。timerFunc函数在cb不存在且浏览器支持Promise时返回一个Promise,允许在不传入回调的情况下通过this.$nextTick().then(cb)进行调用。
timerFunc看似实现关键,实则执行逻辑围绕Promise、MutationObserver、setImmediate与setTimeout(f(), 0)等方法展开。若系统支持Promise,则使用Promise执行延时;不支持Promise时,依次判断是否支持MutationObserver、setImmediate或setTimeout,选择合适的方法执行flushCallbacks函数。
flushCallbacks函数负责将pending状态设为false,并将callbacks数组复制至copies数组,清空callbacks。接着遍历copies数组,依次执行回调函数(即传入nextTick的cb函数)。至此,我们理解了nextTick的核心机制与使用场景。
MutationObserver:在源码阅读中,我们发现若系统不支持Promise,则使用MutationObserver作为替代方案。MutationObserver是监听DOM树变更的接口,其设计用于替代DOM3 Events规范中的Mutation Events功能。简单理解,MutationObserver用于监听DOM变动,当DOM发生任何更改时,它会接收到通知。
MutationObserver的使用方式如代码所示,实例化MutationObserver并指定回调函数与需要监控的DOM元素与变动类型。调用observer.observe(dom, options)方法进行观察。options对象中定义了需要观察的变动类型,如childList、attributes、characterData等。
下面通过一个简单的demo来理解MutationObserver。在运行该demo后,屏幕显示了,说明文本节点已添加至DOM中。然而,控制台打印的I值只有1,这意味着DOM变动只触发了一次。这表明MutationObserver在异步处理DOM变化,直到页面上所有DOM操作完成时执行一次,实现高效处理。
在nextTick中,MutationObserver用于触发flushCallbacks函数。通过文本节点的操作触发MutationObserver,从而执行flushCallbacks。国外银行源码至此,我们理解了nextTick的实现与MutationObserver的用法。
源码阅读让我们发现,nextTick并非传说中的神物,其主要应用场合与DOM操作相关。在遇到无法在DOM更新前操作DOM的情况时,可以考虑使用nextTick。由于nextTick在DOM更新循环结束后执行,因此在created钩子中操作DOM成为可能,实现目标。
学透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更新操作。aosp添加源码
Vue中nextTick的实现涉及异步更新队列的概念。Vue为每个要观察的数据创建Watcher对象,当数据变更时,会触发Watcher对象的update方法,但不再立即执行更新操作,而是将变更的Watcher对象保存到待更新的队列中。在微任务中,Vue执行更新队列中的更新操作。
Vue实现nextTick的核心原理包括依赖收集、数据劫持、事件循环机制和异步更新队列。通过这些原理,Vue能够在确保数据响应式的同时,优化性能,减少无效的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 结构的便利。
Vue—关于响应式(二、异步更新队列原理分析)
本节学习要点:Event Loop、Promise
关于Event Loop的介绍,可以参考阮一峰老师的文章。
关于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发布!
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/
2024-12-22 16:13
2024-12-22 16:11
2024-12-22 15:15
2024-12-22 14:24
2024-12-22 13:47
2024-12-22 13:45