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

閱讀目錄javascript

一. 什麼是響應式?css

咱們能夠這樣理解,當一個數據狀態發生改變的時候,那麼與這個數據狀態相關的事務也會發生改變。用咱們的前端專業術語來說,當咱們JS中的對象數據發生改變的時候,與JS中對象數據相關聯的DOM視圖也會隨着改變。html

咱們能夠先來簡單的理解下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

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

想要完成此過程,咱們須要作以下事情:node

1)偵測對象數據的變化。
2)收集視圖依賴了哪些數據。
3)數據變化時,自動通知和數據相關聯的視圖頁面,並對視圖進行更新。react

2. 如何偵測數據的變化?git

數據對象偵測也能夠叫數據劫持,vue.js 是採用數據劫持及發佈者-訂閱者模式,經過Object.defineProperty()來劫持各個屬性的setter,getter。在數據變更時發佈消息給訂閱者,觸發相應的監聽回調。固然咱們也可使用ES6中的Proxy來對各個屬性進行代理。github

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部分代碼先理解到此了。

相關文章
相關標籤/搜索