小鹿又熬肝寫了一份 Vue 2.0 核心原理!

小鹿又熬肝寫了一份 Vue 2.0 核心原理!

整篇 Vue2.0 核心源碼,差很少寫了一個多半月,因爲文章太長,分兩篇分享,經過動手實踐去實現 Vue 2.0 的核心原理,進一步對 Vue 核心原理的理解和認識。javascript

加上如今面試要求愈來愈高,不管是 Vue 源碼仍是 React 源碼,是常常被面試到的,能夠說是必問。雖然聽起來擼源碼很高大上、很複雜,可是每個複雜的事物都是由簡單構成的,若是經過內部看原理,其實就是基礎+數據結構的還有一些設計模式的實現。html

說實話,這個月,小鹿肝熬的有點多。後續會把這部分都整理到《大前端面試小冊》中去,會根據面試內容進行優化和補充,肝就完事了!前端

目錄vue

小鹿又熬肝寫了一份 Vue 2.0 核心原理!

爲何使用 Vue?java

從前端這麼些年的發展史來看,從網頁設計年代到了如今大前端時代的來臨,各類各樣的技術層出不窮。尤爲是在前端性能優化方面,爲了不頁面的迴流和重繪,前輩們總結出了各類解決優化方案,基本都是儘可能的減小 DOM 操做。node

Vue 的誕生,是一個很大的優化方案,直接用虛擬 DOM 映射真實 DOM,來進行更新,避免了直接操做真實 DOM 帶來的性能缺陷。面試

爲了好理解呢,咱們換個通俗一點的說法,當頁面涉及到操做 DOM 的時候,咱們不直接進行操做,由於這樣下降了前端頁面的性能。而是將 DOM 拿到內存中去,在內存中更改頁面的 DOM ,這時候咱們操做 DOM 不會致使每次操做 DOM 就會形成沒必要要的迴流和重繪。更新完全部 DOM 以後,咱們將更新完的 DOM 再插入到頁面中,這樣大大提升了頁面的性能。算法

雖然這樣講有些欠妥或者不標準,其實 Vue 的虛擬 DOM 的做用能夠這樣去理解,也是爲了照顧到一些剛剛接觸到 Vue 的初學者。本篇寫做的目的不是去寫一高大上的術語,而是能將分享到的內容讓大部分看明白,就已經足夠了。編程

你會學到什麼?設計模式

本篇主要僅供我的 Vue 源碼學習記錄,主要以 Vue2.0 爲主。

主要分享整個 Vue2.0 源碼的核心功能,會將一下幾個功能經過刪減,經過代碼對核心原理部分展開分享,一些用到的變量和函數方法可能與源碼中不相同,因爲時間和精力有限,只分享核心內容部分。主要包括如下幾個核心部分:

一、響應式原理(MVVM)

二、模板編譯 (Compile)

三、依賴追蹤

四、虛擬 DOM (VDDOM)

五、patch

六、diff 算法

帶着問題去學習

有問題纔有學習的動力和激情,若是毫無目的的只扒源碼,顯然是很是枯燥的,前期在挖源碼的時候,小鹿是帶着一下幾個疑問去探索原理的,你是否也存在和小鹿同樣的 vue 問題呢?

一、雙向綁定是怎麼實現的?

二、vue 標籤中的指令內部又是如何解析的?

三、什麼是虛擬 DOM,它比傳統的真實 DOM 有什麼優點?

四、當數據更新時,虛擬 DOM 若是對比新老節點更新真實 DOM 的?

五、頁面多個地方操做 DOM,內部如何實現優化的?

......

以上幾個個問題,前期給我帶來了探索源碼的動力。當看了源碼一個月過去以後,這個期間經過動手實踐和總結,發現這些東西都是在最本來的事物基礎上進行改進和優化,尤爲是對基本功(JS、數據結構與算法)的重要性,越是簡單的東西,越是新事物的組成部分。簡單,簡而不單,單而不簡。能讓你創新出新的事物,萬物皆如此。

Vue2.0 總體歸納

初始化 Vue 實例 ==》 設置數據劫持(Object.defineProperty) ==》模板編譯(compile) ==》渲染(render function) ==》轉化爲虛擬 DOM(Object) ==》對比新老虛擬DOM(patch、diff)==》 更新視圖(真實 dom)

一、傳入實例參數

當咱們開始寫 Vue 項目時,首先初始化一個 Vue 實例,傳入一個對象參數,參數中包括一下幾個重要屬性:

1{
 2    el: '#app',
 3    data: {
 4        student: {
 5            name: '公衆號:小鹿動畫學編程',
 6            age: 20,
 7        }
 8    }
 9    computed:{
10        ...
11    }
12    ...
13}

1) el:將渲染好的 DOM 掛載到頁面中(能夠傳入一個 id,也能夠傳入一個 dom 節點)。

2) data:頁面所須要的數據(對象類型,至於爲何,會在數據劫持內容說明)。

3) computed:計算屬性,隨着 data 中的數據變化,來更新頁面關聯的計算屬性。

4) methods:實例所用到的方法集合。

除此以外,還有一些生命週期鉤子函數等其餘內容。

二、設置數據劫持

所謂的數據劫持,當 Vue 實例上的 data 中的數據改變時,對應的視圖所用到的 data 中數據也會在頁面改變。因此咱們須要給 data 中的全部數據設置一個監聽器,監聽 data 的改變和獲取,一旦數據改變,監聽器會觸發,通知頁面,要改變數據了。

1 Object.defineProperty(obj, key, {
2     get() {
3         return value;
4     },
5     set: newValue => {
6         console.log(---------------更新視圖--------------------)
7     }
8 }

數據劫持的實現就是給每個 data綁定 Object.defineProperty()。對於 Object.defineProperty()的用法,本身詳細看 MDN ,這也是 MVVM的核心實現 API,下遍不少東西都是圍繞着它轉。

三、模板編譯(compile)

拿到傳入 dom 對象和 data 數據了,若是將這些 data 渲染到 HTML 所對應的 {{student.age}}、v-model="student.name" 等標籤中,這個過程就是模板編譯的過程,主要解析模板中的指令、class、style等等數據。

1// 把當前節點放到內存中去(由於頻繁渲染形成迴流和重繪)
2let fragment = this.nodefragment(this.el);
3
4// 把節點在內存中替換(編譯模板,數據編譯)
5this.compile(fragment);
6
7// 把內容塞回頁面
8this.el.appendChild(fragment);

咱們經過 el 拿到 dom 對象,而後將這個當前的 dom 節點拿到內存中去,而後將數據和 dom 節點進行替換合併,而後再把結果塞會到頁面中。下面會根據代碼實現,具體展開分享。

四、虛擬 DOM(Virtual DOM)

所謂虛擬 DOM,其實就是一個 javascript對象,說白了就是對真實 DOM 的一個描述對象,和真實 dom作一個映射。

1// 真實 DOM
 2<div>
 3    <span>HelloWord</span>
 4</div>
 5
 6
 7// 虛擬 DOM —— 以上的真實 DOM 被虛擬 DOM 表示以下:
 8{
 9    children:(1) [{…}]  // 子元素
10    domElement: div        // 對應的真實 dom    
11    key: undefined      // key 值
12    props: {}           // 標籤對應的屬性
13    text: undefined     // 文本內容
14    type: "div"         // 節點類型
15    ...
16}

一旦頁面數據有變化,咱們不直接操做更新真實 DOM,而是更新虛擬 DOM,又由於虛擬 DOM和真實 DOM有映射關係,全部真實 DOM也被簡潔更新,避免了迴流和重繪形成性能上的損失。

對於虛擬 DOM,主要核心涉及到 diff算法,新老虛擬結點如何檢查差別的,而後又是如何進行更新的,後邊會展開一點點講。

五、對比新老虛擬 DOM(patch)

patch 主要是對更新後的新節點和更新前的節點進行比對,比對的核心算法就是 diff 算法,好比新節點的屬性值不一樣,新節點又增長了一個子元素等變化,都須要經過這個過程,將最後新的虛擬 DOM 更新到視圖上,呈現最新的變化,這個過程是一個核心部分,面試也是常常問到的。

六、更新視圖(update view)

當第一次加載 Vue 實例的時候,咱們將渲染好的數據掛載到頁面中。當咱們已經將實例掛載到了真實 dom 上,咱們更新數據時,新老節點對比完成,拿到對比的最新數據狀態,而後更新到視圖上去。

注意:如下代碼並不是原封不動的源代碼,爲了可以清晰易懂,只是將一些核心原理進行抽離,經過本身實現的代碼來展開分享,爲了不沒必要要的爭議,請自行翻看源代碼。

實現一個雙向綁定

1、響應式原理

咱們都用過 Vue 中的 v-model 實現輸入框和數據的雙向綁定,其實就是 MVVM框架的核心原理實現。

若是剛接觸 MVVM,能夠看小鹿以前在公衆號分享的一篇文章:

動畫:淺談後臺 MVC 模型與 MVVM 雙向綁定模型

下面咱們動手來實現一個 MVVM 雙向綁定。

1<!DOCTYPE html>
 2<html lang="en">
 3
 4<head>
 5  <meta charset="UTF-8">
 6  <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7  <meta http-equiv="X-UA-Compatible" content="ie=edge">
 8  <title>Document</title>
 9</head>
10
11<body>
12  <div id="app">
13    <input type="text" v-model="student.name">
14    {{student.age}}
15  </div>
16  <script src="./node_modules/vue/dist/vue.min.js"></script> 
17  <script>
18    let vm = new Vue({
19      el: '#app',
20      data: {
21        student: {
22          name: '公衆號:小鹿動畫學編程',
23          age: 20,
24        }
25      }
26    })
27  </script>
28</body>
29
30</html>

一、初始化

初始化 Vue 實例,這個過程會作不少事情,好比初始化生命週期、data、computed、Method 等。咱們將實例中傳入的數據,進行在構造函數中接收。

1class Vue {
 2  // 傳參接收
 3  constructor(options) {
 4    this.$el = options.el;
 5    this.$data = options.data;
 6    let computed = options.computed;
 7    let methods = options.methods;
 8
 9    // 判斷 $el 根元素是否存在
10    if (this.$el) {
11      // 一、數據劫持
12      new Observer(this.$data);
13
14      // 二、computed 實現
15      this.relatedComputed(computed);
16
17      // 三、methods 實現
18      this.relatedMethods(methods);
19
20      // 四、編譯模板
21      new Compile(this.$el, this);
22
23      // ....
24
25    }
26  }
27}

以上代碼中,判斷當前 $el 是否存在,若是存在,就開始初始化響應式系統以及 computed 、methods的實現,最後編譯模板,顯示在視圖上。

二、數據劫持

響應式的原理就是經過 Object.defineProperty 數據劫持來實現的,也就上述代碼中的 new Observer(this.$data)過程,這個過程發生了什麼?以及如何對 data 中各類類型數據進行監聽的,下面直接看核心實現原理部分。

先看總體的實現代碼,而後分別進行拆分講解:

1class Observer {
 2  constructor(data) {
 3    this.observer(data);
 4  }
 5
 6  // 觀察者(監聽對象的響應式)
 7  observer(obj) {
 8    // 判斷是否爲對象
 9    if (typeof obj !== "object" || obj == null) return obj;
10
11    // 實時響應數組中對象的變化
12    if (Array.isArray(obj)) {
13      Object.setPrototypeOf(obj, proto);
14      this.observerArray(obj);
15    } else {
16      // 遍歷對象 key value 監聽值的變化
17      for (let key in obj) {
18        this.defineReactive(obj, key, obj[key]);
19      }
20    }
21  }
22
23  defineReactive(obj, key, value) {
24    // value 多是對象,須要進行遞歸
25    this.observer(value);
26    Object.defineProperty(obj, key, {
27      get() {
28        return value;
29      },
30      set: newValue => {
31        if (newValue !== value) {
32          // 傳入的可能也是對象,須要遞歸
33          this.observer(value);
34          value = newValue;
35          console.log('-------------------------視圖更新-----------------------------')
36        }
37      }
38    });
39  }

首先,聲明一個 Observer 類,接收傳入 data 中要給頁面渲染的數據。

1class Observer {
2  constructor(data) {
3    this.observer(data);
4  } 
5}

調用 this.observer(data) 方法,遍歷 data 中的每一個數據進,都經過 Object.defineProperty() 方法設置上監聽。

三、監聽對象

observer() 方法實現主要用於實時響應數組中對象的變化。

1observer(obj) {
 2  // 判斷是否爲對象
 3  if (typeof obj !== "object" || obj == null) return obj;
 4
 5  // 遍歷對象 key value 監聽值的變化
 6  for (let key in obj) {
 7      this.defineReactive(obj, key, obj[key]);
 8  }
 9}
10
11defineReactive(obj, key, value) {
12  // 遞歸建立 響應式數據,性能很差
13  this.observer(value);  // 遞歸
14  Object.defineProperty(obj, key, {
15    get() {
16      return value;
17    },
18    set: newValue => {
19      if (newValue !== value) {
20        // 設置某個 key 的時候,多是一個對象
21        this.observer(value);   // 遞歸
22        value = newValue;
23        console.log('-------------------------視圖更新-----------------------------')
24      }
25    }
26  });

data 是一個對象,咱們對 data 數據對象進行遍歷,經過調用 defineReactive 方法,給每一個屬性分別設置監聽(set 和 get 方法)。

咱們對屬性設置的監聽,只是第一層設置了監聽,若是屬性值是個對象,咱們也要進行監聽。或者咱們在給 Vue 實例 vm 中 data 賦值的時候,也多是個對象,以下狀況:

1data: {
 2  student: {
 3    name: '小鹿',
 4    age: 20,
 5    address:{   // address 也是一個對象類型的值,須要對 address 中的屬性值進行監聽
 6        country:'china'
 7        province:'shandong',
 8    }
 9  }
10},

因此咱們要進行遞歸,也給其設置響應式。

1...
 2
 3defineReactive(obj, key, value) {
 4  // 遞歸建立 響應式數據,性能很差
 5  this.observer(value);  // 遞歸
 6  ...
 7}
 8...
 9
10...
11set: newValue => {
12      if (newValue !== value) {
13        // 設置某個 key 的時候,多是一個對象
14        this.observer(value);   // 遞歸
15        value = newValue;
16        console.log('-------------------------視圖更新-----------------------------')
17      }
18    }
19...

設置好以後,當咱們運行程序,給 vm 設置某一值的時候,會觸發視圖的更新。

四、監聽數組

上述咱們只對對象的屬性進行監聽,可是咱們但願監聽的是個數組,對於數組,用Object.defineProperty() 來設置是不起做用的(具體緣由見 MDN),因此不能用此方法。

若是數組中存放的是對象,咱們也應該監聽屬性的變化,好比監聽數組中 name 的變化。

1{
2  d: [1, 2, 3, { name: "小鹿" }]
3};

首先,咱們判斷當前傳入的若是是數組類型,咱們就調用 observerArray 方法。

1// 判斷傳入的參數若是是數組,則執行 observerArray 方法
2if (Array.isArray(obj)) {
3   this.observerArray(obj);
4}

observerArray 方法的具體實現以下:

1// 遍歷數組中的對象,並設置監聽
2observerArray(obj) {
3  for (let i = 0; i < obj.length; i++) {
4    let item = obj[i];
5    this.observer(item);    // 若是數組中是對象會被 defineReactive 監聽
6  }
7}

當咱們進行下方更改值時,視圖被觸發更新。

1// 初始化 data 中的值
2{
3  d: [1, 2, 3, { name: "小鹿" }]
4}
5
6// 更改數組中的對象屬性的值
7vm.$data.d[3].name = "11";  // 此時視圖會更新

還有一點就是,當咱們給當前的數組添加元素時,也要觸發視圖進行更新,好比經過下方的方式更改數組。

1// 經過 push 向 data 中的數組中添加一個值
2vm.$data.d.push({ age: "15" });

除此以外,數組中添加數據的 API 有 push、unshift、splice ,咱們能夠經過重寫這三個原生方法,對其調用時,進行觸發視圖更新。

1let arrProto = Array.prototype; // 數組原型上的方法
 2let proto = Object.create(arrProto); // 複製原型上的方法
 3
 4// 重寫數組的三個方法
 5[`push`, `unshift`, `splice`].forEach(method => {
 6  proto[method] = function(...args) {
 7    // 這個數組傳入的對象也應該進行監控
 8    let inserted; // 默認沒有插入新的數據
 9    switch (method) {
10      case `push`:
11      case `unshift`:
12        inserted = args;
13        break;
14      case `splice`:
15        inserted = args.slice(2); // 截出傳入的數據
16        break;
17      default:
18        break;
19    }
20    console.log("---------------視圖更新-----------------");
21    observerArray(inserted); // 若是數組增長的值是對象類型,須要對其設置監聽
22    arrProto[method].call(this, ...args);
23  };
24});
25
26// 實時響應數組中對象的變化
27if (Array.isArray(obj)) {
28    Object.setPrototypeOf(obj, proto);  // 若是是數組,就設置重寫的數組原型對象
29    this.observerArray(obj);
30} else {
31    // 遍歷對象 key value 監聽值的變化
32    for (let key in obj) {
33        this.defineReactive(obj, key, obj[key]);
34    }
35}

好了,咱們來測試一下,數組被監聽到,視圖已更新。

小鹿又熬肝寫了一份 Vue 2.0 核心原理!

五、computed 實現

computed主要是計算屬性,每當咱們計算屬性所依賴的 data 屬性發生變化時,經過計算,也要更新視圖上的數據。以下實例,若是咱們動態改變 this.student.name 屬性值,頁面中的 getNewName 也會發生改變。

1let vm = new Vue({
 2    el: '#app',
 3    data: {
 4        student: {
 5            name: '小鹿',
 6            age: 20,
 7        },
 8    },
 9    computed: {
10        getNewName() {
11            return this.student.name + ‘公衆號:小鹿動畫學編程’;
12        }
13    },
14})

其實內部的原理作法就是讓 computed 內的計算屬性也依賴於 data 數據,data 變,computed 依賴的數據也變。

1relatedComputed(computed) {
2    for (let key in computed) {
3        Object.defineProperty(this.$data, key, {
4            get: () => {
5                return computed[key].call(this);
6            }
7        });
8    }
9}

六、methods 實現

咱們一般調用方法是經過 vm 實例來調用方法的,因此咱們要把 methods 掛載到 vm 實例上。

1// methods
 2relatedMethods(methods) {
 3  for (let key in methods) {
 4    Object.defineProperty(this, key, {
 5      get: () => {
 6        return methods[key];
 7      }
 8    })
 9  }
10}

七、vm.$data 代理到 vm 實例上

咱們通常能夠經過 vm.$data.student.name = '小鹿' ,可是還可使用 vm.student.name = ‘小鹿’。咱們能夠經過代理,將 vm.$data 代理到 vm 上。

1// 代理 vm.$data
 2proxyVm(data) {
 3    for (let key in data) {
 4        // 綁定到 vm 上
 5        Object.defineProperty(this, key, {
 6            get() {
 7                return data[key];
 8            },
 9            set(newValue) {
10                data[key] = newValue;
11            }
12        });
13    }
14}

依賴收集

一、爲何進行依賴收集

咱們 data 中的數據,有時候咱們在頁面不一樣地方須要使用,因此當咱們動態改變 data 數據的時候,以下:

1<div>{{student.name}}</div>
2<ul>
3    <li>1</li>
4    <li>{{student.name}}</li>
5</ul>
6
7vm.$data.student.name = 'xiaolu '

咱們對視圖中,全部依賴 data 屬性中的值進行更新,那麼咱們須要對依賴的數據的視圖進行數據依賴收集,當數據變化的時候,就對所依賴數據的視圖更新。對於依賴收集,須要使用觀察者-訂閱者模式。

二、觀察者 Watcher

觀察中的 get() 主要用於獲取當前表達式(如:student.name)的 未更新以前的值,當數據更新時,咱們就調用 update 方法,就拿出新值和老值對比,若是有變化,咱們就更新相對應的視圖。

1// 觀察者
 2class Watcher {
 3  /**
 4   * @param {*} vm 當前實例
 5   * @param {*} expr 觀察的值表達式
 6   * @param {*} cb 回調函數
 7   */
 8  constructor(vm, expr, cb) {
 9    this.vm = vm;
10    this.expr = expr;
11    this.cb = cb;
12    // 默認存放一個老值(取出當前表達式的值)
13    this.oldValue = this.get();
14  }
15
16  get() {
17    Dep.target = this;
18    let value = CompileUtil.getValue(this.vm, this.expr);// 根據視圖中的表達式,取 data 中的值
19    Dep.target = null; // 不取消任何值取值 都會添加 water
20    return value;
21  }
22
23  // -> 數據變化後,會調用觀察者的 update 方法
24  update() {
25    let newValue = CompileUtil.getValue(this.vm, this.expr);// 根據視圖中的表達式,取 data 中的值
26    if (newValue !== this.oldValue) {
27      this.cb(newValue);
28    }
29  }
30}

三、訂閱者

訂閱者中主要經過 addSub 方法增長觀察者,經過 notify 通知觀察者,調用觀察者的 update 進行更新相應的視圖。

1// 訂閱者
 2class Dep {
 3  constructor() {
 4    this.subs = []; // 存放全部的 watcher
 5  }
 6
 7  // 訂閱
 8  addSub(watcher) {
 9    this.subs.push(watcher);
10  }
11
12  // 通知
13  notify() {
14    this.subs.forEach(watcher => watcher.update());
15  }
16}

四、依賴收集

在咱們更新視圖的時候進行依賴收集,給每一個屬性建立一個發佈訂閱的功能,當咱們的值在 set 中改變時,咱們就觸發訂閱者的通知,讓各個依賴該數據的視圖進行更新。

1defineReactive(obj, key, value) {
 2  // 遞歸建立 響應式數據,性能很差
 3  this.observer(value);
 4  let dep = new Dep(); // 給每個屬性都加上一個具備發佈訂閱的功能
 5  Object.defineProperty(obj, key, {
 6    get() {
 7      // 建立 watcher 時,會取到響應內容,而且把 watcher 放到了全局上
 8      Dep.target && dep.addSub(Dep.target);  // 增長觀察者
 9      return value;
10    },
11    set: newValue => {
12      if (newValue !== value) {
13        // 設置某個 key 的時候,多是一個對象
14        this.observer(value);
15        value = newValue;
16        console.log('-------------------------視圖更新-----------------------------')
17        dep.notify(); // 通知
18      }
19    }
20  });

剩下的就是咱們調用 new Watcher 地方了,這個過程在編譯模板裏邊。

3、編譯模板

對於模板的編譯,咱們首先須要判斷傳入的 el 類型,而後拿到頁面的結點到內存中去,把節點上有數據編譯的地方,好比:v-model、v-on、{{student.name}} 進行數據的替換,而後再塞回頁面,就完成的頁面的顯示。

1// 編譯類
 2class Compile {
 3  constructor(el, vm) {
 4    // 判斷 el 傳入的類型
 5    this.el = this.isElementNode(el) ? el : document.querySelector(el);
 6    this.vm = vm;
 7
 8    // 把當前節點放到內存中去 —— 之因此塞到內存中,是由於頻繁渲染形成迴流和重繪
 9    let fragment = this.nodefragment(this.el);
10
11    // 把節點在內存中將表達式和命令等進行數據替換
12    this.compile(fragment);
13
14    // 把內容塞回頁面
15    this.el.appendChild(fragment);
16  }
17}

一、將 DOM 拿到內存

首先咱們以前已經聲明好 data 了,以下:

1 let vm = new Vue({
2     el: '#app',
3     data: {
4         student: {
5             name: '小鹿',
6             age: 20,
7         },
8     }
9 })

而後咱們須要拿到頁面的模板,將頁面中的一些指令(v-model="student.name")或者表達{{student.name}} 的結點替換成咱們對應的屬性值。

咱們須要經過傳入的 el 屬性值先拿到頁面的 dom 到內存中。

1/**
 2   * 將 DOM 拿到內存中
 3   * @param {*} node DOM
 4   */
 5nodefragment(node) {
 6    let fragment = document.createDocumentFragment();
 7    let firstChild;
 8    while ((firstChild = node.firstChild)) {
 9        fragment.appendChild(firstChild);
10    }
11    return fragment;
12}

二、數據替換

咱們下一步須要將頁面中的這些表達式,替換成相對應的 data 中的屬性值,那麼頁面就將完成的呈現出帶有數據的視圖來。

1<div id="app">
2    <input type="text" v-model="student.name">
3    {{student.age}}
4</div>

經過上邊的方法,已經將全部的頁面結點循環遍歷拿到。下一步開始進行一層層的遍歷,將數據在內存中進行替換。

1/**
 2 * 核心編譯方法
 3 * 編譯內存中的 DOM 節點
 4 * @param {*} node
 5 */
 6compile(node) {
 7  let childNodes = node.childNodes;
 8  [...childNodes].forEach(child => {
 9    // 判斷當前的是元素仍是文本節點
10    if (this.isElementNode(child)) {
11      this.compileElement(child);
12      // 若是是元素的話,須要把本身傳進去,再去遍歷子節點
13      this.compile(child);
14    } else {
15      this.compileText(child); // 文本節點有 {{student.age}}
16    }
17  });
18}
19
20/**
21 * 判斷當前傳入的節點是否是元素節點
22 * @param {*} node 節點
23 */
24isElementNode(node) {
25  return node.nodeType == 1; // 1 表明元素節點
26}

this.isElementNode(child)

頁面是由不少的 node 結點構成,在上邊的頁面中,v-model="student.name" 主要存在與元素節點中,{{student.age}} 表達式的值存在於文本節點中,因此咱們須要經過 this.isElementNode(child) 進行判斷當前是否爲元素節點,而後對當前節點進行不一樣的處理。

對於元素節點,咱們調用 compileElement(child)方法,固然,元素節點中可能存在子節點的狀況,因此咱們須要遞歸判斷元素節點裏是否還有子節點,再次調用 this.compile(child); 方法。

咱們以解析 v-model 指令爲例,開始對節點進行解析判斷賦值。

1<input type="text" v-model="student.name">

 1/**
 2 * 編譯元素節點 —— 判斷是否存在 v- 指令
 3 * @param {*} node
 4 */
 5compileElement(node) {
 6  let attributes = node.attributes; 
 7  [...attributes].forEach(attr => {
 8    // type = "text" v-model="student.name"
 9    let { name, value: expr } = attr; // name:v-model  expr:"student.name"
10    // 判斷當前是否存在屬性爲 v- 的指令
11    if (this.isDirective(name)) {
12      // v-html  v-bind  v-model
13      let [, directive] = name.split("-");
14      let [directiveName, eventName] = directive.split(":"); // v-on:click
15      // 調用不一樣的指令來處理
16      CompileUtil[directiveName](node, expr, this.vm, eventName);
17    }
18  });
19}
20
21/**
22 * 判斷是夠是 v- 開頭的指令
23 * @param {*} attrName
24 */
25isDirective(attrName) {
26  return attrName.startsWith("v-");
27}

同時咱們還有一個工具類 CompileUtil,主要用於把對應的 data 數據插入到對應節點中。

上一步中,咱們經過 let [directiveName, eventName] = directive.split(":") 解析出了 directiveName= v-model ,eventName = student.name。

而後咱們將兩個參數 directiveName 和 eventName 傳入工具類對象中。

1// node: 當前節點 expr:當前表達式(student.name) vm:當前 vue 實例
2CompileUtil[directiveName](node, expr, this.vm, eventName);

經過調用不一樣的指令進行不一樣的處理。

1/**
 2 * 工具類(把數據插入到 DOM 中)
 3 * expr: 指令的值(v-model="student.name" 中的 student.name)
 4 */
 5let CompileUtil = {
 6  // ---------------------- 匹配指令或者表達式的函數 ----------------------
 7  // 匹配 v-model
 8  model(node, expr, vm) {
 9    let fn = this.updater["modelUpdater"];
10    new Watcher(vm, expr, newValue => {
11      // 給輸入框添加一個觀察者,若是數據更新了,會觸發此方法,將新值付給 input
12      fn(node, newValue);
13    });
14    // 給 input 綁定事件
15    node.addEventListener("input", e => {
16      let value = e.target.value; // 獲取用戶輸入的內容
17      this.setValue(vm, expr, value);
18    });
19    let value = this.getValue(vm, expr);
20    fn(node, value);
21  },
22
23  // ---------------- 其餘用到的工具函數 -------------------
24  // $data取值 [student, name]
25  getValue(vm, expr) {
26    return expr.split(".").reduce((data, current) => {
27      return data[current];
28    }, vm.$data);
29  },
30
31  // 給 vm.$data 中數據賦值
32  setValue(vm, expr, value) {
33    expr.split(".").reduce((data, current, index, arr) => {
34      // 若是遍歷取到最後一個,我就給賦值
35      if (index == arr.length - 1) {
36        return (data[current] = value);
37      }
38      return data[current];
39    }, vm.$data);
40  },
41
42  // -------------- 給對應的 dom 進行賦值 -------------------
43  updater: {
44    modelUpdater(node, value) {
45      // 處理指令結點 v-model
46      node.value = value;
47    }
48  }
49};

以上就會觸發這個函數:

1// 匹配 v-model
 2model(node, expr, vm) {
 3    let fn = this.updater["modelUpdater"];
 4    new Watcher(vm, expr, newValue => {
 5        // 給輸入框添加一個觀察者,若是數據更新了,會觸發此方法,將新值付給 input
 6        fn(node, newValue);
 7    });
 8    // 給 input 綁定事件
 9    node.addEventListener("input", e => {
10        let value = e.target.value; // 獲取用戶輸入的內容
11        this.setValue(vm, expr, value);
12    });
13    let value = this.getValue(vm, expr);
14    fn(node, value);
15},

同時咱們看到了 new Watch 對該屬性建立一個觀察者,用於之後數據更新時,通知視圖進行相應的更新的。

1new Watcher(vm, expr, newValue => {
2    // 給輸入框添加一個觀察者,若是數據更新了,會觸發此方法,將新值付給 input
3    fn(node, newValue);
4});

同時又給 input 綁定了一個事件,用於實現對 input 框的監聽,相對應的 data 也要更新,這就實現了v-model輸入框的雙向綁定功能。

1// 給 input 綁定事件
2node.addEventListener("input", e => {
3    let value = e.target.value; // 獲取用戶輸入的內容
4    this.setValue(vm, expr, value);
5});

每當 data 數據被改變,咱們就觸發 this.updater 中的視圖更新函數。

1let fn = this.updater["textUpdater"];
2fn(node, value);
1// 給 dom 文本結點賦值數據
2updater: {
3  modelUpdater(node, value) {
4    // 處理指令結點 v-model
5    node.value = value;
6  }
7}

對於文本節點,調用 this.compileText(child) 方法和以上一樣的實現方法。這一部分的總體實現代碼以下:

1**
 2 * 工具類(把數據插入到 DOM 中)
 3 * expr: 指令的值(v-model="school.name" 中的 school.name)
 4 */
 5let CompileUtil = {
 6  // $data取值 [school, name]
 7  getValue(vm, expr) {
 8    return expr.split(".").reduce((data, current) => {
 9      return data[current];
10    }, vm.$data);
11  },
12
13  // 給 vm.$data 中數據賦值
14  setValue(vm, expr, value) {
15    expr.split(".").reduce((data, current, index, arr) => {
16      // 若是遍歷取到最後一個,我就給賦值
17      if (index == arr.length - 1) {
18        return (data[current] = value);
19      }
20      return data[current];
21    }, vm.$data);
22  },
23
24  // 匹配 v-model
25  model(node, expr, vm) {
26    let fn = this.updater["modelUpdater"];
27    new Watcher(vm, expr, newValue => {
28      // 給輸入框添加一個觀察者,若是數據更新了,會觸發此方法,將新值付給 input
29      fn(node, newValue);
30    });
31    // 給 input 綁定事件
32    node.addEventListener("input", e => {
33      let value = e.target.value; // 獲取用戶輸入的內容
34      this.setValue(vm, expr, value);
35    });
36    let value = this.getValue(vm, expr);
37    fn(node, value);
38  },
39
40  html(node, expr, vm) {
41    //xss
42    let fn = this.updater["htmlUpdater"];
43    new Watcher(vm, expr, newValue => {
44      console.log(newValue);
45      fn(node, newValue);
46    });
47    let value = this.getValue(vm, expr);
48    fn(node, value);
49  },
50
51  // 獲取 {{a}} 中的值
52  getContentValue(vm, expr) {
53    // 遍歷表達式 將內容 從新特換成一個完整的內容 返還出去
54    return expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
55      return this.getValue(vm, args[1]);
56    });
57  },
58
59  // v-on:click="change"
60  on(node, expr, vm, eventName) {
61    node.addEventListener(eventName, e => {
62      vm[expr].call(vm, e);
63    });
64  },
65
66  // 可能存在 {{a}} {{b}} 多個樣式
67  text(node, expr, vm) {
68    let fn = this.updater["textUpdater"];
69    let content = expr.replace(/\{\{(.+?)\}\}/g, (...args) => {
70      // 給表達式 {{}} 中的值添加一個觀察者,若是數據更新了,會觸發此方法
71      new Watcher(vm, args[1], () => {
72        fn(node, this.getContentValue(vm, expr)); // 返回一個全新的字符串
73      });
74      return this.getValue(vm, args[1]);
75    });
76    fn(node, content);
77  },
78
79// 給 dom 文本結點賦值數據
80updater: {
81  modelUpdater(node, value) {
82    // 處理指令結點 v-model
83    node.value = value;
84  },
85  textUpdater(node, value) {
86    // 處理文本結點 {{}}
87    node.textContent = value;
88  },
89  htmlUpdater(node, value) {
90    // 處理指令結點 v-html
91    node.innerHTML = value;
92  }
93}
94};

三、塞回頁面

此時,咱們將渲染好的 fragment 塞回到真實 DOM中就能夠正常顯示了。

1this.el.appendChild(fragment);

當咱們在輸入框中輸入數據時,相對應的視圖上 {{student.name}} 的地方進行實時的更新;當咱們經過 vm.$data.student.name 改變數據時,輸入框內的數據也會發生改變。

小鹿又熬肝寫了一份 Vue 2.0 核心原理!

從頭至尾咱們實現了一個雙向綁定。

相關文章
相關標籤/搜索