在 Vue2 中,我的以爲對於數據的操做比較 「黑盒」 。
而 Vue3 把響應式系統更顯式地暴露出來,使得咱們對數據的操做有了更多的靈活性。
因此,對於 Vue3 的幾個響應式的 API ,咱們須要更加的理解掌握,才能在實戰中運用自如。javascript
var val1 = 2
var val2 = 3
var sum = val1 + val2
複製代碼
咱們但願 val1 或 val2 的值改變的時候,sum 也會響應的作出正確的改變。html
我依賴了你,你變了。你就通知我讓我知道,好讓我作點 「操做」 。前端
讓咱們記住三個關鍵的英語單詞,它們的順序也是完成一個響應式的順序。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 ~
在瞭解 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);
}
});
複製代碼
咱們來分析兩件事:
[[Handler]]
:處理器,目前攔截了 get
、set
[[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 動做
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
了,那怎麼進行追蹤,觸發更新?讓咱們帶着疑問繼續往下看。
咱們能夠看到無論對 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 訪問器。
第一次看見這個 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);
}
// ...
複製代碼
官網:獲取一個對象 (響應式或純對象) 或 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 傳統的單向數據流。
顧名思義,就是這個代理對象 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);
}
// ...
複製代碼
我的以爲,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;
複製代碼
展開一個 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);
複製代碼
ref 的介紹已經包含了 shallowRef 方法的實現:this._value = _shallow ? _rawValue : convert(_rawValue);
若是傳入的 shallow 值爲 true 那麼直接返回傳入的原始值,也就是說,不會再去深層代理對象了,讓咱們來看兩個場景:
const shallowRefObj = shallowRef({
id: 1,
name: 'front-refiend',
});
複製代碼
上面的對象加工以後,咱們能夠簡單的理解成:
const shallowRefObj = {
value: {
id: 1,
name: 'front-refiend'
}
};
複製代碼
既然是 shallow(淺層)那就止於 value ,再也不進行深層代理。 也就是說,對於嵌套對象的屬性不會進行追蹤,可是咱們修改 shallowRefObj 自己的 value 屬性仍是響應式的,如:shallowRefObj.value = 'hello~';
const shallowRefNum = shallowRef(1);
複製代碼
當傳入的值是一個簡單類型時候,結合這兩句代碼:const convert = val => (isObject(val) ? reactive(val) : val);
,this._value = _shallow ? _rawValue : convert(_rawValue);
咱們就能夠知道 shallowRef 和 ref 對於入參是一個簡單類型時,其最終效果是一致的。
我的以爲這個 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。至此,就能夠完成一個手動更新了~
自定義的 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);
}
// ...
複製代碼
能夠用來爲響應式對象上的 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,且會響應式。
toRefs 底層就是 toRef。
將響應式對象轉換爲普通對象,其中結果對象的每一個 property 都是指向原始對象相應 property 的 ref,保持對其源 property 的響應式鏈接。
toRefs 的出現其實也是爲了開發上的便利。讓咱們直接來看看它的幾個使用場景:
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 對象。
<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;
}
複製代碼
開頭有講過,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 時候,就須要從新計算獲取新值,如此反覆。
關於 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 呢?讓咱們一塊兒結合源碼一塊兒看兩點:
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]) => {
/* ... */
}
);
複製代碼
onInvalidate
,我的認爲,它就是提供了一個在回調以前的操做,具體的例子,能夠參考以前寫過的一篇文章Vue3丨從 5 個維度來說 Vue3 變化 詳情看 watchEffect vs watch 內容。
和 watch 共享底層代碼,在 watch 分析中咱們已經有體現了,小夥伴們能夠往上再看看,這裏再也不贅述~
檢查對象是不是由 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;
}
// ...
};
}
複製代碼
檢查對象是不是 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;
}
// ...
複製代碼
檢查對象是不是由 reactive 或 readonly 建立的 proxy。
function isProxy(value) {
return isReactive(value) || isReadonly(value);
}
複製代碼
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 ,而後根據鍵來取值。
標記一個對象,使其永遠不會轉換爲 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;
}
複製代碼
判斷是不是 ref 對象。__v_isRef
標識就是咱們在建立 ref 的時候在 RefImpl實現類裏賦值的 this.__v_isRef = true;
function isRef(r) {
return Boolean(r && r.__v_isRef === true);
}
複製代碼
以上的 20 個API,在咱們項目實戰中,有些也許幾乎沒有用到。由於有部分API,是 Vue3 整個框架設計有使用到的。對於咱們的業務場景來講,目前使用頻次較高的應該是 reactive
,ref
,computed
,watch
,toRefs
...
理解全部響應式 API 對於咱們在編碼會更加有自信,不會有那麼多的疑惑。也幫助咱們更加理解框架的底層,如:proxy 怎麼用的?Vue3 怎麼追蹤一個簡單類型的?怎樣去編碼才能讓咱們系統更優。這纔是本文分析這幾個 API 的初衷。
怎麼樣,你瞭解這 20 個響應式 API 了嗎?
2021年,公衆號關注「前端精」(front-refined),咱們一塊兒學 Vue3,用 Vue3,深刻 Vue3 。最後,祝小夥伴們新年快樂,開開心心過春節~