1. 程式人生 > >vue系列---響應式原理實現及Observer原始碼解析(一)

vue系列---響應式原理實現及Observer原始碼解析(一)

閱讀目錄

  • 一. 什麼是響應式?
  • 二:如何偵測資料的變化? 2.1 Object.defineProperty() 偵測物件屬性值變化 2.2 如何偵測陣列的索引值的變化 2.3 如何監聽陣列內容的增加或減少? 2.4 使用Proxy來實現資料監聽
  • 三. Observer原始碼解析
回到頂部

一. 什麼是響應式?

我們可以這樣理解,當一個數據狀態發生改變的時候,那麼與這個資料狀態相關的事務也會發生改變。用我們的前端專業術語來講,當我們JS中的物件資料發生改變的時候,與JS中物件資料相關聯的DOM檢視也會隨著改變。

我們可以先來簡單的理解下Vue中如下的一個demo

<!DOCTYPE html>
<html>
<head>
  <title>vue響應性的測試</title>
  <meta charset="utf-8">
  <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
</head>
<body>
  <div id="app">
    <div>{{ count }}</div>
    <button @click="changeValue">點選我自增</button>
  </div>
  <script type="text/javascript">
    var app = new Vue({
      el: '#app',
      data() {
        return {
          count: 1
        }
      },
      methods: {
        changeValue() {
          this.count++;
        }
      }
    })
  </script>
</body>
</html>

如上demo,當我們點選按鈕的時候,我們的count值會自增1,即data物件中的count屬性值發生改變,它會重新對html頁面進行渲染,因此相關聯資料物件屬性值的檢視也會發生改變。

那麼Vue中它是如何做到的呢?

想要完成此過程,我們需要做如下事情:

1)偵測物件資料的變化。
2)收集檢視依賴了哪些資料。
3)資料變化時,自動通知和資料相關聯的檢視頁面,並對檢視進行更新。

2. 如何偵測資料的變化?

資料物件偵測也可以叫資料劫持,vue.js 是採用資料劫持及釋出者-訂閱者模式,通過Object.defineProperty()來劫持各個屬性的setter,getter。在資料變動時釋出訊息給訂閱者,觸發相應的監聽回撥。當然我們也可以使用ES6中的Proxy來對各個屬性進行代理。

回到頂部

2.1 Object.defineProperty() 偵測物件屬性值變化

在Es5中,新增了一個Object.defineProperty這個API,它可以允許我們為物件的屬性設定getter和setter。因此我們可以使用該方法對該物件的屬性值獲取或設定進行劫持。比如如下程式碼:
var obj = {};
var value = '初始化值';
Object.defineProperty(obj, 'name', {
  get() {
    console.log('監聽getter資料值的變化');
    return value;
  },
  set(newVlue) {
    console.log('監聽setter資料值的變化');
    value = newVlue;
  }
});
console.log(obj.name);
obj.name = 'kongzhi';
console.log(obj.name);

如上程式碼列印效果如下所示:

如上我們可以看到,當我們執行 console.log(obj.name); 獲取 obj物件中屬性name的值的時候,Object.defineProperty方法會監聽obj物件屬性值的變化,自動呼叫get方法,因此首先會列印 "監聽getter資料值的變化" 資訊出來,接著列印 "初始化值",當我們給 obj.name 設定值的時候,就會自動呼叫set方法,因此會列印 "監聽setter資料值的變化" 資訊出來;然後我們列印 console.log(obj.name); 又會自動呼叫get方法,因此會列印 "監聽getter資料值的變化", 最後更新資料,打印出 "kongzhi" 資訊。

如上我們已經瞭解了 Object.defineProperty()方法的基本使用了,因此我們現在可以封裝一個數據監聽器函式,比如叫它為 Observer. 它的作用是能夠對資料物件的所有屬性進行監聽。如下程式碼實現:

function Observer(data) {
  this.data = data;
  this.init();
}

Observer.prototype.init = function() {
  var data = this.data;
  // 遍歷data物件
  Object.keys(data).forEach((key) => {
    this.defineReactive(data, key, data[key]);
  });
};

Observer.prototype.defineReactive = function(data, key, value) {
  // 遞迴遍歷子物件
  var childObj = observer(value);

  // 對物件的屬性進行監聽
  Object.defineProperty(data, key, {
    enumerable: true, // 可列舉
    configurable: true, // 可刪除或可修改目標屬性
    get: function() {
      return value;
    },
    set: function(newValue) {
      if (newValue === value) {
        return;
      }
      value = newValue;
      // 如果新值是物件的話,遞迴該物件 進行監聽
      childObj = observer(newValue);
    }
  });
};

function observer (value) {
  if (!value || typeof value !== 'object') {
    return;
  }
  return new Observer(value);
}
// 呼叫方式如下:
var data = { 
  "name": "kongzhi",
  "user": {
    "name": "tugenhua"
  }
};
observer(data);
data.name = 'kongzhi2';
console.log(data.name); // 列印:kongzhi2
data.user.name = 'tugenhua22';
console.log(data.user.name); // 列印:tugenhua22

如上程式碼我們可以監聽每個物件屬性資料的變化了,那麼監聽到該屬性值變化後我們需要把該訊息通知到訂閱者,因此我們需要實現一個訊息訂閱器,該訂閱器的作用是收集所有的訂閱者。當有物件屬性值發生改變的時候,我們會把該訊息通知給所有訂閱者。

假如我們把該訂閱器函式為Dep; 那麼基本程式碼如下:

function Dep() {
  this.subs = [];
}
Dep.prototype.addSub = function(sub) {
  this.subs.push(sub);
}
Dep.prototype.removeSub = function(sub) {
  if (this.subs.length) {
    var index = this.subs.indexOf(sub);
    if (index !== -1) {
      this.subs.splice(index, 1);
    }
  }
}
Dep.prototype.depend = function() {
  Dep.target.addDep(this);
}
Dep.prototype.notify = function() {
  // 遍歷,通知所有的訂閱者
  this.subs.forEach((sub) => {
    sub.update();
  })
}
Dep.target = null;

如上程式碼,我們就可以使用addSub方法來新增一個訂閱者,或者使用removeSub來刪除一個訂閱者, 我們也可以呼叫 notify 方法來通知所有的訂閱者。 如上 Object.prototype.defineReactive 程式碼中我們能監聽物件屬性值發生改變,如果值發生改變我們需要來通知所有的訂閱者,因此上面的程式碼我們需要改變一些程式碼,如下所示:

Object.prototype.defineReactive = function(data, key, value) {
  .....
  // 呼叫管理所有訂閱者的類
  var dep = new Dep();

  // 對物件的屬性進行監聽
  Object.defineProperty(data, key, {
    enumerable: true, // 可列舉
    configurable: true, // 可刪除或可修改目標屬性
    get: function() {
      // 新增的
      if (Dep.target) {
        dep.depend();
      }
      return value;
    },
    set: function(newValue) {
      if (newValue === value) {
        return;
      }
      value = newValue;
      // 如果新值是物件的話,遞迴該物件 進行監聽
      childObj = observer(newValue);

      // 有值發生改變的話,我們需要通知所有的訂閱者
      dep.notify();
    }
  });
}

如上面的demo,我們已經改變了資料後,我們會使用getter/setter監聽到資料的變化,資料變化後,我們會呼叫Dep類中 notify方法,該方法的作用是遍歷通知所有的訂閱者,通知完訂閱者後,我們需要做什麼呢?就是自動幫我們更新頁面,因此每個訂閱者都會呼叫Watcher類中的update方法,來更新資料。

因此我們需要實現一個Watcher類,Watcher的作用是派發資料更新,不過真正修改DOM,還是需要使用VNode. VNode我們後面會講解到。

Watcher是什麼?它和Dep是什麼關係?

Dep用於依賴收集和派發更新,它收集所有的訂閱者,當有資料變動的時候,它會把訊息通知到所有的訂閱者,同時它也呼叫Watcher實列中的update方法,用於派發更新。

Watcher 用於初始化資料的watcher的實列。它原型上有一個update方法,用於派發更新。比如呼叫回撥函式來更新頁面等操作。

Watcher 簡單實現的程式碼如下:

function Watcher (obj, expOrFn, cb) {
  this.obj = obj;
  this.expOrFn = expOrFn;
  this.cb = cb;
  // 如果expOrFn是事件函式的話
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = this.parseGetter(expOrFn);
  };
  // 觸發getter,從而讓Dep新增自己作為訂閱者
  this.value = this.get();
}
Watcher.prototype.addDep = function(dep) {
  dep.addSub(this);
};
Watcher.prototype.update = function() {
  var value = this.get();
  var oldValue = this.value;
  if (oldValue === value) {
    return;
  }
  this.value = value;
  this.cb.call(this.obj, value, oldValue);
}
Watcher.prototype.get = function() {
  Dep.target = this;
  var value = this.getter.call(this.obj, this.obj);
  return value;
};
/*
 如下函式的作用:像vue中的 vm.$watch('xxx.yyy', function() {}); 這樣的資料能監聽到
 比如如下這樣的data資料:
 var data = { 
   "name": "kongzhi",
   "age": 31,
   "user": {
    "name": "tugenhua"
   }
 };
 我們依次會把data物件中的 'name', 'age', 'user' 屬性傳遞呼叫該函式。
 如果是 'name', 'age', 'user' 這樣的,那麼 exp 就等於這些值。因此:
 this.getter = this.parseGetter(expOrFn); 因此最後 this.getter 就返回了一個函式。
 當我們在 Watcher 類中執行 this.value = this.get(); 程式碼的時候 就會呼叫 getter方法,
 因此會自動執行 parseGetter 函式中返回的函式,引數為 data物件,該函式使用了一個閉包,閉包中儲存的
 引數 exps 就是我們的 'name', 'age', 'user' 及 'user.name' 其中一個,然後依次執行。最後返回的值:
 obj = data['name'] 或 data['age'] 等等這些,因此會返回值value了。
*/
Watcher.prototype.parseGetter = function(exp) {
  var reg = /[^\w.$]/;
  if (reg.test(exp)) {
    return;
  }
  var exps = exp.split('.');
  return function(obj) {
    for (var i = 0, len = exps.length; i < len; i++) {
      if (!obj) {
        return;
      }
      obj = obj[exps[i]];
    }
    return obj;
  }
}

如上Watcher類,傳入三個引數,obj 是一個物件屬性,expOrFn 有可能是一個函式或者是其他型別,比如字串等,cb是我們的回撥函式,然後原型上分別有 addDep,update,get方法函式。

現在我們需要如下呼叫即可:

var data = { 
  "name": "kongzhi",
  "age": 31,
  "user": {
    "name": "tugenhua"
  }
};
// 初始化, 對data資料進行監聽
new Observer(data);

// 變數data物件的所有屬性,分別呼叫
Object.keys(data).forEach((key) => {
  if (data.hasOwnProperty(key)) {
    new Watcher(data, key, (newValue, oldValue) => {
      console.log('回撥函式呼叫了');
      console.log('新值返回:' + newValue);
      console.log('舊值返回:' + oldValue);
    });
  }
});

我們可以在控制檯修改下data中的值看下是否要呼叫回撥函式,效果如下所示:

回到頂部

2.2 如何偵測陣列的索引值的變化

在如何偵測陣列之前,我們用過vue的都知道,vue不能監聽到陣列中的索引的變化,換句話說,陣列中某一項發生改變的時候,我們監聽不到的。比如如下測試程式碼:
<!DOCTYPE html>
<html>
  <head>
    <title>vue響應性的測試</title>
    <meta charset="utf-8">
    <script type="text/javascript" src="https://cn.vuejs.org/js/vue.js"></script>
  </head>
  <body>
    <div id="app">
      <div v-if="arrs.length > 0" v-for="(item, index) in arrs">
        {{item}}
      </div>  
    </div>
    <script type="text/javascript">
      var app = new Vue({
        el: '#app',
        data() {
          return {
            arrs: ['1', '2', '3']
          }
        },
        methods: {}
      });
      app.arrs[1] = 'ccc'; // 改變不了的。不是響應性的
    </script>
  </body>
</html>

Vue官網文件建議我們使用 Vue.set(arrs, index, newValue) 方法來達到觸發檢視更新的效果,比如可以改成如下程式碼即可生效:

// app.arrs[1] = 'ccc';  
Vue.set(app.arrs, 1, 'ccc'); // 會生效的

那麼vue為何不能監聽陣列索引的變化?

Vue官方說明的是:由於Javascript的限制。Vue不能檢測以下變動的陣列:

當你利用索引直接設定一個項時,比如:vm.items[indexOfItem] = newValue;
當你修改陣列的長度時:比如 vm.items.length = newLength;

但是我們自己使用 Object.defineProperty 是可以監聽到陣列索引的變化的,如下程式碼:

var arrs = [
  { "name": "kongzhi111", "age": 30 },
  { "name": "kongzhi222", "age": 31 }
];
function defineReactive(obj, key, value) {
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function() {
      console.log('呼叫了getter函式獲取值了');
      return value;
    },
    set: function(newValue) {
      if (value === newValue) {
        return;
      }
      value = newValue;
      console.log('資料發生改變了');
    }
  })
}
// 程式碼初始化呼叫
defineReactive(arrs[0], 'name', 'kongzhi111');

/*
 會先呼叫 getter方法,會列印 "呼叫了getter函式獲取值了"資訊出來。
 然後列印:kongzhi111 值了。
*/
console.log(arrs[0].name); 

// 改變陣列中第一項name資料
arrs[0].name = "tugenhua"; 

/* 
 * 會先呼叫setter方法,列印:"資料發生改變了" 資訊出來。
 * 然後列印結果為:{name: 'tugenhua', age: 30}
*/
console.log(arrs[0]);

如下圖所示:

但是Vue原始碼中並沒有對陣列進行監聽,據說尤大是說為了效能考慮。所以沒有對陣列使用 Object.defineProperty 做監聽,我們可以來看下原始碼就知道了,原始碼js地址為:src/core/observer/index.js 程式碼如下所示:

export class Observer {
  .....
  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)
    }
  }
}

如上程式碼可以看到,如果 Array.isArray(value) 是陣列的話,就呼叫 observeArray函式,否則的話呼叫walk函式,walk函式程式碼如下所示:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
} 
export function defineReactive () {
  ....
  Object.defineProperty(obj, key, {
    get: function reactiveGetter () {},
    set: function reactiveSetter (newVal) {}
  }
}

因此如果是陣列的話,就沒有使用 Object.defineProperty 對資料進行監聽,因此陣列的改變不會有響應性的。
但是陣列的一些push等這樣的方法會進行重寫的,這個晚點再說。因此改變陣列的索引也不會被監聽到的。那麼既然尤大說為了效能考慮,那麼我們就可以來測試下,假如是陣列的話,我們也使用 Object.defineProperty 來監聽下,看下會怎樣影響效能的呢?因此我們需要把原始碼改成如下測試下:

src/core/observer/index.js 對應的程式碼改成如下:

export class Observer {
  ....
  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)
      */
      this.walkTest(value);
    } else {
      this.walk(value)
    }
  }
  walkTest(values: Array) {
    for (let i = 0, l = values.length; i < l; i++) {
      defineReactive(values, values[i]);
    }
  }
}

如上程式碼,如果是陣列的話,我們依然監聽,我們先把原始碼註釋掉,然後新增 walkTest 函式及呼叫該函式。
然後我們需要在defineReactive函式中的get/set中列印一些資訊出來,程式碼改成如下所示:

export function defineReactive () {

  .....

  Object.defineProperty(obj, key, {
    get: function reactiveGetter () {

      // 如下列印是新增的
      typeof key === "number" && console.log('getter');

      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) {

      // 如下列印是新增的
      typeof key === "number" && console.log('setter');

      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()
    }
  }
}

然後我們需要寫一個測試程式碼,我們就在原始碼中的 example/commit/index.html 程式碼中測試下即可,改成如下程式碼:

<!DOCTYPE html>
<html>
  <head>
    <title>Vue.js github commits example</title>
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="demo">
      <span v-for="(item, index) in arrs" @click="clickFunc(item, index)">&nbsp;{{item}}&nbsp;</span>
    </div>
    <script type="text/javascript">
      new Vue({
        el: '#demo',
        data: {
          arrs: [1, 2]
        },
        methods: {
          clickFunc(item, index) {
            console.log(item, index);
            this.arrs[index] = item + 1;
          }
        }
      })
    </script>
  </body>
</html>

如上程式碼,我們改完,等頁面打包完成後,我們重新整理下頁面可以列印資訊如下所示:

如上我們可以看到,數組裡面只有2個元素,長度為2, 但是從上面結果可以看到,陣列被遍歷了2次,頁面渲染一次。
為什麼會遍歷2次呢?那是因為 在getter函式內部如果是陣列的話會呼叫dependArray(value)這個函式,在該函式內部又會遞迴迴圈判斷是不是陣列等操作。

現在當我們點選2的時候,那麼數字就變為3. 效果如下所示:

如上可以看到,會先呼叫 clickFunc 函式,列印console.log(item, index)資訊出來,然後再呼叫 this.arrs[index] = item + 1; 設定值,因此會呼叫 setter函式,然後資料更新了,重新渲染頁面,又會呼叫getter函式,陣列又遍歷了2次。
如果我們的陣列有10000個元素的長度的話,那麼至少要執行2次,也就是遍歷2次10000的,對效能有點影響。這也有可能是尤大考慮的一個因素,因此它把陣列的監聽去掉了,並且對陣列的一些常用的方法進行了重寫。因此陣列中 push, shift 等這樣的會生效,對陣列中索引值改變或改變陣列的長度不會生效。但是Vue官方中可以使用 Vue.set() 這樣的方法代替。

回到頂部

2.3 如何監聽陣列內容的增加或減少?

Object.defineProperty 雖然能監聽到陣列索引值的變化,但是卻監聽不到陣列的增加或刪除的。
我們繼續看如下demo.

var obj = {};
var bvalue = 1;
Object.defineProperty(obj, "b", {
  set: function(value) {
    bvalue = value;
    console.log('監聽了setter方法');
  },
  get: function() {
    console.log('監聽了getter方法');
    return bvalue;
  }
});
obj.b = 1; // 列印:監聽了setter方法
console.log('-------------');

obj.b = []; // 列印:監聽了setter方法
console.log('-------------');

obj.b = [1, 2]; // 列印:監聽了setter方法
console.log('-------------');

obj.b[0] = 11; // 列印:監聽了getter方法
console.log('-------------');

obj.b.push(12); // 列印:監聽了getter方法
console.log('-------------');

obj.b.length = 5; // 列印:監聽了getter方法
console.log('-------------');
obj.b[0] = 12;

如上測試程式碼,我們可以看到,給物件obj中的屬性b設定值,即 obj.b = 1; 可以監聽到 set 方法。給物件中的b賦值一個新陣列物件後,也可以監聽到 set方法,如:obj.b = []; 或 obj.b = [1, 2]; 但是我們給陣列中的某一項設定值,或使用push等方法,或改變陣列的長度,都不會呼叫 set方法。
也就是說 Object.defineProperty()方法對陣列中的push、shift、unshift、等這樣的方法是無法監聽到的,因此我們需要自己去重寫這些方法來實現使用 Object.defineProperty() 監聽到陣列的變化。

下面先看一個簡單的demo,如下所示:

// 獲得原型上的方法
var arrayProto = Array.prototype;
// 建立一個新物件,該物件有陣列中所有的方法
var arrayMethods = Object.create(arrayProto);
// 對新物件做一些攔截操作
Object.defineProperty(arrayMethods, 'push', {
  value(...args) {
    console.log('引數為:' + args);
    // 呼叫真正的 Array.prototype.push 方法
    arrayProto.push.apply(this, args);
  },
  enumerable: false,
  writable: true,
  configurable: true
});

// 方法呼叫如下:
var arrs = [1];
/*
 重置陣列的原型為 arrayMethods
 如果不重置,那麼該arrs陣列中的push方法不會被Object.defineProperty監聽到
*/
arrs.__proto__ = arrayMethods;
/*
 * 會執行 Object.defineProperty 中的push方法,
 * 因此會列印 引數為:2, 3
*/
arrs.push(2, 3); 
console.log(arrs); // 輸出 [1, 2, 3];

如上程式碼,首先我們獲取原型上的方法,使用程式碼:var arrayProto = Array.prototype; 然後我們使用Object.create()方法建立一個相同的物件arrayMethods(為了避免汙染全域性),因此該物件會有 Array.prototype 中的所有屬性和方法。然後對該arrayMethods中的push方法進行監聽。監聽成功後,呼叫陣列真正的push方法,把值push進去。

注意:我們在呼叫的時候 一定要 arrs.__proto__ = arrayMethods; 要把陣列 arrs 的 __proto__ 指向了 arrayMethods 才會被監聽到的。

理解__proto__ 是什麼呢?

var Kongzhi = function () {};
var k = new Kongzhi();
/*
 列印:
 Kongzhi {
   __proto__: {
     constructor: fn()
     __proto__: {
       // ... 
     }
   }
 }
*/
console.log(k); 
console.log(k.__proto__ === Kongzhi.prototype); // ture

如上程式碼,我們首先定義了一個Kongzhi的建構函式,然後實列化該建構函式,最後賦值給k, 那麼new 時候,我們看new做了哪些事情?
其實我們可以把new的過程拆成如下:

var k = {}; // 初始化一個物件
k.__proto__ = Kongzhi.prototype;
Kongzhi.call(k);

因此我們可以把如上的程式碼改成如下也是可以的:

var Kongzhi = function () {};
var k = {};
k.__proto__ = Kongzhi.prototype;
Kongzhi.call(k);
console.log(k);
console.log(k.__proto__ === Kongzhi.prototype); // ture

和上面的效果一樣的。

現在我們來理解下 __proto__ 到底是什麼?其實在我們定義一個物件的時候,它內部會預設初始化一個屬性為 __proto__;  比如如程式碼可以驗證: var obj = {}; console.log(obj);我們在控制檯上看下結果就可以看到,當我們訪問物件中的某個屬性的時候,如果這個物件內部不存在這個屬性的話,那麼它就會去 __proto__ 裡去找這個屬性,這個__proto__又會有自己的 __proto__。因此會這樣一直找下去,這就是我們以前常說的原型鏈的概念。
我們可以再來看如下程式碼:

var Kongzhi = function() {};
Kongzhi.prototype.age = function() { console.log(31) };
var k = new Kongzhi();
k.age(); // 會打印出 31

如上程式碼,首先 var k = new Kongzhi(); 因此我們可以知道 k.__proto__ = Kongzhi.prototype;所以當我們呼叫 k.age()方法的時候,首先 k 中沒有age()這個方法,
因此會去它的 __proto__ 中去找,也就是 Kongzhi.prototype中去找,Kongzhi.prototype.age = function() {}; 正好有這個方法,因此就會執行。

對__proto__ 理解概念後,我們再來看上面中這句程式碼:arrs.__proto__ =arrayMethods;也就是可以繼續轉化變成如下程式碼:
arrs.__proto__ = Object.create(Array.prototype); 同樣的道理,我們使用Object.defineProperty去監聽 arrayMethods這個新陣列原型的話,如程式碼:Object.defineProperty(arrayMethods, 'push', {});因此使用arrs.push(2, 3) 的時候也會被 Object.defineProperty 監聽到的。因為 arrs.__proto__ === arrayMethods 的。

如上只是一個簡單的實現,為了把陣列中的所有方法都加上,因此程式碼改造成如下所示:

function renderFunc() {
  console.log('html頁面被渲染了');
}
// 定義陣列的常見有的方法
var methods = ['pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push'];
// 先獲取原型上的方法
var arrayProto = Array.prototype;
// 建立一個新物件原型,並且重寫methods中的方法
var arrayMethods = Object.create(arrayProto);
methods.forEach((method) => {
  Object.defineProperty(arrayMethods, method, {
    enumerable: false,
    writable: true,
    configurable: true,
    value(...args) {
      console.log('陣列被呼叫了');
      // 呼叫陣列中的方法
      var original = arrayProto[method];
      original.apply(this, args);
      renderFunc();
    }
  })
});
/*
 * 
*/
function observer(obj) {
  if (Array.isArray(obj)) {
    obj.__proto__ = arrayMethods;
  } else if (typeof obj === 'object'){
    for (const key in obj) {
      defineReactive(obj, key, obj[key]);
    }
  }
}
function defineReactive(obj, key, value) {
  // 遞迴迴圈 
  observer(value);
  Object.defineProperty(obj, key, {
    get: function() {
      console.log('監聽getter函式');
      return value;
    },
    set: function(newValue) {
      // 遞迴迴圈 
      observer(value);
      if (newValue === value) {
        return;
      }
      value = newValue;
      renderFunc();
      console.log('監聽setter函式');
    }
  });
}
// 初始化
var obj = [1, 2];
observer(obj);

/*
 * 呼叫push方法,會被監聽到,因此會列印:陣列被呼叫了
 * 然後呼叫 renderFunc 方法,列印:html頁面被渲染了
*/
obj.push(3);
console.log(obj); // 列印:[1, 2, 3]
console.log('-----------');

var obj2 = {'name': 'kongzhi111'};
observer(obj2);
// 會呼叫getter函式,列印:監聽getter函式, 同時列印值: kongzhi111
console.log(obj2.name); 
console.log('-----------');

/* 
 如下會先呼叫:renderFunc() 函式,因此列印:html頁面被渲染了
 同時會打印出:監聽setter函式
*/
obj2.name = 'kongzhi2222';

如上程式碼演示可以看到,我們對陣列中的 'pop', 'shift', 'unshift', 'sort', 'reverse', 'splice', 'push' 等方法做了重寫操作,會監聽到陣列中這些方法。observer方法中會判斷是否是陣列,如果是陣列的話,obj.__proto__ = arrayMethods; 讓該物件的 __proto__ 指向了原型。因此呼叫陣列上的方法就會被監聽到。當然__proto__這邊有瀏覽器相容問題的,這邊先沒有處理,待會在Vue原始碼中我們可以看到尤大是使用什麼方式來處理__proto__的相容性的。同時也對物件進行了監聽了。如上程式碼可以看得到。

回到頂部

2.4 使用Proxy來實現資料監聽

Proxy是Es6的一個新特性,Proxy會在目標物件之前架設一層 "攔截", 當外界對該物件訪問的時候,都必須經過這層攔截,Proxy就相當於這種機制,類似於代理的含義,它可以對外界訪問物件之前進行過濾和改寫該物件。

目前Vue使用的都是Object.defineProperty()方法針對物件通過 遞迴 + 遍歷的方式來實現對資料的監控的。
我們也知道,通過該方法,不能觸發陣列中的方法,比如push,shift等這些,我們需要在vue中重寫該方法,因此Object.defineProperty()方法存在如下缺點:

1. 監聽陣列的方法不能觸發Object.defineProperty方法中set操作(如果我們需要監聽的話,我們需要重寫陣列的方法)。
2. 必須遍歷每個物件的每個屬性,如果物件巢狀比較深的話,我們需要遞迴呼叫。

因此為了解決Object.defineProperty() 如上的缺點,我們監聽物件資料的變化時,我們可以使用Proxy來解決,但是Proxy有相容性問題。我們這邊先來了解下Proxy的基本使用方法吧!
Proxy基本語法如下:

const obj = new Proxy(target, handler);

引數說明如下:
target: 被代理的物件。
handler: 是一個物件,聲明瞭代理target的一些操作。
obj: 是被代理完成之後返回的物件。

下面我們來看一個如下簡單的demo如下:

const target = {
  'name': "kongzhi"
};
const handler = {
  get: function(target, key) {
    console.log('呼叫了getter函式');
    return target[key];
  },
  set: function(target, key, value) {
    console.log('呼叫了setter函式');
    target[key] = value;
  }
};
console.log('------')
const testObj = new Proxy(target, handler);
console.log(testObj.name);
testObj.name = '1122';
console.log(testObj.name);

如上程式碼,我們呼叫 console.log(testObj.name); 這句程式碼的時候,會首先呼叫get()函式,因此會列印:'呼叫了get函式'; 然後輸出 'kongzhi' 資訊出來,當執行 testObj.name = '1122'; 這句程式碼的時候,會呼叫set()函式,因此會列印: "呼叫了setter函式" 資訊出來,接著列印 console.log(testObj.name); 又會呼叫get()函式, 因此會列印 "呼叫了getter函式" 資訊出來,接著執行:console.log(testObj.name); 列印資訊 '1122' 出來。

如上:target是被代理的物件,handler是代理target的,handler上有set和get方法,當我們每次列印target中的name屬性值的時候會自動執行handler中get函式方法,當我們每次設定 target.name屬性值的時候,會自動呼叫handler中的set方法,因此target物件對應的屬性值會發生改變。同時改變後的testObj物件也會發生改變。

我們下面再來看一個使用 Proxy 代理物件的demo,如下程式碼:

function render() {
  console.log('html頁面被渲染了');
}
const obj = {
  name: 'kongzhi',
  love: {
    book: ['nodejs', 'javascript', 'css', 'html'],
    xxx: '111'
  },
  arrs: [1, 2, 3]
};
const handler = {
  get: function(target, key) {
    if (target[key] && typeof target[key] === 'object') {
      return new Proxy(target[key], handler);
    }
    return Reflect.get(target, key);
  },
  set: function(target, key, value) {
    render();
    return Reflect.set(target, key, value);
  }
};
let proxy = new Proxy(obj, handler);

// 會呼叫set函式,然後執行 render 函式 最後列印 "html頁面被渲染了"
proxy.name = 'tugenhua'; 

// 列印:tugenhua
console.log(proxy.name);

// 會呼叫set函式,然後執行 render 函式 最後列印 "html頁面被渲染了"
proxy.love.xxx = '222';

// 列印:222
console.log(proxy.love.xxx);

// 會呼叫set函式,然後執行 render 函式 最後列印 "html頁面被渲染了"
proxy.arrs[0] = 4;

// 列印:4
console.log(proxy.arrs[0]);

// 列印: 3 但是不會呼叫 set 函式
console.log(proxy.arrs.length);
回到頂部

三. Observer原始碼解析

首先我們先來看一個簡單的demo如下:
<!DOCTYPE html>
<html>
  <head>
    <title>Vue.js github commits example</title>
    <!-- 下面的是vue原始碼 -->
    <script src="../../dist/vue.js"></script>
  </head>
  <body>
    <div id="demo">
      <span v-for="(item, index) in arrs">&nbsp;{{item}}&nbsp;</span>
    </div>
    <script type="text/javascript">
      new Vue({
        el: '#demo',
        data: {
          branches: ['master', 'dev'],
          currentBranch: 'master',
          commits: null,
          arrs: [1, 2]
        }
      });
    </script>
  </body>
</html>

如上demo程式碼,我們在vue例項化頁面後,會首先呼叫 src/core/instance/index.js 的程式碼,基本程式碼如下:

import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

如上Vue建構函式中首先會判斷是否是正式環境和是否例項化了Vue。然後會呼叫 this._init(options)方法。因此進入:src/core/instance/init.js程式碼,主要程式碼如下:

import { initState } from './state';
export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this;
    ..... 省略很多程式碼
    initState(vm);
    ..... 省略很多程式碼
  }
}

因此就會進入 src/core/instance/state.js 主要程式碼如下:

import {
  set,
  del,
  observe,
  defineReactive,
  toggleObserving
} from '../observer/index'

.... 省略很多程式碼

export function initState (vm: Component) {
  .....
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  .....
}

.... 省略很多程式碼

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  
  .... 省略了很多程式碼

  // observe data
  observe(data, true /* asRootData */)
}

如上程式碼我們就可以看到,首先會呼叫 initState 這個函式,然後會進行 if 判斷 opts.data 是否有data這個屬性,該data就是我們的在 Vue例項化的時候傳進來的,之前實列化如下:

new Vue({
  el: '#demo',
  data: {
    branches: ['master', 'dev'],
    currentBranch: 'master',
    commits: null,
    arrs: [1, 2]
  }
});

如上的data,因此 opts.data 就為true,有這個屬性,因此會呼叫 initData(vm) 方法,在 initData(vm) 函式中,如上程式碼我們也可以看到,最後會呼叫 observe(data, true /* asRootData */) 方法。該方法中的data引數值就是我們之前 new Vue({ data: {} }) 中的data值,我們通過打斷點的方式可以看到如下值:

因此會進入 src/core/observer/index.js 中的程式碼 observe 函式,程式碼如下所示:

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 函式程式碼,如上程式碼所示,該程式碼的作用是給data建立一個 Observer實列並返回,從最後一句程式碼我們可以看得到,如上程式碼 ob = new Observer(value); return ob;

如上程式碼首先會if 判斷,該value是否有 '__ob__' 這個屬性,我們value是沒有 __ob__ 這個屬性的,如果有 __ob__這個屬性的話,說明已經實列化過Observer,如果實列化過,就直接返回該實列,否則的話,就例項化 Observer, Vue的響應式資料都會有一個__ob__的屬性,裡面存放了該屬性的Observer實列,目的是防止重複繫結。我們現在先來看看 程式碼:

if (hasOwn(value, '__ob__')) {} 中的value屬性值如下所示:

如上我們可以看到,value是沒有 __ob__ 這個屬性的,因此會執行 ob = new Observer(value); 我們再來看看new Observer 實列化過程中發生了什麼。程式碼如下:

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number;
  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 (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i])
    }
  }
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

如上程式碼我們可以看得到,首先會呼叫 this.dep = new Dep() 程式碼,該程式碼在 src/core/observer/dep.js中,基本程式碼如下:

export default class Dep {
  
  ......

  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()
    }
  }
}
Dep.target = null;

......

Dep程式碼的作用和我們之前講的一樣,就是訊息訂閱器,該訂閱器的作用是收集所有的訂閱者。
程式碼往下執行,我們就會執行 def(value, '__ob__', this) 這句程式碼,因此會呼叫 src/core/util/lang.js 程式碼,
程式碼如下:

// ...... 省略了很多的程式碼
import { arrayMethods } from './array';
// ...... 省略了很多的程式碼
/**
 @param obj;
 obj = {
   arrs: [1, 2],
   branches: ["master", "dev"],
   commits: null,
   currentBranch: "master"
 };
 @param key "__ob__";
 @param val: Observer物件 
 val = {
   dep: { "id": 2, subs: [] },
   vmCount: 0,
   value: {
     arrs: [1, 2],
     branches: ["master", "dev"],
     commits: null,
     currentBranch: "master"
   }
 };
 */
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

如上程式碼我們可以看得到,我們使用了 Object.defineProperty(obj, key, {}) 這樣的方法監聽物件obj中的 __ob__ 這個key。但是obj物件中又沒有該key,因此Object.defineProperty會在該物件上定義一個新屬性為 __ob__, 也就是說,如果我們的資料被 Object.defineProperty繫結過的話,那麼繫結完成後,就會有 __ob__這個屬性,因此我們之前通過了這個屬性來判斷是否已經被繫結過了。我們可以看下demo程式碼來理解下 Object.defineProperty的含義:
程式碼如下所示:

var obj = {
  arrs: [1, 2],
  branches: ["master", "dev"],
  commits: null,
  currentBranch: "master"
};
var key = "__ob__";
var val = {
  dep: { "id": 2, subs: [] },
  vmCount: 0,
  value: {
    arrs: [1, 2],
    branches: ["master", "dev"],
    commits: null,
    currentBranch: "master"
  }
};
Object.defineProperty(obj, key, {
  value: val,
  writable: true,
  configurable: true
});
console.log(obj);

列印obj的值如下所示:

如上我們看到,我們通過 Object.defineProperty()方法監聽物件後,如果該物件沒有該key的話,就會在該obj物件中新增該key屬性。

再接著 就會執行如下程式碼:

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
} else {
  this.walk(value)
}

如上程式碼,首先會判斷該 value 是否是一個數組,如果不是陣列的話,就執行 this.walk(value)方法,如果是陣列的話,就判斷 hasProto 是否為true(也就是判斷瀏覽器是否支援__proto__屬性),hasProto 原始碼如下:

export const hasProto = '__proto__' in {};

如果__proto__指向了物件原型的話(換句話說,瀏覽器支援__proto__),就呼叫 protoAugment(value, arrayMethods) 函式,該函式的程式碼如下:

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

其中 arrayMethods 基本程式碼在 原始碼中: src/core/observer/array.js 中,該程式碼是對陣列中的方法進行重寫操作,和我們之前講的是一樣的。基本程式碼如下所示:

import { def } from '../util/index'

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
});

現在我們再來看之前的程式碼 protoAugment 函式中,其實這句程式碼和我們之前講的含義是一樣的,是讓 value物件引數指向了 arrayMethods 原型上的方法,然後我們使用 Obejct.defineProperty去監聽陣列中的原型方法,當我們在data物件引數arrs中呼叫陣列方法,比如push,unshift等方法就可以理解為對映到 arrayMethods 原型上,因此會被 Object.defineProperty方法監聽到。因此會執行對應的set/get方法。

如上 methodsToPatch.forEach(function (method) { } 程式碼中,為什麼針對 方法為 'push, unshift, splice' 等一些陣列新增的元素也會呼叫 ob.observeArray(inserted) 進行響應性變化。inserted 引數為一個數組。也就是說我們不僅僅對data現有的元素進行響應性監聽,還會對陣列中一些新增刪除的元素也會進行響應性監聽。...args運算子會轉化為陣列。
比如如下簡單的測試程式碼如下:

function a(...args) { 
  console.log(args); // 會列印 [1] 
}; 
a(1); // 函式方法呼叫

// observeArray 函式程式碼如下:

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
}

如上程式碼可以看到,我們對使用 push, unshift, splice 新增/刪除 的元素也會遍歷進行監聽, 再回到程式碼中,為了方便檢視,繼續看下程式碼,回到如下程式碼中:

if (Array.isArray(value)) {
  if (hasProto) {
    protoAugment(value, arrayMethods)
  } else {
    copyAugment(value, arrayMethods, arrayKeys)
  }
  this.observeArray(value)
} else {
  this.walk(value)
}

如果我們的瀏覽器不支援 hasProto, 也就是說 有的瀏覽器不支援__proto__這個屬性的話,我們就會呼叫copyAugment(value, arrayMethods, arrayKeys); 方法去處理,我們再來看下該方法的原始碼如下:

/*
 @param {target} 
 target = {
   arrs: [1, 2],
   branches: ["master", "dev"],
   commits: null,
   currentBranch: "master",
   __ob__: {
     dep: {
       id: 2,
       sub: []
     },
     vmCount: 0,
     commits: null,
     branches: ["master", "dev"],
     currentBranch: "master"
   }
 };
 @param {src} arrayMethods 陣列中的方法實列
 @param {keys} ["push", "shift", "unshift", "pop", "splice", "reverse", "sort"]
*/
function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

如上程式碼可以看到,對於瀏覽器不支援 __proto__屬性的話,就會對陣列的方法進行遍歷,然後繼續呼叫def函式進行監聽:
如下 def程式碼,該原始碼是在 src/core/util/lang.js 中:

export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable,
    writable: true,
    configurable: true
  })
}

回到之前的程式碼,如果是陣列的話,就會呼叫 this.observeArray(value) 方法,observeArray方法如下所示:

observeArray (items: Array<any>) {
  for (let i = 0, l = items.length; i < l; i++) {
    observe(items[i])
  }
};

如果它不是陣列的話,那麼有可能是一個物件,或其他型別的值,我們就會呼叫 else 裡面中 this.walk(value) 的程式碼,walk函式程式碼如下所示:

walk (obj: Object) {
  const keys = Object.keys(obj)
  for (let i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i])
  }
}

如上程式碼,進入walk函式,obj是一個物件的話,使用 Object.keys 獲取所有的keys, 然後對keys進行遍歷,依次呼叫defineReactive函式,該函式程式碼如下:

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
  /*
   檢查屬性之前是否設定了 getter / setter
   如果設定了,則在之後的 get/set 方法中執行 設定了的 getter/setter
  */
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }
  /*
   observer原始碼如下:
   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
   }
   let childOb = !shallow && observe(val); 程式碼的含義是:遞迴迴圈該val, 判斷是否還有子物件,如果
   還有子物件的話,就繼續實列化該value,
  */
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      // 如果屬性原本擁有getter方法的話則執行該方法
      const value = getter ? getter.call(obj) : val
      if (Dep.target) {
        dep.depend()
        if (childOb) {
          // 如果有子物件的話,對子物件進行依賴收集
          childOb.dep.depend();
          // 如果value是陣列的話,則遞迴呼叫
          if (Array.isArray(value)) {
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      /*
       如果屬性原本擁有getter方法則執行。然後獲取該值與newValue對比,如果相等的
       話,直接return,否則的值,執行賦值。
      */
      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方法的話則執行
        setter.call(obj, newVal)
      } else {
        // 如果屬性原本沒有setter方法則直接賦新值
        val = newVal
      }
      // 繼續判斷newVal是否還有子物件,如果有子物件的話,繼續遞迴迴圈遍歷
      childOb = !shallow && observe(newVal);
      // 有值發生改變的話,我們需要通知所有的訂閱者
      dep.notify()
    }
  })
}

如上 defineReactive 函式,和我們之前自己編寫的程式碼類似。上面都有一些註釋,可以稍微的理解下。

如上程式碼,如果資料有值發生改變的話,它就會呼叫 dep.notify()方法來通知所有的訂閱者,因此會呼叫 Dep中的notice方法,我們繼續跟蹤下看下該對應的程式碼如下(原始碼在:src/core/observer/dep.js):

import type Watcher from './watcher'
export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;
  ....
  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()
    }
  }
  ....
}

在notice方法中,我們迴圈遍歷訂閱者,然後會呼叫watcher裡面的update的方法來進行派發更新操作。因此我們繼續可以把視線轉移到 src/core/observer/watcher.js 程式碼內部看下相對應的程式碼如下:

export default class Watcher {
  ...

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

  ...
}

如上update方法,首先會判斷 this.lazy 是否為true,該引數的含義可以理解為懶載入型別。
其次會判斷this.sync 是否為同步型別,如果是同步型別的話,就會直接呼叫 run()函式方法,因此就會直接立刻執行回撥函式。我們下面可以稍微簡單的看下run()函式方法如下所示:

run () {
  if (this.active) {
    const value = this.get()
    if (
      value !== this.value ||
      // Deep watchers and watchers on Object/Arrays should fire even
      // when the value is the same, because the value may
      // have mutated.
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      if (this.user) {
        try {
          this.cb.call(this.vm, value, oldValue)
        } catch (e) {
          handleError(e, this.vm, `callback for watcher "${this.expression}"`)
        }
      } else {
        this.cb.call(this.vm, value, oldValue)
      }
    }
  }
}

如上程式碼我們可以看到,const value = this.get(); 獲取到了最新值,然後立即呼叫 this.cb.call(this.vm, value, oldValue); 執行回撥函式。
否則的話就呼叫 queueWatcher(this);函式,從字面意思我們可以理解為佇列Watcher, 也就是說,如果某一次資料發生改變的話,我們先把該更新的資料快取起來,等到下一次DOM更新的時候會執行。我們可以理解為非同步更新,非同步更新往往是同一事件迴圈中多次修改同一個值,那麼Watcher就會被快取多次。

理解同步更新和非同步更新

同步更新:

上面程式碼中執行 this.run()函式是同步更新,所謂的同步更新是指當觀察者的主體發生改變的時候會立刻執行回撥函式,來觸發更新程式碼。但是這種情況,在日常的開發中並不會有很多,在同一個事件迴圈中可能會改變很多次,如果我們每次都觸發更新的話,那麼對效能來講會非常損耗的,因此在日常開發中,我們使用的非同步更新比較多。

非同步更新:

Vue非同步執行DOM更新,只要觀察到資料的變化,Vue將開啟一個佇列,如果同一個Watcher被觸發多次,它只會被推入到佇列中一次。那麼這種緩衝對於去除一些重複操作的資料是很有必要的,因為它不會重複DOM操作。
在下一次的事件迴圈nextTick中,Vue會重新整理佇列並且執行,Vue在內部會嘗試對非同步佇列使用原生的Promise.then和MessageChannel。如果不支援原生的話,就會使用setTimeout(fn, 0)代替操作。

我們現在再回到程式碼中,我們需要執行 queueWatcher (this) 函式,該函式的原始碼在 src/core/observer/scheduler.js 中,如下程式碼所示:

let flushing = false;
let has = {}; // 簡單用個物件儲存一下wather是否已存在
export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true

      if (process.env.NODE_ENV !== 'production' && !config.async) {
        flushSchedulerQueue()
        return
      }
      nextTick(flushSchedulerQueue)
    }
  }
}

如上程式碼,首先獲取 const id = watcher.id; 如果 if (has[id] == null) {} 為null的話,就執行程式碼,如果執行後會把 has[id] 設定為true。防止重複執行。接著程式碼又會判斷 if (!flushing) {};如果flushing為false的話,就執行程式碼: queue.push(watcher); 可以理解為把 Watcher放入一個佇列中,那為什麼要判斷 flushing 呢?那是因為假如我們正在更新佇列中watcher的時候,這個時候我們的資料又被放入佇列中怎麼辦呢?因此我們加了flushing這個引數來表示佇列的更新狀態。

如上flushing代表的更新狀態的含義,那麼這個更新狀態又分為2種情況。

第一種情況是:flushing 為false,說明這個watcher還沒有處理,就找到這個watcher在佇列中的位置,並且把最新的放在後面,如程式碼:queue.push(watcher);

第二種情況是:flushing 為true,說明這個watcher已經更新過了,那麼就把這個watcher再放到當前執行的下一位,當前watcher處理完成後,再會立即處理這個新的。如下程式碼:

let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
  i--
}
queue.splice(i + 1, 0, watcher);

最後程式碼就會呼叫 nextTick 函式的程式碼去非同步執行回撥。nextTick下文會逐漸講解到,我們這邊只要知道他是非同步執行即可。因此watcher部分程式碼先理解到此了。