0 到 1 掌握:Vue 核心之資料雙向繫結

我是你的超級英雄發表於2019-07-31

前言

​ 當被問到 Vue 資料雙向繫結原理的時候,大家可能都會脫口而出:Vue 內部通過 Object.defineProperty方法屬性攔截的方式,把 data 物件裡每個資料的讀寫轉化成 getter/setter,當資料變化時通知檢視更新。雖然一句話把大概原理概括了,但是其內部的實現方式還是值得深究的,本文就以通俗易懂的方式剖析 Vue 內部雙向繫結原理的實現過程。然後再根據 Vue 原始碼的資料雙向繫結實現,來進一步鞏固加深對資料雙向繫結的理解認識。以下為我們實現的資料雙向繫結的效果圖:

1.gif

辛苦編寫良久,如果對您有幫助,請幫忙手動點贊鼓勵~

github地址為:github.com/fengshi123/…,上面彙總了作者所有的部落格文章,如果喜歡或者有所啟發,請幫忙給個 star ~,對作者也是一種鼓勵。

一、什麼是 MVVM 資料雙向繫結

MVVM 資料雙向繫結主要是指:資料變化更新檢視,檢視變化更新資料,如下圖所示:

2.png

即:

  • 輸入框內容變化時,Data 中的資料同步變化。即 View => Data 的變化。
  • Data 中的資料變化時,文字節點的內容同步變化。即 Data => View 的變化。

其中,View 變化更新 Data ,可以通過事件監聽的方式來實現,所以我們本文主要討論如何根據 Data 變化更新 View

我們會通過實現以下 4 個步驟,來實現資料的雙向繫結:

1、實現一個監聽器 Observer ,用來劫持並監聽所有屬性,如果屬性發生變化,就通知訂閱者;

2、實現一個訂閱器 Dep,用來收集訂閱者,對監聽器 Observer 和 訂閱者 Watcher 進行統一管理;

3、實現一個訂閱者 Watcher,可以收到屬性的變化通知並執行相應的方法,從而更新檢視;

4、實現一個解析器 Compile,可以解析每個節點的相關指令,對模板資料和訂閱器進行初始化。

以上四個步驟的流程圖表示如下:

3.png

該例項的原始碼已經放到 github 上面:github.com/fengshi123/…

二、監聽器 Observer 實現

監聽器 Observer 的實現,主要是指讓資料物件變得“可觀測”,即每次資料讀或寫時,我們能感知到資料被讀取了或資料被改寫了。要使資料變得“可觀測”,Vue 2.0 原始碼中用到 Object.defineProperty() 來劫持各個資料屬性的 setter / getterObject.defineProperty 方法,在 MDN 上是這麼定義的:

Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性, 並返回這個物件。

2.1、Object.defineProperty() 語法

Object.defineProperty 語法,在 MDN 上是這麼定義的:

Object.defineProperty(obj, prop, descriptor)

(1)引數

  • obj

    要在其上定義屬性的物件。

  • prop

    要定義或修改的屬性的名稱。

  • descriptor

    將被定義或修改的屬性描述符。

(2)返回值

​ 被傳遞給函式的物件。

(3)屬性描述符

Object.defineProperty() 為物件定義屬性,分 資料描述符 和 存取描述符 ,兩種形式不能混用。

資料描述符和存取描述符均具有以下可選鍵值:

  • configurable

當且僅當該屬性的 configurabletrue 時,該屬性描述符才能夠被改變,同時該屬性也能從對應的物件上被刪除。預設為 false

  • enumerable

當且僅當該屬性的 enumerabletrue 時,該屬性才能夠出現在物件的列舉屬性中。預設為 false

資料描述符具有以下可選鍵值

  • value

該屬性對應的值。可以是任何有效的 JavaScript 值(數值,物件,函式等)。預設為 undefined

  • writable

當且僅當該屬性的 writabletrue 時,value才能被賦值運算子改變。預設為 false

存取描述符具有以下可選鍵值

  • get

一個給屬性提供 getter 的方法,如果沒有 getter 則為 undefined。當訪問該屬性時,該方法會被執行,方法執行時沒有引數傳入,但是會傳入this物件(由於繼承關係,這裡的this並不一定是定義該屬性的物件)。預設為 undefined

  • set

一個給屬性提供 setter 的方法,如果沒有 setter 則為 undefined。當屬性值修改時,觸發執行該方法。該方法將接受唯一引數,即該屬性新的引數值。預設為 undefined

2.2、監聽器 Observer 實現

(1)字面量定義物件

首先,我們先看一下假設我們通過以下字面量的方式定義一個物件:

let person = {
    name:'tom',
    age:15
}
複製程式碼

我們可以通過 person.nameperson.age 直接讀寫這個 person 對應的屬性值,但是,當這個 person 的屬性被讀取或修改時,我們並不知情。那麼,應該如何定義一個物件,它的屬性被讀寫時,我們能感知到呢?

(2)Object.defineProperty() 定義物件

假設我們通過 Object.defineProperty() 來定義一個物件:

let val = 'tom'
let person = {}
Object.defineProperty(person,'name',{
    get(){
        console.log('name屬性被讀取了...');
        return val;
    },
    set(newVal){
        console.log('name屬性被修改了...');
        val = newVal;
    }
})
複製程式碼

我們通過 object.defineProperty() 方法給 personname 屬性定義了 get()set()進行攔截,每當該屬性進行讀或寫操作的時候就會觸發get()set() ,這樣,當物件的屬性被讀寫時,我們就能感知到了。測試結果圖如下所示:

3_2.png

(3)改進方法

通過第(2)步的方法,person 資料物件已經是“可觀測”的了,能滿足我們的需求了。但是如果資料物件的屬性比較多的情況下,我們一個一個為屬性去設定,程式碼會非常冗餘,所以我們進行以下封裝,從而讓資料物件的所有屬性都變得可觀測:

/**
  * 迴圈遍歷資料物件的每個屬性
  */
function observable(obj) {
    if (!obj || typeof obj !== 'object') {
        return;
    }
    let keys = Object.keys(obj);
    keys.forEach((key) => {
        defineReactive(obj, key, obj[key])
    })
    return obj;
}
/**
 * 將物件的屬性用 Object.defineProperty() 進行設定
 */
function defineReactive(obj, key, val) {
    Object.defineProperty(obj, key, {
        get() {
            console.log(`${key}屬性被讀取了...`);
            return val;
        },
        set(newVal) {
            console.log(`${key}屬性被修改了...`);
            val = newVal;
        }
    })
}
複製程式碼

通過以上方法封裝,我們可以直接定義 person

let person = observable({
    name: 'tom',
    age: 15
});
複製程式碼

這樣定義的 person 的 兩個屬性都是“可觀測”的。

三、訂閱器 Dep 實現

3.1、釋出 —訂閱設計模式

​ 釋出-訂閱模式又叫觀察者模式,它定義物件間的一種一對多的依賴關係,當一個物件的狀態改變時,所有依賴於它的物件都將得到通知。

(1)釋出—訂閱模式的優點:

  • 釋出-訂閱模式廣泛應用於非同步程式設計中,這是一種替代傳遞迴調函式的方案,比如,我們可以訂閱 ajax 請求的 error 、succ 等事件。在非同步程式設計中使用釋出-訂閱模式, 我們就無需過多關注物件在非同步執行期間的內部狀態,而只需要訂閱感興趣的事件發生點。
  • 釋出-訂閱模式可以取代物件之間硬編碼的通知機制,一個物件不用再顯式地呼叫另外一個物件的某個介面。釋出-訂閱模式讓兩個物件鬆耦合地聯絡在一起,雖然不太清楚彼此的細節,但這不影響它們之間相互通訊。當有新的訂閱者出現時,釋出者的程式碼不需要任何修改;同樣釋出者需要改變時,也不會影響到之前的訂閱者。只要之前約定的事件名沒有變化,就 可以自由地改變它們。

(2)釋出—訂閱模式的生活例項

​ 我們以售樓處的例子來舉例說明發布-訂閱模式:

​ 小明最近看上了一套房子,到了售樓處之後才被告知,該樓盤的房子早已售罄。好在售樓 MM 告訴小明,不久後還有一些尾盤推出,開發商正在辦理相關手續,手續辦好後便可以購買。

​ 但到底是什麼時候,目前還沒有人能夠知道。 於是小明記下了售樓處的電話,以後每天都會打電話過去詢問是不是已經到了購買時間。除 了小明,還有小紅、小強、小龍也會每天向售樓處諮詢這個問題。一個星期過後,售樓 MM 決定辭職,因為厭倦了每天回答 1000個相同內容的電話。

​ 當然現實中沒有這麼笨的銷售公司,實際上故事是這樣的:小明離開之前,把電話號碼留在 了售樓處。售樓 MM 答應他,新樓盤一推出就馬上發資訊通知小明。小紅、小強和小龍也是一樣,他們的電話號碼都被記在售樓處的花名冊上,新樓盤推出的時候,售樓 MM會翻開花名冊,遍歷上面的電話號碼,依次傳送一條簡訊來通知他們。這就是釋出-訂閱模式在現實中的例子。

3.2、訂閱器 Dep 實現

​ 完成了資料的'可觀測',即我們知道了資料在什麼時候被讀或寫了,那麼,我們就可以在資料被讀或寫的時候通知那些依賴該資料的檢視更新了,為了方便,我們需要先將所有依賴收集起來,一旦資料發生變化,就統一通知更新。其實,這就是前一節所說的“釋出訂閱者”模式,資料變化為“釋出者”,依賴物件為“訂閱者”。

​ 現在,我們需要建立一個依賴收集容器,也就是訊息訂閱器 Dep,用來容納所有的“訂閱者”。訂閱器 Dep 主要負責收集訂閱者,然後當資料變化的時候後執行對應訂閱者的更新函式。

建立訊息訂閱器 Dep:

function Dep () {
    this.subs = [];
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};
Dep.target = null;
複製程式碼

有了訂閱器,我們再將 defineReactive 函式進行改造一下,向其植入訂閱器:

defineReactive: function(data, key, val) {
	var dep = new Dep();
	Object.defineProperty(data, key, {
		enumerable: true,
		configurable: true,
		get: function getter () {
			if (Dep.target) {
				dep.addSub(Dep.target);
			}
			return val;
		},
		set: function setter (newVal) {
			if (newVal === val) {
				return;
			}
			val = newVal;
			dep.notify();
		}
	});
}
複製程式碼

從程式碼上看,我們設計了一個訂閱器 Dep 類,該類裡面定義了一些屬性和方法,這裡需要特別注意的是它有一個靜態屬性 Dep.target,這是一個全域性唯一 的Watcher,因為在同一時間只能有一個全域性的 Watcher 被計算,另外它的自身屬性 subs 也是 Watcher 的陣列。

四、訂閱者 Watcher 實現

訂閱者 Watcher 在初始化的時候需要將自己新增進訂閱器 Dep 中,那該如何新增呢?我們已經知道監聽器Observer 是在 get 函式執行了新增訂閱者 Wather 的操作的,所以我們只要在訂閱者 Watcher 初始化的時候觸發對應的 get 函式去執行新增訂閱者操作即可,那要如何觸發 get 的函式,再簡單不過了,只要獲取對應的屬性值就可以觸發了,核心原因就是因為我們使用了 Object.defineProperty( ) 進行資料監聽。這裡還有一個細節點需要處理,我們只要在訂閱者 Watcher 初始化的時候才需要新增訂閱者,所以需要做一個判斷操作,因此可以在訂閱器上做一下手腳:在 Dep.target 上快取下訂閱者,新增成功後再將其去掉就可以了。訂閱者 Watcher 的實現如下:

function Watcher(vm, exp, cb) {
    this.vm = vm;
    this.exp = exp;
    this.cb = cb;
    this.value = this.get();  // 將自己新增到訂閱器的操作
}

Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this; // 全域性變數 訂閱者 賦值
        var value = this.vm.data[this.exp]  // 強制執行監聽器裡的get函式
        Dep.target = null; // 全域性變數 訂閱者 釋放
        return value;
    }
};
複製程式碼

訂閱者 Watcher 分析如下:

訂閱者 Watcher 是一個 類,在它的建構函式中,定義了一些屬性:

  • **vm:**一個 Vue 的例項物件;
  • **exp:**是 node 節點的 v-model 等指令的屬性值 或者插值符號中的屬性。如 v-model="name"exp 就是name;
  • **cb:**是 Watcher 繫結的更新函式;

當我們去例項化一個渲染 watcher 的時候,首先進入 watcher 的建構函式邏輯,就會執行它的 this.get() 方法,進入 get 函式,首先會執行:

Dep.target = this;  // 將自己賦值為全域性的訂閱者
複製程式碼

實際上就是把 Dep.target 賦值為當前的渲染 watcher ,接著又執行了:

let value = this.vm.data[this.exp]  // 強制執行監聽器裡的get函式
複製程式碼

在這個過程中會對 vm 上的資料訪問,其實就是為了觸發資料物件的 getter

每個物件值的 getter 都持有一個 dep,在觸發 getter 的時候會呼叫 dep.depend() 方法,也就會執行this.addSub(Dep.target),即把當前的 watcher 訂閱到這個資料持有的 depwatchers 中,這個目的是為後續資料變化時候能通知到哪些 watchers 做準備。

這樣實際上已經完成了一個依賴收集的過程。那麼到這裡就結束了嗎?其實並沒有,完成依賴收集後,還需要把 Dep.target 恢復成上一個狀態,即:

Dep.target = null;  // 釋放自己
複製程式碼

update() 函式是用來當資料發生變化時呼叫 Watcher 自身的更新函式進行更新的操作。先通過 let value = this.vm.data[this.exp]; 獲取到最新的資料,然後將其與之前 get() 獲得的舊資料進行比較,如果不一樣,則呼叫更新函式 cb 進行更新。

至此,簡單的訂閱者 Watcher 設計完畢。

五、解析器 Compile 實現

5.1、解析器 Compile 關鍵邏輯程式碼分析

​ 通過監聽器 Observer 訂閱器 Dep 和訂閱者 Watcher 的實現,其實就已經實現了一個雙向資料繫結的例子,但是整個過程都沒有去解析 dom 節點,而是直接固定某個節點進行替換資料的,所以接下來需要實現一個解析器 Compile 來做解析和繫結工作。解析器 Compile 實現步驟:

  • 解析模板指令,並替換模板資料,初始化檢視;
  • 將模板指令對應的節點繫結對應的更新函式,初始化相應的訂閱器;

我們下面對 '{{變數}}' 這種形式的指令處理的關鍵程式碼進行分析,感受解析器 Compile 的處理邏輯,關鍵程式碼如下:

compileText: function(node, exp) {
	var self = this;
	var initText = this.vm[exp]; // 獲取屬性值
	this.updateText(node, initText); // dom 更新節點文字值
    // 將這個指令初始化為一個訂閱者,後續 exp 改變時,就會觸發這個更新回撥,從而更新檢視
	new Watcher(this.vm, exp, function (value) { 
		self.updateText(node, value);
	});
}
複製程式碼

5.2、簡單實現一個 Vue 例項

完成監聽器 Observer 、訂閱器 Dep 、訂閱者 Watcher 和解析器 Compile 的實現,我們就可以模擬初始化一個Vue 例項,來檢驗以上的理論的可行性了。我們通過以下程式碼初始化一個 Vue 例項,該例項的原始碼已經放到 github 上面:github.com/fengshi123/… ,有興趣的可以 git clone:

<body>
    <div id="mvvm-app">
        <input v-model="title">
        <h2>{{title}}</h2>
        <button v-on:click="clickBtn">資料初始化</button>
    </div>
</body>
<script src="../dist/bundle.js"></script>
<script type="text/javascript">
    var vm = new MVVM({
        el: '#mvvm-app',
        data: {
            title: 'hello world'
        },

        methods: {
            clickBtn: function (e) {
                this.title = 'hello world';
            }
        },
    });
</script>
複製程式碼

執行以上例項,效果圖如下所示,跟實際的 Vue 資料繫結效果是不是一樣!

1.gif

六、Vue 原始碼 — 資料雙向繫結

以上第二章節到第六章節,從監聽器 Observer 、訂閱器 Dep 、訂閱者 Watcher 和解析器 Compile 的實現,完成了一個簡單的 Vue 資料繫結例項的實現。本章節,我們從 Vue 原始碼層面分析監聽器 Observer 、訂閱器 Dep 、訂閱者 Watcher 的實現,幫助大家瞭解 Vue 原始碼如何實現資料雙向繫結。

6.1、監聽器 Observer 實現

我們在本小節主要介紹 監聽器 Observer 實現,核心就是利用 Object.defineProperty 給資料新增了 getter 和 setter,目的就是為了在我們訪問資料以及寫資料的時候能自動執行一些邏輯 。

(1)initState

Vue 的初始化階段,_init 方法執行的時候,會執行 initState(vm) 方法,它的定義在 src/core/instance/state.js 中。

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
複製程式碼

initState 方法主要是對 propsmethodsdatacomputed 和 wathcer 等屬性做了初始化操作。這裡我們重點分析 data,對於其它屬性的初始化我們在以後的文章中再做介紹。

(2)initData

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}
複製程式碼

data 的初始化主要過程也是做兩件事,一個是對定義 data 函式返回物件的遍歷,通過 proxy 把每一個值 vm._data.xxx 都代理到 vm.xxx 上;另一個是呼叫 observe 方法觀測整個 data 的變化,把 data 也變成響應式,我們接下去主要介紹 observe 。

(3)observe

observe 的功能就是用來監測資料的變化,它的定義在 src/core/observer/index.js 中:

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}
複製程式碼

observe 方法的作用就是給非 VNode 的物件型別資料新增一個 Observer,如果已經新增過則直接返回,否則在滿足一定條件下去例項化一個 Observer 物件例項。接下來我們來看一下 Observer 的作用。

(4)Observer

Observer 是一個類,它的作用是給物件的屬性新增 getter 和 setter,用於依賴收集和派發更新:

xport class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // number of vms that have this object as root $data

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    def(value, '__ob__', this)
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods)
      } else {
        copyAugment(value, arrayMethods, arrayKeys)
      }
      this.observeArray(value)
    } else {
      this.walk(value)
    }
  }

  /**
   * Walk through all properties and convert them into
   * getter/setters. This method should only be called when
   * value type is Object.
   */
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }

  /**
   * Observe a list of Array items.
   */
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}
複製程式碼

Observer 的建構函式邏輯很簡單,首先例項化 Dep 物件, Dep 物件,我們第2小節會介紹。接下來會對 value 做判斷,對於陣列會呼叫 observeArray 方法,否則對純物件呼叫 walk 方法。可以看到 observeArray 是遍歷陣列再次呼叫 observe 方法,而 walk 方法是遍歷物件的 key 呼叫 defineReactive 方法,那麼我們來看一下這個方法是做什麼的。

(5)defineReactive

defineReactive 的功能就是定義一個響應式物件,給物件動態新增 gettersetter,它的定義在 src/core/observer/index.js 中:

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

  const property = Object.getOwnPropertyDescriptor(obj, key)
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          childOb.dep.depend()
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}
複製程式碼

defineReactive 函式最開始初始化 Dep 物件的例項,接著拿到 obj 的屬性描述符,然後對子物件遞迴呼叫 observe 方法,這樣就保證了無論 obj 的結構多複雜,它的所有子屬性也能變成響應式的物件,這樣我們訪問或修改 obj 中一個巢狀較深的屬性,也能觸發 getter 和 setter。

6.2、訂閱器 Dep 實現

訂閱器Dep 是整個 getter 依賴收集的核心,它的定義在 src/core/observer/dep.js 中:

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

// The current target watcher being evaluated.
// This is globally unique because only one watcher
// can be evaluated at a time.
Dep.target = null
複製程式碼

Dep 是一個 Class,它定義了一些屬性和方法,這裡需要特別注意的是它有一個靜態屬性 target,這是一個全域性唯一 Watcher,這是一個非常巧妙的設計,因為在同一時間只能有一個全域性的 Watcher 被計算,另外它的自身屬性 subs 也是 Watcher 的陣列。Dep 實際上就是對 Watcher 的一種管理,Dep 脫離 Watcher 單獨存在是沒有意義的。

6.3、訂閱者 Watcher 實現

訂閱者Watcher 的一些相關實現,它的定義在 src/core/observer/watcher.js 中

export default class Watcher {
  vm: Component;
  expression: string;
  cb: Function;
  id: number;
  deep: boolean;
  user: boolean;
  lazy: boolean;
  sync: boolean;
  dirty: boolean;
  active: boolean;
  deps: Array<Dep>;
  newDeps: Array<Dep>;
  depIds: SimpleSet;
  newDepIds: SimpleSet;
  before: ?Function;
  getter: Function;
  value: any;

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: ?Object,
    isRenderWatcher?: boolean
  ) {
    this.vm = vm
    if (isRenderWatcher) {
      vm._watcher = this
    }
    vm._watchers.push(this)
    // options
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = process.env.NODE_ENV !== 'production'
      ? expOrFn.toString()
      : ''
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get()
  }
   。。。。。。
}
複製程式碼

Watcher 是一個 Class,在它的建構函式中,定義了一些和 Dep 相關的屬性 ,其中,this.deps 和 this.newDeps 表示 Watcher 例項持有的 Dep 例項的陣列;而 this.depIds 和 this.newDepIds 分別代表 this.deps 和 this.newDeps 的 id Set 。

(1)過程分析

當我們去例項化一個渲染 watcher 的時候,首先進入 watcher 的建構函式邏輯,然後會執行它的 this.get() 方法,進入 get 函式,首先會執行:

pushTarget(this)
複製程式碼

實際上就是把 Dep.target 賦值為當前的渲染 watcher 並壓棧(為了恢復用)。接著又執行了:

value = this.getter.call(vm, vm)
複製程式碼

這個時候就觸發了資料物件的 getter

麼每個物件值的 getter 都持有一個 dep,在觸發 getter 的時候會呼叫 dep.depend() 方法,也就會執行 Dep.target.addDep(this)

剛才我們提到這個時候 Dep.target 已經被賦值為渲染 watcher,那麼就執行到 addDep 方法:

addDep (dep: Dep) {
  const id = dep.id
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id)
    this.newDeps.push(dep)
    if (!this.depIds.has(id)) {
      dep.addSub(this)
    }
  }
}
複製程式碼

這時候會做一些邏輯判斷(保證同一資料不會被新增多次)後執行 dep.addSub(this),那麼就會執行 this.subs.push(sub),也就是說把當前的 watcher 訂閱到這個資料持有的 depsubs 中,這個目的是為後續資料變化時候能通知到哪些 subs 做準備。所以在 vm._render() 過程中,會觸發所有資料的 getter,這樣實際上已經完成了一個依賴收集的過程。

當我們在元件中對響應的資料做了修改,就會觸發 setter 的邏輯,最後呼叫 watcher 中的 update 方法:

  update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      queueWatcher(this)
    }
  }
複製程式碼

這裡會對於 Watcher 的不同狀態,會執行不同的更新邏輯。

6.4、Vue 資料雙向繫結原理圖

以上主要分析了 Vue 資料雙向繫結的關鍵程式碼,其原理圖可以表示如下:

4.png

七、總結

本文通過監聽器 Observer 、訂閱器 Dep 、訂閱者 Watcher 和解析器 ·的實現,模擬初始化一個 Vue 例項,幫助大家瞭解資料雙向繫結的基本原理。接著,從 Vue 原始碼層面介紹了 Vue 資料雙向繫結的實現過程,瞭解 Vue 原始碼的實現邏輯,從而鞏固加深對資料雙向繫結的理解認識。希望本文對您有幫助。

辛苦編寫良久,如果對您有幫助,請幫忙手動點贊鼓勵~

github地址為:github.com/fengshi123/…,上面彙總了作者所有的部落格文章,如果喜歡或者有所啟發,請幫忙給個 star ~,對作者也是一種鼓勵。

參考文獻

1、Vue 的雙向繫結原理及實現:www.cnblogs.com/canfoo/p/68…

2、Vue 技術揭祕:ustbhuangyi.github.io/vue-analysi…

相關文章