<kbd id="5sdj3"></kbd>
<th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>

    Vue的異步更新實現(xiàn)原理

    共 3459字,需瀏覽 7分鐘

     ·

    2020-12-22 22:45


    作者:Liqiuyue

    鏈接:https://juejin.cn/post/6908264284032073736

    最近面試總是會被問到這么一個問題:在使用vue的時候,將for循環(huán)中聲明的變量i從1增加到100,然后將i展示到頁面上,頁面上的i是從1跳到100,還是會怎樣?答案當(dāng)然是只會顯示100,并不會有跳轉(zhuǎn)的過程。

    怎么可以讓頁面上有從1到100顯示的過程呢,就是用setTimeout或者Promise.then等方法去模擬。

    講道理,如果不在vue里,單獨運行這段程序的話,輸出一定是從1到100,但是為什么在vue中就不一樣了呢?

    for(let?i=1;?i<=100;?i++){
    ?console.log(i);
    }

    這就涉及到Vue底層的異步更新原理,也要說一說nextTick的實現(xiàn)。不過在說nextTick之前,有必要先介紹一下JS的事件運行機制。

    JS運行機制

    眾所周知,JS是基于事件循環(huán)的單線程的語言。執(zhí)行的步驟大致是:

    1. 當(dāng)代碼執(zhí)行時,所有同步的任務(wù)都在主線程上執(zhí)行,形成一個執(zhí)行棧;
    2. 在主線程之外還有一個任務(wù)隊列(task queue),只要異步任務(wù)有了運行結(jié)果就在任務(wù)隊列中放置一個事件;
    3. 一旦執(zhí)行棧中所有同步任務(wù)執(zhí)行完畢(主線程代碼執(zhí)行完畢),此時主線程不會空閑而是去讀取任務(wù)隊列。此時,異步的任務(wù)就結(jié)束等待的狀態(tài)被執(zhí)行。
    4. 主線程不斷重復(fù)以上的步驟。

    我們把主線程執(zhí)行一次的過程叫一個tick,所以nextTick就是下一個tick的意思,也就是說用nextTick的場景就是我們想在下一個tick做一些事的時候。

    所有的異步任務(wù)結(jié)果都是通過任務(wù)隊列來調(diào)度的。而任務(wù)分為兩類:宏任務(wù)(macro task)和微任務(wù)(micro task)。它們之間的執(zhí)行規(guī)則就是每個宏任務(wù)結(jié)束后都要將所有微任務(wù)清空。常見的宏任務(wù)有setTimeout/MessageChannel/postMessage/setImmediate,微任務(wù)有MutationObsever/Promise.then。

    想要透徹學(xué)習(xí)事件循環(huán),推薦Jake在JavaScript全球開發(fā)者大會的演講,保證講懂!

    nextTick原理

    派發(fā)更新

    大家都知道vue的響應(yīng)式的靠依賴收集和派發(fā)更新來實現(xiàn)的。在修改數(shù)組之后的派發(fā)更新過程,會觸發(fā)setter的邏輯,執(zhí)行dep.notify()

    //?src/core/observer/watcher.js
    class?Dep?{
    ?notify()?{
    ?????//subs是Watcher的實例數(shù)組
    ?????const?subs?=?this.subs.slice()
    ????????for(let?i=0,?l=subs.length;?i?????????subs[i].update()
    ????????}
    ????}
    }

    遍歷subs里每一個Watcher實例,然后調(diào)用實例的update方法,下面我們來看看update是怎么去更新的:

    class?Watcher?{
    ?update()?{
    ?????...
    ?????//各種情況判斷之后
    ????????else{
    ?????????queueWatcher(this)
    ????????}
    ????}
    }

    update執(zhí)行后又走到了queueWatcher,那就繼續(xù)去看看queueWatcher干啥了(希望不要繼續(xù)套娃了:

    //queueWatcher?定義在?src/core/observer/scheduler.js
    const?queue:?Array?=?[]
    let?has:?{?[key:?number]:??true?}?=?{}
    let?waiting?=?false
    let?flushing?=?false
    let?index?=?0

    export?function?queueWatcher(watcher:?Watcher)?{
    ?const?id?=?watcher.id
    ????//根據(jù)id是否重復(fù)做優(yōu)化
    ????if(has[id]?==?null){
    ?????has[id]?=?true
    ????????if(!flushing){
    ?????????queue.push(watcher)
    ????????}else{
    ?????????let?i=queue.length?-?1
    ????????????while(i?>?index?&&?queue[i].id?>?watcher.id){
    ?????????????i--
    ????????????}
    ????????????queue.splice(i?+?1,?0,?watcher)
    ????????}
    ???????
    ?????if(!waiting){
    ??????waiting?=?true
    ?????????//flushSchedulerQueue函數(shù):?Flush?both?queues?and?run?the?watchers
    ?????????nextTick(flushSchedulerQueue)
    ?????}
    ????}
    }

    這里queue在pushwatcher時是根據(jù)idflushing做了一些優(yōu)化的,并不會每次數(shù)據(jù)改變都觸發(fā)watcher的回調(diào),而是把這些watcher先添加到?個隊列?,然后在nextTick后執(zhí)?flushSchedulerQueue。

    flushSchedulerQueue函數(shù)是保存更新事件的queue的一些加工,讓更新可以滿足Vue更新的生命周期。

    這里也解釋了為什么for循環(huán)不能導(dǎo)致頁面更新,因為for是主線程的代碼,在一開始執(zhí)行數(shù)據(jù)改變就會將它push到queue里,等到for里的代碼執(zhí)行完畢后i的值已經(jīng)變化為100時,這時vue才走到nextTick(flushSchedulerQueue)這一步。

    nextTick源碼

    接著打開vue2.x的源碼,目錄core/util/next-tick.js,代碼量很小,加上注釋才110行,是比較好理解的。

    const?callbacks?=?[]
    let?pending?=?false

    export?function?nextTick?(cb?:?Function,?ctx?:?Object)?{
    ??let?_resolve
    ??callbacks.push(()?=>?{
    ????if?(cb)?{
    ??????try?{
    ????????cb.call(ctx)
    ??????}?catch?(e)?{
    ????????handleError(e,?ctx,?'nextTick')
    ??????}
    ????}?else?if?(_resolve)?{
    ??????_resolve(ctx)
    ????}
    ??})
    ??if?(!pending)?{
    ????pending?=?true
    ????timerFunc()
    ??}

    首先將傳入的回調(diào)函數(shù)cb(上節(jié)的flushSchedulerQueue)壓入callbacks數(shù)組,最后通過timerFunc函數(shù)一次性解決。

    let?timerFunc

    if?(typeof?Promise?!==?'undefined'?&&?isNative(Promise))?{
    ??const?p?=?Promise.resolve()
    ??timerFunc?=?()?=>?{
    ????p.then(flushCallbacks)
    ????if?(isIOS)?setTimeout(noop)
    ????}
    ??isUsingMicroTask?=?true
    }?else?if?(!isIE?&&?typeof?MutationObserver?!==?'undefined'?&&?(
    ??isNative(MutationObserver)?||
    ??//?PhantomJS?and?iOS?7.x
    ??MutationObserver.toString()?===?'[object?MutationObserverConstructor]'
    ))?{
    ??let?counter?=?1
    ??const?observer?=?new?MutationObserver(flushCallbacks)
    ??const?textNode?=?document.createTextNode(String(counter))
    ??observer.observe(textNode,?{
    ????characterData:?true
    ??})
    ??timerFunc?=?()?=>?{
    ????counter?=?(counter?+?1)?%?2
    ????textNode.data?=?String(counter)
    ??}
    ??isUsingMicroTask?=?true
    }?else?if?(typeof?setImmediate?!==?'undefined'?&&?isNative(setImmediate))?{
    ??timerFunc?=?()?=>?{
    ????setImmediate(flushCallbacks)
    ??}
    }?else?{
    ??timerFunc?=?()?=>?{
    ????setTimeout(flushCallbacks,?0)
    ??}
    }

    timerFunc下面一大片if else是在判斷不同的設(shè)備和不同情況下選用哪種特性去實現(xiàn)異步任務(wù):優(yōu)先檢測是否原生?持Promise,不?持的話再去檢測是否?持MutationObserver,如果都不行就只能嘗試宏任務(wù)實現(xiàn),首先是setImmediate,這是?個?版本 IE 和 Edge 才?持的特性,如果都不?持的話最后就會降級為 setTimeout 0。

    這?使?callbacks?不是直接在nextTick中執(zhí)?回調(diào)函數(shù)的原因是保證在同?個 tick 內(nèi)多次執(zhí)?nextTick,不會開啟多個異步任務(wù),?把這些異步任務(wù)都壓成?個同步任務(wù),在下?個 tick 執(zhí)?完畢。

    nextTick使用

    nextTick不僅是vue的源碼文件,更是vue的一個全局API。下面來看看怎么使用吧。

    當(dāng)設(shè)置 vm.someData = 'new value',該組件不會立即重新渲染。當(dāng)刷新隊列時,組件會在下一個事件循環(huán)tick中更新。多數(shù)情況我們不需要關(guān)心這個過程,但是如果你想基于更新后的 DOM 狀態(tài)來做點什么,這就可能會有些棘手。雖然 Vue.js 通常鼓勵開發(fā)人員使用數(shù)據(jù)驅(qū)動的方式思考,避免直接接觸 DOM,但是有時我們必須要這么做。為了在數(shù)據(jù)變化之后等待 Vue 完成更新 DOM,可以在數(shù)據(jù)變化之后立即使用Vue.nextTick(callback)。這樣回調(diào)函數(shù)將在 DOM 更新完成后被調(diào)用。

    官網(wǎng)用例:

    <div?id="example">{{message}}div>
    var?vm?=?new?Vue({
    ??el:?'#example',
    ??data:?{
    ????message:?'123'
    ??}
    })
    vm.message?=?'new?message'?//?更改數(shù)據(jù)

    vm.$el.textContent?===?'new?message'?//?false
    Vue.nextTick(function?()?{
    ??vm.$el.textContent?===?'new?message'?//?true
    })

    并且因為$nextTick() 返回一個 Promise 對象,所以也可以使用async/await 語法去處理事件,非常方便。

    關(guān)注數(shù):10億+?文章數(shù):10億+
    粉絲量:10億+?點擊量:10億+

    ?


    懸賞博主專區(qū)請掃描這里


    喜愛數(shù):?1億+?發(fā)帖數(shù):?1億+
    回帖數(shù):?1億+?結(jié)貼率:?99.9%


    —————END—————



    喜歡本文的朋友,歡迎關(guān)注公眾號?程序員哆啦A夢,收看更多精彩內(nèi)容

    點個[在看],是對小達最大的支持!


    如果覺得這篇文章還不錯,來個【分享、點贊、在看】三連吧,讓更多的人也看到~

    瀏覽 53
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報
    評論
    圖片
    表情
    推薦
    點贊
    評論
    收藏
    分享

    手機掃一掃分享

    分享
    舉報

    <kbd id="5sdj3"></kbd>
    <th id="5sdj3"></th>

  • <dd id="5sdj3"><form id="5sdj3"></form></dd>
    <td id="5sdj3"><form id="5sdj3"><big id="5sdj3"></big></form></td><del id="5sdj3"></del>

  • <dd id="5sdj3"></dd>
    <dfn id="5sdj3"></dfn>
  • <th id="5sdj3"></th>
    <tfoot id="5sdj3"><menuitem id="5sdj3"></menuitem></tfoot>

  • <td id="5sdj3"><form id="5sdj3"><menu id="5sdj3"></menu></form></td>
  • <kbd id="5sdj3"><form id="5sdj3"></form></kbd>
    伊人色色影院 | 深爱五月网| 婷婷亚洲丁香 | 日韩欧美在线免费观看 | 在线观看AA |