Vue 及框架響應式系統原理

我的bolg地址

全局概覽

Vue運行內部運行機制 總覽圖:react

初始化及掛載

在 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.";  /* 視圖更新啦~ */
相關文章
相關標籤/搜索