1. 程式人生 > >Vue 及框架響應式系統原理

Vue 及框架響應式系統原理

dev 方法 writable 技術分享 構造函數 問題 color 子節點 跨平臺

個人bolg地址

全局概覽

Vue運行內部運行機制 總覽圖:技術分享圖片

初始化及掛載

技術分享圖片

new Vue()之後。 Vue 會調用 _init 函數進行初始化,也就是這裏的 init 過程,它會初始化生命周期、事件、 props、 methods、 data、 computed 與 watch 等。其中最重要的是通過 Object.defineProperty 設置 setter 與 getter 函數,用來實現「響應式」以及「依賴收集」,後面會詳細講到,這裏只要有一個印象即可。

初始化之後調用 $mount 會掛載組件,如果是運行時編譯,即不存在 render function但是存在 template 的情況,需要進行「編譯」步驟。
因為編譯有構建時編譯與運行時編譯的,其目的都是將template轉化炒年糕render function,所以如果運行時檢查到template存在但是沒有render function的情況下會把template編譯成render function。

編譯

compile編譯可以分成 parse、optimize 與 generate 三個階段,最終需要得到 render function

技術分享圖片

parse(解析)

parse 會用正則等方式解析 template 模板中的指令、class、style等數據,形成AST。

optimize(優化)

optimize 的主要作用是標記 static 靜態節點,這是 Vue 在編譯過程中的一處優化,後面當 update 更新界面時,會有一個 patch 的過程, diff 算法會直接跳過靜態節點,從而減少了比較的過程,優化了 patch 的性能。

generate(生成)

generate 是將 AST 轉化成 render function 字符串的過程,得到結果是 render 的字符串以及 staticRenderFns 字符串。
在經歷過 parse、optimize 與 generate 這三個階段以後,組件中就會存在渲染 VNode 所需的 render function 了。

響應式

接下來也就是 Vue.js 響應式核心部分。
這裏的 getter 跟 setter 已經在之前介紹過了,在 init 的時候通過 Object.defineProperty 進行了綁定,它使得當被設置的對象被讀取的時候會執行 getter 函數,而在當被賦值的時候會執行 setter 函數。
當 render function 被渲染的時候,因為會讀取所需對象的值,所以會觸發 getter 函數進行「依賴收集」,「依賴收集」的目的是將觀察者 Watcher 對象存放到當前閉包中的訂閱者 Dep 的 subs 中。形成如下所示的這樣一個關系。

技術分享圖片

在修改對象的值的時候,會觸發對應的 setter, setter 通知之前「依賴收集」得到的 Dep 中的每一個 Watcher,告訴它們自己的值改變了,需要重新渲染視圖。這時候這些 Watcher 就會開始調用 update 來更新視圖,當然這中間還有一個 patch 的過程以及使用隊列來異步更新的策略,這個我們後面再講。

Virtual DOM

我們知道,render function 會被轉化成 VNode 節點。Virtual DOM 其實就是一棵以 JavaScript 對象( VNode 節點)作為基礎的樹,用對象屬性來描述節點,實際上它只是一層對真實 DOM 的抽象。最終可以通過一系列操作使這棵樹映射到真實環境上。由於 Virtual DOM 是以 JavaScript 對象為基礎而不依賴真實平臺環境,所以使它具有了跨平臺的能力,比如說瀏覽器平臺、Weex、Node 等。
比如說下面這樣一個例子:

{
    tag: ‘div‘,                 /*說明這是一個div標簽*/
    children: [                 /*存放該標簽的子節點*/
        {
            tag: ‘a‘,           /*說明這是一個a標簽*/
            text: ‘click me‘    /*標簽的內容*/
        }
    ]
}

渲染後可以得到

<div>
    <a>click me</a>
</div>

這只是一個簡單的例子,實際上的節點有更多的屬性來標誌節點,比如 isStatic (代表是否為靜態節點)、 isComment (代表是否為註釋節點)等。

更新視圖

技術分享圖片

前面我們說到,在修改一個對象值的時候,會通過 setter -> Watcher -> update 的流程來修改對應的視圖,那麽最終是如何更新視圖的呢?

當數據變化後,執行 render function 就可以得到一個新的 VNode 節點,我們如果想要得到新的視圖,最簡單粗暴的方法就是直接解析這個新的 VNode 節點,然後用 innerHTML 直接全部渲染到真實 DOM 中。但是其實我們只對其中的一小塊內容進行了修改,這樣做似乎有些「浪費」。

那麽我們為什麽不能只修改那些「改變了的地方」呢?這個時候就要介紹「patch」了。我們會將新的 VNode 與舊的 VNode 一起傳入 patch 進行比較,經過 diff 算法得出它們的「差異」。最後我們只需要將這些「差異」的對應 DOM 進行修改即可。

響應式系統的基本原理

響應式系統

Vue.js 是一款 MVVM 框架,數據模型僅僅是普通的 JavaScript 對象,但是對這些對象進行操作時,卻能影響對應視圖,它的核心實現就是「響應式系統」。盡管我們在使用 Vue.js 進行開發時不會直接修改「響應式系統」,但是理解它的實現有助於避開一些常見的「坑」,也有助於在遇見一些琢磨不透的問題時可以深入其原理來解決它。
Object.defineProperty
首先我們來介紹一下 Object.defineProperty,Vue.js就是基於它實現「響應式系統」的。

首先是使用方法:

/*
    obj: 目標對象
    prop: 需要操作的目標對象的屬性名
    descriptor: 描述符=>{
      enumerable: false,  //對象的屬性是否可以在 for...in 循環和 Object.keys() 中被枚舉
      configurable: false,  //對象的屬性是否可以被刪除,以及除writable特性外的其他特性是否可以被修改。
      writable: false,  //為true時,value才能被賦值運算符改變。默認為 false。
      value: "static", //該屬性對應的值。可以是任何有效的 JavaScript 值(數值,對象,函數等)。默認為 undefined。
      get : function(){   //一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。該方法返回值被用作屬性值。默認為 undefined。
        return this.value;
      },
      set : function(newValue){ //提供 setter 的方法,如果沒有 setter 則為 undefined。將該參數的新值分配給該屬性。默認為 undefined。
        this.value = newValue;
      },
    }
    return value 傳入對象
*/
Object.defineProperty(obj, prop, descriptor)

// 舉個栗子

// 使用 __proto__
var obj = {};
var descriptor = Object.create(null); // 沒有繼承的屬性
// 默認沒有 enumerable,沒有 configurable,沒有 writable
descriptor.value = ‘static‘;
Object.defineProperty(obj, ‘key‘, descriptor);

// 顯式
Object.defineProperty(obj, "key", {
  enumerable: false,
  configurable: false,
  writable: false,
  value: "static"
});
// 在對象中添加一個屬性與存取描述符的示例
var bValue;
Object.defineProperty(o, "b", {
  get : function(){
    return bValue;
  },
  set : function(newValue){
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

要熟悉Object.defineProperty可以去MDN文檔復習示例。

實現 observer(可觀察的)

知道了 Object.defineProperty 以後,我們來用它使對象變成可觀察的。
這一部分的內容我們在第二小節中已經初步介紹過,在 init 的階段會進行初始化,對數據進行「響應式化」

技術分享圖片

為了便於理解,我們不考慮數組等復雜的情況,只對對象進行處理。

首先我們定義一個 cb 函數,這個函數用來模擬視圖更新,調用它即代表更新視圖,內部可以是一些更新視圖的方法。

function cb (val) {
    /* 渲染視圖 */
    console.log("視圖更新啦~");
}

然後我們定義一個 defineReactive ,這個方法通過 Object.defineProperty 來實現對對象的「響應式」化,入參是一個 obj(需要綁定的對象)、key(obj的某一個屬性),val(具體的值)。經過 defineReactive 處理以後,我們的 obj 的 key 屬性在「讀」的時候會觸發 reactiveGetter 方法,而在該屬性被「寫」的時候則會觸發 reactiveSetter 方法。

function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 屬性可枚舉 */
        configurable: true,     /* 屬性可被修改或刪除 */
        get: function reactiveGetter () {
            return val;         /* 實際上會依賴收集,下一小節會講 */
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}

當然這是不夠的,我們需要在上面再封裝一層 observer 。這個函數傳入一個 value(需要「響應式」化的對象),通過遍歷所有屬性的方式對該對象的每一個屬性都通過 defineReactive 處理。

function observer (value) {
    if (!value || (typeof value !== ‘object‘)) {/*只考慮對象,非對象返回*/
        return;
    }
    
    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}

最後,讓我們用 observer 來封裝一個 Vue 吧!

在Vue的構造函數中,對options的data進行處理,這裏的data想必大家很熟悉,就是平時我們在寫Vue項目時組件中的data屬性(實際上是一個函數,這裏當做一個對象來簡單處理)

class Vue{
  /* Vue 構造類 */
  constructor(options) {
    this._data = options.data;
    observer(this._data);
  }
}

這樣我們只要 new 一個 Vue 對象,就會將 data 中的數據進行「響應式」化。如果我們對 data 的屬性進行下面的操作,就會觸發 cb 方法更新視圖。

let o = new Vue({
    data: {
        test: "I am test."
    }
});
o._data.test = "hello,world.";  /* 視圖更新啦~ */

Vue 及框架響應式系統原理