摸索着看vue源碼 - 部分響應式原理

前言

閱讀完該文章, 你不必定會掌握響應式原理, 但必定會有助於你掌握響應式原理, 源碼這玩意兒若是光看看文章視頻, 不本身親手調試一下的話, 很難掌握.vue

準備工做

爲了方便調試, 我這裏調試的不是源碼, 而是打包好後的vue/dist/vue.esm.js, 這樣方便打日誌, 也不用切不一樣的文件. 因此準備工做就是用vue init webpack vuedemo初始化一個項目, 而後在main.js中初始化一些demo, 以下react

import Vue from 'vue'
/* eslint-disable no-new */
new Vue({
  el: '#app',
  data(){
    return {
      msg: '天氣不錯',
    }
  },
  methods: {
    click() {
      //一些邏輯
    }
  },
  template: `
  <div>
    <div>{{msg}}</div>
    <button @click='click'>按鈕</button>
  </div>
  `
})

複製代碼

ps: 1. 切記不要經過掛載App組件的方式調試, 直接用template, 若是掛載組件, 會多不少重複的日誌, 很是不利於調試 2. 這裏只是給你們一些調試的建議(由於一開始我掛了個APP, 調得我好麻煩), 下文中不會出現 日誌 相關的內容webpack

再次前言

本文標題是 部分響應式原理, 響應式分三塊: 偵聽器(watch), computed(計算屬性), render(模板渲染). 實現響應式的原理都是相同的, 只是在針對特性的業務邏輯上有些不一樣. 本文就選render(模板渲染)的響應式原理具體展開. 接下來會以調用棧會主線, 進行原理的分析web

原理分析

爲了縮減篇幅, 在一些代碼截取上, 我會忽略報錯的警示代碼以及與響應式原理無關的邏輯代碼, 經過...取代, 不過仍是建議你們看下原函數,幫助理解數組

  1. initmixin 入口函數
function initmixin(Vue) {
    Vue.prototype._init = function(options) {
        var vm  = this
        ...
        // 
        initState(vm)
        if (vm.$options.el) { 
            // 這行代碼, 在第7步中'呼應1'會解釋
            vm.$mount(vm.$options.el);
        }
    }
}
複製代碼
  1. initState(vm), 入口函數, 邏輯很簡單. 注意: 這裏的opts.data不是咱們寫的那個data(){return {}}方法, 而是一個name是mergedInstanceDataFn的通過包裝的方法
var opts = vm.$options;
if (opts.data) {
    initData(vm);
} else {
    observe(vm._data = {}, true /* asRootData */);
}
複製代碼
  1. initData(vm)
data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {};
// 這裏獲取的data, 若是用我給的demo就是{msg: '天氣不錯'}
var keys = Object.keys(data);
var i = keys.length;
  while (i--) {
    var key = keys[i];
    ...
    if (!isReserved(key)) {
      // isReserved 函數是用來判斷 data中的屬性是否已 $ 或者 _ 開頭, 由於這倆開頭的屬性可能會和vue內置的屬性, API衝突, 因此vue選擇不代理他們
      // proxy 只是將data中的屬性代理到vm實例上,這樣就能夠用this.xxx直接獲取數據, 要實現響應式還得看下面的observe
      proxy(vm, "_data", key);
    }
  }
  ...
  //觀察他們!
  observe(data, true /* asRootData */);
複製代碼
  1. observer(value, asRootData)
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
  ) {
  // 生成一個Observer實例
    ob = new Observer(value);
  }
複製代碼
  1. class Observer 聲明一個觀察者類
if (Array.isArray(value)) {
  // 若是是數組, 則調用observeArray, 其最終仍是會走walk
    this.observeArray(value);
  } else {
    this.walk(value);
  }
複製代碼
  1. Observer.prototype.walk
// 遍歷每一個屬性, 並執行defineReactive$$1
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};
複製代碼
  1. defineReactive$$1(直譯就是定義響應式), 這是很關鍵的一個函數, 這個函數中定義了每一個響應式屬性的getter& setter, 並在getter中執行依賴收集, 在setter中執行派發更新. 看這個函數前, 我強烈建議讀者打開源碼一塊兒往下走, 由於這裏特別繞, 若是光看文章, 很難理解
// 這裏進行了大幅的代碼刪減, 只爲展現最直接的邏輯
function defineReactive$$1(obj, key, val, customSetter, shallow) {
    // Dep是一個依賴類, 看下面代碼
    var dep = new Dep()
    Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    //標記①:
    get: function reactiveGetter () {
    // 注①
    // Dep.target 是一個Watcher實例(可理解爲一個訂閱者), 若是Dep.target 不爲undefined, 則去收集依賴
      if (Dep.target) {//
        dep.depend();
      }
      return value
    },
    //標記②:
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      dep.notify(); //派發更新
    }
  });
}
// Dep
var Dep = function Dep () {
  this.id = uid++; // id
  this.subs = [];  // 訂閱者數組
};

複製代碼

注①:bash

  1. Dep.target 何時被賦值, 它是個什麼?

1.全局搜索Dep.target, 會有兩個函數中對其進行了賦值, 1. pushTarget 2. popTarget 能夠看出這是邏輯相反的兩個函數, 咱們就看pushTarget
2. 全局搜索pushTarget, 會發現有5處地方調用了, 但只有Watcher.prototype.get中給他傳參了, 由於傳的是this, 因此很明顯Dep.target是一個Watcher的實例(即訂閱者)app

/**
 * Evaluate the getter, and re-collect dependencies.(翻譯: 計算一個getter, 並從新收集依賴)
 */
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    ...
  } finally {
    // "touch" every roperty so they are all tracked as
    // dependencies for deep watching
    // watch中 有deep:true 屬性的 會進入traverse, 進行遞歸綁定, 這裏咱們忽略遞歸綁定邏輯
    if (this.deep) {
      traverse(value);
    } 
    //這兩步目前不用管
    popTarget();
    this.cleanupDeps();
  }
  return value
};
複製代碼

3 找到了給Dep.target調用的地方, 也引入了一個Watcher的概念, 那系統是何時建立的Watcher實例的呢?全局搜索new Watcher你會發現3個地方用到了, 分別是在Vue.prototype.$watch(偵聽器)中, initComputed(計算屬性)中, 以及mountComponent(掛載組件)中(呼應1:mountComponent是在Vue.prototype.$mount中調用的),因此全部訂閱者均來自於這三個地方.本要講的也就是mountComponent時建立的訂閱者. 因此若是已我本文開頭是給的demo爲例, 只會生成一個Watcher(解釋一下: demo中我只訂閱了msg一個依賴, 若是我多訂閱幾個依賴, 依舊是一個Watcher.而若是是偵聽器或者計算屬性,則會生成對應多個watcher)
4 那麼Watcher.prototype.get是在何時調用的呢?在render Watcher(就是本文要講的Watcher, 即模板渲染Watcher)中, 有兩個地方調用了, 一個是在function Watcher的最下面,這個很明確, 意思就每新建一個Watcher實例, 必然會執行一次Watcher.prototype.get, 一個是在Watcher.prototype.run 中.這個根據調用棧 去倒推, 會發現是這樣的調用棧: 1. 觸發屬性的set(標記②) => 2. dep.notify => 3.Watcher.prototype.update => 4. queueWatcher => 5. flushSchedulerQueue => 6. watcher.run()async

//function Watcher
...
  this.value = this.lazy
    ? undefined
    : this.get();  <= 這個就是調用```Watcher.prototype.get```
// Watcher.prototype.run
...
  if (this.active) {
    var value = this.get();<= 這個就是調用```Watcher.prototype.get```
複製代碼

5 目前爲止咱們知道了何時觸發Watcher.prototype.get即(Dep.target何時是一個Watcher), 這個時候咱們還須要搞清楚何時觸發屬性的get(標記①).在Watcher.prototype.get函數

Watcher.prototype.get = function get () {
    ...
    try {
        console.log(this.getter)
        value = this.getter.call(vm, vm); <=這一行是觸發了屬性的get, 能夠嘗試註釋它, 頁面就會不渲染
    } catch (e) {
    ...
}
複製代碼

log的getter

6 整理下觸發訂閱者進行依賴收集(呼應2:或者說依賴進行訂閱者收集,後文會講到)邏輯: 1. 執行模板渲染函數時, 若是有用到依賴屬性, 則會觸發依賴屬性的get(好比我頁面要渲染{{msg}}, 則會觸發msg的get), 並執行依賴收集 2.當屬性的值發生改變時, 會執行dep.notify通知視圖層更新, 一樣會觸發依賴屬性的get. 咱們知道了觸發依賴收集的條件, 而後咱們研究下屬性是如何執行依賴收集學習

//defineReactive$$1
function defineReactive$$1() {
    ...
    Object.defineProperty(obj, key, {
    ...
    get: function reactiveGetter () {
      if (Dep.target) {
        dep.depend();    <= 依賴收集入口
      }
      return value
    },
}
// depend
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);    <= 注意這的Dep.target是一個Watcher
  }
};
// addDep
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
  // 訂閱者執行依賴收集
    this.newDepIds.add(id);   
    this.newDeps.push(dep);
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};
// addSub
Dep.prototype.addSub = function addSub (sub) {
//呼應2: 依賴執行訂閱者收集
  this.subs.push(sub);   
};

複製代碼

7 收集完訂閱者, 來看看如何通知訂閱者完成派發更新

function defineReactive$$1(obj, key, val, customSetter, shallow) {
    ...
    Object.defineProperty(obj, key, {
    ...
    set: function reactiveSetter (newVal) {
      ...
      dep.notify(); //派發更新
    }
  });
}
// notify
Dep.prototype.notify = function notify () {
  var subs = this.subs.slice();
  if (process.env.NODE_ENV !== 'production' && !config.async) {
    // 對訂閱者排序
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
  // 呼應2: 挨個通知訂閱者該更新了. 這也是爲何爲了便於理解,我偏向於叫訂閱者收集,
  // 由於他派發更新的主邏輯是,依賴收集訂閱者,而後依賴挨個通知訂閱者
    subs[i].update();
  }
};

複製代碼

8 subs[i].update()後還有一系列邏輯, 主要就是queueWatcher => flushSchedulerQueue => Watcher.prototype.run => Watcher.prototype.get => this.getter.call(vm, vm);執行渲染 並觸發屬性中get

以上就是實現響應式的一系列最簡潔的邏輯

總結一下

  1. Dep類是一個依賴類, 有一個自增id屬性和一個訂閱者數組屬性
  2. Watcher類是一個訂閱者類, 有三種類型: 渲染Watcher, 偵聽器Watcher, 計算屬性Watcher.
  3. 觸發依賴收集有兩種邏輯,1. 每一個新建Watcher, 都會觸發訂閱者收集相關依賴 2. 當收到派發更新通知時, 會更新視圖層, 並觸發相關依賴的get, 並而後從新收集依賴(re-collect dep). 但本質都是觸發渲染, 收集相關依賴

深刻一下

  1. Watcher.prototype.get中有一步是cleanupDeps()
Watcher.prototype.get = function get () {
  console.log('執行watcherget')
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    ...
  } catch (e) {
    ...
    ...
    this.cleanupDeps();  // 這裏
  }
  return value
};
// cleanupDeps
Watcher.prototype.cleanupDeps = function cleanupDeps () {
  var i = this.deps.length;
  while (i--) {
    var dep = this.deps[i];
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this);
    }
  }
  var tmp = this.depIds;
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  this.newDepIds.clear();
  tmp = this.deps;
  this.deps = this.newDeps;
  this.newDeps = tmp;
  this.newDeps.length = 0;
};
複製代碼

顯然這一步是爲了清洗依賴, 何時須要清洗依賴?

<template>
    // 手動的將v-if置爲false時, 本來須要訂閱的msg,就無需再訂閱了, 這也是cleanupDeps的做用
    <div v-if=false>{{msg}}</div>
</template>
複製代碼

最後

以上是我總結的響應式原理的最簡化邏輯, 其實有不少須要拓展的分支, 但要經過文章媒介實在過於麻煩. 因此我以爲想要學習源碼的最好途徑就是去debugger

相關文章
相關標籤/搜索