亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

首頁 慕課教程 ES6-10 入門教程 ES6-10 入門教程 ES6實戰2-實現 Vue3 effect 源碼

ES6實戰2-實現 Vue3 effect 源碼

1. 前言

上一節我們實現了 Vue3 的數據劫持功能,并對一些邊界值做了處理。但是,當數據改變了我們希望更新試圖,這個時候雖然我們能劫持到數據的變化但是沒有做任何處理,我們需要對數據的獲取和修改增加更新的邏輯,并提供一個 API 給業務用來響應式的處理數據的變化。Vue3 中提供了 effect,當 effect 回調函數中引用的響應式數據變化時,會觸發 effect 回調函數的執行,相當于 vue2 中的 watcher。我們來看下面的應用示例:

// /vue-next/public/index.html
<script src="./vue.reactivity.js"></script>
<script>
  const { reactive, effect } = VueReactivity;
  const proxy = reactive({
    name: 'ES6 Wiki',
  })

  effect(() => {
    document.getElementById('app').innerHTML = proxy.name;
  })

  setTimeout(() => {
    proxy.name = 'imooc ES6 Wiki 實戰'
  }, 1000)
</script>

上面的代碼中我們引入了 Vue3 的 reactivity 庫,初始化網頁內容后,在 1 秒以后更新網頁中的內容。本節我們就來實現 effect 這個 API 的功能,本節源碼參考: ES6 Wiki 。

2. effect 實現

2.1 創建響應式 effect

effect 在 Vue3 的響應式系統中是一個非常關鍵的函數,后面的 ref、computed 等函數都會用到 effect 中的功能。在 Vue3 中的 effect 會接受不了兩個參數:

effect(fn, options)

基于 Vue3 響應式 API 的 effect 特點,需要將 effect 變成一個響應式函數,effect 的響應式就是當數據變化時 fn 會自動執行。實現 effect 這個函數的一個目標就是,將 effect 回調函數中所有引用了響應式數據的屬性收集起來,并和 effect 的回調函數關聯上,在數據變化時在執行 effect 的回調函數。也就是上面的測試案例中,proxy 對象的 name 屬性在 effect 的回調函數中。要想讓 effect 成為響應式的,就需要將 name 和 effect 關聯起來,當 name 的值變化了,就執行 effect 的回調函數。

在本節 options 沒用到,但是在 computed 中會使用到,本節使用了 options.lazy 屬性,用于判斷是否在第一次的時候執行回調函數中的內容。effect 中是默認執行回調函數的。

如果要把 effect 變成響應式,需要定義一個創建響應式的方法(createReactiveEffect)用于創建一個 effect 函數。createReactiveEffect 執行后會返回一個 effect 函數,在 createReactiveEffect 函數中會默認執行 fn。

export function effect(fn, options){
  const effect = createReactiveEffect(fn, options)
  if (!options.lazy) {
    effect()
  }
  return effect
}

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    return fn();	// 用戶創建的回調函數,fn函數內部會對響應式數據進行取值操作
  }
  return effect
}

我們定義一個全局變量 activeEffect,這樣做是為了把 effect 存起來,方便后面調用,在取值的時候就可以拿到這個 activeEffect。

let activeEffect;
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    activeEffect = effect;
    return fn();
  }
  return effect
}		

2.2 屬性和 effect 關聯

怎么才能讓屬性和這個函數進行關聯呢?首先我們要創建一個收集函數(track)用于收集屬性 key 和 effect 回調函數的關聯,并且只有在 effect 中使用到的 key,更新時才會執行 effect 中的回調,所以我們在收集依賴時需要先判斷。

function track(target, key) {
  if (activeEffect === viod 0) {
    return;
  }
}

什么時候進行收集呢?effect 回調函數會默認執行,在獲取值的時候對響應式對象上的 key 進行依賴收集,也就是在 createGetter 函數中進行收集。

function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    if (isSymbol(key)) {
      return res;
    }

    // 依賴收集
    track(target, key);
    
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  };
}

如何關聯呢?就是需要在 target 上的 key 中存放若干個 effect,那這要怎么存放呢?這時我們想到了 WeakMap,創建一個 WeakMap 來保持 target 上的需要關聯 effect 的屬性。同時,

下面的偽代碼數據結構是我們希望存放在 WeakMap 中的映射,其中 target 是目標對象。

{
  target1: {
    key: [effect, effect]
  },
  target2: {
    key: [effect, effect]
  }
}

在存放 effect 時可能還需要給 effect 加上一些標識,如:id、deps、options 等,后面會用到。

Let uid = 0;
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    activeEffect = effect;
    return fn();
  }
  effect.id = uid++;
  effect.deps = [];
  effect.options = opntions;
  return effect
}	

const targetMap = new WeakMap();
function track(target, key) {
  if (activeEffect === undefined) {
    return;
  }
  // 目標是創建一個映射:{target1: {name: [effect, effect]},target2: {name: [effect, effect]}}
  let depsMap = targetMap.get(target);	// depsMap存放target的值,是一個Map對象
  if(!depsMap) {	// 如果targetMap中沒用target對象,則創建一個。
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);	// 獲取depsMap對象中屬性是target上的key值
  if(!dep) {
    depsMap.set(key, (dep = new Set())); // 存放effect的集合
  }
  if(!dep.has(effect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

上面的代碼中,收集目標對象上所有的依賴,在 effect 的回調函數中沒有使用到的屬性,就不需要進行依賴收集。在執行完創建響應式 effec 函數 createReactiveEffect 后需要把 activeEffect 置為 null。

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    try {
      activeEffect = effect;
    	return fn();
    } finally {
      activeEffect = null;
    }
  }
  return effect
}	

上面的代碼中 finally 是一定會執行的。在 effect 回調函數中嵌套使用 effect,并且在嵌套的 effect 后還有響應式數據,如果是下面這種寫法,state.c = 300 將不會收集。

effect(() => {
  state.a = 100;
  effect(() => {
    state.b = 200;
  })
  state.c = 300;
})

這個時候我們就需要創建一個存放棧的數組(effectStack)來存放 activeEffect,執行完畢后也不用賦值 null 了,通過出棧的形式把最后一個移除,讓當前的 activeEffect 值等于 effectStack 最后一個值 effectStack[effectStack.length-1] 。這樣我們在執行完創建響應式 effect 函數時,控制權又會交到上一層的 activeEffect 上,這樣上面代碼中的 state.c=300 就會被收集到第一層的 effect 中去。具體執行代碼如下:

const effectStack = [];
function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    try {
      activeEffect = effect;
      effectStack.push(activeEffect);
    	return fn();
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  }
  return effect
}

使用棧的還有一個好處可以防止遞歸執行,在 effect 如果有數據持續變化是如: state.a++ 這樣的邏輯就會形成遞歸。這時需要處理為只執行一次,增加一個條件判斷,如下代碼:

function createReactiveEffect(fn, options) {
  const effect = function reactiveEffect() {
    if (!effectStack.includes(effect)) {	// 防止死循環
      try {
        activeEffect = effect;
        effectStack.push(activeEffect);
        return fn();
      } finally {
        effectStack.pop();
        activeEffect = effectStack[effectStack.length - 1];
      }
    }
  }
  return effect
}

2.3 執行收集的函數

上面的內容是依賴收集的過程,主要在響應式數據獲取時執行,也就是在調用 createGetter 的時候執行,那么依賴收集完后,當數據發生變化的時候,需要讓收集的回調函數依次執行。而執行這樣收集函數的過程是在 createSetter 中完成,因為在這里是更新數據的過程。上節中我們在 createSetter 中預留了新增和更新屬性的判斷:

function createSetter() {
  return function get(target, key, value, receiver) {
		...
    if (!hadKey) {
      console.log('新增屬性');
      trigger(target, 'ADD', key, value)
    } else if (hasChanged(value, oldValue)) {
      console.log('更新屬性');
      trigger(target, 'SET', key, value, oldValue)
    }

    return result;
  };
}

Vue3 中執行依賴的函數是 trigger,這個函數一共接受五個參數,在執行 trigger 時會傳入修改數據的類型:新增(ADD)和更新(SET),這是 Vue 為了處理不同場景而設置的屬性。這里我們先創建 tigger 函數,首先需要判斷在 targetMap 中是否有被依賴的對象,沒有則直接返回。

export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
}

如何讓依賴的 effect 執行呢?

  • 首先要判斷 key 是不是 undefined;
  • 獲取 key 中的 effect 函數,并執行。
export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const run = (effects) => {
    if (effects) {
      effects.forEarch(effect => effect())
    }
  }
  if (key == void 0) {
    run(depsMap.get(key));
  }
}

上面是對對象的處理,但是在處理數組的時候還會有問題,如下代碼:

const state = reactive([1,2,3]);
effect(() => {
  document.getElementById('app').innerHTML = state[2];
})

setTimeout(() => {
  state.length = 1;
}, 1000)

上面的代碼中,數據變化是直接更新數組的長度,而在 effect 中沒有使用 length 屬性,所以在更新 length 屬性時不會觸發 run(depsMap.get(key)); 的依次執行,這樣 length 改變 effect 回調函數不會執行,視圖也不會被更新。這時就需要對屬性是 length 的數組進行驗證,如果直接更新的是數組的長度就需要單獨處理:

export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const run = (effects) => {
    if (effects) {
      effects.forEarch(effect => effect())
    }
  }
  if (key === 'length' && isArray(target)) {
    depsMap.forEarch((deps, key) => {
      if(key === 'length' || key >= newValue) {	// newValue是更新后的值,
        run(deps)
      }
    })
  } else {
    if (key == void 0) {
      run(depsMap.get(key));
    }
  }
}

上面的代碼是在修改數組 length 屬性時,讓收集依賴的函數執行。還有一種情況,是在 effect 回調中沒有直接取索引的值,而且在修改數組時,直接在超過數組長度的位置上新增一個元素。

const state = reactive([1,2,3]);
effect(() => {
  document.getElementById('app').innerHTML = state;
})

setTimeout(() => {
  state[5] = 5;
}, 1000)

在這種情況下也沒有索引 key 進行收集,但是確實使用數組的索引增加了值。這時我們就需要借助 trigger 中的 type 類型來進行處理,當對數組索引進行添加操作時,需要觸發數組的更新。

export function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target)
  if (!depsMap) {
    return
  }
  const run = (effects) => {
    if (effects) {
      effects.forEarch(effect => effect())
    }
  }
  if (key === 'length' && isArray(target)) {
    depsMap.forEarch((deps, key) => {
      if(key === 'length' || key >= newValue) {	// newValue是更新后的值,
        run(deps)
      }
    })
  } else {
    if (key == void 0) {
      run(depsMap.get(key));
    }
    switch (type) {
      case 'ADD':
        if(isArray(target)) {
          if(isIntergerKey) {	// 判斷key是否是索引類型
            run(depsMap.get('length'));	// 新增屬性時直接觸發length收集的依賴即可
          }
        }
        break;
    }
  }
}

這樣我們就基本上實現了 effect 的響應式的源碼。

小結

本節我們主要實現了 Vue3 中 effect 函數,它是一個響應式的函數,在源碼實現過程中需要注意幾點:

  • 使用 WeakMap 數據結構來存放 target 上的 key 和 effect 的關系;
  • 對 effect 的嵌套處理時,引入了棧的方式來控制當前的 activeEffect 值;
  • 在使用數組時,在對 length 直接修改等操作時進行特殊的處理。