vue $set到底幹了什麼

黑黑穀物餐發表於2018-11-02

Question問題

寫元件的時候遇到這樣一個問題,在template裡繫結了一個物件<span>{{aa.bb}}</span>類似這個,,然後進行點選按鈕的函式裡執行this.aa.bb = 1,但是我忘記在data裡的aa物件提前註冊bb這個屬性了,頁面上的值竟然也變成了1,我記得vue文件裡有這樣一句話向響應式物件中新增一個屬性,並確保這個新屬性同樣是響應式的,且觸發檢視更新。它必須用於向響應式物件上新增新屬性,因為 Vue 無法探測普通的新增屬性 (比如 this.myObject.newProperty = 'hi')以上出自vue文件set描述。所以說為什麼我並沒有用$set,為什麼還產生了響應式效果了呢?

找到問題所在

仔細一看,是因為我在觸發this.aa.bb=1這個函式裡,也修改了一個響應式的變數this.cc = 1,所以導致模版整個重新渲染,把bb的值也一起重新渲染了,也就是為什麼我看到了bb的值也在變化,誤以為是響應式了。

繼續深挖

之後我做了一系列的測試,首先我把影響判斷的cc幹掉,果然我繼續觸發this.aa.bb=1,頁面果然沒有響應了,之後我在替換成this.$set(this.aa, 'bb', '1'),果然有了效果,所以引出主題$set到底幹了什麼?

Answer答案

vue原始碼裡的set定義

function set (target, key, val) {
  if (process.env.NODE_ENV !== 'production' &&
    (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)) {
    process.env.NODE_ENV !== '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
  }
  defineReactive(ob.value, key, val);
  ob.dep.notify();
  return val
}
複製程式碼

很明顯,set函式裡有加限制,if (key in target && !(key in Object.prototype)) {,物件裡含有這個變數的話,就直接賦值然後return val了。注意下defineReactive這個函式,這個就是響應式的繫結,通過debugger發現,vue初始化過程會把data裡的值一一遞迴進行響應式繫結。然後我們們看看他是怎麼繫結的?

function defineReactive (
  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;
  if (!getter && arguments.length === 2) {
    val = obj[key];
  }
  var setter = property && property.set;

  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 (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}
複製程式碼

看重點,Object.defineProperty看一下這個函式的定義,Object.defineProperty() 方法會直接在一個物件上定義一個新屬性,或者修改一個物件的現有屬性, 並返回這個物件。來自MDN[developer.mozilla.org/zh-CN/docs/…]。

可以看到vue利用Object.defineProperty這個函式的get,set進行響應式繫結,當變數被重新賦值的時候會觸發set函式,重新渲染更新dom,進行更復雜的事情。通過文件可以得知,set,get函式是與value和writable配置項是互斥不共存的。在嚴格模式下會報錯的。也就是為什麼vue要攔截住不會再次進行響應式繫結。

相關文章