深刻了解 Vue 響應式原理(數據攔截)

前言

在上一章節咱們已經粗略的分析了整個的Vue 的源碼(還在草稿箱,須要梳理清楚才放出來),可是還有不少東西沒有深刻的去進行分析,我會經過以下幾個重要點,進行進一步深刻分析。javascript

  1. 深刻了解 Vue 響應式原理(數據攔截)
  2. 深刻了解 Vue.js 是如何進行「依賴收集」,準確地追蹤全部修改
  3. 深刻了解 Virtual DOM
  4. 深刻了解 Vue.js 的批量異步更新策略
  5. 深刻了解 Vue.js 內部運行機制,理解調用各個 API 背後的原理

這一章節咱們針對1. 深刻了解 Vue 響應式原理(數據攔截) 來進行分析。vue

initState

咱們在上一章節中已經分析了,在初始化Vue實例的時候,會執行_init方法, 其中會執行initState方法, 這個方法很是重要, 其對咱們new Vue 實例化對象時,傳遞經來的參數props, methods,data, computed,watch的處理。 其代碼以下:java

function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }
複製代碼

這一章節,咱們只分析對data的處理, 也就是initData(vm)方法, 其代碼以下(刪除了異常處理的代碼):react

function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
 
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key);
      }
    }
    // observe data
    observe(data, true /* asRootData */);
  }
複製代碼

從上面的代碼分析,首先能夠得出以下一個數組

總結:bash

  1. data裏面的key必定不能和methods, props裏面的key重名
  2. proxy(vm, "_data", key);只是將data裏面的屬性從新掛載(代理)在vm實例上,咱們能夠經過以下兩種方式訪問data裏面的數據, 如vm.visibility 或者vm._data.visibility效果是同樣的。 observe(data, true /* asRootData */);是最重要的一個方法,下面咱們來分析這個方法

observe

observe中文翻譯就是觀察, 就是將原始的data變成一個可觀察的對象, 其代碼以下(刪除了一些邏輯判斷):app

function observe (value, asRootData) {
    ob = new Observer(value);
  }
複製代碼

這個方法就是new了一個Observer對象, 其構造函數以下:異步

var Observer = function Observer (value) {
    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作特殊處理,咱們如今傳遞的對象是一個Object, 可是裏面todos是一個數組,咱們後面會分析數組處理的狀況, 接下來調用this.walk方法,就是遍歷對象中的每個屬性:函數

Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive$$1(obj, keys[i]);
    }
  };
複製代碼

defineReactive$$1方法經過Object.defineProperty來從新封裝data, 給每個屬性添加一個getter,setter來作數據攔截post

function defineReactive$$1 ( obj, key, val, customSetter, shallow ) {
    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;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && 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;
        /* eslint-disable no-self-compare */
        if (newVal === value || (newVal !== newVal && value !== value)) {
          return
        }
        /* eslint-enable no-self-compare */
        if (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();
      }
    });
  }
複製代碼

defineReactive$$1方法就是利用Object.defineProperty來設置data裏面已經存在的屬性來設置getter,setter, 具體getset在何時發揮效用咱們先不分析。

var childOb = !shallow && observe(val);是一個遞歸調observe來攔截全部的子屬性。

data中的屬性todos是一個數組, 咱們又回到observe方法, 其主要目的是經過ob = new Observer(value);來生成一個Observer對象:

var Observer = function Observer (value) {
    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有特殊的處理,下面咱們咱們來具體分析protoAugment方法

protoAugment(數組)

protoAugment(value, arrayMethods);傳了兩個參數,第一個參數,就是咱們的數組,第二個參數arrayMethods須要好好分析,是Vue中對Array的特殊處理的地方。

其源碼文件在vue\src\core\observer\array.js下,

  1. 首先基於Array.prototype原型建立了一個新的對象arrayMethods
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
複製代碼
  1. 重寫了Array以下7 個方法:
var 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
  })
})
複製代碼

總結:從上面可知, Vue只會對上述七個方法進行監聽, 若是使用Array 的其餘的方法是不會觸發Vue 的雙向綁定的。好比說用concat,map等方法都不會觸發雙向綁定。

this.$set

上面已經分析了Object,Array 的數據監聽,可是上面的狀況都是在初始化Vue實例的時候,已經知道了data中有哪些屬性了,而後對每一個屬性進行數據攔截,如今有一種狀況就是,若是咱們有須要須要給data動態的添加屬性,咱們該怎麼作呢?

Vue單獨開放出了一個接口$set, 他掛載在vm原型上,咱們先說下其使用方式是: this.$set(this.newTodo,"name", '30')

function set (target, key, val) {
    if (isUndef(target) || isPrimitive(target)
    ) {
      warn(("Cannot set reactive property on undefined, null, or primitive value: " + ((target))));
    }
    if (Array.isArray(target) && isValidArrayIndex(key)) {
      target.length = Math.max(target.length, key);
      target.splice(key, 1, val);
      return val
    }
    if (key in target && !(key in Object.prototype)) {
      target[key] = val;
      return val
    }
    var ob = (target).__ob__;
    if (target._isVue || (ob && ob.vmCount)) {
      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
    }
    defineReactive$$1(ob.value, key, val);
    ob.dep.notify();
    return val
  }
複製代碼

經過上面的分析,使用$set方法,須要注意以下幾點:

  1. target 不能是undefined, null, string, number, symbol, boolean六種基礎數據類型
  2. target 不能直接掛載在Vue 實例對象上, 並且不能直接掛載在root data屬性上

$set最終調用defineReactive$$1(ob.value, key, val);方法去動態添加屬性, 而且給該屬性添加gettersetter

動態添加的屬性,一樣也須要動態更新視圖,則是調用ob.dep.notify();方法來動態更新視圖

總結

  1. 若是data屬性是一個Object, 則將其將其進行轉換,主要是作以下兩件事情:
  1. 給對象添加一個__ob__的屬性, 其是一個Observer對象
  1. 遍歷data的說有屬性('key'), 經過Object.defineProperty 設置其gettersetter來進行數據攔截
  1. 若是data(或者子屬性)是一個Array, 則將其原型轉換成arrayMethods(基於Array.prototype原型建立的一個新的對象,可是從新定義了 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse')七個方法,來進行對Array的數據攔截(這也就是Vue 對數組操做,只有這七個方法能實現雙向綁定的緣由)

在這篇文章咱們已經分析了Vue 響應式原理 , 咱們接下來會繼續分析深刻了解 Vue.js 是如何進行「依賴收集」,準確地追蹤所需修改

相關文章
相關標籤/搜索