Vue源碼解讀一:Vue數(shù)據(jù)響應式原理

這方面的文章很多呀癣,但是我感覺很多寫的比較抽象美浦,本文會通過舉例更詳細的解釋。(此文面向的Vue新手們项栏,如果你是個大牛浦辨,看到這篇文章就可以點個贊,關掉頁面了沼沈。)通過閱讀這篇文章流酬,你將了解到:

1.Vue數(shù)據(jù)響應式的設計思想
2.了解Observer,Dep,Watcher的源碼實現(xiàn)原理
3.getter/setter 攔截數(shù)據(jù)方式的不足及解決方案

一、設計模式

Vue 采用數(shù)據(jù)劫持結合發(fā)布者-訂閱者模式的方式來實現(xiàn)數(shù)據(jù)的響應式列另,通過Object.defineProperty(點我查看該屬性)來劫持數(shù)據(jù)的setter芽腾,getter,在數(shù)據(jù)變動時發(fā)布消息給訂閱者页衙,訂閱者收到消息后進行相應的處理摊滔。

現(xiàn)在我們來看一下下面的圖,共涉及5個概念店乐,data和view的意義很明顯艰躺,主要講解Observer,Dep和Watcher。
Observer:數(shù)據(jù)的觀察者,讓數(shù)據(jù)對象的讀寫操作都處于自己的監(jiān)管之下眨八。當初始化實例的時候描滔,會遞歸遍歷data,用Object.defineProperty來攔截數(shù)據(jù)(包含數(shù)組里的每個數(shù)據(jù))踪古。
Dep:數(shù)據(jù)更新的發(fā)布者含长,get數(shù)據(jù)的時候,收集訂閱者伏穆,觸發(fā)Watcher的依賴收集拘泞;set數(shù)據(jù)時發(fā)布更新,通知Watcher 枕扫。
Watcher:數(shù)據(jù)更新的訂閱者陪腌,訂閱的數(shù)據(jù)改變時執(zhí)行相應的回調(diào)函數(shù)(更新視圖或表達式的值)。
一個Watcher可以更新視圖烟瞧,如html模板中用到的{{test}}诗鸭,也可以執(zhí)行一個$watch監(jiān)督的表達式的回調(diào)函數(shù)(Vue實例中的watch項底層是調(diào)用的$watch實現(xiàn)的),還可以更新一個計算屬性(即Vue實例中的computed項)。

圖中紅色的箭頭表示的是收集依賴時獲取數(shù)據(jù)的流程参滴。Watcher會收集依賴的時候(這個時機可能是實例創(chuàng)建時强岸,解析模板、初始化watch砾赔、初始化computed蝌箍,也可能是數(shù)據(jù)改變后,Watcher執(zhí)行回調(diào)函數(shù)前)暴心,會獲取數(shù)據(jù)的值妓盲,此時Observer會攔截數(shù)據(jù)(即調(diào)用get函數(shù)),然后通知Dep可以收集訂閱者啦专普。Dep將訂閱數(shù)據(jù)的Watcher保存下來,便于后面通知更新悯衬。

圖中綠色的箭頭表示的是數(shù)據(jù)改變時,發(fā)布更新的流程檀夹。當數(shù)據(jù)改變時筋粗,即設置數(shù)據(jù)時,此時Observer會攔截數(shù)據(jù)(即調(diào)用set函數(shù))击胜,然后通知Dep亏狰,數(shù)據(jù)改變了,此時Dep通知Watcher偶摔,可以更新視圖啦暇唾。

二、 代碼實現(xiàn)

Observer

下圖是Observer類的結構



我們先來看看Observer的實現(xiàn)(大家看代碼的時候可以注意下引文的注釋辰斋,英文的注釋是官方的策州,說的很棒。中文注釋是我加的)

/**
 * 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.
(observer實例的生成函數(shù)宫仗,如果數(shù)據(jù)沒有被observe過够挂,那么新建一個observer類并返回,否則直接返回observer類)
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  //如果存在__ob__屬性藕夫,說明該對象沒被observe過孽糖,不是observer類
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //如果數(shù)據(jù)沒有被observe過枯冈,且數(shù)據(jù)是array或object類型,那么將數(shù)據(jù)轉化為observer類型办悟,所以observer類接收的是對象和數(shù)組尘奏。
    ob = new Observer(value);
  }
  //如果是RootData,即咱們在新建Vue實例時病蛉,傳到data里的值炫加,只有RootData在每次observe的時候,會進行計數(shù)铺然。
  vmCount是用來記錄此Vue實例被使用的次數(shù)的俗孝,
  比如,我們有一個組件logo魄健,頁面頭部和尾部都需要展示logo赋铝,都用了這個組件,那么這個時候vmCount就會計數(shù)诀艰,值為2
  if (asRootData && ob) {
    ob.vmCount++;
  }
  return ob
}
//下面是oberver類
/**
 * 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是定義的函數(shù)柬甥,使用Object.defineProperty()給value添加不可枚舉的屬性,__ob__是一個對象被observe的標志。
    我們在開發(fā)的過程中其垄,有時會遇到苛蒲,數(shù)據(jù)改變但視圖沒有更新的問題。
    這個時候绿满,你可以log一下臂外,看看該對象是否有__ob__屬性來判斷該對象是不是被observe了,如果沒有喇颁,那么數(shù)據(jù)改變后視圖是不可能更新的漏健。
  def(value, '__ob__', this);
  //數(shù)組特殊處理,下面詳細講解
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    //對于對象橘霎,遍歷對象蔫浆,并用Object.defineProperty轉化為getter/setter,便于監(jiān)控數(shù)據(jù)的get和set
    this.walk(value);
  }
};

//遍歷對象姐叁,調(diào)用defineReactive將每個屬性轉化為getter/setter
/**
 * 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]]);
  }
};

/**
 * Observe a list of Array items.
 */
//observe每個數(shù)組元素(observe會生成Observer類)
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};
/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  //實例化一個Dep瓦盛,這個Dep存在在下面的get和set函數(shù)的作用域中,用于收集訂閱數(shù)據(jù)更新的Watcher外潜。這里一個Dep與一個屬性(即參數(shù)里的key)相對應原环,一個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;
  //注意下面這行代碼处窥,設置getter/setter之前嘱吗,會observe該屬性對應的值(val)。
  比如此時參數(shù)傳入的obj是{ objKey: { objValueKey1:{ objValueKey2: objValueValue2 } } },
  key是objKey滔驾,
  val是{ objValueKey1:{ objValueKey2: objValueValue2 } }谒麦,
  那么這個時候{ objValueKey1:{ objValueKey2: objValueValue2 } }對象也會被observe到俄讹,在observe該對象的時候,{ objValueKey2: objValueValue2 }也會被observe到弄匕。
  以此類推颅悉,不管對象的結構有多深都會被observe到。
  var childOb = observe(val);
  //設置getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //獲取屬性的值迁匠,如果這個屬性在轉化之前定義過getter,那么調(diào)用該getter得到value的值驹溃,否則直接返回val城丧。
      var value = getter ? getter.call(obj) : val;
      注意這里,這里是Dep收集訂閱者的過程豌鹤,只有在Dep.target存在的情況下才進行這個操作亡哄,在Watcher收集依賴的時候才會設置Dep.target,所以Watcher收集依賴的時機就是Dep收集訂閱者的時機布疙。
      調(diào)用get的情況有兩種蚊惯,一是Watcher收集依賴的時候(此時Dep收集訂閱者),二是模板或js代碼里用到這個值灵临,這個時候是不需要收集依賴的截型,只要返回值就可以了。
      if (Dep.target) {
        dep.depend();
        //注意這里,不僅這個屬性需要添加到依賴列表中儒溉,如果這個屬性對應的值是對象或數(shù)組宦焦,那么這個屬性對應的值也需要添加到依賴列表中,原因后面詳細解釋
        if (childOb) {
          childOb.dep.depend();
        }
        //如果是數(shù)組顿涣,那么數(shù)組中的每個值都添加到依賴列表里
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var 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 ("development" !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      //當為屬性設置了新的值波闹,是需要observe的
      childOb = observe(newVal);
      //set的時候數(shù)據(jù)變化了,通知更新數(shù)據(jù)
      dep.notify();
    }
  });
}

/**
 * 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];
    //在調(diào)用這個函數(shù)的時候涛碑,數(shù)組已經(jīng)被observe過了精堕,且會遞歸observe。(看上面defineReactive函數(shù)里的這行代碼:var childOb = observe(val);)
    所以正常情況下都會存在__ob__屬性蒲障,這個時候就可以調(diào)用dep添加依賴了歹篓。
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

我們關注以下幾點:

1.Observer類中的屬性和方法都比較好理解,我在這里只說一下vmCount屬性:
vmCount屬性是用來記錄該實例被創(chuàng)建的次數(shù)晌涕,我們看下面的代碼(戳我查看demo源碼及效果)滋捶,調(diào)用了兩次my-component組件,這個時候vmCount為2.

<div >
        <div id="example">
          <my-component></my-component>
          <my-component></my-component>
        </div>
    </div>
        <script src="./vue.js"></script>
        <script>
        var data = { counter: 1 }
        Vue.component('my-component', {
          template: '<div>{{ counter }}</div>',
          data: function () {
            return data
          }
        })
        // 創(chuàng)建根實例
        var app2 = new Vue({
          el: '#example'
        })
</script>

效果:

getter/setter方法攔截數(shù)據(jù)的不足:

  1. 當對象增刪的時候余黎,是監(jiān)控不到的重窟。比如:data={a:"a"},這個時候如果我們設置data.test="test",這個時候是監(jiān)控不到的。因為在observe data的時候惧财,會遍歷已有的每個屬性(比如a)巡扇,添加getter/setter扭仁,而后面設置的test屬性并沒有機會設置getter/setter,所以檢測不到變化厅翔。同樣的乖坠,刪除對象屬性的時候,getter/setter會跟著屬性一起被刪除掉刀闷,攔截不到變化熊泵。
  2. getter/setter是針對對象的,那么對于數(shù)組的修改甸昏,怎樣監(jiān)控變化呢
  3. 每次給數(shù)據(jù)設置值得時候顽分,都會調(diào)用setter函數(shù),這個時候就會發(fā)布屬性更新消息施蜜,即使數(shù)據(jù)的值沒有變卒蘸。從性能方便考慮我們肯定希望值沒有變化的時候,不更新模板翻默。

對于第一個問題缸沃,Vue官方給出了vm.$set/Vue.set和vm.$delete/Vue.delete這樣的api來解決這個問題。我們來看下$set的代碼:


/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
function set (target, key, val) {
   //對于數(shù)組的處理修械,調(diào)用變異方法splice趾牧,這個時候數(shù)組的Dep會發(fā)布更新消息
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  //如果set的是對象已經(jīng)有的屬性,那么該屬性已經(jīng)有getter/setter函數(shù)了祠肥,此時直接修改即可
  if (hasOwn(target, key)) {
    target[key] = val;
    return val
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    "development" !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data ' +
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  if (!ob) {
    target[key] = val;
    return val
  }
  //如果是對象沒有的屬性武氓,則添加getter/setter
  defineReactive$$1(ob.value, key, val);
  //注意此處,對象的Dep會發(fā)布更新
  ob.dep.notify();
  return val
}

上面的代碼比較簡單仇箱,看過注釋應該就能明白县恕,我不做過多解釋。我們著重注意下這句代碼:ob.dep.notify()剂桥,對象的Dep發(fā)布更新忠烛。可是這個dep是在什么地方收集的訂閱者呢权逗?

還記得defineReactive函數(shù)里讓大家注意的這句代碼嗎:childOb.dep.depend(),這句代碼就是在收集訂閱者美尸。
仔細閱讀Observer相關的代碼,我們會發(fā)現(xiàn)斟薇,dep實例化的地方有兩處
一處是在defineReactive函數(shù)里师坎,每次調(diào)用這個函數(shù)的時候都會創(chuàng)建一個新的Dep實例,存在在getter/setter閉包函數(shù)的作用域鏈上堪滨,是為對象屬性服務的啊掏。在Watcher獲取屬性的值的時候收集訂閱者北启,在設置屬性值的時候發(fā)布更新尾菇。
另一處是在observe函數(shù)中,此時的dep掛在被observe的數(shù)據(jù)的__ obj__屬性上义矛,他是為對象或數(shù)組服務的,在Watcher獲取屬性的值的時候盟萨,如果值被observe后返回observer對象(對象和數(shù)組才會返回observer)凉翻,那么就會在此時收集訂閱者,在對象或數(shù)組增刪元素時調(diào)用$set等api時發(fā)布更新的捻激;
defineReactive函數(shù)的getter函數(shù)里這段代碼就是在收集訂閱者:

get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          //注意這里制轰,此處的dep就是在執(zhí)行var childOb = observe(val)時產(chǎn)生的,是用來收集childOb的訂閱者的
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }

我們來看個例子铺罢,data={testkey:{testValueKey:testValueValue}}艇挨,這個時候模板里有{{testkey}},模板的Watcher在執(zhí)行getter函數(shù)的時候韭赘,testkey屬性的getter對應的dep會將此Watcher收集為訂閱者;同時{testValueKey:testValueValue}對象也會將此Watcher收集為訂閱者(我們在給testkey屬性設置getter/setter函數(shù)時势就,會執(zhí)行var childOb = observe(val)和childOb.dep.depend()泉瞻,而此時的val就是{testValueKey:testValueValue}對象)。
在這個時候我們設置this.$set(this.testkey, "addKey", "addValue")苞冯,就會觸發(fā)this.testkey對應的值:{testValueKey:testValueValue}對象的dep發(fā)布更新袖牙,而此時dep的訂閱者中包含模板{{testkey}}的watcher,此時模板更新視圖。同理每個數(shù)組也是有相應的dep來發(fā)布更新的舅锄,比如data={arr:[1,2,2]}}鞭达,此時[1,2,2]這個數(shù)組的__ obj__屬性下也會有dep的。

其實只有對象和數(shù)組才會有這種刪除和增加的操作皇忿,而其他的字符串等都是直接賦值修改的畴蹭,getter/setter都是能檢測到的,所以observe對象和數(shù)組的時候會創(chuàng)建一個dep鳍烁,用來收集訂閱和發(fā)布更新

對于第二個問題叨襟,我們先來考慮下數(shù)組修改有哪幾種情況
1.當你利用索引直接設置一個項時
比如:data={arr:[1,2,3]},這個時候我設置this.arr[0] = 4,會發(fā)現(xiàn)數(shù)據(jù)改變了幔荒,但是視圖沒有更新糊闽,Vue根本沒有檢測到變化。
這個時候可能你會說爹梁,observeArray的時候不是會遍歷數(shù)組右犹,observe每個元素嗎?可是Observe數(shù)據(jù)的時候是會判斷數(shù)據(jù)類型的姚垃,只會處理數(shù)組和對象念链,而this.arr里面的元素是字符串,所以無法轉化成observer類,也就不會有getter/setter钓账。另一方面碴犬,即便arr里面是對象,比如{arr:[{testobj: true}]}梆暮,數(shù)組元素{testobj: true}會被observe到服协,那也只是在{testobj: true}對象里面的屬性改變的時候響應,而{testobj: true}對象被替換是無法感知的啦粹。
2.調(diào)用數(shù)組的變異方法(push(),pop(),shift(),unshift(),splice(),sort(),reverse())偿荷,這些方法是會讓數(shù)組的值發(fā)生改變的,比如:arr=[0,1];arr.puah(3);此時arr=[1,2,3]唠椭,arr發(fā)生了改變跳纳,此時是需要更新視圖的,但是arr的getter/setter攔截不到變化(只有在賦值的時候才會調(diào)用setter贪嫂,比如:arr=[6,7,8])寺庄。
3.當你修改數(shù)組的長度時,例如:vm.items.length = newLength

對于第一種情況力崇,和對象的增減一樣斗塘,可以使用vm.$set/Vue.set和vm.$delete/Vue.delete這幾個api.
對于第二種情況,可以通過改寫這些變異方法完成亮靴,在調(diào)用這些方法的時候發(fā)布更新消息馍盟。下面我們來看代碼

/*
 * 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];
  //重寫arrayMethods里的變異方法
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    //inserted存儲的是新加到數(shù)組里的元素,需要被observe
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    //發(fā)布更新
    ob.dep.notify();
    return result
  });
});

回過頭再看Observer類中對于數(shù)組的處理茧吊,先覆蓋變異數(shù)組贞岭,再observe每個數(shù)組元素。所以每當調(diào)用數(shù)組的變異方法的時候搓侄,都會更新視圖瞄桨。

if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    //用改變變異數(shù)組后的arrayMethods的方法覆蓋被observe的數(shù)組的方法
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } 

對于第三種情況,可以使用splice來完成休讳,splice是變異方法讲婚,會發(fā)布更新。
戳這俊柔,查看官網(wǎng)對此的解決方案筹麸。

對于上面提到的第三個問題,Watcher在run方法里解決了這個問題雏婶,他會檢測value !== this.value物赶,只更新值變化的情況。

Observer相關代碼就看到這里留晚,下面來看Dep

Dep

下圖是Dep的結構圖



下面是源碼:

//全局變量酵紫,每個實例中的dep實例的id都是從0開始累加的
var uid$1 = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid$1++;
  //subscribe的簡寫告嘲,存放訂閱者
  this.subs = [];
};
//添加一個訂閱者
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//刪除一個訂閱者
Dep.prototype.removeSub = function removeSub (sub) {
  remove$1(this.subs, sub);
};
//讓Watcher收集依賴并添加訂閱者。
Dep.target是一個Watcher, 可以查看Watcher的addDep方法奖地。
這個方法做的事情是:收集依賴后橄唬,調(diào)用了Dep的addSub方法,給Dep添加了一個訂閱者
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
//發(fā)布數(shù)據(jù)更新:通過調(diào)用subs里面的每個Watcher的update發(fā)布更新
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();
  }
};

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
//注意這里参歹,target是全局唯一的仰楚。下面詳細講解。
Dep.target = null;

關于Dep主要注意以下幾點:

1.Dep是發(fā)布訂閱者模型中的發(fā)布者犬庇,Watcher是觀察者僧界,一個Dep實例對應一個對象屬性或一個被觀察的對象,用來收集訂閱者和在數(shù)據(jù)改變時臭挽,發(fā)布更新捂襟。
2.Dep實例有兩種實例:
*第一種:在observe方法里生成的,用來給被觀察的對象收集訂閱者和發(fā)布更新欢峰,掛在對象的__ ob__對象上葬荷,通常在defineReactive函數(shù)里的getter函數(shù)里調(diào)用childOb.dep.depend()來收集依賴,在vm.$set/Vue.set和vm.$delete/Vue.delete這些api中調(diào)用來發(fā)布更新纽帖。
*第二種:在defineReactive函數(shù)里闯狱,是用來set/get數(shù)據(jù)時收集訂閱者和發(fā)布更新的,保存在getter/setter閉包函數(shù)的作用域上抛计。set數(shù)據(jù)時收集依賴,get數(shù)據(jù)時發(fā)布更新照筑。

比如我們有一個data:data={testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}};
這個時候吹截,這個Vue實例上會有四個Dep實例:
第一個是調(diào)用data的observe方法時生成的掛在{testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}}對象的__ ob__方法上的,屬于上面說的第一種實例凝危;
第二個是調(diào)用defineReactive函數(shù)給屬性testVal添加getter,setter函數(shù)時生成的波俄。保存在getter/setter閉包函數(shù)的作用域上,屬于第二種實例蛾默。
第三個是調(diào)用defineReactive函數(shù)給屬性testObj添加getter,setter函數(shù)時生成的懦铺。保存在getter/setter閉包函數(shù)的作用域上,屬于第二種實例支鸡。
第四種是Watcher收集依賴時冬念,調(diào)用testObj屬性的set函數(shù)添加依賴時observe屬性的值(即{testObjFirstEle: "testObjFirstEle"}對象)生成的。

現(xiàn)在我們來驗證一下:
*源碼:http://runjs.cn/code/9p5ydg84
*效果:http://sandbox.runjs.cn/show/9p5ydg84


dep的id從0開始牧挣,到3結束一共四個急前。這個時候你可能會問,dep的id為2和3的實例去哪里了瀑构?注意上面我們說的第二種Dep實例對存在在getter/setter閉包函數(shù)的作用域中的裆针,我們獲取不到,你可以在源碼里debugger來看。

3.當我們想要給{testObjFirstEle: "testObjFirstEle"}對象添加屬性并更新視圖時有兩種方式:
一世吨、利用getter/setter澡刹,重新設置testObj屬性的值,testObj屬性的setter執(zhí)行的過程中會調(diào)用dep.notify()發(fā)布更新耘婚。比如:this.testObj = {testObjFirstEle: "testObjFirstEle", "newEle": "newEle};
二罢浇、利用$set函數(shù):this.$set(this.testObj, "newEle", "newEle")。此時是{testObjFirstEle: "testObjFirstEle"}對象的__obj __對象上的dep發(fā)布的更新边篮。

4.Dep.target:Dep.target為什么是全局唯一的呢己莺?這是我以前一直不理解的地方。這一點我想戈轿,在講完Watcher時會更清晰凌受,所以請大家耐心讀完下面的內(nèi)容,就明白了思杯。

Watcher

Watcher里面的屬性很多胜蛉,我們下面只注釋本文關心的內(nèi)容

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options
) {
  this.vm = vm;
  vm._watchers.push(this);
  // options
  if (options) {
    //對應$watch參數(shù)的deep,具體的可以參考官網(wǎng)文檔:https://cn.vuejs.org/v2/api/#vm-watch
    this.deep = !!options.deep;
    this.user = !!options.user;
    //跟computed相關色乾,這里不具體講解
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
  } 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
  //注意這里誊册,關于deps和newDeps下面詳細講解
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = expOrFn.toString();
  // parse expression for getter
  //這里的getter會有兩種情況:
   一、一個函數(shù)暖璧,比如在生命周期mount的時候案怯,需要watch模板中的值,這個時候傳過來的是一個函數(shù)澎办,后面在get函數(shù)里調(diào)用時這個函數(shù)時嘲碱,這個函數(shù)會調(diào)用數(shù)據(jù)的getter函數(shù)。
   二局蚀、一個表達式麦锯,比如我們在Vue實例的watch中寫的表達式,后面在get函數(shù)里獲取表達式的值的時候會調(diào)用數(shù)據(jù)的getter函數(shù)琅绅。
   expOrFn參數(shù)是一個字符串扶欣,比如testObj.testObjFirstVal,此時testObj僅僅是一個字符串千扶,而不是對象料祠,我們無法直接獲取testObjFirstVal屬性的值。
   所以我們在獲取值得時候不能直接拿到值县貌,parsePath函數(shù)就是用來解決這個問題的术陶,這個函數(shù)具體的操作,在后面的代碼里煤痕。
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    //這里是針對表達式
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
      "development" !== 'production' && warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  //注意這個地方梧宫,在非computed調(diào)用Watch函數(shù)外接谨,都會調(diào)用get函數(shù)(computed有自己的邏輯)
  this.value = this.lazy
    ? undefined
    : this.get();
};

/**
 * Evaluate the getter, and re-collect dependencies.
 */
//get函數(shù),用來收集依賴和獲取數(shù)據(jù)的值
Watcher.prototype.get = function get () {
  pushTarget(this);
  var value;
  var vm = this.vm;
  try {
    value = this.getter.call(vm, vm);
  } catch (e) {
    if (this.user) {
      handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
    } else {
      throw e
    }
  } finally {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    //注意這里下面詳細講解
    this.cleanupDeps();
  }
  return value
};

// 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;
var targetStack = [];

function pushTarget (_target) {
  if (Dep.target) { targetStack.push(Dep.target); }
  Dep.target = _target;
}

function popTarget () {
  Dep.target = targetStack.pop();
}

/**
 * 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);
    //這里做了一個去重塘匣,如果depIds里包含這個id脓豪,說明在之前給depIds添加這個id的時候,已經(jīng)調(diào)用過 dep.addSub(this)忌卤,即添加過訂閱扫夜,不需要重復添加。
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

/**
 * Clean up for dependency collection.
 */
//下面詳細講
Watcher.prototype.cleanupDeps = function cleanupDeps () {
    var this$1 = this;

  var i = this.deps.length;
  //去除多余的訂閱者
  while (i--) {
    var dep = this$1.deps[i];
    //如果Watcher不依賴于某個數(shù)據(jù)驰徊,即某個Dep,那么不需要再訂閱這個數(shù)據(jù)的消息笤闯。
    if (!this$1.newDepIds.has(dep.id)) {
      dep.removeSub(this$1);
    }
  }
  var tmp = this.depIds;
  //更新depIds
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  //清空newDepIds
  this.newDepIds.clear();
  tmp = this.deps;
  //更新deps
  this.deps = this.newDeps;
  this.newDeps = tmp;
  //清空newDeps
  this.newDeps.length = 0;
};

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
//更新模板或表達式:調(diào)用run方法
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  //下面三種情況均會調(diào)用run方法
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    //queueWatcher這個函數(shù)最終會調(diào)用run方法。
    queueWatcher(this);
  }
};

/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
//注意這里調(diào)用了get方法棍厂,會更新模板颗味,且重新收集依賴
Watcher.prototype.run = function run () {
  if (this.active) {
    //獲取值,且重新收集依賴
    var 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
      var oldValue = this.value;
      this.value = value;
      //注意下面 this.cb.call牺弹,調(diào)用回調(diào)函數(shù)來更新模板或表達式的值($watch表達式的時候浦马,會更新表達式的值)
      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);
      }
    }
  }
};

/**
 * Parse simple path.
 */
var bailRE = /[^\w.$]/;
function parsePath (path) {
  //如果字符串中沒有符號".",直接返回即可张漂,比如:testVal
  if (bailRE.test(path)) {
    return
  }
  //用符號"."分割字符串晶默,遍歷數(shù)組,依次獲取obj對象上相應的值航攒。
  比如:testObj.testObjFirstVal磺陡,先分割成數(shù)組[testObj,testObjFirstVal],其次遍歷數(shù)組,獲取obj[testObj]的值漠畜,最后獲取obj[testObj][testObjFirstVal]的值仅政。
  var segments = path.split('.');
  //這個地方將函數(shù)作為返回值,感興趣的話盆驹,可以看一下函數(shù)式編程
  return function (obj) {
    for (var i = 0; i < segments.length; i++) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

我們先來理一理watch函數(shù)做的事情:初始化變量——>獲取getter函數(shù),這里的getter函數(shù)是用來獲取數(shù)據(jù)的值滩愁,函數(shù)執(zhí)行過程中會調(diào)用數(shù)據(jù)的getter函數(shù)躯喇,會收集依賴——>調(diào)用watcher的get方法,收集依賴硝枉,獲取值廉丽,并將這些東西記錄下來。
這個過程就完成了收集依賴的過程妻味,而update函數(shù)是用來接收數(shù)據(jù)發(fā)布更新的消息并更新模板或表達式的正压。
下面我們重點來關注這幾點,這些是我剛接觸Vue時想不清楚的地方
1.收集依賴指的是誰收集依賴责球,依賴又是指的什么焦履?這是我一直很迷惑的問題拓劝。看英文注釋:Watcher的作用是分割表達式嘉裤,收集依賴并且在值變化的時候調(diào)用回調(diào)函數(shù)郑临。那么我們很明確知道是Watcher在收集依賴,依賴到底指什么呢屑宠?
我們上面說過一個Dep對應著一個數(shù)據(jù)(這個數(shù)據(jù)可能是:對象的屬性厢洞、一個對象、一個數(shù)組);一個Watcher對應可以是一個模板也可以是一個$watch對應的表達式典奉、函數(shù)等躺翻,無論那種情況,他們都依賴于data里面的數(shù)據(jù)卫玖,所以這里說的依賴其實就是模板或表達式所依賴的數(shù)據(jù)公你,對應著相關數(shù)據(jù)的Dep。
舉個例子:下面這個$watch對應的函數(shù)依賴的數(shù)據(jù)就是testWatcher和testVal骇笔。所以這個$watch對應的Watcher收集的依賴就是testWatcher和testVal對應的Dep省店。

app.$watch(function(){
     return this.testWatcher + this.testVal;
},function(newVal){
    console.log(newVal
 })

2.Watcher有四個使用的場景,只有在這四種場景中笨触,Watcher才會收集依賴懦傍,更新模板或表達式,否則芦劣,數(shù)據(jù)改變后粗俱,無法通知依賴這個數(shù)據(jù)的模板或表達式
*第一種:觀察模板中的數(shù)據(jù)
*第二種:觀察創(chuàng)建Vue實例時watch選項里的數(shù)據(jù)
*第三種:觀察創(chuàng)建Vue實例時computed選項里的數(shù)據(jù)所依賴的數(shù)據(jù)
*第四種:調(diào)用$watch api觀察的數(shù)據(jù)或表達式
所以在解決數(shù)據(jù)改變,模板或表達式?jīng)]有改變的問題時虚吟,可以這么做:
首先仔細看一看數(shù)據(jù)是否在上述四種應用場景中寸认,以便確認數(shù)據(jù)已經(jīng)收集依賴;其次查看改變數(shù)據(jù)的方式串慰,確定這種方式會使數(shù)據(jù)的改變被攔截(關于這一點偏塞,上面Obsever相關內(nèi)容中說的比較多)

3.Dep.target的作用:我們前面說過收集依賴的時機是在調(diào)用數(shù)據(jù)的getter函數(shù)的時候邦鲫,但是在這個時候數(shù)據(jù)的getter函數(shù)不知道當前的Watcher是哪一個灸叼,所以這里使用了一個全局變量來記錄當前的Watcher,方便添加依賴到正在執(zhí)行的Watcher庆捺。關于這點官方的英文注釋寫的挺清楚的古今。
4.targetStack的作用(Watcher函數(shù)的get方法中pushTarget和popTarget方法中用到):Vue2 中(本文源碼為Vue2),視圖被抽象為一個 render 函數(shù)滔以,一個 render 函數(shù)只會生成一個 watcher捉腥。比如我們有如下一個模板,模板中使用了Header組件你画。Vue2 中組件數(shù)的結構在視圖渲染時就映射為 render 函數(shù)的嵌套調(diào)用抵碟,有嵌套調(diào)用就會有調(diào)用棧桃漾。當 render模板時,遇到Header組件會調(diào)用Header組件的render函數(shù)立磁,兩個render函數(shù)依次入棧呈队,執(zhí)行完函數(shù),依次出棧唱歧。

<div id="app">
  <Header></Header>
</div>

5.Watcher函數(shù)的get方法中調(diào)用this.getter.call(vm, vm)收集完依賴后宪摧,又調(diào)用this.cleanupDeps()清除依賴。excus me ??颅崩?第一次看這個地方的時候几于,我很困擾,為什么添加完依賴后要清楚沿后。后面仔細看了代碼發(fā)現(xiàn)是這個樣子的:
Watcher里面有兩個屬性:deps和newDeps沿彭。他們是用來記錄上一次Watcher收集的依賴和新一輪Watcher收集的依賴,每一次有數(shù)據(jù)的更新都需要重新收集依賴(數(shù)據(jù)發(fā)布更新后尖滚,會調(diào)用Watcher的notify方法喉刘,notify方法會調(diào)用run方法,run方法會調(diào)用get方法漆弄,重新獲取值睦裳,并重新收集依賴)。舉個簡單的例子:我們點擊一個按鈕撼唾,用$set給data添加了一個新屬性newVal廉邑。上一輪收集的依賴中并沒有newVal的依賴,所以需要重新收集依賴倒谷。
this.cleanupDeps()這個函數(shù)的作用就是將新收集的依賴newDeps賦值給deps蛛蒙,并將newDeps清空,準備在下一次數(shù)據(jù)更新時收集依賴渤愁。所以這個函數(shù)不是真正的清空Watcher的依賴牵祟,而是清除臨時保存依賴的newDeps。

看完上面的這些后抖格,再看官方給出的圖课舍,就更明白了,不過官方的圖中他挎,并沒有標提到Dep和Observer。

三捡需、相關概念

1.雙向數(shù)據(jù)綁定

M 办桨,即 model,指的是模型站辉,也就是數(shù)據(jù)呢撞;V 即view损姜,指的是視圖,也就是頁面展現(xiàn)的部分殊霞。
雙向數(shù)據(jù)綁定大概概括為:每當數(shù)據(jù)有變更時摧阅,會進行渲染,從而更新視圖绷蹲,使得視圖與數(shù)據(jù)保持一致(model到view層)棒卷;而另一方面,頁面也會通過用戶的交互祝钢,產(chǎn)生狀態(tài)比规、數(shù)據(jù)的變化,這個時候拦英,這時需要將視圖對數(shù)據(jù)的更新同步到數(shù)據(jù)(view到model層)蜒什。

不同的前端 MV* 框架對于這種 Model 和 View 間的數(shù)據(jù)同步有不同的處理,如:
臟值檢查(angular.js)
數(shù)據(jù)劫持 + 發(fā)布者-訂閱者模式(Vue)

我們上面說的Vue的數(shù)據(jù)響應式原理其實就是實現(xiàn)數(shù)據(jù)到視圖更新原理疤估,而視圖到數(shù)據(jù)的更新灾常,其實就是此基礎上給可表單元素(input等)添加了change等事件監(jiān)聽,來動態(tài)修改model和 view铃拇。

2.發(fā)布-訂閱者模型

訂閱發(fā)布模式定義了一種一對多的依賴關系钞瀑,讓多個訂閱者對象同時監(jiān)聽某一個主題對象。這個主題對象在自身狀態(tài)變化時锚贱,會通知所有訂閱者對象仔戈,使它們能夠自動更新自己的狀態(tài)。
Vue中的Dep和Watcher共同實現(xiàn)了這個模型

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末拧廊,一起剝皮案震驚了整個濱河市监徘,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吧碾,老刑警劉巖凰盔,帶你破解...
    沈念sama閱讀 206,126評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異倦春,居然都是意外死亡户敬,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,254評論 2 382
  • 文/潘曉璐 我一進店門睁本,熙熙樓的掌柜王于貴愁眉苦臉地迎上來尿庐,“玉大人,你說我怎么就攤上這事呢堰〕” “怎么了?”我有些...
    開封第一講書人閱讀 152,445評論 0 341
  • 文/不壞的土叔 我叫張陵枉疼,是天一觀的道長皮假。 經(jīng)常有香客問我鞋拟,道長,這世上最難降的妖魔是什么惹资? 我笑而不...
    開封第一講書人閱讀 55,185評論 1 278
  • 正文 為了忘掉前任贺纲,我火速辦了婚禮,結果婚禮上褪测,老公的妹妹穿的比我還像新娘猴誊。我一直安慰自己,他們只是感情好汰扭,可當我...
    茶點故事閱讀 64,178評論 5 371
  • 文/花漫 我一把揭開白布稠肘。 她就那樣靜靜地躺著,像睡著了一般萝毛。 火紅的嫁衣襯著肌膚如雪项阴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 48,970評論 1 284
  • 那天笆包,我揣著相機與錄音环揽,去河邊找鬼。 笑死庵佣,一個胖子當著我的面吹牛歉胶,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播巴粪,決...
    沈念sama閱讀 38,276評論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼通今,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了肛根?” 一聲冷哼從身側響起辫塌,我...
    開封第一講書人閱讀 36,927評論 0 259
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎派哲,沒想到半個月后臼氨,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,400評論 1 300
  • 正文 獨居荒郊野嶺守林人離奇死亡芭届,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 35,883評論 2 323
  • 正文 我和宋清朗相戀三年储矩,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片褂乍。...
    茶點故事閱讀 37,997評論 1 333
  • 序言:一個原本活蹦亂跳的男人離奇死亡持隧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出逃片,到底是詐尸還是另有隱情屡拨,我是刑警寧澤,帶...
    沈念sama閱讀 33,646評論 4 322
  • 正文 年R本政府宣布,位于F島的核電站洁仗,受9級特大地震影響,放射性物質發(fā)生泄漏性锭。R本人自食惡果不足惜赠潦,卻給世界環(huán)境...
    茶點故事閱讀 39,213評論 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望草冈。 院中可真熱鬧她奥,春花似錦、人聲如沸怎棱。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,204評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽拳恋。三九已至凡资,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間谬运,已是汗流浹背隙赁。 一陣腳步聲響...
    開封第一講書人閱讀 31,423評論 1 260
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留梆暖,地道東北人伞访。 一個月前我還...
    沈念sama閱讀 45,423評論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像轰驳,于是被迫代替她去往敵國和親厚掷。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 42,722評論 2 345

推薦閱讀更多精彩內(nèi)容

  • 我的github: vue雙向綁定原理 MVC模式 以往的MVC模式是單向綁定级解,即Model綁定到View冒黑,當我們...
    KlausXu閱讀 44,812評論 7 91
  • Vue 依賴收集原理分析 Vue實例在初始化時,可以接受以下幾類數(shù)據(jù): 模板 初始化數(shù)據(jù) 傳遞給組件的屬性值 co...
    wuww閱讀 6,856評論 3 19
  • 幾種雙向綁定的做法目前幾種主流的mvc(vm)框架都實現(xiàn)了單向數(shù)據(jù)綁定,我認為的雙向數(shù)據(jù)綁定其實就是在單向綁定的基...
    Picidae閱讀 5,619評論 2 4
  • 凌晨一點蠕趁,鞭炮齊鳴后的沉寂薛闪,大年初一頭一天,我坐在異鄉(xiāng)的出租屋內(nèi)俺陋,努力想接地氣卻落入俗套的春晚剛結束豁延,公子我毫無睡...
    蕭水默閱讀 322評論 0 0
  • 我的文藝氣息是從逛書店,買書開始的腊状。時常這種文藝氣質會順帶傳染我對其他美好的物品的喜愛诱咏。 記得在一天下班,我坐著公...
    FreeManFree閱讀 275評論 0 0