源碼分析:Vue的雙向數據綁定

雖然工做中一直使用Vue做爲基礎庫,可是對於其實現機理僅限於道聽途說,這樣對長期的技術發展很不利。因此最近攻讀了其源碼的一部分,先把雙向數據綁定這一塊的內容給整理一下,也算是一種學習的反芻。html

本篇文章的Vue源碼版本爲v2.2.0開發版vue

Vue源碼的總體架構無非是初始化Vue對象,掛載數據data/props等,在不一樣的時期觸發不一樣的事件鉤子,如created() / mounted() / update()等,後面專門整理各個模塊的文章。這裏先講雙向數據綁定的部分,也是最主要的部分。react

設計思想:觀察者模式

Vue的雙向數據綁定的設計思想爲觀察者模式,爲了方便,下文中將被觀察的對象稱爲觀察者,將觀察者對象觸發更新的稱爲訂閱者。主要涉及到的概念有:git

  1. Dep對象:Dependency依賴的簡寫,包含有三個主要屬性id, subs, target和四個主要函數addSub, removeSub, depend, notify,是觀察者的依賴集合,負責在數據發生改變時,使用notify()觸發保存在subs下的訂閱列表,依次更新數據和DOM。es6

    id: 每一個觀察者(依賴對象)的惟一標識。
    subs: 觀察者對象的訂閱者列表。
    target: 全局惟一的訂閱者對象,由於只能同時計算和更新一個訂閱者的值。
    addSub(): 使用`push()`方法添加一個訂閱者。
    removeSub(): 使用`splice()`方法移除一個訂閱者。
    depend(): 將本身添加到當前訂閱者對象的依賴列表。
    notify(): 在數據被更新時,會遍歷subs對象,觸發每個訂閱者的更新。
  2. Observer對象:即觀察者,包含兩個主要屬性value, dep。作法是使用getter/setter方法覆蓋默認的取值和賦值操做,將對象封裝爲響應式對象,每一次調用時更新依賴列表,更新值時觸發訂閱者。綁定在對象的__ob__原型鏈屬性上。github

    value: 原始值。
    dep: 依賴列表。

源碼實戰解析

有過Vue開發基礎的應該都瞭解其怎麼初始化一個Vue對象:json

new Vue({
    el: '#container',
    data: {
        count: 100
    },
    ...
});

那麼咱們就從這個count提及,看它是怎麼完成雙向數據綁定的。數組

下面的代碼片斷中英文註釋爲尤雨溪所寫,中文註釋爲我所寫,英文註釋更能表明開發者的清晰思路。閉包

首先從全局的初始化函數調用:initMixin(Vue$3);,這裏的Vue$3對象就是全局的Vue對象,在此以前已經掛載了Vue的各類基本數據和函數。這個函數體就是初始化咱們上面聲明Vue語句的過程化邏輯,取主體代碼來看:架構

// 這裏的options就是上面聲明Vue對象的json對象
Vue.prototype._init = function (options) {
    ...
    var vm = this;
    ...
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    // 這裏就是咱們接下來要跟進的初始化Vue參數
    initState(vm);
    initInjections(vm);
    callHook(vm, 'created');
    ...
  };

這裏主要完成了初始化事件、渲染、參數、注入等過程,並不斷調用事件鉤子的回調函數。下面來到如何初始化參數:

function initState (vm) {
  vm._watchers = [];
  var opts = vm.$options;
  if (opts.props) { initProps(vm, opts.props); }
  if (opts.methods) { initMethods(vm, opts.methods); }
  // 咱們的count在這裏初始化
  if (opts.data) {
    initData(vm);
  } else {
    observe(vm._data = {}, true /* asRootData */);
  }
  if (opts.computed) { initComputed(vm, opts.computed); }
  if (opts.watch) { initWatch(vm, opts.watch); }
}

這裏依次檢測參數中包含的props/methods/data/computed/watch並進入不一樣的函數進行初始化,這裏咱們只關心initData:

function initData (vm) {
  var data = vm.$options.data;
  data = vm._data = typeof data === 'function'
    ? data.call(vm)
    : data || {};
  if (!isPlainObject(data)) {
    data = {};
  }
  ...
  // observe data
  observe(data, true /* asRootData */);

能夠看到Vue的data參數支持對象和回調函數,但最終返回的必定是對象,不然使用空對象。接下來就是重頭戲了,咱們如何將data參數設置爲響應式的:

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    /* 爲了防止value不是單純的對象而是Regexp或者函數之類的,或者是vm實例再或者是不可擴展的 */
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);
  }
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}

這裏的英文註釋很是清晰,就是爲了給該對象新建一個觀察者類,若是存在則返回已存在的(好比互相引用或依賴重複),能夠看到這個觀察者列表放置在對象的__ob__屬性下。下面咱們看下這個Observer觀察者類:

/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  // def函數是defineProperty的簡單封裝
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    // 在es5及更低版本的js裏,沒法完美繼承數組,這裏檢測並選取合適的函數
    // protoAugment函數使用原型鏈繼承,copyAugment函數使用原型鏈定義(即對每一個數組defineProperty)
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};

在Observer類的註釋裏也清楚的說明,它會被關聯到每個被檢測的對象,使用getter/setter修改其默認讀寫,用於收集依賴和發佈更新。其中出現了三個咱們須要關心的東西Dep類/observeArray/walk,咱們先看observeArray的源碼:

/**
 * Observe a list of Array items.
 */
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

它不過是在Observer類和observe方法中間的一層遞歸,由於咱們觀察的只能是對象,而不能是數字、字符串或者數組(數組的觀察比較特殊,事實上是重構了方法來觸發更新,後面會講到)。那咱們接下來看下Dep類是作什麼用的:

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid$1++;
  this.subs = [];
};

註釋裏告訴咱們Dep類是一個會被多個指令訂閱的可被觀察的對象,這裏的指令就是咱們在html代碼裏書寫的東西,如:class={active: hasActive}{{ count }} {{ count * price }},而他們就會訂閱hasActive/count/price這些對象,而這些訂閱他們的對象就會被放置在Dep.subs列表中。每一次新建Dep對象,就會全局uid遞增,而後傳給該Dep對象,保證惟一性id。
咱們接着看剛纔的walk函數作了什麼:

/**
 * Walk through each property and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

看來和名字同樣,它只是走了一遍,那咱們來看下defineReactive$$1作了什麼:

/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (obj, key, val, customSetter) {
  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;

  var childOb = observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var 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) {
      var value = getter ? getter.call(obj) : val;
      // 髒檢查,排除了NaN !== NaN的影響
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = observe(newVal);
      dep.notify();
    }
  });
}

終於找到重頭戲了,這裏真正使用了getter/setter代理了對象的默認讀寫。咱們首先新建一個Dep對象,利用閉包準備收集依賴,而後咱們使用observe觀察該對象,注意此時與上面相比少了一個asRootData = true的參數。
咱們先來看取值的代理get,這裏用到了Dep.target屬性和depend()方法,咱們來看看它是作什麼的:

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;

Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};

Dep.prototype.notify = function notify () {
  // stablize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};

註釋看的出來Dep.target是全局惟一的watcher對象,也就是當前正在指令計算的訂閱者,它會在計算時賦值成一個watcher對象,計算完成後賦值爲null。而depend是用於對該訂閱者添加依賴,告訴它你的值依賴於我,每次更新時應該來找我。另外還有notify()的函數,用於遍歷全部的依賴,通知他們更新數據。這裏多看一下addDep()的源碼:

/**
 * Add a dependency to this directive.
 */
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)) {
      // 使用push()方法添加一個訂閱者
      dep.addSub(this);
    }
  }
};

能夠看到它有去重的機制,當重複依賴時保證相同ID的依賴只有一個。訂閱者包含3個屬性newDepIds/newDeps/depIds分別存儲依賴信息,若是以前就有了這個依賴,那麼反過來將該訂閱者加入到這個依賴關係中去。
接着看get方法中的dependArray()

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
    e = value[i];
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

能夠看到咱們不能像對象同樣監聽數組的變化,因此若是獲取一個數組的值,那麼就須要將數組中全部的對象的觀察者列表都加入到依賴中去。
這樣get方法讀取值就代理完成了,接下來咱們看set方法代理賦值的實現,咱們先獲取原始值,而後與新賦的值進行比較,也叫髒檢查,若是數據發生了改變,則對該數據進行從新創建觀察者,並通知全部的訂閱者更新。
接下來咱們看下數組的更新檢測是如何實現的:

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var arguments$1 = arguments;
    // avoid leaking arguments:
    // http://jsperf.com/closure-with-arguments
    var i = arguments.length;
    var args = new Array(i);
    while (i--) {
      args[i] = arguments$1[i];
    }
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
        inserted = args;
        break
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});

看的出來咱們模擬了一個數組對象,代理了push/pop/shift/unshift/splice/sort/reverse方法,用於檢測數組的變化,並通知全部訂閱者更新。若是有新建元素,會補充監聽新對象。
這就是從代碼上解釋爲何Vue不支持數組下標修改和長度修改的緣由,至於爲何這麼設計,我後面會再次更新或再開篇文章,講一些通用的設計問題以及Js機制和缺陷。

總結

從上面的代碼中咱們能夠一步步由深到淺的看到Vue是如何設計出雙向數據綁定的,最主要的兩點:

  1. 使用getter/setter代理值的讀取和賦值,使得咱們能夠控制數據的流向。
  2. 使用觀察者模式設計,實現了指令和數據的依賴關係以及觸發更新。
  3. 對於數組,代理會修改原數組對象的方法,並觸發更新。

明白了這些原理,其實你也能夠實現一個簡單的數據綁定,造一個小輪子,固然,Vue的強大之處不止於此,咱們後面再來聊一聊它的組件和渲染,看它是怎麼一步一步將咱們從DOM對象的魔爪裏拯救出來的。

參考資料

  1. 數據的響應化:https://github.com/Ma63d/vue-...
  2. Vue v2.2.0 源代碼文件
  3. es6 Proxy: http://es6.ruanyifeng.com/#do...
相關文章
相關標籤/搜索