對VUE框架的理解

yj_xy發表於2020-10-30

Vue.js概述

Vue 是一套用於構建使用者介面的漸進式MVVM框架。那怎麼理解漸進式呢?漸進式含義:強制主張最少。

 

面試官:聊聊對Vue.js框架的理解

 

 

Vue.js包含了宣告式渲染、元件化系統、客戶端路由、大規模狀態管理、構建工具、資料持久化、跨平臺支援等,但在實際開發中,並沒有強制要求開發者之後某一特定功能,而是根據需求逐漸擴充套件。

Vue.js的核心庫只關心檢視渲染,且由於漸進式的特性,Vue.js便於與第三方庫或既有專案整合。

元件機制

定義:元件就是對一個功能和樣式進行獨立的封裝,讓HTML元素得到擴充套件,從而使得程式碼得到複用,使得開發靈活,更加高效。

與HTML元素一樣,Vue.js的元件擁有外部傳入的屬性(prop)和事件,除此之外,元件還擁有自己的狀態(data)和通過資料和狀態計算出來的計算屬性(computed),各個維度組合起來決定元件最終呈現的樣子與互動的邏輯。

資料傳遞

每一個元件之間的作用域是孤立的,這個意味著元件之間的資料不應該出現引用關係,即使出現了引用關係,也不允許元件操作元件內部以外的其他資料。Vue中,允許向元件內部傳遞prop資料,元件內部需要顯性地宣告該prop欄位,如下宣告一個child元件:

<!-- child.vue -->
<template>
    <div>{{msg}}</div>
</template>
<script>
export default {
    props: {
        msg: {
            type: String,
            default: 'hello world' // 當default為引用型別時,需要使用 function 形式返回
        }
    }
}
</script>

父元件向該元件傳遞資料:

<!-- parent.vue -->
<template>
    <child :msg="parentMsg"></child>
</template>
<script>
import child from './child';
export default {
    components: {
        child
    },
    data () {
        return {
            parentMsg: 'some words'
        }
    }
}
</script>

事件傳遞

Vue內部實現了一個事件匯流排系統,即EventBus。在Vue中可以使用 EventBus 來作為溝通橋樑的概念,每一個Vue的元件例項都繼承了 EventBus,都可以接受事件$on和傳送事件$emit。

如上面一個例子,child.vue 元件想修改 parent.vue 元件的 parentMsg 資料,怎麼辦呢?為了保證資料流的可追溯性,直接修改元件內 prop 的 msg 欄位是不提倡的,且例子中為非引用型別 String,直接修改也修改不了,這個時候需要將修改 parentMsg 的事件傳遞給 child.vue,讓 child.vue 來觸發修改 parentMsg 的事件。如:

<!-- child.vue -->
<template>
    <div>{{msg}}</div>
</template>
<script>
export default {
    props: {
        msg: {
            type: String,
            default: 'hello world'
        }
    },
    methods: {
        changeMsg(newMsg) {
            this.$emit('updateMsg', newMsg);
        }
    }
}
</script>

父元件:

<!-- parent.vue -->
<template>
    <child :msg="parentMsg" @updateMsg="changeParentMsg"></child>
</template>
<script>
import child from './child';
export default {
    components: {
        child
    },
    data () {
        return {
            parentMsg: 'some words'
        }
    },
    methods: {
        changeParentMsg: function (newMsg) {
            this.parentMsg = newMsg
        }
    }
}
</script>

父元件 parent.vue 向子元件 child.vue 傳遞了 updateMsg 事件,在子元件例項化的時候,子元件將 updateMsg 事件使用$on函式註冊到元件內部,需要觸發事件的時候,呼叫函式this.$emit來觸發事件。

除了父子元件之間的事件傳遞,還可以使用一個 Vue 例項為多層級的父子元件建立資料通訊的橋樑,如:

const eventBus = new Vue();

// 父元件中使用$on監聽事件
eventBus.$on('eventName', val => {
    //  ...do something
})

// 子元件使用$emit觸發事件
eventBus.$emit('eventName', 'this is a message.');

除了$on和$emit以外,事件匯流排系統還提供了另外兩個方法,$once和$off,所有事件如下:

  • $on:監聽、註冊事件。
  • $emit:觸發事件。
  • $once:註冊事件,僅允許該事件觸發一次,觸發結束後立即移除事件。
  • $off:移除事件。

內容分發

Vue實現了一套遵循 Web Components 規範草案 的內容分發系統,即將<slot>元素作為承載分發內容的出口。

插槽slot,也是元件的一塊HTML模板,這一塊模板顯示不顯示、以及怎樣顯示由父元件來決定。實際上,一個slot最核心的兩個問題在這裡就點出來了,是顯示不顯示和怎樣顯示。

插槽又分預設插槽、具名插槽。

預設插槽

又名單個插槽、匿名插槽,與具名插槽相對,這類插槽沒有具體名字,一個元件只能有一個該類插槽。

如:

<template>
<!-- 父元件 parent.vue -->
<div class="parent">
    <h1>父容器</h1>
    <child>
        <div class="tmpl">
            <span>選單1</span>
        </div>
    </child>
</div>
</template>

<template>
<!-- 子元件 child.vue -->
<div class="child">
    <h1>子元件</h1>
    <slot></slot>
</div>
</template>

如上,渲染時子元件的slot標籤會被父元件傳入的div.tmpl替換。

具名插槽

匿名插槽沒有name屬性,所以叫匿名插槽。那麼,插槽加了name屬性,就變成了具名插槽。具名插槽可以在一個元件中出現N次,出現在不同的位置,只需要使用不同的name屬性區分即可。

如:

<template>
<!-- 父元件 parent.vue -->
<div class="parent">
    <h1>父容器</h1>
    <child>
        <div class="tmpl" slot="up">
            <span>選單up-1</span>
        </div>
        <div class="tmpl" slot="down">
            <span>選單down-1</span>
        </div>
        <div class="tmpl">
            <span>選單->1</span>
        </div>
    </child>
</div>
</template>

<template>
    <div class="child">
        <!-- 具名插槽 -->
        <slot name="up"></slot>
        <h3>這裡是子元件</h3>
        <!-- 具名插槽 -->
        <slot name="down"></slot>
        <!-- 匿名插槽 -->
        <slot></slot>
    </div>
</template>

如上,slot 標籤會根據父容器給 child 標籤內傳入的內容的 slot 屬性值,替換對應的內容。

其實,預設插槽也有 name 屬性值,為default,同樣指定 slot 的 name 值為 default,一樣可以顯示父元件中傳入的沒有指定slot的內容。

作用域插槽

作用域插槽可以是預設插槽,也可以是具名插槽,不一樣的地方是,作用域插槽可以為 slot 標籤繫結資料,讓其父元件可以獲取到子元件的資料。

如:

<template>
    <!-- parent.vue -->
    <div class="parent">
        <h1>這是父元件</h1>
        <current-user>
            <template slot="default" slot-scope="slotProps">
                {{ slotProps.user.name }}
            </template>
        </current-user>
    </div>
</template>
複製程式碼
<template>
    <!-- child.vue -->
    <div class="child">
        <h1>這是子元件</h1>
        <slot :user="user"></slot>
    </div>
</template>
<script>
export default {
    data() {
        return {
            user: {
                name: '小趙'
            }
        }
    }
}
</script>

如上例子,子元件 child 在渲染預設插槽 slot 的時候,將資料 user 傳遞給了 slot 標籤,在渲染過程中,父元件可以通過slot-scope屬性獲取到 user 資料並渲染檢視。

slot 實現原理:當子元件vm例項化時,獲取到父元件傳入的 slot 標籤的內容,存放在vm.$slot中,預設插槽為vm.$slot.default,具名插槽為vm.$slot.xxx,xxx 為 插槽名,當元件執行渲染函式時候,遇到<slot>標籤,使用$slot中的內容進行替換,此時可以為插槽傳遞資料,若存在資料,則可曾該插槽為作用域插槽。

至此,父子元件的關係如下圖:

 

面試官:聊聊對Vue.js框架的理解

 

模板渲染

Vue.js 的核心是宣告式渲染,與命令式渲染不同,宣告式渲染只需要告訴程式,我們想要的什麼效果,其他的事情讓程式自己去做。而命令式渲染,需要命令程式一步一步根據命令執行渲染。如下例子區分:

var arr = [1, 2, 3, 4, 5];

// 命令式渲染,關心每一步、關心流程。用命令去實現
var newArr = [];
for (var i = 0; i < arr.length; i++) {
    newArr.push(arr[i] * 2);
}

// 宣告式渲染,不用關心中間流程,只需要關心結果和實現的條件
var newArr1 = arr.map(function (item) {
    return item * 2;
});

Vue.js 實現了if、for、事件、資料繫結等指令,允許採用簡潔的模板語法來宣告式地將資料渲染出檢視。

模板編譯

為什麼要進行模板編譯?實際上,我們元件中的 template 語法是無法被瀏覽器解析的,因為它不是正確的 HTML 語法,而模板編譯,就是將元件的 template 編譯成可執行的 JavaScript 程式碼,即將 template 轉化為真正的渲染函式。

模板編譯分三個階段,parse、optimize、generate,最終生成render函式。

 

面試官:聊聊對Vue.js框架的理解

 

parse階段:使用正在表示式將template進行字串解析,得到指令、class、style等資料,生成抽象語法樹 AST。

optimize階段:尋找 AST 中的靜態節點進行標記,為後面 VNode 的 patch 過程中對比做優化。被標記為 static 的節點在後面的 diff 演算法中會被直接忽略,不做詳細的比較。

generate階段:根據 AST 結構拼接生成 render 函式的字串。

預編譯

對於 Vue 元件來說,模板編譯只會在元件例項化的時候編譯一次,生成渲染函式之後在也不會進行編譯。因此,編譯對元件的 runtime 是一種效能損耗。而模板編譯的目的僅僅是將template轉化為render function,而這個過程,正好可以在專案構建的過程中完成。

比如webpack的vue-loader依賴了vue-template-compiler模組,在 webpack 構建過程中,將template預編譯成 render 函式,在 runtime 可直接跳過模板編譯過程。

回過頭看,runtime 需要是僅僅是 render 函式,而我們有了預編譯之後,我們只需要保證構建過程中生成 render 函式就可以。與 React 類似,在新增JSX的語法糖編譯器babel-plugin-transform-vue-jsx之後,我們可以在 Vue 元件中使用JSX語法直接書寫 render 函式。

<script>
export default {
    data() {
        return {
            msg: 'Hello JSX.'
        }
    },
    render() {
        const msg = this.msg;
        return <div>
            {msg}
        </div>;
    }
}
</script>

如上面元件,使用 JSX 之後,可以在 JS 程式碼中直接使用 html 標籤,而且宣告瞭 render 函式以後,我們不再需要宣告 template。當然,假如我們同時宣告瞭 template 標籤和 render 函式,構建過程中,template 編譯的結果將覆蓋原有的 render 函式,即 template 的優先順序高於直接書寫的 render 函式。

相對於 template 而言,JSX 具有更高的靈活性,面對與一些複雜的元件來說,JSX 有著天然的優勢,而 template 雖然顯得有些呆滯,但是程式碼結構上更符合檢視與邏輯分離的習慣,更簡單、更直觀、更好維護。

需要注意的是,最後生成的 render 函式是被包裹在with語法中執行的。

 

小結

Vue 元件通過 prop 進行資料傳遞,並實現了資料匯流排系統EventBus,元件整合了EventBus進行事件註冊監聽、事件觸發,使用slot進行內容分發。

除此以外,實現了一套宣告式模板系統,在runtime或者預編譯是對模板進行編譯,生成渲染函式,供元件渲染檢視使用。

 

響應式系統

Vue.js 是一款 MVVM 的JS框架,當對資料模型data進行修改時,檢視會自動得到更新,即框架幫我們完成了更新DOM的操作,而不需要我們手動的操作DOM。可以這麼理解,當我們對資料進行賦值的時候,Vue 告訴了所有依賴該資料模型的元件,你依賴的資料有更新,你需要進行重渲染了,這個時候,元件就會重渲染,完成了檢視的更新。

 

資料模型 && 計算屬性 && 監聽器

在元件中,可以為每個元件定義資料模型data、計算屬性computed、監聽器watch。

資料模型:Vue 例項在建立過程中,對資料模型data的每一個屬性加入到響應式系統中,當資料被更改時,檢視將得到響應,同步更新。data必須採用函式的方式 return,不使用 return 包裹的資料會在專案的全域性可見,會造成變數汙染;使用return包裹後資料中變數只在當前元件中生效,不會影響其他元件。

計算屬性:computed基於元件響應式依賴進行計算得到結果並快取起來。只在相關響應式依賴發生改變時它們才會重新求值,也就是說,只有它依賴的響應式資料(data、prop、computed本身)發生變化了才會重新計算。那什麼時候應該使用計算屬性呢?模板內的表示式非常便利,但是設計它們的初衷是用於簡單運算的。在模板中放入太多的邏輯會讓模板過重且難以維護。對於任何複雜邏輯,你都應當使用計算屬性。

監聽器:監聽器watch作用如其名,它可以監聽響應式資料的變化,響應式資料包括 data、prop、computed,當響應式資料發生變化時,可以做出相應的處理。當需要在資料變化時執行非同步或開銷較大的操作時,這個方式是最有用的。

 

響應式原理

在 Vue 中,資料模型下的所有屬性,會被 Vue 使用Object.defineProperty(Vue3.0 使用 Proxy)進行資料劫持代理。響應式的核心機制是觀察者模式,資料是被觀察的一方,一旦發生變化,通知所有觀察者,這樣觀察者可以做出響應,比如當觀察者為檢視時,檢視可以做出檢視的更新。

Vue.js 的響應式系統以來三個重要的概念,Observer、Dep、Watcher。

釋出者-Observer

Observe 扮演的角色是釋出者,他的主要作用是在元件vm初始化的時,呼叫defineReactive函式,使用Object.defineProperty方法對物件的每一個子屬性進行資料劫持/監聽,即為每個屬性新增getter和setter,將對應的屬性值變成響應式。

在元件初始化時,呼叫initState函式,內部執行initState、initProps、initComputed方法,分別對data、prop、computed進行初始化,讓其變成響應式。

初始化props時,對所有props進行遍歷,呼叫defineReactive函式,將每個 prop 屬性值變成響應式,然後將其掛載到_props中,然後通過代理,把vm.xxx代理到vm._props.xxx中。

同理,初始化data時,與prop相同,對所有data進行遍歷,呼叫defineReactive函式,將每個 data 屬性值變成響應式,然後將其掛載到_data中,然後通過代理,把vm.xxx代理到vm._data.xxx中。

初始化computed,首先建立一個觀察者物件computed-watcher,然後遍歷computed的每一個屬性,對每一個屬性值呼叫defineComputed方法,使用Object.defineProperty將其變成響應式的同時,將其代理到元件例項上,即可通過vm.xxx訪問到xxx計算屬性。

排程中心/訂閱器-Dep

Dep 扮演的角色是排程中心/訂閱器,在呼叫defineReactive將屬性值變成響應式的過程中,也為每個屬性值例項化了一個Dep,主要作用是對觀察者(Watcher)進行管理,收集觀察者和通知觀察者目標更新,即當屬性值資料發生改變時,會遍歷觀察者列表(dep.subs),通知所有的 watcher,讓訂閱者執行自己的update邏輯。

其dep的任務是,在屬性的getter方法中,呼叫dep.depend()方法,將觀察者(即 Watcher,可能是元件的render function,可能是 computed,也可能是屬性監聽 watch)儲存在內部,完成其依賴收集。在屬性的setter方法中,呼叫dep.notify()方法,通知所有觀察者執行更新,完成派發更新。

觀察者-Watcher

Watcher 扮演的角色是訂閱者/觀察者,他的主要作用是為觀察屬性提供回撥函式以及收集依賴,當被觀察的值發生變化時,會接收到來自排程中心Dep的通知,從而觸發回撥函式。

而Watcher又分為三類,normal-watcher、 computed-watcher、 render-watcher。

  • normal-watcher:在元件鉤子函式watch中定義,即監聽的屬性改變了,都會觸發定義好的回撥函式。
  • computed-watcher:在元件鉤子函式computed中定義的,每一個computed屬性,最後都會生成一個對應的Watcher物件,但是這類Watcher有個特點:當計算屬性依賴於其他資料時,屬性並不會立即重新計算,只有之後其他地方需要讀取屬性的時候,它才會真正計算,即具備lazy(懶計算)特性。
  • render-watcher:每一個元件都會有一個render-watcher, 當data/computed中的屬性改變的時候,會呼叫該Watcher來更新元件的檢視。

這三種Watcher也有固定的執行順序,分別是:computed-render -> normal-watcher -> render-watcher。這樣就能儘可能的保證,在更新元件檢視的時候,computed 屬性已經是最新值了,如果 render-watcher 排在 computed-render 前面,就會導致頁面更新的時候 computed 值為舊資料。

小結

 

面試官:聊聊對Vue.js框架的理解

 

 

Observer 負責將資料進行攔截,Watcher 負責訂閱,觀察資料變化, Dep 負責接收訂閱並通知 Observer 和接收發布並通知所有 Watcher。

Virtual DOM

在 Vue 中,template被編譯成瀏覽器可執行的render function,然後配合響應式系統,將render function掛載在render-watcher中,當有資料更改的時候,排程中心Dep通知該render-watcher執行render function,完成檢視的渲染與更新。

 

面試官:聊聊對Vue.js框架的理解

 

 

整個流程看似通順,但是當執行render function時,如果每次都全量刪除並重建 DOM,這對執行效能來說,無疑是一種巨大的損耗,因為我們知道,瀏覽器的DOM很“昂貴”的,當我們頻繁的更新 DOM,會產生一定的效能問題。

為了解決這個問題,Vue 使用 JS 物件將瀏覽器的 DOM 進行的抽象,這個抽象被稱為 Virtual DOM。Virtual DOM 的每個節點被定義為VNode,當每次執行render function時,Vue 對更新前後的VNode進行Diff對比,找出儘可能少的我們需要更新的真實 DOM 節點,然後只更新需要更新的節點,從而解決頻繁更新 DOM 產生的效能問題。

VNode

VNode,全稱virtual node,即虛擬節點,對真實 DOM 節點的虛擬描述,在 Vue 的每一個元件例項中,會掛載一個$createElement函式,所有的VNode都是由這個函式建立的。

比如建立一個 div:

// 宣告 render function
render: function (createElement) {
    // 也可以使用 this.$createElement 建立 VNode
    return createElement('div', 'hellow world');
}
// 以上 render 方法返回html片段 <div>hellow world</div>
複製程式碼

render 函式執行後,會根據VNode Tree將 VNode 對映生成真實 DOM,從而完成檢視的渲染。

Diff

Diff 將新老 VNode 節點進行比對,然後將根據兩者的比較結果進行最小單位地修改檢視,而不是將整個檢視根據新的 VNode 重繪,進而達到提升效能的目的。

patch

Vue.js 內部的 diff 被稱為patch。其 diff 演算法的是通過同層的樹節點進行比較,而非對樹進行逐層搜尋遍歷的方式,所以時間複雜度只有O(n),是一種相當高效的演算法。

 

面試官:聊聊對Vue.js框架的理解

 

 

首先定義新老節點是否相同判定函式sameVnode:滿足鍵值key和標籤名tag必須一致等條件,返回true,否則false。

在進行patch之前,新老 VNode 是否滿足條件sameVnode(oldVnode, newVnode),滿足條件之後,進入流程patchVnode,否則被判定為不相同節點,此時會移除老節點,建立新節點。

patchVnode

patchVnode 的主要作用是判定如何對子節點進行更新,

  1. 如果新舊VNode都是靜態的,同時它們的key相同(代表同一節點),並且新的 VNode 是 clone 或者是標記了 once(標記v-once屬性,只渲染一次),那麼只需要替換 DOM 以及 VNode 即可。
  2. 新老節點均有子節點,則對子節點進行 diff 操作,進行updateChildren,這個 updateChildren 也是 diff 的核心。
  3. 如果老節點沒有子節點而新節點存在子節點,先清空老節點 DOM 的文字內容,然後為當前 DOM 節點加入子節點。
  4. 當新節點沒有子節點而老節點有子節點的時候,則移除該 DOM 節點的所有子節點。
  5. 當新老節點都無子節點的時候,只是文字的替換。

updateChildren

Diff 的核心,對比新老子節點資料,判定如何對子節點進行操作,在對比過程中,由於老的子節點存在對當前真實 DOM 的引用,新的子節點只是一個 VNode 陣列,所以在進行遍歷的過程中,若發現需要更新真實 DOM 的地方,則會直接在老的子節點上進行真實 DOM 的操作,等到遍歷結束,新老子節點則已同步結束。

updateChildren內部定義了4個變數,分別是oldStartIdx、oldEndIdx、newStartIdx、newEndIdx,分別表示正在 Diff 對比的新老子節點的左右邊界點索引,在老子節點陣列中,索引在oldStartIdx與oldEndIdx中間的節點,表示老子節點中為被遍歷處理的節點,所以小於oldStartIdx或大於oldEndIdx的表示未被遍歷處理的節點。同理,在新的子節點陣列中,索引在newStartIdx與newEndIdx中間的節點,表示老子節點中為被遍歷處理的節點,所以小於newStartIdx或大於newEndIdx的表示未被遍歷處理的節點。

每一次遍歷,oldStartIdx和oldEndIdx與newStartIdx和newEndIdx之間的距離會向中間靠攏。當 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx 時結束迴圈。

 

面試官:聊聊對Vue.js框架的理解

 

 

在遍歷中,取出4索引對應的 Vnode節點:

  • oldStartIdx:oldStartVnode
  • oldEndIdx:oldEndVnode
  • newStartIdx:newStartVnode
  • newEndIdx:newEndVnode

diff 過程中,如果存在key,並且滿足sameVnode,會將該 DOM 節點進行復用,否則則會建立一個新的 DOM 節點。

首先,oldStartVnode、oldEndVnode與newStartVnode、newEndVnode兩兩比較,一共有 2*2=4 種比較方法。

情況一:當oldStartVnode與newStartVnode滿足 sameVnode,則oldStartVnode與newStartVnode進行 patchVnode,並且oldStartIdx與newStartIdx右移動。

 

面試官:聊聊對Vue.js框架的理解

 

 

情況二:與情況一類似,當oldEndVnode與newEndVnode滿足 sameVnode,則oldEndVnode與newEndVnode進行 patchVnode,並且oldEndIdx與newEndIdx左移動。

 

面試官:聊聊對Vue.js框架的理解

 

 

情況三:當oldStartVnode與newEndVnode滿足 sameVnode,則說明oldStartVnode已經跑到了oldEndVnode後面去了,此時oldStartVnode與newEndVnode進行 patchVnode 的同時,還需要將oldStartVnode的真實 DOM 節點移動到oldEndVnode的後面,並且oldStartIdx右移,newEndIdx左移。

 

面試官:聊聊對Vue.js框架的理解

 

 

情況四:與情況三類似,當oldEndVnode與newStartVnode滿足 sameVnode,則說明oldEndVnode已經跑到了oldStartVnode前面去了,此時oldEndVnode與newStartVnode進行 patchVnode 的同時,還需要將oldEndVnode的真實 DOM 節點移動到oldStartVnode的前面,並且oldStartIdx右移,newEndIdx左移。

 

面試官:聊聊對Vue.js框架的理解

 

 

當這四種情況都不滿足,則在oldStartIdx與oldEndIdx之間查詢與newStartVnode滿足sameVnode的節點,若存在,則將匹配的節點真實 DOM 移動到oldStartVnode的前面。

 

面試官:聊聊對Vue.js框架的理解

 

 

若不存在,說明newStartVnode為新節點,建立新節點放在oldStartVnode前面即可。

 

面試官:聊聊對Vue.js框架的理解

 

 

當 oldStartIdx > oldEndIdx 或者 newStartIdx > newEndIdx,迴圈結束,這個時候我們需要處理那些未被遍歷到的 VNode。

當 oldStartIdx > oldEndIdx 時,說明老的節點已經遍歷完,而新的節點沒遍歷完,這個時候需要將新的節點建立之後放在oldEndVnode後面。

 

面試官:聊聊對Vue.js框架的理解

 

 

當 newStartIdx > newEndIdx 時,說明新的節點已經遍歷完,而老的節點沒遍歷完,這個時候要將沒遍歷的老的節點全都刪除。

 

面試官:聊聊對Vue.js框架的理解

 

 

此時已經完成了子節點的匹配。下面是一個例子 patch 過程圖:

 

面試官:聊聊對Vue.js框架的理解

 

 

總結

借用官方的一幅圖:

 

面試官:聊聊對Vue.js框架的理解

 

 

Vue.js 實現了一套宣告式渲染引擎,並在runtime或者預編譯時將宣告式的模板編譯成渲染函式,掛載在觀察者 Watcher 中,在渲染函式中(touch),響應式系統使用響應式資料的getter方法對觀察者進行依賴收集(Collect as Dependency),使用響應式資料的setter方法通知(notify)所有觀察者進行更新,此時觀察者 Watcher 會觸發元件的渲染函式(Trigger re-render),元件執行的 render 函式,生成一個新的 Virtual DOM Tree,此時 Vue 會對新老 Virtual DOM Tree 進行 Diff,查詢出需要操作的真實 DOM 並對其進行更新。

相關文章