Vue3丨進一步瞭解這 20 個響應式 API,寫碼若有神

前面說的話

在 Vue2 中,我的以爲對於數據的操做比較 「黑盒」 。
而 Vue3 把響應式系統更顯式地暴露出來,使得咱們對數據的操做有了更多的靈活性。
因此,對於 Vue3 的幾個響應式的 API ,咱們須要更加的理解掌握,才能在實戰中運用自如。javascript

先了解,什麼是響應式 ?

  • Vue3 官網有舉過一個例子
var val1 = 2
var val2 = 3
var sum = val1 + val2
複製代碼

咱們但願 val1 或 val2 的值改變的時候,sum 也會響應的作出正確的改變。html

  • 大白話

我依賴了你,你變了。你就通知我讓我知道,好讓我作點 「操做」 。前端

  • 從 Vue3 的源碼來說

讓咱們記住三個關鍵的英語單詞,它們的順序也是完成一個響應式的順序。java

effect > track > trigger > effectreact

淺淺的解釋一下:在組件渲染過程當中,假設當前正在走一個 「effect」(反作用),這個 effect 會在過程當中把它接觸到的值(也就是說會觸發到值的 get 方法),從而對值進行 track(追蹤)。當值發生改變,就會進行 trigger(觸發),執行 effect 來完成一個響應!數組

  • 用代碼來解釋

在 Vue 中,有三種 effect ,且說是視圖渲染effect、計算屬性effect、偵聽器effect瀏覽器

<template>
  <div>count:{{count}}</div>
  <div>computedCount:{{computedCount}}</div>
  <button @click="handleAdd">add</button>
</template>

// ...
setup() {
  const count = ref(1);
  const computedCount = computed(() => {
    return count.value + 1;
  });
  watch(count, (val, oldVal) => {
    console.log('val :>> ', val);
  });
  const handleAdd = () => {
    count.value++;
  };
  return {
    count,
    computedCount,
    handleAdd
  };
}
// ...
複製代碼

上面這段代碼,對於依賴值的追蹤以後會被存放於這樣的一個集合中,如圖:緩存

注:以上的最內層集合數組裏的 reactiveEffect 方法分別是 偵聽器effect、視圖渲染effect、計算屬性effect。markdown

當執行 handleAdd 動做時,就會觸發 count.value 的 set 方法,進行 trigger 響應式調用集合相關的 3 個 effect ,而後分別去更新視圖,更新 computedCount 的值,調用 watch 偵聽器的回調方法進行輸出。
不太理解不要緊,腦殼瓜先有個大致的結構便可 ~閉包

簡單的介紹了響應式是什麼以後,讓咱們來進入本文的主題,進一步瞭解 Vue3 的響應式 API ~

Vue3 內置的 20 個響應式 API

1. reactive

先看 Proxy

在瞭解 reactive 以前,咱們先來了解一波實現 reactive API 的關鍵類 > ES6 的 Proxy ,它還有一個好基友 Reflect。這裏咱們先看一個簡單的例子:

const targetObj = {
  id: 1,
  name: 'front-refined',
  childObj: {
    hobby: 'coding'
  }
};
const proxyObj = new Proxy(targetObj, {
  get(target, key, receiver) {
    console.log(`get key:${key}`);
    return Reflect.get(...arguments);
  },
  set(target, key, value, receiver) {
    console.log(`set key:${key},value:${value}`);
    return Reflect.set(...arguments);
  }
});
複製代碼

咱們來分析兩件事:

  1. 在瀏覽器打印一下代理以後的對象

[[Handler]]:處理器,目前攔截了 getset
[[Target]]:代理的目標對象
[[IsRevoked]]:代理是否撤銷

第一次接觸 [[IsRevoked]] 的時候,有點好奇它的做用。也好奇的小夥伴看下這段代碼:

// 用 Proxy 的靜態方法 revocable 代理一個對象
const targetObj = { id: 1, name: 'front-refined' };
const { proxy, revoke } = Proxy.revocable(targetObj, {});
revoke();
console.log('proxy-after :>> ', proxy);
proxy.id = 2;
複製代碼

輸出如圖:

報錯:由於代理已經被撤回了,因此不能對 id 進行 set 動做

  1. 對上面的代碼在控制檯打印看看輸出了啥?
proxyObj.name
// get key:name
proxyObj.name="hello~"
// set key:name,value:hello~

proxyObj.childObj.hobby
// get key:childObj
proxyObj.childObj.hobby="play"
// get key:childObj
複製代碼

咱們能夠看到對於 hobby 的 get/set 輸出只到了 childObj 。若是是這樣的話,不就攔截不了 hobby 的 get/set 了,那怎麼進行追蹤,觸發更新?讓咱們帶着疑問繼續往下看。

reactive 源碼(深層對象的代理)

咱們能夠看到無論對 hobby 進行 get 或 set,都會先去 get childObj // get key:childObj,那麼咱們就能夠在 get 訪問器裏作點操做,這裏拿 reactive 相關源碼舉個例子(我知道看源碼複雜,因此我已經精簡了,而且加上了註釋。這段代碼能夠直接 copy 運行哦~):

// 工具方法:判斷是不是一個對象(注:typeof 數組 也等於 'object'
const isObject = val => val !== null && typeof val === 'object';

// 工具方法:值是否改變,改變才觸發更新
const hasChanged = (value, oldValue) =>
  value !== oldValue && (value === value || oldValue === oldValue);

// 工具方法:判斷當前的 key 是不是已經存在的
const hasOwn = (val, key) => hasOwnProperty.call(val, key);

// 閉包:生成一個 get 方法
function createGetter() {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    console.log(`getting key:${key}`);
    // track(target, 'get' /* GET */, key);

    // 深層代理對象的關鍵!!!判斷這個屬性是不是一個對象,是的話繼續代理動做,使對象內部的值可追蹤
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  };
}

// 閉包:生成一個 set 方法
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];
    const hadKey = hasOwn(target, key);
    const result = Reflect.set(target, key, value, receiver);

    // 判斷當前 key 是否已經存在,不存在的話表示爲新增的 key ,後續 Vue 「標記」新的值使它其成爲響應式
    if (!hadKey) {
      console.log(`add key:${key},value:${value}`);
      // trigger(target, 'add' /* ADD */, key, value);
    } else if (hasChanged(value, oldValue)) {
      console.log(`set key:${key},value:${value}`);
      // trigger(target, 'set' /* SET */, key, value, oldValue);
    }
    return result;
  };
}

const get = createGetter();
const set = createSetter();
// 基礎的處理器對象
const mutableHandlers = {
  get,
  set
  // deleteProperty
};
// 暴露出去的方法,reactive
function reactive(target) {
  return createReactiveObject(target, mutableHandlers);
}
// 建立一個響應式對象
function createReactiveObject(target, baseHandlers) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

const proxyObj = reactive({
  id: 1,
  name: 'front-refined',
  childObj: {
    hobby: 'coding'
  }
});

proxyObj.childObj.hobby
// get key:childObj
// get key:hobby
proxyObj.childObj.hobby="play"
// get key:childObj
// set key:hobby,value:play
複製代碼

能夠看見通過 Vue 的「洗禮」以後,咱們就能夠攔截到 hobby 的 get/set 了。

不須要 Vue.set()

在 Vue3 咱們已經不須要用 Vue.set 方法來動態添加一個響應式 property,由於背後的實現機制已經不一樣:
在 Vue2,使用了 Object.defineProperty 只能預先對某些屬性進行攔截,粒度較小。
在 Vue3,使用的 Proxy,攔截的是整個對象。
簡單用代碼解釋如:

// Object.defineProperty
const obj1 = {};
Object.defineProperty(obj1, 'a', {
  get() {
    console.log('get1');
  },
  set() {
    console.log('set1');
  }
});
obj1.b = 2;
複製代碼

上面的代碼無任何輸出!

// Proxy
const obj2 = {};
const proxyObj2 = new Proxy(obj2, {
  get() {
    console.log('get2');
  },
  set() {
    console.log('set2');
  }
});
proxyObj2.b = 2;
// set2
複製代碼

觸發了 set 訪問器。

2. shallowReactive

第一次看見這個 shallow 的字眼,我就聯想到了 React 中經典的淺比較,這個「淺」的概念是一致的,讓咱們來看下:

const shallowReactiveObj = shallowReactive({
  id: 1,
  name: 'front-refiend',
  childObj: { hobby: 'coding' }
});
// 改變 id 是響應式的
shallowReactiveObj.id = 2;
// 改變嵌套對象的屬性是非響應式的,可是自己的值是有被改變的
shallowReactiveObj.childObj.hobby = 'play';
複製代碼

咱們看看在源碼中是怎麼控制的,讓咱們對上面的 reactive 精簡過的源碼加點東西(這裏簡單用 // +++ 註釋來表示新增的代碼塊):

// ...
// +++ 新增了 shallow 入參
// 閉包:生成一個 get 方法
function createGetter(shallow = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    console.log(`get key:${key}`);

    // track(target, 'get' /* GET */, key);

    // +++
    // shallow=true,就直接 return 結果,因此不會深層追蹤
    if (shallow) {
      return res;
    }

    // 深層代理對象的關鍵!!!判斷這個屬性是不是一個對象,是的話繼續代理動做,使對象內部的值可追蹤
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  };
}

// +++
const shallowGet = createGetter(true);
// +++
// 淺處理器對象,合併覆蓋基礎的處理器對象
const shallowReactiveHandlers = Object.assign({}, mutableHandlers, {
  get: shallowGet
});
// +++
// 暴露出去的方法,shallowReactive
function shallowReactive(target) {
  return createReactiveObject(target, shallowReactiveHandlers);
}
// ...
複製代碼

3. readonly

官網:獲取一個對象 (響應式或純對象) 或 ref 並返回原始 proxy 的只讀 proxy。只讀 proxy 是深層的:訪問的任何嵌套 property 也是隻讀的。

舉例:

const proxyObj = reactive({
  childObj: {
    hobby: 'coding'
  }
});
const readonlyObj = readonly(proxyObj);

// 若是被拷貝對象 proxyObj 作了修改,打印 readonlyObj.childObj.hobby 也會看到有變動
proxyObj.childObj.hobby = 'play';

console.log('readonlyObj.childObj.hobby :>> ', readonlyObj.childObj.hobby);
// readonlyObj.childObj.hobby :>> play

// 只讀對象被改變,警告
readonlyObj.childObj.hobby = 'play';
// ⚠️ Set operation on key "hobby" failed: target is readonly.
複製代碼

在這個例子中,readonlyObj 與 proxyObj 共享全部,除了不能被改變。它的全部屬性也都是響應式的,讓咱們再看下源碼,咱們依然是對上面 reactive 精簡過的源碼加點東西:

// +++ 新增了 isReadonly 參數
// 閉包:生成一個 get 方法
function createGetter(shallow = false, isReadonly = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);
    console.log(`get key:${key}`);

    // +++
    // 當前是隻讀的狀況,本身不會被改變,因此就不必進行追蹤變化
    if (!isReadonly) {
      // track(target, "get" /* GET */, key);
    }

    // shallow=true,就直接 return 結果,因此不會深層追蹤
    if (shallow) {
      return res;
    }

    // 深層代理對象的關鍵!!!判斷這個屬性是不是一個對象,是的話繼續代理動做,使對象內部的值可追蹤
    if (isObject(res)) {
      // +++
      // 若是是隻讀,也要同步進行深層代理
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}
// +++
const readonlyGet = createGetter(false, true);
// +++
// 只讀處理器對象
const readonlyHandlers = {
  get: readonlyGet,
  // 只讀,不容許 set ,因此這裏警告
  set(target, key) {
    {
      console.warn(
        `Set operation on key "${String(
          key
        )}" failed: target is readonly.`,
        target
      );
    }
    return true;
  }
};
// +++
// 暴露出去的方法,readonly
function readonly(target) {
  return createReactiveObject(target, readonlyHandlers);
}
複製代碼

如上,新增了一個 isReadonly 參數,用來標記是否進行深層代理。

上面的 readonly 例子就相似是「代理一個代理」,即:proxy(proxy(原始對象)),如圖:

咱們日常接觸最多的子組件接收父組件傳遞的 props。它就是用 readonly 建立的,因此保持了只讀。要修改的話只能經過 emit 提交至父組件,從而保證了 Vue 傳統的單向數據流。

4. shallowReadonly

顧名思義,就是這個代理對象 shallow=true & readonly=true,那這樣會發生什麼呢?

舉個例子:

const shallowReadonlyObj = shallowReadonly({
  id: 1,
  name: 'front-refiend',
  childObj: { hobby: 'coding' }
});

shallowReadonlyObj.id = 2;
// ⚠️ Set operation on key "id" failed: target is readonly. 
// 對象自己的屬性不能被修改

shallowReadonlyObj.childObj.hobby = 'runnnig';
// 嵌套對象的屬性能夠被修改,可是是非響應式的!
複製代碼

咱們看看在源碼中是怎麼控制的,讓咱們繼續對上面的 reactive 精簡過的源碼加點東西:

// ...
// +++
// shallow=true & readonly=true
const shallowReadonlyGet = createGetter(true, true);
// +++
// 淺只讀處理器對象,合併覆蓋 readonlyHandlers 處理器對象
const shallowReadonlyHandlers = Object.assign({}, readonlyHandlers, {
  get: shallowReadonlyGet
});
// +++
// 暴露出去的方法,shallowReadonly
function shallowReadonly(target) {
  return createReactiveObject(target, shallowReadonlyHandlers);
}
// ...
複製代碼

5. ref

我的以爲,ref 方法更加提高咱們去理解 js 中的引用類型。簡單的來說就是把一個簡單類型包裝成一個對象,使它能夠被追蹤(響應式)。

ref 返回的是一個包含 .value 屬性的對象。

例子:

const refNum = ref(1);
refNum.value++;
複製代碼

讓咱們來扒一扒背後的實現原理(精簡了 ref 相關源碼):

// 工具方法:值是否改變,改變才觸發更新
const hasChanged = (value, oldValue) =>
  value !== oldValue && (value === value || oldValue === oldValue);

// 工具方法:判斷是不是一個對象(注:typeof 數組 也等於 'object'
const isObject = val => val !== null && typeof val === 'object';

// 工具方法:判斷傳入的值是不是一個對象,是的話就用 reactive 來代理
const convert = val => (isObject(val) ? reactive(val) : val);

function toRaw(observed) {
  return (observed && toRaw(observed['__v_raw' /* RAW */])) || observed;
}

// ref 實現類
class RefImpl {
  constructor(_rawValue, _shallow = false) {
    this._rawValue = _rawValue;
    this._shallow = _shallow;
    this.__v_isRef = true;
    this._value = _shallow ? _rawValue : convert(_rawValue);
  }
  get value() {
    // track(toRaw(this), 'get' /* GET */, 'value');
    return this._value;
  }
  set value(newVal) {
    if (hasChanged(toRaw(newVal), this._rawValue)) {
      this._rawValue = newVal;
      this._value = this._shallow ? newVal : convert(newVal);
      // trigger(toRaw(this), 'set' /* SET */, 'value', newVal);
    }
  }
}
// 建立一個 ref
function createRef(rawValue, shallow = false) {
  return new RefImpl(rawValue, shallow);
}
// 暴露出去的方法,ref
function ref(value) {
  return createRef(value);
}
// 暴露出去的方法,shallowRef
function shallowRef(value) {
  return createRef(value, true);
}
複製代碼

核心類 RefImpl ,咱們能夠看到在類中使用了經典的 get/set 存取器,來進行追蹤和觸發。
convert 方法讓咱們知道了 ref 不只僅用來包裝一個值類型,也能夠是一個對象/數組,而後把對象/數組再交給 reactive 進行代理。直接看個例子:

const refArr = ref([1, 2, 3]);
const refObj = ref({ id: 1, name: 'front-refined' });

// 操做它們
refArr.value.push(1);
refObj.value.id = 2;
複製代碼

6. unref

展開一個 ref:判斷參數爲 ref ,則返回 .value ,不然返回參數自己。

源碼:

function isRef(r) {
  return Boolean(r && r.__v_isRef === true);
}
function unref(ref) {
  return isRef(ref) ? ref.value : ref;
}
複製代碼

爲了方便開發,Vue 處理了在 template 中用到的 ref 將會被自動展開,也就是不用寫 .value 了,背後的實現,讓咱們一塊兒來看一下:

這裏用「模擬」的方式來闡述,核心邏輯沒有改變~

// 模擬:在 setup 內定義一個 ref
const num = ref(1);
// 模擬:在 setup 返回,提供 template 使用
function setup() {
  return { num };
}
// 模擬:接收了 setup 返回的對象
const setupReturnObj = setup();
// 定義處理器對象,get 訪問器裏的 unref 是關鍵
const shallowUnwrapHandlers = {
  get: (target, key, receiver) =>
    unref(Reflect.get(target, key, receiver)),
  set: (target, key, value, receiver) => {
    const oldValue = target[key];
    if (isRef(oldValue) && !isRef(value)) {
      oldValue.value = value;
      return true;
    } else {
      return Reflect.set(target, key, value, receiver);
    }
  }
};
// 模擬:返回組件實例上下文
const ctx = new Proxy(setupReturnObj, shallowUnwrapHandlers);
// 模擬:template 最終被編譯成 render 函數
/* <template> <input v-model="num" /> <div>num:{{num}}</div> </template> */
function render(ctx) {
  with (ctx) {
    // 模擬:在template中,進行賦值動做 "onUpdate:modelValue": $event => (num = $event)
    // num = 666;
    // 模擬:在template中,進行讀取動做 {{num}}
    console.log('num :>> ', num);
  }
}
render(ctx);

// 模擬:在 setup 內部進行賦值動做
num.value += 1;
// 模擬: num 改變 trigger 視圖渲染effect,更新視圖
render(ctx);
複製代碼

7. shallowRef

ref 的介紹已經包含了 shallowRef 方法的實現:
this._value = _shallow ? _rawValue : convert(_rawValue);
若是傳入的 shallow 值爲 true 那麼直接返回傳入的原始值,也就是說,不會再去深層代理對象了,讓咱們來看兩個場景:

  1. 傳入的是一個對象
const shallowRefObj = shallowRef({
  id: 1,
  name: 'front-refiend',
});
複製代碼

上面的對象加工以後,咱們能夠簡單的理解成:

const shallowRefObj = {
  value: {
    id: 1,
    name: 'front-refiend'
  }
};
複製代碼

既然是 shallow(淺層)那就止於 value ,再也不進行深層代理。 也就是說,對於嵌套對象的屬性不會進行追蹤,可是咱們修改 shallowRefObj 自己的 value 屬性仍是響應式的,如:shallowRefObj.value = 'hello~';

  1. 傳入的是一個簡單類型
const shallowRefNum = shallowRef(1);
複製代碼

當傳入的值是一個簡單類型時候,結合這兩句代碼:
const convert = val => (isObject(val) ? reactive(val) : val);
this._value = _shallow ? _rawValue : convert(_rawValue);
咱們就能夠知道 shallowRef 和 ref 對於入參是一個簡單類型時,其最終效果是一致的。

8. triggerRef

我的以爲這個 API 理解起來較爲抽象,小夥伴們一塊兒仔細琢磨琢磨~

triggerRef 是和 shallowRef 配合使用的,例子:

const shallowRefObj = shallowRef({
  name: 'front-refined'
});
// 這裏不會觸發反作用,由於是這個 ref 是淺層的
shallowRefObj.value.name = 'hello~';

// 手動執行與 shallowRef 關聯的任何反作用,這樣子就能觸發了。
triggerRef(shallowRefObj);
複製代碼

看下背後的實現原理:

在開篇咱們有講到的 effect 這個概念,假設當前正在走 視圖渲染effect

template 綁定的了值,如:

<template> {{shallowRefObj.name}} </template>
複製代碼

當執行 「render」 時,就會讀取到了 shallowRefObj.value.name ,因爲當前的 ref 是淺層的,只能追蹤到 value 的變化,因此在 value 的 get 方法進行 track 如:
track(toRaw(this), "get" /* GET */, 'value');

track 方法源碼精簡:

// targetMap 是一個大集合
// activeEffect 表示當前正在走的 effect ,假設當前是 視圖渲染effect
function track(target, type, key) {
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
  }
}
複製代碼

打印 targetMap

也就是說,若是 shallowRefObj.value 有改變就能夠 trigger 視圖渲染effect 來更新視圖,或着咱們也能夠手動 trigger 它。

可是,咱們目前改變的是 shallowRefObj.value.name = 'hello~';,因此咱們要 「騙」 trigger 方法。手動 trigger,只要咱們的入參對了,就會響應式更新視圖了,看一下 triggerRef 與 trigger 的源碼:

function triggerRef(ref) {
  trigger(toRaw(ref), 'set' /* SET */, 'value', ref.value);
}

// trigger 響應式觸發
function trigger(target, type, key, newValue, oldValue, oldTarget) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    // 沒有被追蹤,直接 return
    return;
  }
  // 拿到了 視圖渲染effect 就能夠進行排隊更新 effect 了
  const run = depsMap.get(key);

  /* 開始執行 effect,這裏作了不少事... */
  run(); 
}
複製代碼

咱們用 target 和 key 拿到了 視圖渲染的effect。至此,就能夠完成一個手動更新了~

9. customRef

自定義的 ref 。這個 API 就更顯式的讓咱們瞭解 track 與 trigger,看個例子:

<template>
  <div>name:{{name}}</div>
  <input v-model="name" />
</template>

// ...
setup() {
  let value = 'front-refined';
  // 參數是一個工廠函數
  const name = customRef((track, trigger) => {
    return {
      get() {
        // 收集依賴它的 effect
        track();
        return value;
      },
      set(newValue) {
        value = newValue;
        // 觸發更新依賴它的全部 effect
        trigger();
      }
    };
  });
  return {
    name
  };
}
複製代碼

讓咱們看下源碼實現:

// 自定義ref 實現類
class CustomRefImpl {
  constructor(factory) {
    this.__v_isRef = true;
    const { get, set } = factory(
      () => track(this, 'get' /* GET */, 'value'),
      () => trigger(this, 'set' /* SET */, 'value')
    );
    this._get = get;
    this._set = set;
  }
  get value() {
    return this._get();
  }
  set value(newVal) {
    this._set(newVal);
  }
}
function customRef(factory) {
  return new CustomRefImpl(factory);
}
複製代碼

結合咱們上面有提過的 ref 源碼相關,咱們能夠看到 customRef 只是把 ref 內部的實現,更顯式的暴露出來,讓咱們更靈活的控制。好比能夠延遲 trigger ,如:

// ...
set(newValue) {
  clearTimeout(timer);
  timer = setTimeout(() => {
    value = newValue;
    // 觸發更新依賴它的全部 effect
    trigger();
  }, 2000);
}
// ...
複製代碼

10. toRef

能夠用來爲響應式對象上的 property 新建立一個 ref ,從而保持對其源 property 的響應式鏈接。舉個例子:

假設咱們傳遞給一個組合式函數一個響應式數據,在組合式函數內部就能夠響應式的修改它:

// 1. 傳遞整個響應式對象
function useHello(state) {
  state.name = 'hello~';
}
// 2. 傳遞一個具體的 ref
function useHello2(name) {
  name.value = 'hello~';
}

export default {
  setup() {
    const state = reactive({
      id: 1,
      name: 'front-refiend'
    });
    // 1. 直接傳遞整個響應式對象
    useHello(state);
    // 2. 傳遞一個新建立的 ref
    useHello2(toRef(state, 'name'));
  }
};
複製代碼

讓咱們看下源碼實現:

// ObjectRef 實現類
class ObjectRefImpl {
  constructor(_object, _key) {
    this._object = _object;
    this._key = _key;
    this.__v_isRef = true;
  }
  get value() {
    return this._object[this._key];
  }
  set value(newVal) {
    this._object[this._key] = newVal;
  }
}
// 暴露出去的方法
function toRef(object, key) {
  return new ObjectRefImpl(object, key);
}
複製代碼

即便 name 屬性不存在,toRef 也會返回一個可用的 ref,如:咱們在上面那個例子指定了一個對象沒有的屬性:

useHello2(toRef(state, 'other'));
複製代碼

這個動做就至關於往對象新增了一個屬性 other,且會響應式。

11. toRefs

toRefs 底層就是 toRef。

將響應式對象轉換爲普通對象,其中結果對象的每一個 property 都是指向原始對象相應 property 的 ref,保持對其源 property 的響應式鏈接。

toRefs 的出現其實也是爲了開發上的便利。讓咱們直接來看看它的幾個使用場景:

  1. 解構 props
export default {
  props: {
    id: Number,
    name: String
  },
  setup(props, ctx) {
    const { id, name } = toRefs(props);
    watch(id, () => {
      console.log('id change');
    });
    
    // 沒有使用 toRefs 的話,須要經過這種方式監聽
    watch(
      () => props.id,
      () => {
        console.log('id change');
      }
    );
  }
};
複製代碼

這樣子咱們就能保證能監聽到 id 的變化(沒有使用 toRefs 的解構是不行的),由於經過 toRefs 方法以後,id 其實就是一個 ref 對象。

  1. setup return 時轉換
<template>
  <div>id:{{id}}</div>
  <div>name:{{name}}</div>
</template>
// ...
setup() {
  const state = reactive({
    id: 1,
    name: 'front-refiend'
  });

  return {
    ...toRefs(state)
  };
}
複製代碼

這樣的寫法咱們就更加方便的在模板上直接寫對應的值,而不須要 {{state.id}}{{state.name}}

讓咱們看下源碼:

function toRefs(object) {
  const ret = {};
  for (const key in object) {
    ret[key] = toRef(object, key);
  }
  return ret;
}
複製代碼

12. compouted

開頭有講過,compouted 是一個 「計算屬性effect」 。它依賴響應式基礎數據,當數據變化時候會觸發它的更新。computed 主要的靚點就是緩存了,能夠緩存性能開銷比較大的計算。它返回一個 ref 對象。

讓咱們一塊兒來看一個 computed 閉環的精簡源碼(主要是瞭解思路,雖然精簡了,但代碼仍是有一丟丟多,不夠看完你確定有收穫。直接 copy 能夠運行哦~):

<body>
  <fieldset>
    <legend>包含get/set方法的 computed</legend>
    <button onclick="handleChangeFirsttName()">changeFirsttName</button>
    <button onclick="handleChangeLastName()">changeLastName</button>
    <button onclick="handleSetFullName()">setFullName</button>
  </fieldset>

  <fieldset>
    <legend>只讀 computed</legend>
    <button onclick="handleAddCount1()">handleAddCount1</button>
    <button onclick="handleSetCount()">handleSetCount</button>
  </fieldset>

  <script> // 大集合,存放依賴相關 const targetMap = new WeakMap(); // 當前正在走的 effect let activeEffect; // 精簡:建立一個 effect const createReactiveEffect = (fn, options) => { const effect = function reactiveEffect() { try { activeEffect = effect; return fn(); } finally { // 當前的 effect 走完以後(相關的依賴收集完畢以後),就退出 activeEffect = undefined; } }; effect.options = options; // 該反作用的依賴集合 effect.deps = []; return effect; }; //#region 精簡:ref 方法 // 工具方法:值是否改變,改變才觸發更新 const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue); // ref 實現類 class RefImpl { constructor(_rawValue) { this._rawValue = _rawValue; this.__v_isRef = true; this._value = _rawValue; } get value() { track(this, 'get', 'value'); return this._value; } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = newVal; trigger(this, 'set', 'value', newVal); } } } // 建立一個 ref function createRef(rawValue) { return new RefImpl(rawValue); } // 暴露出去的方法,ref function ref(value) { return createRef(value); } //#endregion //#region 精簡:track、trigger const track = (target, type, key) => { if (activeEffect === undefined) { return; } let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); // 存儲該反作用相關依賴集合 activeEffect.deps.push(dep); } }; const trigger = (target, type, key, newValue) => { const depsMap = targetMap.get(target); if (!depsMap) { // 沒有被追蹤,直接 return return; } const effects = depsMap.get(key); const run = effect => { if (effect.options.scheduler) { // 調度執行 effect.options.scheduler(); } }; effects.forEach(run); }; //#endregion //#region 精簡:computed 方法 const isFunction = val => typeof val === 'function'; // 暴露出去的方法 function computed(getterOrOptions) { let getter; let setter; if (isFunction(getterOrOptions)) { getter = getterOrOptions; setter = () => { // 提示,當前的 computed 若是是隻讀的,也就是說沒有在調用的時候傳入 set 方法 console.warn('Write operation failed: computed value is readonly'); }; } else { getter = getterOrOptions.get; setter = getterOrOptions.set; } return new ComputedRefImpl(getter, setter); } // computed 核心方法 class ComputedRefImpl { constructor(getter, _setter) { this._setter = _setter; this._dirty = true; this.effect = createReactiveEffect(getter, { scheduler: () => { // 依賴的數據改變了,標記爲髒值,等 get value 時進行計算獲取 if (!this._dirty) { this._dirty = true; } } }); } get value() { // 髒值須要計算 _dirty=true 表明須要計算 if (this._dirty) { console.log('髒值,須要計算...'); this._value = this.effect(); // 標記髒值爲 false,進行緩存值(下次獲取時,不須要計算) this._dirty = false; } return this._value; } set value(newValue) { this._setter(newValue); } } //#endregion //#region 例子 // 1. 建立一個只讀 computed const count1 = ref(0); const count = computed(() => { return count1.value * 10; }); const handleAddCount1 = () => { count1.value++; console.log('count.value :>> ', count.value); }; const handleSetCount = () => { count.value = 1000; }; // 2. 建立一個包含 get/set 方法的 computed // 獲取的 computed 數據 const consoleFullName = () => console.log('fullName.value :>> ', fullName.value); const firsttName = ref('san'); const lastName = ref('zhang'); const fullName = computed({ get: () => firsttName.value + '.' + lastName.value, set: val => { lastName.value += val; } }); // 改變依賴的值觸發 computed 更新 const handleChangeFirsttName = () => { firsttName.value = 'si'; consoleFullName(); }; // 改變依賴的值觸發 computed 更新 const handleChangeLastName = () => { lastName.value = 'li'; consoleFullName(); }; // 觸發 fullName set,若是 computed 爲只讀就警告 const handleSetFullName = () => { fullName.value = ' happy niu year~'; consoleFullName(); }; // 必需要有讀取行爲,纔會進行依賴收集。當依賴改變時候,纔會響應式更新! consoleFullName(); //#endregion </script>
</body>
複製代碼

computed 的閉環流程是這樣子的:
computed 建立的 ref 對象初次被調用 get(讀 computed 的 value),會進行依賴收集,當依賴改變時,調度執行觸發 dirty = true,標記髒值,須要計算。下一次再去調用 computed 的 get 時候,就須要從新計算獲取新值,如此反覆。

13. watch

關於 watch ,這裏直接先上一段稍長的源碼例子(代碼挺長,可是都是精簡過的,並且有註釋分塊。小夥伴們耐心看,copy 能夠直接運行哦~)

<body>
  <button onclick="handleChangeCount()">點我觸發watch</button>
  <button onclick="handleChangeCount2()">點我觸發watchEffect</button>
  <script> // 大集合,存放依賴相關 const targetMap = new WeakMap(); // 當前正在走的 effect let activeEffect; // 精簡:建立一個 effect const createReactiveEffect = (fn, options) => { const effect = function reactiveEffect() { try { activeEffect = effect; return fn(); } finally { // 當前的 effect 走完以後(相關的依賴收集完畢以後),就退出 activeEffect = undefined; } }; effect.options = options; // 該反作用的依賴集合 effect.deps = []; return effect; }; //#region 精簡:ref 方法 // 工具方法:判斷是不是一個 ref 對象 const isRef = r => { return Boolean(r && r.__v_isRef === true); }; // 工具方法:值是否改變,改變才觸發更新 const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue); // 工具方法:判斷是不是一個方法 const isFunction = val => typeof val === 'function'; // ref 實現類 class RefImpl { constructor(_rawValue) { this._rawValue = _rawValue; this.__v_isRef = true; this._value = _rawValue; } get value() { track(this, 'get', 'value'); return this._value; } set value(newVal) { if (hasChanged(newVal, this._rawValue)) { this._rawValue = newVal; this._value = newVal; trigger(this, 'set', 'value', newVal); } } } // 建立一個 ref function createRef(rawValue) { return new RefImpl(rawValue); } // 暴露出去的方法,ref function ref(value) { return createRef(value); } //#endregion //#region 精簡:track、trigger const track = (target, type, key) => { if (activeEffect === undefined) { return; } let depsMap = targetMap.get(target); if (!depsMap) { targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); if (!dep) { depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { dep.add(activeEffect); // 存儲該反作用相關依賴集合 activeEffect.deps.push(dep); } }; const trigger = (target, type, key, newValue) => { const depsMap = targetMap.get(target); if (!depsMap) { // 沒有被追蹤,直接 return return; } const effects = depsMap.get(key); const run = effect => { if (effect.options.scheduler) { // 調度執行 effect.options.scheduler(); } }; effects.forEach(run); }; //#endregion //#region 中止監聽相關 // 中止偵聽,若是有 onStop 方法一併調用,onStop 也就是 onInvalidate 回調方法 function stop(effect) { cleanup(effect); if (effect.options.onStop) { effect.options.onStop(); } } // 清空改 effect 收集的依賴相關,這樣子改變了就再也不繼續觸發了,也就是「中止偵聽」 function cleanup(effect) { const { deps } = effect; if (deps.length) { for (let i = 0; i < deps.length; i++) { deps[i].delete(effect); } deps.length = 0; } } //#endregion //#region 暴露出去的 watchEffect 方法 function watchEffect(effect, options) { return doWatch(effect, null, options); } //#endregion //#region 暴露出去的 watch 方法 function watch(source, cb, options) { return doWatch(source, cb, options); } function doWatch(source, cb, { immediate, deep } = {}) { let getter; // 判斷是否 ref 對象 if (isRef(source)) { getter = () => source.value; } // 判斷是一個 reactive 對象,默認遞歸追蹤 deep=true else if (/*isReactive(source)*/ 0) { // 省略... // getter = () => source; // deep = true; } // 判斷是一個數組,也就是 Vue3 新的特性,watch 能夠以數組的方式偵聽 else if (/*isArray(source)*/ 0) { // 省略... } // 判斷是不是一個方法,這樣子的入參 else if (isFunction(source)) { debugger; // 這裏是相似這樣子的入參,() => proxyObj.id if (cb) { // 省略... } else { // cb 爲 null,表示當前爲 watchEffect getter = () => { if (cleanup) { cleanup(); } return source(onInvalidate); }; } } // 判斷是否 deep 就會遞歸追蹤 if (/*cb && deep*/ 0) { // const baseGetter = getter; // getter = () => traverse(baseGetter()); } // 清理 effect let cleanup; const onInvalidate = fn => { cleanup = runner.options.onStop = () => { fn(); }; }; let oldValue = undefined; const job = () => { if (cb) { // 獲取改變改變後的新值 const newValue = runner(); if (hasChanged(newValue, oldValue)) { if (cleanup) { cleanup(); } // 觸發回調 cb(newValue, oldValue, onInvalidate); // 把新值賦值給舊值 oldValue = newValue; } } else { // watchEffect runner(); } }; // 調度 let scheduler; // default: 'pre' scheduler = () => { job(); }; // 建立一個 effect,調用 runner 其實就是在進行依賴收集 const runner = createReactiveEffect(getter, { scheduler }); // 初始化 run if (cb) { if (immediate) { job(); } else { oldValue = runner(); } } else { // watchEffect 默認當即執行 runner(); } // 返回一個方法,調用即中止偵聽 return () => { stop(runner); }; } //#endregion //#region 例子 // 1. watch 例子 const count = ref(0); const myStop = watch( count, (val, oldVal, onInvalidate) => { onInvalidate(() => { console.log('watch-clear...'); }); console.log('watch-val :>> ', val); console.log('watch-oldVal :>> ', oldVal); }, { immediate: true } ); // 改變依賴的值觸發 觸發偵聽器回調 const handleChangeCount = () => { count.value++; }; // 中止偵聽 // myStop(); // 2. watchEffect 例子 const count2 = ref(0); watchEffect(() => { console.log('watchEffect-count2.value :>> ', count2.value); }); // 改變依賴的值觸發 觸發偵聽器回調 const handleChangeCount2 = () => { count2.value++; }; //#endregion </script>
</body>
複製代碼

以上的代碼簡單的實現了 watch 監聽 ref 對象的例子,那麼咱們該如何去正確的使用 watch 呢?讓咱們一塊兒結合源碼一塊兒看兩點:

  • 關於偵聽源的寫法,官網有描述,能夠是返回值的 getter 函數,也能夠直接是 ref,也就是:
const state = reactive({ id: 1 });
// 使用
() => state.id
// 或
const count = ref(0);
// 使用 count
count
// 看完源碼,咱們也能夠這樣子寫~
() => count.value
複製代碼

結合源碼,咱們發現也能夠直接偵聽一個 reactive 對象,並且默認會進進行深度監聽(deep=true),會對對象進行遞歸遍歷追蹤。可是偵聽一個數組的話,只有當數組被替換時纔會觸發回調。若是你須要在數組改變時觸發回調,必須指定 deep 選項。當沒有指定 deep = true

const arr = ref([1, 2, 3]);
// 只有這種方式纔會生效
arr.value = [4, 5, 6];
// 其餘的沒法觸發回調
arr.value[0] = 111;
arr.value.push(4);
複製代碼

我的建議儘可能避免深度偵聽,由於這可能會影響性能,大部分場景咱們均可以使用偵聽一個 getter 的方式,好比須要偵聽數組的變化 () => arr.value.length。若是你想要同時監聽一個對象多個值的變化,Vue3 提供了數組的操做:

watch(
  [() => state.id, () => state.name],
  ([id, name], [oldId, oldName]) => {
    /* ... */
  }
);
複製代碼
  • watch 返回值也就是一箇中止偵聽的方法,它與 onInvalidate 本質是不一樣的,當咱們調用了中止偵聽,底層是作了移除當前清空該 effect 收集的依賴集合,這樣子依賴數據改變了就再也不繼續觸發了,也就是「中止偵聽」。而 onInvalidate,我的認爲,它就是提供了一個在回調以前的操做,具體的例子,能夠參考以前寫過的一篇文章

Vue3丨從 5 個維度來說 Vue3 變化 詳情看 watchEffect vs watch 內容。

14. watchEffect

和 watch 共享底層代碼,在 watch 分析中咱們已經有體現了,小夥伴們能夠往上再看看,這裏再也不贅述~


看了那麼多有些許複雜的源碼以後,讓咱們來輕鬆一下,來看下 Vue3 一些響應式 API 的小工具。小夥伴應該都有看到一些源碼中帶有 `__v_` 前綴的屬性,其實這些屬性是用來作一些判斷的標識,讓咱們一塊兒來看看:

15. isReadonly

檢查對象是不是由 readonly 建立的只讀 proxy。

function isReadonly(value) {
    return !!(value && value["__v_isReadonly" /* IS_READONLY */]);
}

// readonly
const originalObj = reactive({ id: 1 });
const copyObj = readonly(originalObj);
isReadonly(copyObj); // true

// 只讀 computed 
const firsttName = ref('san');
const lastName = ref('zhang');
const fullName = computed(
  () => firsttName.value + ' ' + lastName.value
);
isReadonly(fullName); // true
複製代碼

其實在建立一個 get 訪問器的時候,利用閉包就已經記錄了,而後經過對應的 key 去獲取,如:

function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    // ...
    if (key === '__v_isReadonly') {
      return isReadonly;
    }
    // ...
  };
}
複製代碼

16. isReactive

檢查對象是不是 reactive 建立的響應式 proxy。

function isReactive(value) {
    if (isReadonly(value)) {
        return isReactive(value["__v_raw" /* RAW */]);
    }
    return !!(value && value["__v_isReactive" /* IS_REACTIVE */]);
}
複製代碼

createGetter 方法判斷相關:

// ...
if (key === '__v_isReactive' /* IS_REACTIVE */) {
  return !isReadonly;
} else if (key === '__v_isReadonly' /* IS_READONLY */) {
  return isReadonly;
}
// ... 
複製代碼

17. isProxy

檢查對象是不是由 reactive 或 readonly 建立的 proxy。

function isProxy(value) {
    return isReactive(value) || isReadonly(value);
}
複製代碼

18. toRaw

toRaw 能夠用來打印原始對象,有時候咱們在調試查看控制檯的時候,就比較方便。

function toRaw(observed) {
    return ((observed && toRaw(observed["__v_raw" /* RAW */])) || observed);
}
複製代碼

toRaw 對於轉換 ref 對象,仍然保留包裝過的對象,例子:

const obj = reactive({ id: 1, name: 'front-refiend' });
console.log(toRaw(obj));
// {id: 1, name: "front-refiend"}
const count = ref(0);
console.log(toRaw(count));
// {__v_isRef: true, _rawValue: 0, _shallow: false, _value: 0, value: 0}
複製代碼

createGetter 方法判斷相關:

// ...
if (
  key === '__v_raw' /* RAW */ &&
  receiver === reactiveMap.get(target)
) {
  return target;
}
// ...
複製代碼

咱們能夠在 createGetter 時就會把對象用 {key:原始對象,value:proxy 代理對象} 這樣子的形式存放於 reactiveMap ,而後根據鍵來取值。

19. markRaw

標記一個對象,使其永遠不會轉換爲 proxy。返回對象自己。

const def = (obj, key, value) => {
    Object.defineProperty(obj, key, {
        configurable: true,
        enumerable: false,
        value
    });
};

function markRaw(value) {
    // 標記跳過對該對象的代理
    def(value, "__v_skip" /* SKIP */, true);
    return value;
}
複製代碼

createReactiveObject 方法相關:

function createReactiveObject(target) {
  //...
  // 判斷對象中是否含有 __v_skip 屬性是的話,直接返回對象自己
  if (target['__v_skip']) {
    return target;
  }
  const proxy = new Proxy(target);
  // ...
  return proxy;
}
複製代碼

20. isRef

判斷是不是 ref 對象。__v_isRef 標識就是咱們在建立 ref 的時候在 RefImpl實現類裏賦值的 this.__v_isRef = true;

function isRef(r) {
    return Boolean(r && r.__v_isRef === true);
}
複製代碼

總結

以上的 20 個API,在咱們項目實戰中,有些也許幾乎沒有用到。由於有部分API,是 Vue3 整個框架設計有使用到的。對於咱們的業務場景來講,目前使用頻次較高的應該是 reactiverefcomputedwatchtoRefs...
理解全部響應式 API 對於咱們在編碼會更加有自信,不會有那麼多的疑惑。也幫助咱們更加理解框架的底層,如:proxy 怎麼用的?Vue3 怎麼追蹤一個簡單類型的?怎樣去編碼才能讓咱們系統更優。這纔是本文分析這幾個 API 的初衷。
怎麼樣,你瞭解這 20 個響應式 API 了嗎?

😁 前端精,求關注~

2021年,公衆號關注「前端精」(front-refined),咱們一塊兒學 Vue3,用 Vue3,深刻 Vue3 。最後,祝小夥伴們新年快樂,開開心心過春節~

相關文章
相關標籤/搜索