网站建设专业性评价内容,58同城成都网站建设名录,少儿编程哪个教育平台比较好,wordpress 发布文章功能修改前言
这篇文章分析了 Vue 更新过程中使用的异步更新队列的相关代码。通过对异步更新队列的研究和学习#xff0c;加深对 Vue 更新机制的理解
什么是异步更新队列
先看看下面的例子#xff1a;
div idappdiv iddiv v-ifisShow加深对 Vue 更新机制的理解
什么是异步更新队列
先看看下面的例子
div idappdiv iddiv v-ifisShow被隐藏的内容/divinput clickgetDiv value按钮 typebutton/divscriptlet vm new Vue({el: #app,data: {//控制是否显示#divisShow: false},methods:{getDiv: function () {this.isShowtruevar content document.getElementById(div).innerHTML;console.log(content,content)}}})
/script上面的例子是点击按钮显示被隐藏的 div同时打印 div 内部 html 的内容。按照我们一般的认知应该是点击按钮能够显示 div 并且在控制台看到 div 的内部 html 的内容。 但是实际执行的结果确是div 可以显示出来但是打印结果的时候会报错错误原因就是 innerHTML 为 null也就是 div 不存在。 只有当我们再次点击按钮的时候才会打印出 div 里面的内容。这就是 Vue 的异步更新队列的结果 异步更新队列的概念 Vue 的 dom 更新是异步的当数据发生变化时 Vue 不是立刻去更新 dom而是开启一个队列并缓冲在同一个事件中循环发生的所有数据变化。 在缓冲时会去除重复的数据避免多余的计算和 dom 操作。在下一个事件循环 tick 中刷新队列并执行已去重的工作。 所以上面的代码报错是因为当执行 this.isShowtrue 时div 还未被创建出来知道下次 Vue 事件循环时才开始创建 查重机制降低了 Vue 的开销 异步更新队列实现的选择由于浏览器的差异Vue 会根据当前环境选择 Promise.then 或者 MuMutationObserver如果两者都不支持则会用 setImmediate 或者 setTimeout 代替
异步更新队列解析
异步队列源码入口
通过之前对 Vue 数据响应式的分析我们知道当 Vue 数据发生变化时会触发 dep 的 notify() 方法该方法通知观察者 watcher 去更新 dom我们先看一下这的源码
from src/core/observer/dep.js
//直接看核心代码notify () {//这是Dep的notify方法Vue的会对data数据进行数据劫持该方法被放到data数据的set方法中最后执行//也就是通知更新操作// stabilize the subscriber list firstconst subs this.subs.slice()if (process.env.NODE_ENV ! production !config.async) {subs.sort((a, b) a.id - b.id)}for (let i 0, l subs.length; i l; i) {// 核心通知watcher进行数据更新//这里的subs[i]其实是Dep维护的一个watcher数组所以我们下面是执行的watcher中的update方法subs[i].update()}}上面的代码简单来说就是 dep 通知 watcher 尽心更新操作我们看一下 watcher 相关的代码 from :src/core/observer/watcher.js
//这里只展示部分核心代码//watcher的update方法update () {/* istanbul ignore else *///判断是否存在lazy和sync属性if (this.lazy) {this.dirty true} else if (this.sync) {this.run()} else {//核心将当前的watcher放到一个队列中queueWatcher(this)}}上面 watcher 的 update 更新方法简单来说就是调用了一个 queueWatcher 方法这个方法其实是将当前的 watcher 实例放入到一个队列中以便完成后面的异步更新队列操作
异步队列入队
下面看看 queueWatcher 的逻辑 from src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {const id watcher.id//去重的操作先判断是否在当前队列中存在避免重复操作if (has[id] null) {has[id] trueif (!flushing) {queue.push(watcher)} else {// if already flushing, splice the watcher based on its id// if already past its id, it will be run next immediately.let i queue.length - 1while (i index queue[i].id watcher.id) {i--}queue.splice(i 1, 0, watcher)}// queue the flushif (!waiting) {waiting trueif (process.env.NODE_ENV ! production !config.async) {flushSchedulerQueue()return}// 启动异步任务(刷新当前的计划任务)nextTick(flushSchedulerQueue)}}}上面这段 queueWatcher 的代码的主要作用就是对任务去重然后启动异步任务进行跟新操作。接下来我们看一线 nextTick 里面的操作
from src/core/util/next-tick.js
//cb:
export function nextTick (cb?: Function, ctx?: Object) {let _resolve//callbacks这个方法维护了一个回调函数的数组将回调函数添家进数组callbacks.push(() {//添加错误处理if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, nextTick)}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending true//启动异步函数timerFunc()}// $flow-disable-lineif (!cb typeof Promise ! undefined) {return new Promise(resolve {_resolve resolve})}这里的核心其实就在 timerFunc 的函数上该函数根据不同的运行时环境调用不同的异步更新队列下面看一下代码
from src/core/util/next-tick.js
/**这部分逻辑就是根据环境来判断timerFunc到底是使用什么样的异步队列**/let timerFunc//首选微任务执行异步操作Promise、MutationObserver//次选setImmediate最后选择setTimeout// 根据当前浏览器环境选择用什么方法来执行异步任务if (typeof Promise ! undefined isNative(Promise)) {//如果当前环境支持Promise则使用Promise执行异步任务const p Promise.resolve()timerFunc () {//最终是执行的flushCallbacks方法p.then(flushCallbacks)//如果是IOS则回退因为IOS不支持Promiseif (isIOS) setTimeout(noop)}//当前使用微任务执行isUsingMicroTask true} else if (!isIE typeof MutationObserver ! undefined (//如果当前浏览器支持MutationObserver则使用MutationObserverisNative(MutationObserver) ||MutationObserver.toString() [object MutationObserverConstructor])) {let counter 1const observer new MutationObserver(flushCallbacks)const textNode document.createTextNode(String(counter))observer.observe(textNode, {characterData: true})timerFunc () {counter (counter 1) % 2textNode.data String(counter)}isUsingMicroTask true} else if (typeof setImmediate ! undefined isNative(setImmediate)) {//如果支持setImmediate则使用setImmediatetimerFunc () {setImmediate(flushCallbacks)}} else {//如果上面的条件都不满足那么最后选择setTimeout方法来完成异步更新队列timerFunc () {setTimeout(flushCallbacks, 0)}}从上面代码可以看出不论 timerFunc 使用的是什么样的异步更新队列最终执行的函数还是落在了 flushCallbacks 上面那么我们来看一看这个方法到底是什么
from src/core/util/next-tick.js
function flushCallbacks () {pending false//拷贝callbacks数组内容const copies callbacks.slice(0)//清空callbackscallbacks.length 0//遍历执行for (let i 0; i copies.length; i) {//执行回调方法copies[i]()}}上面的这个方法就是遍历执行了我们 nextTick 维护的那个回调函数数组, 其实就是将数组的方法依次添加进异步队列进行执行。同时清空 callbacks 数组为下次更新作准备。 上面这几段代码其实都是 watcher 的异步队列更新中的入队操作通过 queueWatcher 方法中调用的 nextTick(flushSchedulerQueue), 我们知道其实是将 flushSchedulerQueue 这个方法入队 异步队列的具体更新方法
所以下面我们看一下 flushSchedulerQueue 这个方法到底执行了什么操作
from src/core/observer/scheduler.js
/**我们这里只粘贴跟本次异步队列更新相关的核心代码**///具体的更新操作
function flushSchedulerQueue () {currentFlushTimestamp getNow()flushing truelet watcher, id//重新排列queue数组是为了确保//更新顺序是从父组件到子组件//用户的watcher先于render 的watcher执行(因为用户watcher先于render watcher创建)//当子组件的watcher在父组件的watcher执行时被销毁则跳过该子组件的watcherqueue.sort((a, b) a.id - b.id)//queue数组维护的一个watcher数组//遍历queue数组在queueWatcher方法中我们将传入的watcher实例push到了该数组中for (index 0; index queue.length; index) {watcher queue[index]if (watcher.before) {watcher.before()}id watcher.id//清空has对象里面的id属性(这个id属性之前在queueWatcher方法里面查重的时候用到了)has[id] null//核心最终执行的其实是watcher的run方法watcher.run()//下面是一些警告提示可以先忽略if (process.env.NODE_ENV ! production has[id] ! null) {circular[id] (circular[id] || 0) 1if (circular[id] MAX_UPDATE_COUNT) {warn(You may have an infinite update loop (watcher.user? in watcher with expression ${watcher.expression}: in a component render function.),watcher.vm)break}}}//调用组件updated生命周期钩子相关先跳过const activatedQueue activatedChildren.slice()const updatedQueue queue.slice()resetSchedulerState()callActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)if (devtools config.devtools) {devtools.emit(flush)}
}上面的一堆 flushSchedulerQueue 代码简单来说就是排列了 queue 数组然后遍历该数组执行 watcher.run 方法。所以异步队列更新当我们入队完以后真正执行的方法其实是 watcher.run 方法
下面我们来继续看一下 watcher.run 方法到底执行了什么操作
from src/core/observer/watcher.js
/*** Scheduler job interface.* Will be called by the scheduler.* 上面这段英文注释 是官方注释从这我们看出该方法最终会被scheduler调用*/run () {if (this.active) {//这里调用了watcher的get方法const value this.get()if (value ! this.value ||// Deep watchers and watchers on Object/Arrays should fire even// when the value is the same, because the value may// have mutated.isObject(value) ||this.deep) {// set new valueconst oldValue this.valuethis.value valueif (this.user) {try {this.cb.call(this.vm, value, oldValue)} catch (e) {handleError(e, this.vm, callback for watcher ${this.expression})}} else {this.cb.call(this.vm, value, oldValue)}}}}上述 run 方法最终要的操作就是调用了 watcher 的 get 方法该方法我们在之前的源码分析有讲过主要实现的功能是调用了 data 数据的 get 方法获取最新数据。 至此Vue 异步更新队列的核心代码我们就分析完了为了便于理清思路我们来一张图总结一下 关于 Vue.$nextTick
我们都知道 . n e x t T i c k 方 法 其 实 这 个 ∗ ∗ .nextTick 方法其实这个 ** .nextTick 方法其实这个∗∗nextTick** 方法就是直接调用的上面的 nextTick 方法
from src/core/instance/render.js
Vue.prototype.$nextTick function (fn: Function) {return nextTick(fn, this)}由上面的代码我们可以看出$nextTick 是将我们传入的回调函数加入到了异步更新队列所以它才能实现 dom 更新后回调 注意$nextTick() 是会将我们传入的函数加入到异步更新队列中的但是这里有个问题如果我们想获得 dom 更新后的数据我们应该把该逻辑放到更新操作之后 因为加入异步队列先后的问题如果我们在更新数据之前入队的话 是获取不到更新之后的数据的 总结 总结起来就是当触发数据更新通知时dep 通知 watcher 进行数据更新这时 watcher 会将自己加入到一个异步的更新队列中。然后更新队列会将传入的更新操作进行批量处理。 这样就达到了多次更新同时完成提升了用户体验减少了浏览器的开销增强了性能。