05Vue源碼剖析2

Vue 源碼剖析2

異步更新隊列

Vue 高效的秘訣是一套批量、異步的更新策略

概念解釋

image.png
  • 事件循環(huán) Event Loop:瀏覽器為了協(xié)調(diào)事件處理、腳本執(zhí)行、網(wǎng)絡(luò)請求和渲染等任務(wù)而制定的工作機制。

  • 宏任務(wù) Task:代表一個個離散的羞迷、獨立的工作單元。瀏覽器完成一個宏任務(wù)画饥,在下一個宏任務(wù)執(zhí)行開始前衔瓮,會對頁面進行重新渲染。主要包括創(chuàng)建文檔對象抖甘、解析 HTML热鞍、執(zhí)行主線 JS 代碼以及各種事件如頁面加載、輸入衔彻、網(wǎng)絡(luò)事件和定時器等薇宠。

  • 微任務(wù):微任務(wù)是更小的任務(wù),是在當前宏任務(wù)執(zhí)行結(jié)束后立即執(zhí)行的任務(wù)米奸。如果存在微任務(wù)昼接,瀏覽器會清空微任務(wù)之后再重新渲染爽篷。微任務(wù)的例子有 Promise 回調(diào)函數(shù)悴晰、DOM 變化等。

體驗一下

Vue 中的具體實現(xiàn)

image.png
  • 異步:只要偵聽到數(shù)據(jù)變化,Vue 將開啟一個隊列铡溪,并緩沖在同一事件循環(huán)中發(fā)生的所有數(shù)據(jù)變更漂辐。

  • 批量:如果同一個 Watcher 被多次觸發(fā),只會被推入到隊列中一次棕硫。去重對于避免不必要的計算和 DOM 操作是非常重要的髓涯。然后,在下一個的事件循環(huán) tick 中哈扮,Vue 刷新隊列執(zhí)行實際工作纬纪。

  • 異步策略:Vue 在內(nèi)部對異步隊列嘗試使用原生的 Promise.then、MutationObserver 或 setTmmediate滑肉,如果執(zhí)行環(huán)境都不支持包各,則會采用 setTimeout 代替。

update() core\observer\watcher.js

dep.notify() 之后 watcher 執(zhí)行更新靶庙,執(zhí)行入隊操作

queueWatcher(watcher) core\observer\scheduler.js

執(zhí)行 watcher 入隊操作

nextTick(flushSchedulerQueue) core\util\next-tick.js

nextTick 按照特定異步策略執(zhí)行隊列操作

測試代碼:03-timerFunc.html

watcher 中 update 執(zhí)行三次问畅,但 run 僅執(zhí)行一次,且數(shù)值變化對 dom 的影響也不是立竿見影的六荒。

可以研究下相關(guān) API:vm.$nextTick(cb)

$nextTick 把傳入回調(diào)函數(shù)放入 callbacks 隊尾

$nextTick 原理執(zhí)行順序:

Promise==>MutationObserver==>SetImmediate==>setTimeout

虛擬 DOM

概念

虛擬 DOM(Vitual DOM)是對 DOM 的 JS 抽象表示护姆,他們是 JS 對象,能夠描述 DOM 結(jié)構(gòu)和關(guān)系掏击。應(yīng)用的各種狀態(tài)變化會作用于虛擬 DOM卵皂,最終映射到 DOM 上。

image.png

體驗虛擬 DOM

Vue 中虛擬 dom 基于 snabbdom 實現(xiàn)砚亭,安裝 snabbdom 并體驗

<!DOCTYPE html>
<html lang="en">

<head></head>

<body>
  <div id="app"></div>
  <!--安裝并引?snabbdom-->
  <script src="../../node_modules/snabbdom/dist/snabbdom.js"></script>
  <script>
    // 之前編寫的響應(yīng)式函數(shù)
    function defineReactive(obj, key, val) {
      Object.defineProperty(obj, key, {
        get() {
          return val
        },
        set(newVal) {
          val = newVal
          // 通知更新
          update()
        }
      })
    }
    // 導?patch的??init渐裂,h是產(chǎn)?vnode的??
    const { init, h } = snabbdom
    // 獲取patch函數(shù)
    const patch = init([])
    // 上次vnode,由patch()返回
    let vnode;
    // 更新函數(shù)钠惩,將數(shù)據(jù)操作轉(zhuǎn)換為dom操作柒凉,返回新vnode
    function update() {
      if (!vnode) {
        // 初始化,沒有上次vnode篓跛,傳?宿主元素和vnode
        vnode = patch(app, render())
      }
      else {
        // 更新膝捞,傳?新舊vnode對?并做更新
        vnode = patch(vnode, render())
      }
    }
    // 渲染函數(shù),返回vnode描述dom結(jié)構(gòu)
    function render() {
      return h('div', obj.foo)
    }
    // 數(shù)據(jù)
    const obj = {}
    // 定義響應(yīng)式
    defineReactive(obj, 'foo', '')
    // 賦?個?期作為初始值
    obj.foo = new Date().toLocaleTimeString()
    // 定時改變數(shù)據(jù)愧沟,更新函數(shù)會重新執(zhí)?
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString()
    }, 1000);
  </script>
</body>

</html>

優(yōu)點

  1. 虛擬 DOM 輕量蔬咬、快速:當它們發(fā)生變化時通過新舊虛擬 DOM 比對可以得到最小 DOM 操作量,配合異步更新策略減少刷新頻率沐寺,從而提升性能
patch(vnode, h('div', obj.foo))
  1. 跨平臺:將虛擬 dom 更新轉(zhuǎn)換為不同運行時特殊操作實現(xiàn)跨平臺
<script src="../../node_modules/snabbdom/dist/snabbdom-style.js"></script>
<script>
    // 增加style模塊
    const patch = init([snabbdom_style.default])
    function render() {
      // 添加節(jié)點樣式描述
      return h('div', {style: {color: 'red' } }, obj.foo)
    }
</script>
  1. 兼容性:還可以加入兼容性代碼增強操作的兼容性

必要性

vue 1.0 中有細粒度的數(shù)據(jù)變化偵測林艘,它是不需要虛擬 DOM 的,但是細粒度造成了大量開銷混坞,這對于大型項目來說是不可接受的狐援。因此钢坦,vue 2.0 選擇了中等粒度的解決方案,每?個組件?個 watcher 實例啥酱,這樣狀態(tài)變化時只能通知到組件爹凹,再通過引入虛擬 DOM 去進行比對和渲染。

整體流程

mountComponent() core/instance/lifecycle.js

渲染镶殷、更新組件

// 定義更新函數(shù)
const updateComponent = () => {
  // 實際調(diào)?是在lifeCycleMixin中定義的_update和renderMixin中定義的_render
  vm._update(vm._render(), hydrating)
}

_render core/instance/render.js

生成虛擬 dom

_update core\instance\lifecycle.js

update 負責更新 dom禾酱,轉(zhuǎn)換 vnode 為 dom

patch() platforms/web/runtime/index.js

patch是在平臺特有代碼中指定的

Vue.prototype.__patch__ = inBrowser ? patch : noop

測試代碼,examples\test\04-vdom.html

patch

patch 獲取

patch 是 createPatchFunction 的返回值绘趋,傳遞 nodeOps 和 modules 是 web 平臺特別實現(xiàn)

export const patch: Function = createPatchFunction({ nodeOps, modules })

platforms\web\runtime\node-ops.js

定義各種原生 dom 基礎(chǔ)操作方法

platforms\web\runtime\modules\index.js

modules 定義了屬性更新實現(xiàn)

watcher.run() => componentUpdate() => render() => update() => patch()

patch 實現(xiàn)

patch core\vdom\patch.js

首先進行樹級別比較颤陶,可能有三種情況:增刪改。

  • new VNode 不存在就刪陷遮;

  • old VNode 不存在就增指郁;

  • 都存在就執(zhí)行 diff 執(zhí)行更新

image.png

patchVnode

比較兩個 VNode,包括三種類型操作:屬性更新拷呆、文本更新闲坎、子節(jié)點更新

具體規(guī)則如下:

  1. 新老節(jié)點均有 children 子節(jié)點,則對子節(jié)點進行 diff 操作茬斧,調(diào)用 updateChildren

  2. 如果新節(jié)點有子節(jié)點點而老節(jié)點沒有子節(jié)點腰懂,先清空老節(jié)點的文本內(nèi)容,然后為其新增子節(jié)點

  3. 當新節(jié)點沒有子節(jié)點而老節(jié)點有子節(jié)點的時候项秉,則移除該節(jié)點的所有子節(jié)點

  4. 當新老節(jié)點都無子節(jié)點的時候绣溜,只是文本的替換

測試,04-vdom.html

image.png
// patchVnode過程分解
// 1.div#demo updateChildren
// 2.h1 updateChildren
// 3.text ?本相同跳過
// 4.p updateChildren
// 5.text setTextContent

updateChildren

updateChildren 主要作用是用?種較高效的方式比對新舊兩個 VNode 的 children 得出最小操作補丁娄蔼。執(zhí)行?個雙循環(huán)是傳統(tǒng)方式怖喻,Vue 中針對 web 場景特點做了特別的算法優(yōu)化,我們看圖說話:

image.png

在新老兩組 VNode 節(jié)點的左右頭尾兩側(cè)都有?個變量標記岁诉,在遍歷過程中這幾個變量都會向中間靠攏锚沸。

當oldStartIdx > oldEndIdx或者newStartIdx > newEndIdx時結(jié)束循環(huán)。

下面是遍歷規(guī)則:

首先涕癣,oldStartVnode哗蜈、oldEndVnode與newStartVnode、newEndVnode 兩兩交叉比較坠韩,共有4種比較方法距潘。

當 oldStartVnode 和 newStartVnode 或者 oldEndVnode 和 newEndVnode 滿足 sameVnode,直接將該 VNode 節(jié)點進行 patchVnode 即可只搁,不需再遍歷就完成了?次循環(huán)音比。如下圖:

image.png

如果 oldStartVnode 與 newEndVnode 滿足 sameVnode。說明 oldStartVnode 已經(jīng)跑到了 oldEndVnode 后面去了氢惋,進行 patchVnode 的同時還需要將真實 DOM 節(jié)點移動到 oldEndVnode 的后面洞翩。

image.png

如果 oldEndVnode 與 newStartVnode 滿足 sameVnode稽犁,說明 oldEndVnode 跑到了 oldStartVnode 的前面,進行 patchVnode 的同時要將 oldEndVnode 對應(yīng) DOM 移動到 oldStartVnode 對應(yīng) DOM 的前面菱农。

image.png

如果以上情況均不符合,則在 old VNode 中找與 newStartVnode 相同的節(jié)點柿估,若存在執(zhí)行 patchVnode循未,同時將 elmToMove 移動到 oldStartIdx 對應(yīng)的 DOM 的前面。

image.png

當然也有可能 newStartVnode 在 old VNode 節(jié)點中找不到?致的 sameVnode秫舌,這個時候會調(diào)用 createElm 創(chuàng)建?個新的 DOM 節(jié)點的妖。

image.png

至此循環(huán)結(jié)束,但是我們還需要處理剩下的節(jié)點足陨。

當結(jié)束時 oldStartIdx > oldEndIdx嫂粟,這個時候舊的 VNode 節(jié)點已經(jīng)遍歷完了,但是新的節(jié)點還沒有墨缘。說明了新的 VNode 節(jié)點實際上比老的 VNode 節(jié)點多星虹,需要將剩下的 VNode 對應(yīng)的 DOM 插入到真實 DOM 中,此時調(diào)用 addVnodes(批量調(diào)用 createElm 接口)镊讼。

image.png

但是宽涌,當結(jié)束時 newStartIdx > newEndIdx 時,說明新的 VNode 節(jié)點已經(jīng)遍歷完了蝶棋,但是老的節(jié)點還有剩余卸亮,需要從文檔中將老的節(jié)點刪除。

image.png

總結(jié)&&思考

const app = new Vue({
  el: '#demo',
  data: { foo: 'ready~~' },
  mounted () {
    // 批量玩裙、異步
    // 每次賦值兼贸,watcher入隊
    // $nextTick()把傳入回調(diào)函數(shù)放入callbacks隊尾
    this.foo = Math.random()
    console.log('1:' + this.foo);

    this.foo = Math.random()
    console.log('2:' + this.foo);

    this.foo = Math.random()
    console.log('3:' + this.foo);

    // 異步行為,此時內(nèi)容沒變
    console.log('p1.innerHTML:' + p1.innerHTML) // ready~~

    // [callbacks, fn]
    // Promise.resolve().then(() => {
    //     console.log('promise, p1.innerHTML:' + p1.innerHTML)
    // })

    this.$nextTick(() => {
      // 這里才是最新的值
      console.log('p1.innerHTML:' + p1.innerHTML)
    })
  }
});

面試官:在 Vue 里面執(zhí)行 mounted 里面的內(nèi)容吃溅,輸出結(jié)果如何溶诞?

面試官:如果把 $nextTick 放在中間位置呢?

面試官:如果把 $nextTick 放在最上面位置呢决侈?

面試官:如果再加上 Promise 呢很澄?

如果...

如果沒有如果...

附思維導圖:

image.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市颜及,隨后出現(xiàn)的幾起案子甩苛,更是在濱河造成了極大的恐慌,老刑警劉巖俏站,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件讯蒲,死亡現(xiàn)場離奇詭異,居然都是意外死亡肄扎,警方通過查閱死者的電腦和手機墨林,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門赁酝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人旭等,你說我怎么就攤上這事酌呆。” “怎么了搔耕?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵隙袁,是天一觀的道長。 經(jīng)常有香客問我弃榨,道長菩收,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任鲸睛,我火速辦了婚禮娜饵,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘官辈。我一直安慰自己箱舞,他們只是感情好,可當我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布拳亿。 她就那樣靜靜地躺著褐缠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪风瘦。 梳的紋絲不亂的頭發(fā)上队魏,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機與錄音万搔,去河邊找鬼胡桨。 笑死,一個胖子當著我的面吹牛瞬雹,可吹牛的內(nèi)容都是我干的昧谊。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼酗捌,長吁一口氣:“原來是場噩夢啊……” “哼呢诬!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起胖缤,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤尚镰,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后哪廓,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狗唉,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年涡真,在試婚紗的時候發(fā)現(xiàn)自己被綠了分俯。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片肾筐。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖缸剪,靈堂內(nèi)的尸體忽然破棺而出吗铐,到底是詐尸還是另有隱情,我是刑警寧澤杏节,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布唬渗,位于F島的核電站,受9級特大地震影響拢锹,放射性物質(zhì)發(fā)生泄漏谣妻。R本人自食惡果不足惜萄喳,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一卒稳、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧他巨,春花似錦充坑、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至份企,卻和暖如春也榄,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背司志。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工甜紫, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人骂远。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓囚霸,卻偏偏與公主長得像,于是被迫代替她去往敵國和親激才。 傳聞我的和親對象是個殘疾皇子拓型,可洞房花燭夜當晚...
    茶點故事閱讀 44,713評論 2 354