vue2 響應式細節

data 中的數據是如何處理的?

每一次實例化一個組件,都會調用 initData 然後調用 observe 方法,observe 方法調用了 new Observer(value), 並且返回 __ob__

在 new Observer 中做了兩件事:

  1. 把當前實例掛載到數據的__ob__屬性上,這個實例在後面有用處。
  2. 根據數據類型(數組還是對象)區別處理

如果是對象:

橫向遍歷對象屬性,調用 defineReactive;

遞歸調用 observe 方法, 當屬性值不是數組或者對象停止遞歸

下面對 defineReactive 方法做了詳細的注釋:

export function defineReactive(
  obj: Object,
  key: string,
  val: any,
  customSetter ? : ? Function,
  shallow ? : boolean
) {
  const dep = new Dep(); // 閉包創建依賴對象;   每個對象的屬性都有自己的dep
  // 下面是針對已經通過Object.defineProperty 或者Object.seal Object.freeze 處理過的數據
  const property = Object.getOwnPropertyDescriptor(obj, key);
  // 如果configurable為false ,再次Object.defineProperty(obj, key)會報錯,並且不會成功;所以直接返回
  // 所以可以針對性的使用Object.freeze/seal優化性能。
  if (property && property.configurable === false) {
    return;
  }
  const getter = property && property.get;
  const setter = property && property.set;
  // 正常情況下 我們使用的數據getter、setter都是不存在的,並且在new Observer()中調用defineReactive的參數只有兩個
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]; // 也就是說 這行代碼一般情況下會執行
  }
  // 一般情況下 shallow是false  ;childOb就是返回的Observer實例,這個實例是存儲在數據的__ob__屬性上的
  //
  let childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      const value = getter ? getter.call(obj) : val; // getter 不存在 ,直接用val
      if (Dep.target) {
        // Dep.target是一個全局數據,保存的是watcher棧(targetStack)棧頂的watcher,
        dep.depend(); // 閉包dep把當前watcher收集起來; 收集依賴真正發生在render方法執行的時候(也就是虛擬dom生成的時候)
        if (childOb) {
          // val不是對象(非Array 或者object) observe方法才會返回一個Observer實例,否則返回undefined

          // 此處為什麼要執行childOb.dep.depend()呢?
          // 這麼做的效果是:在對象上掛載的__ob__的dep對象把點前watcher添加到了依賴里,這個dep和閉包dep不是一個。
          // 目的在於:
          // 		1.針對對象:要想this.$set/$del時候能夠觸發組件重新渲染,需要把渲染watcher保存下來,然後在$set中調用 ob.dep.notify();這裡就用到了__ob__屬性
          // 		2.針對數組:數組的攔截中(調用splice push 等法法)要想觸發重新渲染,調用 ob.dep.notify() 這裡就用到了__ob__屬性
          childOb.dep.depend();
          if (Array.isArray(value)) {
            // 如果value是一個數組,在observe方法中走的是數組那套程序,這些元素沒有被Object.defineProperty這一系列的處理(元素當做val處理),即便元素是object/array ,沒有childOb.dep.depend()這樣的一個過程,導致上面this.$set/$del、數組無法觸發重新渲染;
            // 所以調用dependArray 針對數組做處理  這裡就用到了__ob__屬性
            dependArray(value);
          }
        }
      }
      return value;
    },
    set: function reactiveSetter(newVal) {
      // 一般沒有getter
      const value = getter ? getter.call(obj) : val;
      // 值未變化,  newVal !== newVal && value !== value 應該針對的是NaN
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return;
      }
      if (process.env.NODE_ENV !== "production" && customSetter) {
        customSetter();
      }
      // getter 和setter 要成對才行
      if (getter && !setter) return;
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      // 重新設置值之後,需要重新observe ,並且更新閉包變量 childOB
      childOb = !shallow && observe(newVal);
      // 更新
      dep.notify();
    },
  });
}

如果是數組:

修改數組的 __proto__ 屬性值,指向一個新的對象;

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

這個新對象中重新定義如下方法:

'push','pop','shift','unshift','splice','sort','reverse'

同時這個對象的 __proto__ 指向 Array.prototype。

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);

最後項目的代碼在控制台打印出下面的截圖

data() {
  return {
    data1: [{
      name: 1
    }]
  }
},

image-20210213184124423

同時對數組中的每個元素做 observe 遞歸處理。

watch 選項是如何處理的?

watch 的使用方法一般如下:

watch: {
  a: function(newVal, oldVal) {
    console.log(newVal, oldVal);
  },
  b: 'someMethod',
  c: {
    handler: function(val, oldVal) {
      /* ... */
    },
    deep: true
  },
  d: {
    handler: 'someMethod',
    immediate: true
  },
  e: [
    'handle1',
    function handle2(val, oldVal) {},
    {
      handler: function handle3(val, oldVal) {},
    }
  ],
  'e.f': function(val, oldVal) {
    /* ... */
  }
}

watch 的處理按照如下流程, 把其中的關鍵代碼羅列出來了:

-- > initData() // 初始化組件的時候調用  如果組件中有watch選項,調用initWatch
  -- > initWatch()
if (Array.isArray(handler)) { // 這裡處理數組的情況,也就是上面e的情況
  for (let i = 0; i < handler.length; i++) {
    createWatcher(vm, key, handler[i])
  }
} else {
  createWatcher(vm, key, handler)
}
-- > createWatcher()
if (isPlainObject(handler)) { // 兼容c(對象)
  options = handler
  handler = handler.handler
}
// 如果是b 字符串的情況,需要在vm上有對應的數據
if (typeof handler === 'string') {
  handler = vm[handler]
}
// 默認是 a(函數)
vm.$watch(expOrFn, handler, options)
  -- > vm.$watch()
options.user = true // 添加參數 options.user = true ,處理immediate:true的情況
const watcher = new Watcher(vm, expOrFn, cb, options)
  -- > new Watcher() // 創建watcher
this.getter = parsePath(expOrFn) // 這個getter方法主要是get一下watch的變量,在get的過程中觸發依賴收集,把當前watcher添加到依賴
this.value = this.lazy // 選項lazy是false
  ?
  undefined :
  this.get() // 在constructor中直接調用get方法
  -- > watcher.get()
pushTarget(this) // 把當前watcher推入棧頂
value = this.getter.call(vm, vm) // 這時候這個watch的變量的依賴里就有了當前watcher
  -- > watcher.getter() // 依賴收集的地方

當 watch 的變量變化的時候,會執行 watcher 的 run 方法:

run() {
  if (this.active) {
    const value = this.get()
    // 渲染watcher情況下 value是undefined
    // 在自定義watcher的情況下 value就是監聽的值
    if (
      value !== this.value || // 當watch的值有變化的時候
      isObject(value) ||
      this.deep
    ) {
      // set new value
      const oldValue = this.value
      this.value = value
      // 自定義watcher的user是true ,cb就是那個handler
      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)
      }
    }
  }
}

上面的的代碼中 value !== this.value 和 deep 比較好理解,數值變化觸發 handler;

但是 isObject(value)對應的什麼情況呢?看一下下面的例子就知道了:

data() {
  return {
    data1: [{
      name: 1
    }],
  }
},
computed: {
  data2() {
    let value = this.data1[0].name //
    return this.data1 // 返回的是一個數組,所以data2一致是不變的
  }
},
watch: {
  data2: function() {
    // 雖然data2的值一直是data1,沒有變化;但是因為data2滿足isObject,所以仍然能觸發handler
    // 由此可以想到,可以在computed中主動去獲取某個數據屬性來觸發watch,並且避免在watch中使用deep
    // 但是這樣也不太合適,因為可以直接使用'e.f'這種例子來代替;
    // 所以根據要實際情況確定
    console.log('data2');
  }
}
created() {
  setInterval(() => {
    this.data1[0].name++
  }, 2000)
}

computed 數據是如何處理的?

computed 首先是創建 watcher,與渲染 watcher、自定義 watcher 不同之處:初始化的時候不會執行 get 方法,也就是不會做依賴收集。

另外使用 Object.defineProperty 定義 get 方法:

function createComputedGetter(key) {
  return function computedGetter() {
    const watcher = this._computedWatchers && this._computedWatchers[key];
    if (watcher) {
      // lazy=true  然後 dirty 也是true
      if (watcher.dirty) {
        watcher.evaluate(); // 把computed watcher添加到涉及到的所有的變量的依賴中;
      }
      if (Dep.target) {
        watcher.depend(); // 主動調用depend方法;假如這個computed是用在頁面渲染上,就會把渲染watcher添加到變量的依賴中
      }
      return watcher.value;
    }
  };
}

當 computed 數據在初次渲染中:

-- > render // 渲染
-- > computedGetter // computed    Object.defineProperty 定義get方法:
-- > watcher.evaluate() // 計算得到watcher.value
-- > watcher.get()
-- > pushTarget(this) // 把當前computed watcher 推入watcher棧頂
-- > watcher.getter() // getter方法就是組件中computed定義的方法,執行的時候會做依賴收集
-- > dep.depend() // 把當前computed watcher加入變量的依賴中
-- > popTarget() // 把當前	computed watcher 移除棧,一般來說渲染watcher會被推出到棧頂
-- > cleanupDeps() // 清除多餘的watcher 和 dep
-- > watcher.depend() // 這是computed比較特殊的地方。假如computed中依賴變量data中的數據,這個步驟把當前watcher添加到變量的依賴中;為什麼要這麼做呢?個人猜測意圖是computed的目的是做一個處理數據的橋樑,真正的響應式還是需要落實到data中的數據。

當 computed 中的依賴數據變化的時候會走如下流程:

-- > watcher.update() // 這是個 computed watcher,其中lazy為true,所以不會往下走
if (this.lazy) {
  this.dirty = true
}
-- > watcher.update() // 渲染watcher render 之後的過程就如同初次渲染一樣

渲染 watcher 的流程?

渲染 watcher 相對好理解一些

new Watcher(渲染 watcher) ->watcher.get-> pushTarget(this) ->watcher.getter()-> render -> Object.defineProperty(get) -dep.depend()-> popTarget()->watcher.cleanupDeps()

watcher.getter 是下面方法:

updateComponent = () => {
   vm._update(vm._render(), hydrating)
}

Vue 中依賴清除?

源碼在 vue/src/core/observer/watcher.js 中;

需要注意到 vue 中有一套清除 watcher 和 dep 的方案;vue 中的依賴收集並不是一次性的,重新 render 會觸發新一次的依賴收集,這時候會把無效的 watcher 和 dep 去除掉,這樣能夠避免無效的更新。

如下 computed ,只要有一次 temp<=0.5 , 改變 b 都不再會在打印 temp ;原因在於當 temp<0.5 之後, this.b 不會把當前 a 放進自己的 dep 中,也就不會再觸發這個 computed watcher 了

data() {
  return {
    b: 1
  }
},
computed: {
  a() {
    var temp = Math.random()
    console.log(temp); // 只要有一次a<=0.5 接下來就不會打印temp了
    if (temp > 0.5) {
      return this.b
    } else {
      return 1
    }
  }
},
created() {
  setTimeout(() => {
    this.b++
  }, 5000)
},

這裏面主要是 watcher.js 中的 cleanupDeps 方法在處理;

cleanupDeps() {
  let i = this.deps.length
  // 遍歷上次保存的deps
  while (i--) { // i--
    const dep = this.deps[i]
    // newDepIds 是在本次依賴收集中加入的新depId集合
    // 把不在newDepIds中的dep清除
    if (!this.newDepIds.has(dep.id)) {
      dep.removeSub(this)
    }
  }
  // depIds是一個es6 set集合 ,是引用類數據
  // newDepIds類似於一個臨時保存的地方,最終需要把數據保存到depIds。左手到右手的把戲
  // newDeps 和 newDepIds 是一樣的
  let 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
}

vuex 響應式原理?

依賴於 vue 自身的響應式原理,通過構建一個 Vue 實例,在 render 過程中完成依賴的收集。

store._vm = new Vue({
  data: {
    $$state: state, // 自定義的state數據
  },
  computed,
});

vue-router 響應式處理?

Vue.mixin({
  beforeCreate() {
    if (isDef(this.$options.router)) {
      this._routerRoot = this;
      this._router = this.$options.router;
      this._router.init(this);
      // 關鍵是這行代碼,把_route屬性進行響應式處理
      Vue.util.defineReactive(
        this,
        "_route",
        this._router.history.current
      );
    } else {
      this._routerRoot =
        (this.$parent && this.$parent._routerRoot) || this;
    }
    registerInstance(this, this);
  },
  destroyed() {
    registerInstance(this);
  },
});

Object.defineProperty(Vue.prototype, "$route", {
  get() {
    return this._routerRoot._route;
  },
});

渲染 router-view 的時候會觸發上面的

// 該組件渲染的時候render方法
render() {
  ...
  // 當調用$route的時候會觸發依賴收集
  var route = parent.$route;
}
Tags: