dw網(wǎng)頁(yè)設(shè)計(jì)期末作業(yè)seo的主要分析工具
前言
這篇文章分析了 Vue 更新過(guò)程中使用的異步更新隊(duì)列的相關(guān)代碼。通過(guò)對(duì)異步更新隊(duì)列的研究和學(xué)習(xí),加深對(duì) Vue 更新機(jī)制的理解
什么是異步更新隊(duì)列
先看看下面的例子:
<div id="app"><div id="div" v-if="isShow">被隱藏的內(nèi)容</div><input @click="getDiv" value="按鈕" type="button"></div><script>let vm = new Vue({el: '#app',data: {//控制是否顯示#divisShow: false},methods:{getDiv: function () {this.isShow=truevar content = document.getElementById('div').innerHTML;console.log('content',content)}}})
</script>
- 上面的例子是,點(diǎn)擊按鈕顯示被隱藏的 div,同時(shí)打印 div 內(nèi)部 html 的內(nèi)容。
- 按照我們一般的認(rèn)知,應(yīng)該是點(diǎn)擊按鈕能夠顯示 div 并且在控制臺(tái)看到 div 的內(nèi)部 html 的內(nèi)容。
但是實(shí)際執(zhí)行的結(jié)果確是,div 可以顯示出來(lái),但是打印結(jié)果的時(shí)候會(huì)報(bào)錯(cuò),錯(cuò)誤原因就是 innerHTML 為 null,也就是 div 不存在。
只有當(dāng)我們?cè)俅吸c(diǎn)擊按鈕的時(shí)候才會(huì)打印出 div 里面的內(nèi)容。這就是 Vue 的異步更新隊(duì)列的結(jié)果
異步更新隊(duì)列的概念
Vue 的 dom 更新是異步的,當(dāng)數(shù)據(jù)發(fā)生變化時(shí) Vue 不是立刻去更新 dom,而是開(kāi)啟一個(gè)隊(duì)列,并緩沖在同一個(gè)事件中循環(huán)發(fā)生的所有數(shù)據(jù)變化。
在緩沖時(shí),會(huì)去除重復(fù)的數(shù)據(jù),避免多余的計(jì)算和 dom 操作。在下一個(gè)事件循環(huán) tick 中,刷新隊(duì)列并執(zhí)行已去重的工作。
-
所以上面的代碼報(bào)錯(cuò)是因?yàn)楫?dāng)執(zhí)行 this.isShow=true 時(shí),div 還未被創(chuàng)建出來(lái),知道下次 Vue 事件循環(huán)時(shí)才開(kāi)始創(chuàng)建
-
查重機(jī)制降低了 Vue 的開(kāi)銷(xiāo)
-
異步更新隊(duì)列實(shí)現(xiàn)的選擇:由于瀏覽器的差異,Vue 會(huì)根據(jù)當(dāng)前環(huán)境選擇 Promise.then 或者 MuMutationObserver,如果兩者都不支持,則會(huì)用 setImmediate 或者 setTimeout 代替
異步更新隊(duì)列解析
異步隊(duì)列源碼入口
通過(guò)之前對(duì) Vue 數(shù)據(jù)響應(yīng)式的分析我們知道,當(dāng) Vue 數(shù)據(jù)發(fā)生變化時(shí),會(huì)觸發(fā) dep 的 notify() 方法,該方法通知觀察者 watcher 去更新 dom,我們先看一下這的源碼
- from src/core/observer/dep.js
//直接看核心代碼notify () {//這是Dep的notify方法,Vue的會(huì)對(duì)data數(shù)據(jù)進(jìn)行數(shù)據(jù)劫持,該方法被放到data數(shù)據(jù)的set方法中最后執(zhí)行//也就是通知更新操作// 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進(jìn)行數(shù)據(jù)更新//這里的subs[i]其實(shí)是Dep維護(hù)的一個(gè)watcher數(shù)組,所以我們下面是執(zhí)行的watcher中的update方法subs[i].update()}}
- 上面的代碼簡(jiǎn)單來(lái)說(shuō)就是 dep 通知 watcher 盡心更新操作,我們看一下 watcher 相關(guān)的代碼
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 {//核心:將當(dāng)前的watcher放到一個(gè)隊(duì)列中queueWatcher(this)}}
- 上面 watcher 的 update 更新方法簡(jiǎn)單來(lái)說(shuō)就是調(diào)用了一個(gè) queueWatcher 方法,這個(gè)方法其實(shí)是將當(dāng)前的 watcher 實(shí)例放入到一個(gè)隊(duì)列中,以便完成后面的異步更新隊(duì)列操作
異步隊(duì)列入隊(duì)
下面看看 queueWatcher 的邏輯 from src/core/observer/scheduler.js
export function queueWatcher (watcher: Watcher) {const id = watcher.id//去重的操作,先判斷是否在當(dāng)前隊(duì)列中存在,避免重復(fù)操作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}// 啟動(dòng)異步任務(wù)(刷新當(dāng)前的計(jì)劃任務(wù))nextTick(flushSchedulerQueue)}}}
- 上面這段 queueWatcher 的代碼的主要作用就是對(duì)任務(wù)去重,然后啟動(dòng)異步任務(wù),進(jìn)行跟新操作。接下來(lái)我們看一線 nextTick 里面的操作
from src/core/util/next-tick.js
//cb:
export function nextTick (cb?: Function, ctx?: Object) {let _resolve//callbacks:這個(gè)方法維護(hù)了一個(gè)回調(diào)函數(shù)的數(shù)組,將回調(diào)函數(shù)添家進(jìn)數(shù)組callbacks.push(() => {//添加錯(cuò)誤處理if (cb) {try {cb.call(ctx)} catch (e) {handleError(e, ctx, 'nextTick')}} else if (_resolve) {_resolve(ctx)}})if (!pending) {pending = true//啟動(dòng)異步函數(shù)timerFunc()}// $flow-disable-lineif (!cb && typeof Promise !== 'undefined') {return new Promise(resolve => {_resolve = resolve})}
- 這里的核心,其實(shí)就在 timerFunc 的函數(shù)上,該函數(shù)根據(jù)不同的運(yùn)行時(shí)環(huán)境,調(diào)用不同的異步更新隊(duì)列,下面看一下代碼
from src/core/util/next-tick.js
/**這部分邏輯就是根據(jù)環(huán)境來(lái)判斷timerFunc到底是使用什么樣的異步隊(duì)列**/let timerFunc//首選微任務(wù)執(zhí)行異步操作:Promise、MutationObserver//次選setImmediate最后選擇setTimeout// 根據(jù)當(dāng)前瀏覽器環(huán)境選擇用什么方法來(lái)執(zhí)行異步任務(wù)if (typeof Promise !== 'undefined' && isNative(Promise)) {//如果當(dāng)前環(huán)境支持Promise,則使用Promise執(zhí)行異步任務(wù)const p = Promise.resolve()timerFunc = () => {//最終是執(zhí)行的flushCallbacks方法p.then(flushCallbacks)//如果是IOS則回退,因?yàn)镮OS不支持Promiseif (isIOS) setTimeout(noop)}//當(dāng)前使用微任務(wù)執(zhí)行isUsingMicroTask = true} else if (!isIE && typeof MutationObserver !== 'undefined' && (//如果當(dāng)前瀏覽器支持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方法來(lái)完成異步更新隊(duì)列timerFunc = () => {setTimeout(flushCallbacks, 0)}}
- 從上面代碼可以看出,不論 timerFunc 使用的是什么樣的異步更新隊(duì)列,最終執(zhí)行的函數(shù)還是落在了 flushCallbacks 上面,那么我們來(lái)看一看,這個(gè)方法到底是什么
from src/core/util/next-tick.js
function flushCallbacks () {pending = false//拷貝callbacks數(shù)組內(nèi)容const copies = callbacks.slice(0)//清空callbackscallbacks.length = 0//遍歷執(zhí)行for (let i = 0; i < copies.length; i++) {//執(zhí)行回調(diào)方法copies[i]()}}
- 上面的這個(gè)方法就是遍歷執(zhí)行了我們 nextTick 維護(hù)的那個(gè)回調(diào)函數(shù)數(shù)組, 其實(shí)就是將數(shù)組的方法依次添加進(jìn)異步隊(duì)列進(jìn)行執(zhí)行。同時(shí)清空 callbacks 數(shù)組為下次更新作準(zhǔn)備。
上面這幾段代碼其實(shí)都是 watcher 的異步隊(duì)列更新中的入隊(duì)操作,通過(guò) queueWatcher 方法中調(diào)用的 nextTick(flushSchedulerQueue), 我們知道,其實(shí)是將 flushSchedulerQueue 這個(gè)方法入隊(duì)
異步隊(duì)列的具體更新方法
所以下面我們看一下 flushSchedulerQueue 這個(gè)方法到底執(zhí)行了什么操作
from src/core/observer/scheduler.js
/**我們這里只粘貼跟本次異步隊(duì)列更新相關(guān)的核心代碼**///具體的更新操作
function flushSchedulerQueue () {currentFlushTimestamp = getNow()flushing = truelet watcher, id//重新排列queue數(shù)組,是為了確保://更新順序是從父組件到子組件//用戶的watcher先于render 的watcher執(zhí)行(因?yàn)橛脩魒atcher先于render watcher創(chuàng)建)//當(dāng)子組件的watcher在父組件的watcher執(zhí)行時(shí)被銷(xiāo)毀,則跳過(guò)該子組件的watcherqueue.sort((a, b) => a.id - b.id)//queue數(shù)組維護(hù)的一個(gè)watcher數(shù)組//遍歷queue數(shù)組,在queueWatcher方法中我們將傳入的watcher實(shí)例push到了該數(shù)組中for (index = 0; index < queue.length; index++) {watcher = queue[index]if (watcher.before) {watcher.before()}id = watcher.id//清空has對(duì)象里面的"id"屬性(這個(gè)id屬性之前在queueWatcher方法里面查重的時(shí)候用到了)has[id] = null//核心:最終執(zhí)行的其實(shí)是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}}}//調(diào)用組件updated生命周期鉤子相關(guān),先跳過(guò)const activatedQueue = activatedChildren.slice()const updatedQueue = queue.slice()resetSchedulerState()callActivatedHooks(activatedQueue)callUpdatedHooks(updatedQueue)if (devtools && config.devtools) {devtools.emit('flush')}
}
- 上面的一堆 flushSchedulerQueue 代碼,簡(jiǎn)單來(lái)說(shuō)就是排列了 queue 數(shù)組,然后遍歷該數(shù)組,執(zhí)行 watcher.run 方法。所以,異步隊(duì)列更新當(dāng)我們?nèi)腙?duì)完以后,真正執(zhí)行的方法其實(shí)是 watcher.run 方法
下面我們來(lái)繼續(xù)看一下 watcher.run 方法,到底執(zhí)行了什么操作
from src/core/observer/watcher.js
/*** Scheduler job interface.* Will be called by the scheduler.* 上面這段英文注釋 是官方注釋,從這我們看出該方法最終會(huì)被scheduler調(diào)用*/run () {if (this.active) {//這里調(diào)用了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 方法最終要的操作就是調(diào)用了 watcher 的 get 方法,該方法我們?cè)谥暗脑创a分析有講過(guò),主要實(shí)現(xiàn)的功能是調(diào)用了 data 數(shù)據(jù)的 get 方法,獲取最新數(shù)據(jù)。
至此,Vue 異步更新隊(duì)列的核心代碼我們就分析完了,為了便于理清思路,我們來(lái)一張圖總結(jié)一下
關(guān)于 Vue.$nextTick
我們都知道 . n e x t T i c k 方 法 , 其 實(shí) 這 個(gè) ? ? .nextTick 方法,其實(shí)這個(gè) ** .nextTick 方法,其實(shí)這個(gè)??nextTick** 方法就是直接調(diào)用的上面的 nextTick 方法
from src/core/instance/render.js
Vue.prototype.$nextTick = function (fn: Function) {return nextTick(fn, this)}
- 由上面的代碼我們可以看出,$nextTick 是將我們傳入的回調(diào)函數(shù)加入到了異步更新隊(duì)列,所以它才能實(shí)現(xiàn) dom 更新后回調(diào)
注意,$nextTick() 是會(huì)將我們傳入的函數(shù)加入到異步更新隊(duì)列中的,但是這里有個(gè)問(wèn)題,如果我們想獲得 dom 更新后的數(shù)據(jù),我們應(yīng)該把該邏輯放到更新操作之后
因?yàn)榧尤氘惒疥?duì)列先后的問(wèn)題,如果我們?cè)诟聰?shù)據(jù)之前入隊(duì)的話 ,是獲取不到更新之后的數(shù)據(jù)的
總結(jié)
總結(jié)起來(lái)就是,當(dāng)觸發(fā)數(shù)據(jù)更新通知時(shí),dep 通知 watcher 進(jìn)行數(shù)據(jù)更新,這時(shí) watcher 會(huì)將自己加入到一個(gè)異步的更新隊(duì)列中。然后更新隊(duì)列會(huì)將傳入的更新操作進(jìn)行批量處理。
這樣就達(dá)到了多次更新同時(shí)完成,提升了用戶體驗(yàn),減少了瀏覽器的開(kāi)銷(xiāo),增強(qiáng)了性能。