深刻剖析Vue源碼 - 響應式系統構建(中)

爲了深刻介紹響應式系統的內部實現原理,咱們花了一整節的篇幅介紹了數據(包括data, computed,props)如何初始化成爲響應式對象的過程。有了響應式數據對象的知識,上一節的後半部分咱們還在保留源碼結構的基礎上構建了一個以data爲數據的響應式系統,而這一節,咱們繼續深刻響應式系統內部構建的細節,詳細分析Vue在響應式系統中對data,computed的處理。node

7.8 相關概念

在構建簡易式響應式系統的時候,咱們引出了幾個重要的概念,他們都是響應式原理設計的核心,咱們先簡單回顧一下:react

  • Observer類,實例化一個Observer類會經過Object.defineProperty對數據的getter,setter方法進行改寫,在getter階段進行依賴的收集,在數據發生更新階段,觸發setter方法進行依賴的更新
  • watcher類,實例化watcher類至關於建立一個依賴,簡單的理解是數據在哪裏被使用就須要產生了一個依賴。當數據發生改變時,會通知到每一個依賴進行更新,前面提到的渲染wathcer即是渲染dom時使用數據產生的依賴。
  • Dep類,既然watcher理解爲每一個數據須要監聽的依賴,那麼對這些依賴的收集和通知則須要另外一個類來管理,這個類即是Dep,Dep須要作的只有兩件事,收集依賴和派發更新依賴。

這是響應式系統構建的三個基本核心概念,也是這一節的基礎,若是尚未印象,請先回顧上一節對極簡風響應式系統的構建算法

7.9 data

7.9.1 問題思考

在開始分析data以前,咱們先拋出幾個問題讓讀者思考,而答案都包含在接下來內容分析中。express

  • 前面已經知道,Dep是做爲管理依賴的容器,那麼這個容器在何時產生?也就是實例化Dep發生在何時?數組

  • Dep收集了什麼類型的依賴?即watcher做爲依賴的分類有哪些,分別是什麼場景,以及區別在哪裏?緩存

  • Observer這個類具體對getter,setter方法作了哪些事情?markdown

  • 手寫的watcher和頁面數據渲染監聽的watch若是同時監聽到數據的變化,優先級怎麼排?數據結構

  • 有了依賴的收集是否是還有依賴的解除,依賴解除的意義在哪裏?dom

帶着這幾個問題,咱們開始對data的響應式細節展開分析。異步

7.9.2 依賴收集

data在初始化階段會實例化一個Observer類,這個類的定義以下(忽略數組類型的data):

// initData 
function initData(data) {
  ···
  observe(data, true)
}
// observe
function observe(value, asRootData) {
  ···
  ob = new Observer(value);
  return ob
}

// 觀察者類,對象只要設置成擁有觀察屬性,則對象下的全部屬性都會重寫getter和setter方法,而getter,setting方法會進行依賴的收集和派發更新
var Observer = function Observer (value) {
    ···
    // 將__ob__屬性設置成不可枚舉屬性。外部沒法經過遍歷獲取。
    def(value, '__ob__', this);
    // 數組處理
    if (Array.isArray(value)) {
        ···
    } else {
      // 對象處理
      this.walk(value);
    }
  };

function def (obj, key, val, enumerable) {
  Object.defineProperty(obj, key, {
    value: val,
    enumerable: !!enumerable, // 是否可枚舉
    writable: true,
    configurable: true
  });
}
複製代碼

Observer會爲data添加一個__ob__屬性, __ob__屬性是做爲響應式對象的標誌,同時def方法確保了該屬性是不可枚舉屬性,即外界沒法經過遍歷獲取該屬性值。除了標誌響應式對象外,Observer類還調用了原型上的walk方法,遍歷對象上每一個屬性進行getter,setter的改寫。

Observer.prototype.walk = function walk (obj) {
    // 獲取對象全部屬性,遍歷調用defineReactive###1進行改寫
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
        defineReactive###1(obj, keys[i]);
    }
};
複製代碼

defineReactive###1是響應式構建的核心,它會先實例化一個Dep類,即爲每一個數據都建立一個依賴的管理,以後利用Object.defineProperty重寫getter,setter方法。這裏咱們只分析依賴收集的代碼。

function defineReactive###1 (obj,key,val,customSetter,shallow) {
    // 每一個數據實例化一個Dep類,建立一個依賴的管理
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    // 屬性必須知足可配置
    if (property && property.configurable === false) {
      return
    }
    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    // 這一部分的邏輯是針對深層次的對象,若是對象的屬性是一個對象,則會遞歸調用實例化Observe類,讓其屬性值也轉換爲響應式對象
    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,s
      get: function reactiveGetter () {
        var value = getter ? getter.call(obj) : val;
        if (Dep.target) {
          // 爲當前watcher添加dep數據
          dep.depend();
          if (childOb) {
            childOb.dep.depend();
            if (Array.isArray(value)) {
              dependArray(value);
            }
          }
        }
        return value
      },
      set: function reactiveSetter (newVal) {}
    });
  }
複製代碼

主要看getter的邏輯,咱們知道當data中屬性值被訪問時,會被getter函數攔截,根據咱們舊有的知識體系能夠知道,實例掛載前會建立一個渲染watcher

new Watcher(vm, updateComponent, noop, {
  before: function before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate');
    }
  }
}, true /* isRenderWatcher */);
複製代碼

與此同時,updateComponent的邏輯會執行實例的掛載,在這個過程當中,模板會被優先解析爲render函數,而render函數轉換成Vnode時,會訪問到定義的data數據,這個時候會觸發gettter進行依賴收集。而此時數據收集的依賴就是這個渲染watcher自己。

代碼中依賴收集階段會作下面幾件事:

  1. 爲當前的watcher(該場景下是渲染watcher)添加擁有的數據
  2. 爲當前的數據收集須要監聽的依賴

如何理解這兩點?咱們先看代碼中的實現。getter階段會執行dep.depend(),這是Dep這個類定義在原型上的方法。

dep.depend();


Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };
複製代碼

Dep.target爲當前執行的watcher,在渲染階段,Dep.target爲組件掛載時實例化的渲染watcher,所以depend方法又會調用當前watcheraddDep方法爲watcher添加依賴的數據。

Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      // newDepIds和newDeps記錄watcher擁有的數據
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      // 避免重複添加同一個data收集器
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
複製代碼

其中newDepIds是具備惟一成員是Set數據結構,newDeps是數組,他們用來記錄當前watcher所擁有的數據,這一過程會進行邏輯判斷,避免同一數據添加屢次。

addSub爲每一個數據依賴收集器添加須要被監聽的watcher

Dep.prototype.addSub = function addSub (sub) {
  //將當前watcher添加到數據依賴收集器中
    this.subs.push(sub);
};
複製代碼
  1. getter若是遇到屬性值爲對象時,會爲該對象的每一個值收集依賴

這句話也很好理解,若是咱們將一個值爲基本類型的響應式數據改變成一個對象,此時新增對象裏的屬性,也須要設置成響應式數據。

  1. 遇到屬性值爲數組時,進行特殊處理,這點放到後面講。

通俗的總結一下依賴收集的過程,每一個數據就是一個依賴管理器,而每一個使用數據的地方就是一個依賴。當訪問到數據時,會將當前訪問的場景做爲一個依賴收集到依賴管理器中,同時也會爲這個場景的依賴收集擁有的數據。

7.9.3 派發更新

在分析依賴收集的過程當中,可能會有很多困惑,爲何要維護這麼多的關係?在數據更新時,這些關係會起到什麼做用?帶着疑惑,咱們來看看派發更新的過程。 在數據發生改變時,會執行定義好的setter方法,咱們先看源碼。

Object.defineProperty(obj,key, {
  ···
  set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      // 新值和舊值相等時,跳出操做
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      ···
      // 新值爲對象時,會爲新對象進行依賴收集過程
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
})
複製代碼

派發更新階段會作如下幾件事:

  • 判斷數據更改先後是否一致,若是數據相等則不進行任何派發更新操做
  • 新值爲對象時,會對該值的屬性進行依賴收集過程
  • 通知該數據收集的watcher依賴,遍歷每一個watcher進行數據更新,這個階段是調用該數據依賴收集器的dep.notify方法進行更新的派發。
Dep.prototype.notify = function notify () {
    var subs = this.subs.slice();
    if (!config.async) {
      // 根據依賴的id進行排序
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      // 遍歷每一個依賴,進行更新數據操做。
      subs[i].update();
    }
  };
複製代碼
  • 更新時會將每一個watcher推到隊列中,等待下一個tick到來時取出每一個watcher進行run操做
Watcher.prototype.update = function update () {
    ···
    queueWatcher(this);
  };
複製代碼

queueWatcher方法的調用,會將數據所收集的依賴依次推到queue數組中,數組會在下一個事件循環'tick'中根據緩衝結果進行視圖更新。而在執行視圖更新過程當中,不免會由於數據的改變而在渲染模板上添加新的依賴,這樣又會執行queueWatcher的過程。因此須要有一個標誌位來記錄是否處於異步更新過程的隊列中。這個標誌位爲flushing,當處於異步更新過程時,新增的watcher會插入到queue中。

function queueWatcher (watcher) {
    var id = watcher.id;
    // 保證同一個watcher只執行一次
    if (has[id] == null) {
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else {
        var i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      ···
      nextTick(flushSchedulerQueue);
    }
  }
複製代碼

nextTick的原理和實現先不講,歸納來講,nextTick會緩衝多個數據處理過程,等到下一個事件循環tick中再去執行DOM操做,它的原理,本質是利用事件循環的微任務隊列實現異步更新

當下一個tick到來時,會執行flushSchedulerQueue方法,它會拿到收集的queue數組(這是一個watcher的集合),並對數組依賴進行排序。爲何進行排序呢?源碼中解釋了三點:

  • 組件建立是先父後子,因此組件的更新也是先父後子,所以須要保證父的渲染watcher優先於子的渲染watcher更新。
  • 用戶自定義的watcher,稱爲user watcheruser watcherrender watcher執行也有前後,因爲user watchersrender watcher要先建立,因此user watcher要優先執行
  • 若是一個組件在父組件的 watcher 執行階段被銷燬,那麼它對應的 watcher 執行均可以被跳過。
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;
    // 對queue的watcher進行排序
    queue.sort(function (a, b) { return a.id - b.id; });
    // 循環執行queue.length,爲了確保因爲渲染時添加新的依賴致使queue的長度不斷改變。
    for (index = 0; index < queue.length; index++) {
      watcher = queue[index];
      // 若是watcher定義了before的配置,則優先執行before方法
      if (watcher.before) {
        watcher.before();
      }
      id = watcher.id;
      has[id] = null;
      watcher.run();
      // in dev build, check and stop circular updates.
      if (has[id] != null) {
        circular[id] = (circular[id] || 0) + 1;
        if (circular[id] > MAX_UPDATE_COUNT) {
          warn(
            'You may have an infinite update loop ' + (
              watcher.user
                ? ("in watcher with expression \"" + (watcher.expression) + "\"")
                : "in a component render function."
            ),
            watcher.vm
          );
          break
        }
      }
    }

    // keep copies of post queues before resetting state
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();
    // 重置恢復狀態,清空隊列
    resetSchedulerState();

    // 視圖改變後,調用其餘鉤子
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
      devtools.emit('flush');
    }
  }
複製代碼

flushSchedulerQueue階段,重要的過程能夠總結爲四點:

  • queue中的watcher進行排序,緣由上面已經總結。
  • 遍歷watcher,若是當前watcherbefore配置,則執行before方法,對應前面的渲染watcher:在渲染watcher實例化時,咱們傳遞了before函數,即在下個tick更新視圖前,會調用beforeUpdate生命週期鉤子。
  • 執行watcher.run進行修改的操做。
  • 重置恢復狀態,這個階段會將一些流程控制的狀態變量恢復爲初始值,並清空記錄watcher的隊列。
new Watcher(vm, updateComponent, noop, {
  before: function before () {
    if (vm._isMounted && !vm._isDestroyed) {
      callHook(vm, 'beforeUpdate');
    }
  }
}, true /* isRenderWatcher */);
複製代碼

重點看看watcher.run()的操做。

Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if ( value !== this.value || isObject(value) || this.deep ) {
        // 設置新值
        var oldValue = this.value;
        this.value = value;
        // 針對user watcher,暫時不分析
        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);
        }
      }
    }
  };
複製代碼

首先會執行watcher.prototype.get的方法,獲得數據變化後的當前值,以後會對新值作判斷,若是判斷知足條件,則執行cb,cb爲實例化watcher時傳入的回調。

在分析get方法前,回頭看看watcher構造函數的幾個屬性定義

var watcher = function Watcher( vm, // 組件實例 expOrFn, // 執行函數 cb, // 回調 options, // 配置 isRenderWatcher // 是否爲渲染watcher ) {
  this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    // lazy爲計算屬性標誌,當watcher爲計算watcher時,不會理解執行get方法進行求值
    this.value = this.lazy
      ? undefined
      : this.get();
  
}
複製代碼

方法get的定義以下:

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
     ···
    } finally {
      ···
      // 把Dep.target恢復到上一個狀態,依賴收集過程完成
      popTarget();
      this.cleanupDeps();
    }
    return value
  };
複製代碼

get方法會執行this.getter進行求值,在當前渲染watcher的條件下,getter會執行視圖更新的操做。這一階段會從新渲染頁面組件

new Watcher(vm, updateComponent, noop, { before: () => {} }, true);

updateComponent = function () {
  vm._update(vm._render(), hydrating);
};
複製代碼

執行完getter方法後,最後一步會進行依賴的清除,也就是cleanupDeps的過程。

關於依賴清除的做用,咱們列舉一個場景: 咱們常常會使用v-if來進行模板的切換,切換過程當中會執行不一樣的模板渲染,若是A模板監聽a數據,B模板監聽b數據,當渲染模板B時,若是不進行舊依賴的清除,在B模板的場景下,a數據的變化一樣會引發依賴的從新渲染更新,這會形成性能的浪費。所以舊依賴的清除在優化階段是有必要。

// 依賴清除的過程
  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;
  };
複製代碼

把上面分析的總結成依賴派發更新的最後兩個點

  • 執行run操做會執行getter方法,也就是從新計算新值,針對渲染watcher而言,會從新執行updateComponent進行視圖更新
  • 從新計算getter後,會進行依賴的清除

7.10 computed

計算屬性設計的初衷是用於簡單運算的,畢竟在模板中放入太多的邏輯會讓模板太重且難以維護。在分析computed時,咱們依舊遵循依賴收集和派發更新兩個過程進行分析。

7.10.1 依賴收集

computed的初始化過程,會遍歷computed的每個屬性值,併爲每個屬性實例化一個computed watcher,其中{ lazy: true}computed watcher的標誌,最終會調用defineComputed將數據設置爲響應式數據,對應源碼以下:

function initComputed() {
  ···
  for(var key in computed) {
    watchers[key] = new Watcher(
        vm,
        getter || noop,
        noop,
        computedWatcherOptions
      );
  }
  if (!(key in vm)) {
    defineComputed(vm, key, userDef);
  }
}

// computed watcher的標誌,lazy屬性爲true
var computedWatcherOptions = { lazy: true };
複製代碼

defineComputed的邏輯和分析data的邏輯類似,最終調用Object.defineProperty進行數據攔截。具體的定義以下:

function defineComputed (target,key,userDef) {
  // 非服務端渲染會對getter進行緩存
  var shouldCache = !isServerRendering();
  if (typeof userDef === 'function') {
    // 
    sharedPropertyDefinition.get = shouldCache
      ? createComputedGetter(key)
      : createGetterInvoker(userDef);
    sharedPropertyDefinition.set = noop;
  } else {
    sharedPropertyDefinition.get = userDef.get
      ? shouldCache && userDef.cache !== false
        ? createComputedGetter(key)
        : createGetterInvoker(userDef.get)
      : noop;
    sharedPropertyDefinition.set = userDef.set || noop;
  }
  if (sharedPropertyDefinition.set === noop) {
    sharedPropertyDefinition.set = function () {
      warn(
        ("Computed property \"" + key + "\" was assigned to but it has no setter."),
        this
      );
    };
  }
  Object.defineProperty(target, key, sharedPropertyDefinition);
}
複製代碼

在非服務端渲染的情形,計算屬性的計算結果會被緩存,緩存的意義在於,只有在相關響應式數據發生變化時,computed纔會從新求值,其他狀況屢次訪問計算屬性的值都會返回以前計算的結果,這就是緩存的優化computed屬性有兩種寫法,一種是函數,另外一種是對象,其中對象的寫法須要提供gettersetter方法。

當訪問到computed屬性時,會觸發getter方法進行依賴收集,看看createComputedGetter的實現。

function createComputedGetter (key) {
    return function computedGetter () {
      var watcher = this._computedWatchers && this._computedWatchers[key];
      if (watcher) {
        if (watcher.dirty) {
          watcher.evaluate();
        }
        if (Dep.target) {
          watcher.depend();
        }
        return watcher.value
      }
    }
  }
複製代碼

createComputedGetter返回的函數在執行過程當中會先拿到屬性的computed watcher,dirty是標誌是否已經執行過計算結果,若是執行過則不會執行watcher.evaluate重複計算,這也是緩存的原理。

Watcher.prototype.evaluate = function evaluate () {
    // 對於計算屬性而言 evaluate的做用是執行計算回調
    this.value = this.get();
    this.dirty = false;
  };
複製代碼

get方法前面介紹過,會調用實例化watcher時傳遞的執行函數,在computer watcher的場景下,執行函數是計算屬性的計算函數,他能夠是一個函數,也能夠是對象的getter方法。

列舉一個場景避免和data的處理脫節,computed在計算階段,若是訪問到data數據的屬性值,會觸發data數據的getter方法進行依賴收集,根據前面分析,dataDep收集器會將當前watcher做爲依賴進行收集,而這個watcher就是computed watcher,而且會爲當前的watcher添加訪問的數據Dep

回到計算執行函數的this.get()方法,getter執行完成後一樣會進行依賴的清除,原理和目的參考data階段的分析。get執行完畢後會進入watcher.depend進行依賴的收集。收集過程和data一致,將當前的computed watcher做爲依賴收集到數據的依賴收集器Dep中。

這就是computed依賴收集的完整過程,對比data的依賴收集,computed會對運算的結果進行緩存,避免重複執行運算過程。

7.10.2 派發更新

派發更新的條件是data中數據發生改變,因此大部分的邏輯和分析data時一致,咱們作一個總結。

  • 當計算屬性依賴的數據發生更新時,因爲數據的Dep收集過computed watch這個依賴,因此會調用depnotify方法,對依賴進行狀態更新。
  • 此時computed watcher和以前介紹的watcher不一樣,它不會馬上執行依賴的更新操做,而是經過一個dirty進行標記。咱們再回頭看依賴更新的代碼。
Dep.prototype.notify = function() {
  ···
   for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
}

Watcher.prototype.update = function update () {
  // 計算屬性分支 
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    queueWatcher(this);
  }
};
複製代碼

因爲lazy屬性的存在,update過程不會執行狀態更新的操做,只會將dirty標記爲true

  • 因爲data數據擁有渲染watcher這個依賴,因此同時會執行updateComponent進行視圖從新渲染,而render過程當中會訪問到計算屬性,此時因爲this.dirty值爲true,又會對計算屬性從新求值。

7.11 小結

咱們在上一節的理論基礎上深刻分析了Vue如何利用data,computed構建響應式系統。響應式系統的核心是利用Object.defineProperty對數據的getter,setter進行攔截處理,處理的核心是在訪問數據時對數據所在場景的依賴進行收集,在數據發生更改時,通知收集過的依賴進行更新。這一節咱們詳細的介紹了data,computed對響應式的處理,二者處理邏輯存在很大的類似性但卻各有的特性。源碼中會computed的計算結果進行緩存,避免了在多個地方使用時頻繁重複計算的問題。因爲篇幅有限,對於用戶自定義的watcher咱們會放到下一小節分析。文章還留有一個疑惑,依賴收集時若是遇到的數據是數組時應該怎麼處理,這些疑惑都會在以後的文章一一解開。


相關文章
相關標籤/搜索