摘要:我們發(fā)現(xiàn)默認(rèn)是使用異步執(zhí)行更新。優(yōu)先使用,在不存在的情況下使用,這兩個(gè)方法的回調(diào)函數(shù)都會(huì)在中執(zhí)行,它們會(huì)比更早執(zhí)行,所以優(yōu)先使用。是最后的一種備選方案,它會(huì)將回調(diào)函數(shù)加入中,等到執(zhí)行。
寫(xiě)在前面
因?yàn)閷?duì)Vue.js很感興趣,而且平時(shí)工作的技術(shù)棧也是Vue.js,這幾個(gè)月花了些時(shí)間研究學(xué)習(xí)了一下Vue.js源碼,并做了總結(jié)與輸出。
文章的原地址:https://github.com/answershuto/learnVue。
在學(xué)習(xí)過(guò)程中,為Vue加上了中文的注釋https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以對(duì)其他想學(xué)習(xí)Vue源碼的小伙伴有所幫助。
可能會(huì)有理解存在偏差的地方,歡迎提issue指出,共同學(xué)習(xí),共同進(jìn)步。
在使用vue.js的時(shí)候,有時(shí)候因?yàn)橐恍┨囟ǖ臉I(yè)務(wù)場(chǎng)景,不得不去操作DOM,比如這樣:
{{test}}
export default { data () { return { test: "begin" }; }, methods () { handleClick () { this.test = "end"; console.log(this.$refs.test.innerText);//打印“begin” } } }
打印的結(jié)果是begin,為什么我們明明已經(jīng)將test設(shè)置成了“end”,獲取真實(shí)DOM節(jié)點(diǎn)的innerText卻沒(méi)有得到我們預(yù)期中的“end”,而是得到之前的值“begin”呢?
Watcher隊(duì)列帶著疑問(wèn),我們找到了Vue.js源碼的Watch實(shí)現(xiàn)。當(dāng)某個(gè)響應(yīng)式數(shù)據(jù)發(fā)生變化的時(shí)候,它的setter函數(shù)會(huì)通知閉包中的Dep,Dep則會(huì)調(diào)用它管理的所有Watch對(duì)象。觸發(fā)Watch對(duì)象的update實(shí)現(xiàn)。我們來(lái)看一下update的實(shí)現(xiàn)。
update () { /* istanbul ignore else */ if (this.lazy) { this.dirty = true } else if (this.sync) { /*同步則執(zhí)行run直接渲染視圖*/ this.run() } else { /*異步推送到觀察者隊(duì)列中,下一個(gè)tick時(shí)調(diào)用。*/ queueWatcher(this) } }
我們發(fā)現(xiàn)Vue.js默認(rèn)是使用異步執(zhí)行DOM更新。
當(dāng)異步執(zhí)行update的時(shí)候,會(huì)調(diào)用queueWatcher函數(shù)。
/*將一個(gè)觀察者對(duì)象push進(jìn)觀察者隊(duì)列,在隊(duì)列中已經(jīng)存在相同的id則該觀察者對(duì)象將被跳過(guò),除非它是在隊(duì)列被刷新時(shí)推送*/ export function queueWatcher (watcher: Watcher) { /*獲取watcher的id*/ const id = watcher.id /*檢驗(yàn)id是否存在,已經(jīng)存在則直接跳過(guò),不存在則標(biāo)記哈希表has,用于下次檢驗(yàn)*/ if (has[id] == null) { has[id] = true if (!flushing) { /*如果沒(méi)有flush掉,直接push到隊(duì)列中即可*/ 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 - 1 while (i >= 0 && queue[i].id > watcher.id) { i-- } queue.splice(Math.max(i, index) + 1, 0, watcher) } // queue the flush if (!waiting) { waiting = true nextTick(flushSchedulerQueue) } } }
查看queueWatcher的源碼我們發(fā)現(xiàn),Watch對(duì)象并不是立即更新視圖,而是被push進(jìn)了一個(gè)隊(duì)列queue,此時(shí)狀態(tài)處于waiting的狀態(tài),這時(shí)候會(huì)繼續(xù)會(huì)有Watch對(duì)象被push進(jìn)這個(gè)隊(duì)列queue,等待下一個(gè)tick時(shí),這些Watch對(duì)象才會(huì)被遍歷取出,更新視圖。同時(shí),id重復(fù)的Watcher不會(huì)被多次加入到queue中去,因?yàn)樵谧罱K渲染時(shí),我們只需要關(guān)心數(shù)據(jù)的最終結(jié)果。
那么,什么是下一個(gè)tick?
nextTickvue.js提供了一個(gè)nextTick函數(shù),其實(shí)也就是上面調(diào)用的nextTick。
nextTick的實(shí)現(xiàn)比較簡(jiǎn)單,執(zhí)行的目的是在microtask或者task中推入一個(gè)funtion,在當(dāng)前棧執(zhí)行完畢(也行還會(huì)有一些排在前面的需要執(zhí)行的任務(wù))以后執(zhí)行nextTick傳入的funtion,看一下源碼:
/** * Defer a task to execute it asynchronously. */ /* 延遲一個(gè)任務(wù)使其異步執(zhí)行,在下一個(gè)tick時(shí)執(zhí)行,一個(gè)立即執(zhí)行函數(shù),返回一個(gè)function 這個(gè)函數(shù)的作用是在task或者microtask中推入一個(gè)timerFunc,在當(dāng)前調(diào)用棧執(zhí)行完以后以此執(zhí)行直到執(zhí)行到timerFunc 目的是延遲到當(dāng)前調(diào)用棧執(zhí)行完以后執(zhí)行 */ export const nextTick = (function () { /*存放異步執(zhí)行的回調(diào)*/ const callbacks = [] /*一個(gè)標(biāo)記位,如果已經(jīng)有timerFunc被推送到任務(wù)隊(duì)列中去則不需要重復(fù)推送*/ let pending = false /*一個(gè)函數(shù)指針,指向函數(shù)將被推送到任務(wù)隊(duì)列中,等到主線程任務(wù)執(zhí)行完時(shí),任務(wù)隊(duì)列中的timerFunc被調(diào)用*/ let timerFunc /*下一個(gè)tick時(shí)的回調(diào)*/ function nextTickHandler () { /*一個(gè)標(biāo)記位,標(biāo)記等待狀態(tài)(即函數(shù)已經(jīng)被推入任務(wù)隊(duì)列或者主線程,已經(jīng)在等待當(dāng)前棧執(zhí)行完畢去執(zhí)行),這樣就不需要在push多個(gè)回調(diào)到callbacks時(shí)將timerFunc多次推入任務(wù)隊(duì)列或者主線程*/ pending = false /*執(zhí)行所有callback*/ const copies = callbacks.slice(0) callbacks.length = 0 for (let i = 0; i < copies.length; i++) { copies[i]() } } // the nextTick behavior leverages the microtask queue, which can be accessed // via either native Promise.then or MutationObserver. // MutationObserver has wider support, however it is seriously bugged in // UIWebView in iOS >= 9.3.3 when triggered in touch event handlers. It // completely stops working after triggering a few times... so, if native // Promise is available, we will use it: /* istanbul ignore if */ /* 這里解釋一下,一共有Promise、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法 優(yōu)先使用Promise,在Promise不存在的情況下使用MutationObserver,這兩個(gè)方法都會(huì)在microtask中執(zhí)行,會(huì)比setTimeout更早執(zhí)行,所以優(yōu)先使用。 如果上述兩種方法都不支持的環(huán)境則會(huì)使用setTimeout,在task尾部推入這個(gè)函數(shù),等待調(diào)用執(zhí)行。 參考:https://www.zhihu.com/question/55364497 */ if (typeof Promise !== "undefined" && isNative(Promise)) { /*使用Promise*/ var p = Promise.resolve() var logError = err => { console.error(err) } timerFunc = () => { p.then(nextTickHandler).catch(logError) // in problematic UIWebViews, Promise.then doesn"t completely break, but // it can get stuck in a weird state where callbacks are pushed into the // microtask queue but the queue isn"t being flushed, until the browser // needs to do some other work, e.g. handle a timer. Therefore we can // "force" the microtask queue to be flushed by adding an empty timer. if (isIOS) setTimeout(noop) } } else if (typeof MutationObserver !== "undefined" && ( isNative(MutationObserver) || // PhantomJS and iOS 7.x MutationObserver.toString() === "[object MutationObserverConstructor]" )) { // use MutationObserver where native Promise is not available, // e.g. PhantomJS IE11, iOS7, Android 4.4 /*新建一個(gè)textNode的DOM對(duì)象,用MutationObserver綁定該DOM并指定回調(diào)函數(shù),在DOM變化的時(shí)候則會(huì)觸發(fā)回調(diào),該回調(diào)會(huì)進(jìn)入主線程(比任務(wù)隊(duì)列優(yōu)先執(zhí)行),即textNode.data = String(counter)時(shí)便會(huì)觸發(fā)回調(diào)*/ var counter = 1 var observer = new MutationObserver(nextTickHandler) var textNode = document.createTextNode(String(counter)) observer.observe(textNode, { characterData: true }) timerFunc = () => { counter = (counter + 1) % 2 textNode.data = String(counter) } } else { // fallback to setTimeout /* istanbul ignore next */ /*使用setTimeout將回調(diào)推入任務(wù)隊(duì)列尾部*/ timerFunc = () => { setTimeout(nextTickHandler, 0) } } /* 推送到隊(duì)列中下一個(gè)tick時(shí)執(zhí)行 cb 回調(diào)函數(shù) ctx 上下文 */ return function queueNextTick (cb?: Function, ctx?: Object) { let _resolve /*cb存到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() } if (!cb && typeof Promise !== "undefined") { return new Promise((resolve, reject) => { _resolve = resolve }) } } })()
它是一個(gè)立即執(zhí)行函數(shù),返回一個(gè)queueNextTick接口。
傳入的cb會(huì)被push進(jìn)callbacks中存放起來(lái),然后執(zhí)行timerFunc(pending是一個(gè)狀態(tài)標(biāo)記,保證timerFunc在下一個(gè)tick之前只執(zhí)行一次)。
timerFunc是什么?
看了源碼發(fā)現(xiàn)timerFunc會(huì)檢測(cè)當(dāng)前環(huán)境而不同實(shí)現(xiàn),其實(shí)就是按照Promise,MutationObserver,setTimeout優(yōu)先級(jí),哪個(gè)存在使用哪個(gè),最不濟(jì)的環(huán)境下使用setTimeout。
這里解釋一下,一共有Promise、MutationObserver以及setTimeout三種嘗試得到timerFunc的方法。
優(yōu)先使用Promise,在Promise不存在的情況下使用MutationObserver,這兩個(gè)方法的回調(diào)函數(shù)都會(huì)在microtask中執(zhí)行,它們會(huì)比setTimeout更早執(zhí)行,所以優(yōu)先使用。
如果上述兩種方法都不支持的環(huán)境則會(huì)使用setTimeout,在task尾部推入這個(gè)函數(shù),等待調(diào)用執(zhí)行。
為什么要優(yōu)先使用microtask?我在顧軼靈在知乎的回答中學(xué)習(xí)到:
JS 的 event loop 執(zhí)行時(shí)會(huì)區(qū)分 task 和 microtask,引擎在每個(gè) task 執(zhí)行完畢,從隊(duì)列中取下一個(gè) task 來(lái)執(zhí)行之前,會(huì)先執(zhí)行完所有 microtask 隊(duì)列中的 microtask。 setTimeout 回調(diào)會(huì)被分配到一個(gè)新的 task 中執(zhí)行,而 Promise 的 resolver、MutationObserver 的回調(diào)都會(huì)被安排到一個(gè)新的 microtask 中執(zhí)行,會(huì)比 setTimeout 產(chǎn)生的 task 先執(zhí)行。 要?jiǎng)?chuàng)建一個(gè)新的 microtask,優(yōu)先使用 Promise,如果瀏覽器不支持,再嘗試 MutationObserver。 實(shí)在不行,只能用 setTimeout 創(chuàng)建 task 了。 為啥要用 microtask? 根據(jù) HTML Standard,在每個(gè) task 運(yùn)行完以后,UI 都會(huì)重渲染,那么在 microtask 中就完成數(shù)據(jù)更新,當(dāng)前 task 結(jié)束就可以得到最新的 UI 了。 反之如果新建一個(gè) task 來(lái)做數(shù)據(jù)更新,那么渲染就會(huì)進(jìn)行兩次。 參考顧軼靈知乎的回答:https://www.zhihu.com/question/55364497/answer/144215284
首先是Promise,(Promise.resolve()).then()可以在microtask中加入它的回調(diào),
MutationObserver新建一個(gè)textNode的DOM對(duì)象,用MutationObserver綁定該DOM并指定回調(diào)函數(shù),在DOM變化的時(shí)候則會(huì)觸發(fā)回調(diào),該回調(diào)會(huì)進(jìn)入microtask,即textNode.data = String(counter)時(shí)便會(huì)加入該回調(diào)。
setTimeout是最后的一種備選方案,它會(huì)將回調(diào)函數(shù)加入task中,等到執(zhí)行。
綜上,nextTick的目的就是產(chǎn)生一個(gè)回調(diào)函數(shù)加入task或者microtask中,當(dāng)前棧執(zhí)行完以后(可能中間還有別的排在前面的函數(shù))調(diào)用該回調(diào)函數(shù),起到了異步觸發(fā)(即下一個(gè)tick時(shí)觸發(fā))的目的。
flushSchedulerQueue/*Github:https://github.com/answershuto*/ /** * Flush both queues and run the watchers. */ /*nextTick的回調(diào)函數(shù),在下一個(gè)tick時(shí)flush掉兩個(gè)隊(duì)列同時(shí)運(yùn)行watchers*/ function flushSchedulerQueue () { flushing = true let watcher, id // Sort queue before flush. // This ensures that: // 1. Components are updated from parent to child. (because parent is always // created before the child) // 2. A component"s user watchers are run before its render watcher (because // user watchers are created before the render watcher) // 3. If a component is destroyed during a parent component"s watcher run, // its watchers can be skipped. /* 給queue排序,這樣做可以保證: 1.組件更新的順序是從父組件到子組件的順序,因?yàn)楦附M件總是比子組件先創(chuàng)建。 2.一個(gè)組件的user watchers比render watcher先運(yùn)行,因?yàn)閡ser watchers往往比render watcher更早創(chuàng)建 3.如果一個(gè)組件在父組件watcher運(yùn)行期間被銷毀,它的watcher執(zhí)行將被跳過(guò)。 */ queue.sort((a, b) => a.id - b.id) // do not cache length because more watchers might be pushed // as we run existing watchers /*這里不用index = queue.length;index > 0; index--的方式寫(xiě)是因?yàn)椴灰獙ength進(jìn)行緩存,因?yàn)樵趫?zhí)行處理現(xiàn)有watcher對(duì)象期間,更多的watcher對(duì)象可能會(huì)被push進(jìn)queue*/ for (index = 0; index < queue.length; index++) { watcher = queue[index] id = watcher.id /*將has的標(biāo)記刪除*/ has[id] = null /*執(zhí)行watcher*/ watcher.run() // in dev build, check and stop circular updates. /* 在測(cè)試環(huán)境中,檢測(cè)watch是否在死循環(huán)中 比如這樣一種情況 watch: { test () { this.test++; } } 持續(xù)執(zhí)行了一百次watch代表可能存在死循環(huán) */ if (process.env.NODE_ENV !== "production" && has[id] != null) { circular[id] = (circular[id] || 0) + 1 if (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 } } } // keep copies of post queues before resetting state /**/ /*得到隊(duì)列的拷貝*/ const activatedQueue = activatedChildren.slice() const updatedQueue = queue.slice() /*重置調(diào)度者的狀態(tài)*/ resetSchedulerState() // call component updated and activated hooks /*使子組件狀態(tài)都改編成active同時(shí)調(diào)用activated鉤子*/ callActivatedHooks(activatedQueue) /*調(diào)用updated鉤子*/ callUpdateHooks(updatedQueue) // devtool hook /* istanbul ignore if */ if (devtools && config.devtools) { devtools.emit("flush") } }
flushSchedulerQueue是下一個(gè)tick時(shí)的回調(diào)函數(shù),主要目的是執(zhí)行Watcher的run函數(shù),用來(lái)更新視圖
為什么要異步更新視圖來(lái)看一下下面這一段代碼
{{test}}
export default { data () { return { test: 0 }; }, created () { for(let i = 0; i < 1000; i++) { this.test++; } } }
現(xiàn)在有這樣的一種情況,created的時(shí)候test的值會(huì)被++循環(huán)執(zhí)行1000次。
每次++時(shí),都會(huì)根據(jù)響應(yīng)式觸發(fā)setter->Dep->Watcher->update->patch。
如果這時(shí)候沒(méi)有異步更新視圖,那么每次++都會(huì)直接操作DOM更新視圖,這是非常消耗性能的。
所以Vue.js實(shí)現(xiàn)了一個(gè)queue隊(duì)列,在下一個(gè)tick的時(shí)候會(huì)統(tǒng)一執(zhí)行queue中Watcher的run。同時(shí),擁有相同id的Watcher不會(huì)被重復(fù)加入到該queue中去,所以不會(huì)執(zhí)行1000次Watcher的run。最終更新視圖只會(huì)直接將test對(duì)應(yīng)的DOM的0變成1000。
保證更新視圖操作DOM的動(dòng)作是在當(dāng)前棧執(zhí)行完以后下一個(gè)tick的時(shí)候調(diào)用,大大優(yōu)化了性能。
所以我們需要在修改data中的數(shù)據(jù)后訪問(wèn)真實(shí)的DOM節(jié)點(diǎn)更新后的數(shù)據(jù),只需要這樣,我們把文章第一個(gè)例子進(jìn)行修改。
{{test}}
export default { data () { return { test: "begin" }; }, methods () { handleClick () { this.test = "end"; this.$nextTick(() => { console.log(this.$refs.test.innerText);//打印"end" }); console.log(this.$refs.test.innerText);//打印“begin” } } }
使用Vue.js的global API的$nextTick方法,即可在回調(diào)中獲取已經(jīng)更新好的DOM實(shí)例了。
關(guān)于作者:染陌
Email:answershuto@gmail.com or answershuto@126.com
Github: https://github.com/answershuto
Blog:http://answershuto.github.io/
知乎主頁(yè):https://www.zhihu.com/people/cao-yang-49/activities
知乎專欄:https://zhuanlan.zhihu.com/ranmo
掘金: https://juejin.im/user/58f87ae844d9040069ca7507
osChina:https://my.oschina.net/u/3161824/blog
轉(zhuǎn)載請(qǐng)注明出處,謝謝。
歡迎關(guān)注我的公眾號(hào)
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://specialneedsforspecialkids.com/yun/88651.html
摘要:核心的異步延遲函數(shù),用于異步延遲調(diào)用函數(shù)優(yōu)先使用原生原本支持更廣,但在的中,觸摸事件處理程序中觸發(fā)會(huì)產(chǎn)生嚴(yán)重錯(cuò)誤的,回調(diào)被推入隊(duì)列但是隊(duì)列可能不會(huì)如期執(zhí)行。 淺析 Vue 2.6 中的 nextTick 方法。 事件循環(huán) JS 的 事件循環(huán) 和 任務(wù)隊(duì)列 其實(shí)是理解 nextTick 概念的關(guān)鍵。這個(gè)網(wǎng)上其實(shí)有很多優(yōu)質(zhì)的文章做了詳細(xì)介紹,我就簡(jiǎn)單過(guò)過(guò)了。 以下內(nèi)容適用于瀏覽器端 JS,...
摘要:而和的延遲明顯是小于的。因?yàn)榈氖录C(jī)制是通過(guò)事件隊(duì)列來(lái)調(diào)度執(zhí)行,會(huì)等主進(jìn)程執(zhí)行空閑后進(jìn)行調(diào)度,所以先回去等待所有的進(jìn)程執(zhí)行完成之后再去一次更新。因?yàn)槭紫扔|發(fā)了,導(dǎo)致觸發(fā)了的,從而將更新操作進(jìn)入的事件隊(duì)列。這種情況會(huì)導(dǎo)致順序成為了。 背景 我們先來(lái)看一段Vue的執(zhí)行代碼: export default { data () { return { msg: 0 ...
摘要:而和的延遲明顯是小于的。因?yàn)榈氖录C(jī)制是通過(guò)事件隊(duì)列來(lái)調(diào)度執(zhí)行,會(huì)等主進(jìn)程執(zhí)行空閑后進(jìn)行調(diào)度,所以先回去等待所有的進(jìn)程執(zhí)行完成之后再去一次更新。因?yàn)槭紫扔|發(fā)了,導(dǎo)致觸發(fā)了的,從而將更新操作進(jìn)入的事件隊(duì)列。這種情況會(huì)導(dǎo)致順序成為了。 背景 我們先來(lái)看一段Vue的執(zhí)行代碼: export default { data () { return { msg: 0 ...
摘要:在查詢了各種資料后,總結(jié)了一下其原理和用途,如有錯(cuò)誤,請(qǐng)不吝賜教。截取關(guān)鍵部分如下具體來(lái)說(shuō),異步執(zhí)行的運(yùn)行機(jī)制如下。知乎上的例子改變數(shù)據(jù)想要立即使用更新后的。需要注意的是,在和階段,如果需要操作渲染后的試圖,也要使用方法。 對(duì)于 Vue.nextTick 方法,自己有些疑惑。在查詢了各種資料后,總結(jié)了一下其原理和用途,如有錯(cuò)誤,請(qǐng)不吝賜教。 概覽 官方文檔說(shuō)明: 用法: 在下次 DO...
閱讀 3328·2023-04-26 00:07
閱讀 3922·2021-11-23 10:08
閱讀 2940·2021-11-22 09:34
閱讀 859·2021-09-22 15:27
閱讀 1749·2019-08-30 15:54
閱讀 3743·2019-08-30 14:07
閱讀 913·2019-08-30 11:12
閱讀 678·2019-08-29 18:44