Vue響應式系統的核心依然是對數據進行劫持,只不過Vue3採樣點是Proxy類,而Vue2採用的是Object.defineProperty()。Vue3之因此採用Proxy類主要有兩個緣由:vue
// 展現使用Object.defineProperty()存在的缺點 const obj = {name: "vue", arr: [1, 2, 3]}; Object.keys(obj).forEach((key) => { let value = obj[key]; Object.defineProperty(obj, key, { get() { console.log(`get key is ${key}`); return value; }, set(newVal) { console.log(`set key is ${key}, newVal is ${newVal}`); value = newVal; } }); }); // 此時給對象新增一個age屬性 obj.age = 18; // 由於對象劫持的時候,沒有對age進行劫持,因此新增屬性沒法劫持 delete obj.name; // 刪除對象上已經進行劫持的name屬性,發現刪除屬性操做也沒法劫持 obj.arr.push(4); // 沒法劫持數組的push等方法 obj.arr[3] = 4; // 沒法劫持數組的索引操做,由於沒有對數組的每一個索引進行劫持,而且因爲性能緣由,Vue2並無對數組的每一個索引進行劫持
// 使用Proxy實現完美劫持 const obj = {name: "vue", arr: [1, 2, 3]}; function proxyData(value) { const proxy = new Proxy(value, { get(target, key) { console.log(`get key is ${key}`); const val = target[key]; if (typeof val === "object") { return proxyData(val); } return val; }, set(target, key, value) { console.log(`set key is ${key}, value is ${value}`); return target[key] = value; }, deleteProperty(target, key) { console.log(`delete key is ${key}`); } }); return proxy; } const proxy = proxyData(obj); proxy.age = 18; // 可對新增屬性進行劫持 delete proxy.name; // 可對刪除屬性進行劫持 proxy.arr.push(4); // 可對數組的push等方法進行劫持 proxy.arr[3] = 4; // 可對象數組的索引操做進行劫持
Vue3的響應式系統被放到了一個單獨的@vue/reactivity模塊中,其提供了reactive、effect、computed等方法,其中reactive用於定義響應式的數據,effect至關因而Vue2中的watcher,computed用於定義計算屬性。咱們先來看一下這幾個函數的簡單示例,如:react
import {reactive, effect, computed} from "@vue/reactivity"; const state = reactive({ name: "lihb", age: 18, arr: [1, 2, 3] }); console.log(state); // 這裏返回的是Proxy代理後的對象 effect(() => { console.log("effect run"); console.log(state.name); // 每當name數據變化將會致使effect從新執行 }); state.name = "vue"; // 數據發生變化後會觸發使用了該數據的effect從新執行 const info = computed(() => { // 建立一個計算屬性,依賴name和age return `name: ${state.name}, age: ${state.age}`; }); effect(() => { // name和age變化會致使計算屬性的value發生變化,從而致使當前effect從新執行 console.log(`info is ${info.value}`); });
reactive() 方法本質是傳入一個要定義成響應式的target目標對象,而後經過Proxy類去代理這個target對象,最後返回代理以後的對象,如:數組
export function reactive(target) { return new Proxy(target, { get() { }, set() { } }); }
若是咱們代理的僅僅是普通對象或者數組,那麼咱們能夠直接採用上面的形式,可是咱們還須要代理Set、Map、WeakMap、WeakSet等集合類。因此爲了程序的擴展性,咱們須要根據target的類型動態的返回Proxy類的handler。咱們能夠改寫成以下形式:數據結構
// shared/index.js export const isObject = (val) => val !== null && typeof val === 'object';
import {isObject} from "./shared"; import {mutableHandlers, mutableCollectionHandlers} from "./handlers"; const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]); export function reactive(target) { // 給函數傳入不一樣的handlers而後經過target類型進判斷 return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers); } function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) { if (!isObject(target)) { // 若是傳入的target不是對象,那麼直接返回該對象便可 return target; } // 根據傳入的target的類型判斷該使用哪一種handler,若是是Set或Map則採用collectionHandlers,若是是普通對象或數組則採用baseHandlers const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers); return observed; }
接下來咱們就須要實現Proxy的handlers,對於普通對象和數組,咱們須要使用baseHandlers即mutableHandlers,Proxy的handler能夠代理不少方法,好比get、set、deleteProperty、has、ownKeys,若是將這些方法直接都寫在handlers上,那麼handlers就會變得很是多代碼,因此能夠將這些方法分開,以下:函數
// handlers.js const get = createGetter(); const set = createSetter(); function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等價於target[key] console.log(`攔截到了get取值操做`, target, key); return res; } } function createSetter(shallow = false) { return function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value console.log(`攔截到了set設置值操做`, target, key, value); return result; // set方法必須返回一個值 } } export const mutableHandlers = { get, set, // deleteProperty, // has, // ownKeys } export const mutableCollectionHandlers = { }
Proxy的handlers對象中的get和set方法均可以拿到被代理的對象target、獲取或修改了對象的哪一個key,設置了新的值value,以及被代理後的對象receiver,目前咱們攔截到用戶的get操做後僅僅是從target中取出對應的值並返回回去,攔截到用戶的set操做後僅僅是修改了target中對應key的值並返回回去。性能
此時會存在一個問題,若是咱們執行state.arr.push(4)這樣的一個操做,會發現僅僅觸發了arr的取值操做,並沒有收到arr新增了一個值的通知。由於Proxy代理只是淺層的代理,只代理了一層,因此咱們拿到的arr是一個普通數組,此時對普通數組進行操做是不會收到通知的。正是因爲Proxy是淺層代理,因此避免了一上來就遞歸,咱們須要修改get,在取到的值是對象的時候再去代理這個對象,如:ui
+ import { isObject } from "./shared"; + import { reactive } from "./reactive"; function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等價於target[key] console.log(`攔截到了get取值操做`, target, key); + if (isObject(res)) { // 若是取到的值是一個對象,則代理這個值 + return reactive(res); + } return res; } }
此時咱們再次執行state.arr.push(4),能夠看到輸出結果以下:prototype
攔截到了get取值操做 {name: "lihb", age: 18, arr: Array(3)} arr 攔截到了get取值操做 (3) [1, 2, 3] push 攔截到了get取值操做 (3) [1, 2, 3] length 攔截到了set設置值操做 (4) [1, 2, 3, 4] 3 4 攔截到了set設置值操做 (4) [1, 2, 3, 4] length 4
同時也觸發了length的修改,其實咱們將4 push進入數組後,數組的length會自動修改,也就是說不須要再去設置一遍length的值了,一樣的咱們執行state.arr[0] = 1也會觸發set操做,設置的是一樣的值也會觸發set操做,因此咱們須要判斷一下設置的新值和舊值是否相同,不一樣才須要觸發set操做。代理
// shared/index.js + export const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key); + export const hasChanged = (newValue, oldValue) => newValue !== oldValue;
import { isObject, hasOwn, hasChanged } from "./shared"; function createSetter(shallow = false) { return function set(target, key, value, receiver) { const hadKey = hasOwn(target, key); const oldValue = target[key]; // 修改前獲取到舊的值 const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value if (!hadKey) { // 若是當前target對象中沒有該key,則表示是新增屬性 console.log(`用戶新增了一個屬性,key is ${key}, value is ${value}`); } else if (hasChanged(value, oldValue)) { // 判斷一下新設置的值和以前的值是否相同,不一樣則屬於更新操做 console.log(`用戶修改了一個屬性,key is ${key}, value is ${value}`); } return result; } }
此時再次執行state.arr.push(4)就不會觸發length的更新了,執行state.arr[0] = 1也不會觸發索引爲0的值更新了。code
通過前面reactive()方法的實現,咱們已經可以拿到一個響應式的數據對象了,咱們進行get和set操做都可以被攔截。接下來就是實現effect()方法,當咱們修改數據的時候,可以觸發傳入effect的回調函數執行。
effect()方法的回調函數要想在數據發生變化後可以執行,必須返回一個響應式的effect()函數,因此effect()內部會返回一個響應式的effect。
所謂響應式的effect,就是該effect在執行的時候會在取值以前將本身放入到effectStack收到棧頂,同時將本身標記爲activeEffect,以便進行依賴收集與reactive進行關聯。
export function effect(fn, options = {}) { const effect = createReactiveEffect(fn, options); // 返回一個響應式的effect函數 if (!options.lazy) { // 若是不是計算屬性的effect,那麼會當即執行該effect effect(); } return effect; } let uid = 0; let activeEffect; // 存放當前執行的effect const effectStack = []; // 若是存在多個effect,則依次放入棧中 function createReactiveEffect(fn, options) { /** * 所謂響應式的effect,就是該effect在執行的時候會將本身放入到effectStack收到棧頂, * 同時將本身標記爲activeEffect,以便進行依賴收集與reactive進行關聯 * */ const effect = function reactiveEffect() { if (!effectStack.includes(effect)) { // 防止不停的更改屬性致使死循環 try { // 在取值以前將當前effect放到棧頂並標記爲activeEffect effectStack.push(effect); // 將本身放到effectStack的棧頂 activeEffect = effect; // 同時將本身標記爲activeEffect return fn(); // 執行effect的回調就是一個取值的過程 } finally { effectStack.pop(); // 從effectStack棧頂將本身移除 activeEffect = effectStack[effectStack.length - 1]; // 將effectStack的棧頂元素標記爲activeEffect } } } effect.options = options; effect.id = uid++; effect.deps = []; // 依賴了哪些屬性,哪些屬性變化了須要執行當前effect return effect; }
這裏的取值操做就是傳入effect(fn)函數的fn的執行,fn中會使用到響應式數據。
此時數據發生變化還沒法通知effect的回調函數執行,由於reactive和effect還未關聯起來,也就是說尚未進行依賴收集,因此接下來須要進行依賴收集。
① 何時收集依賴?
咱們須要在取值的時候開始收集依賴,因此須要在取值以前將依賴的effect放到棧頂並標識爲activeEffect,而前面響應式effect執行的時候已經實現,而執行effect回調取值的時候會在Proxy的handlers的get中進行取值,因此咱們須要在這裏進行依賴收集。
+ import { track, trigger } from "./effect"; function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等價於target[key] console.log(`攔截到了get取值操做`, target, key); + track(target, "get", key); // 取值的時候開始收集依賴 if (isObject(res)) { return reactive(res); } return res; } }
一樣的,須要在Proxy類的handlers的set中觸發依賴的執行。
function createSetter(shallow = false) { return function set(target, key, value, receiver) { const hadKey = hasOwn(target, key); const oldValue = target[key]; // 修改前獲取到舊的值 const result = Reflect.set(target, key, value, receiver); // 等價於target[key] = value if (!hadKey) { // 若是當前target對象中沒有該key,則表示是新增屬性 console.log(`用戶新增了一個屬性,key is ${key}, value is ${value}`); + trigger(target, "add", key, value); // 新增了一個屬性,觸發依賴的effect執行 } else if (hasChanged(value, oldValue)) { // 判斷一下新設置的值和以前的值是否相同,不一樣則屬於更新操做 console.log(`用戶修改了一個屬性,key is ${key}, value is ${value}`); + trigger(target, "set", key, value); // 修改了屬性值,觸發依賴的effect執行 } return result; } }
② 如何收集依賴,如何保存依賴?
首先依賴是一個一個的effect函數,咱們能夠經過Set集合進行存儲,而這個Set集合確定是要和對象的某個key進行對應,即哪些effect依賴了對象中某個key對應的值,這個對應關係能夠經過一個Map對象進行保存,即:
// depMap { someKey: [effect1, effect2,..., effectn] // 用集合存儲依賴的effect,並放入Map對象中與對象的key相對應 }
若是隻有一個響應式對象,那麼咱們直接用一個全局的Map對象根據不一樣的key進行保存便可,即用上面的Map結構就能夠了。
可是咱們的響應式對象是能夠建立多個的,而且每一個響應式對象的key也可能相同,因此僅僅經過一個Map結構以key的方式保存是沒法實現的。
既然響應式對象有多個,那麼就能夠以整個響應式對象做爲key進行區分,而可以用一個對象做爲key的數據結構就是WeakMap,因此咱們能夠用一個全局的WeakMap結構進行存儲,以下:
// 全局的WeakMap { targetObj1: { someKey: [effect1, effect2,..., effectn] }, targetObj2: { someKey: [effect1, effect2,..., effectn] } ... }
當咱們取值的時候,首先經過該target對象從全局的WeakMap對象中取出對應的depsMap對象,而後根據修改的key獲取到對應的dep依賴集合對象,而後將當前effect放入到dep依賴集合中,完成依賴的收集。
// 用一個全局的WeakMap結構以target做爲key保存該target對象下的key對應的依賴 const targetMap = new WeakMap(); /** * 取值的時候開始收集依賴,即收集effect */ export function track(target, type, key) { if (activeEffect == undefined) { // 收集依賴的時候必需要存在activeEffect return; } let depsMap = targetMap.get(target); // 根據target對象取出當前target對應的depsMap結構 if (!depsMap) { // 第一次收集依賴可能不存在 targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); // 根據key取出對應的用於存儲依賴的Set集合 if (!dep) { // 第一次可能不存在 depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { // 若是依賴集合中不存在activeEffect dep.add(activeEffect); // 將當前effect放到依賴集合中 // 一個effect可能使用到了多個key,因此會有多個dep依賴集合 activeEffect.deps.push(dep); // 讓當前effect也保存一份dep依賴集合 } }
觸發依賴更新,當修改值的時候,也是經過target對象從全局的WeakMap對象中取出對應的depMap對象,而後根據修改的key取出對應的dep依賴集合,並遍歷該集合中的全部effect,並執行effect。
每次effect執行,都會從新將當前effect放到棧頂,而後執行effect回調再次取值的時候,再一次執行track收集依賴,不過第二次track的時候,對應的依賴集合中已經存在當前effect了,因此不會再次將當前effect添加進去了。
/** * 數據發生變化的時候,觸發依賴的effect執行 */ export function trigger(target, type, key, value) { const depsMap = targetMap.get(target); // 獲取當前target對應的Map if (!depsMap) { // 若是該對象沒有收集依賴 console.log("該對象還未收集依賴"); // 好比修改值的時候,沒有調用過effect return; } const effects = new Set(); // 存儲依賴的effect const add = (effectsToAdd) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { effects.add(effect); }); } }; const run = (effect) => { effect(); // 當即執行effect } /** * 對於effect中使用到的數據,那確定是響應式對象中已經存在的key,當數據變化後確定能經過該key拿到對應的依賴, * 對於新增的key,咱們也不須要通知effect執行。 * 可是對於數組而言,若是給數組新增了一項,咱們是須要通知的,若是咱們仍然以key的方式去獲取依賴那確定是沒法獲取到的, * 由於也是屬於新增的一個索引,以前沒有對其收集依賴,可是咱們使用數組的時候會使用JSON.stringify(arr),此時會取length屬性, * 索引會收集length的依賴,數組新增元素後,其length會發生變化,咱們能夠經過length屬性去獲取依賴 */ if (key !== null) { add(depsMap.get(key)); // 對象新增一個屬性,因爲沒有依賴故不會執行 } if (type === "add") { // 處理數組元素的新增 add(depsMap.get(Array.isArray(target)? "length": "")); } // 遍歷effects並執行 effects.forEach(run); }
此時已經完成了effect和active的關聯了,當數據發生變化的時候,就會遍歷以前收集的依賴,從而從新執行effect,effect的執行必然會致使effect的回調函數執行。
計算屬性本質也是一個effect,也就是說,計算屬性內部會建立一個effect對象,只不過這個effect不是當即執行,而是等到取值的時候再執行,從以前computed的用法中,能夠看到,computed()函數返回一個對象,而且這個對象中有一個value屬性,能夠進行get和set操做。
import {isFunction} from './shared/index'; import { effect, track, trigger } from './effect'; export function computed(getterOrOptions) { let getter; let setter; if (isFunction(getterOrOptions)) { getter = getterOrOptions; setter = () => {}; } else { getter = getterOrOptions.get; setter = getterOrOptions.set; } let dirty = true; // 默認是髒的數據 let computed; // 計算屬性本質也是一個effect,其回調函數就是計算屬性的getter let runner = effect(getter, { lazy: true, // 默認是非當即執行,等到取值的時候再執行 computed: true, // 標識這個effect是計算屬性的effect scheduler: () => { // 數據發生變化的時候不是直接執行當前effect,而是執行這個scheduler弄髒數據 if (!dirty) { // 若是數據是乾淨的 dirty = true; // 弄髒數據 trigger(computed, "set", "value"); // 數據變化後,觸發value依賴 } } }); let value; computed = { get value() { if (dirty) { value = runner(); // 等到取值的時候再執行計算屬性內部建立的effect dirty = false; // 取完值後數據就不是髒的了 track(computed, "get", "value"); // 對計算屬性對象收集value屬性 } return value; }, set value(newVal) { setter(newVal); } } return computed; }
因爲計算屬性的effect比較特殊,不是當即執行,因此不能像以前同樣,數據發生變化後,都遍歷並當即執行effect,須要將計算屬性的effect和普通的effect分開處理,若是是計算屬性的effect,則執行其scheduler()方法將數據弄髒便可。僅僅修改run()方法便可,如:
/** * 數據發生變化的時候,觸發依賴的effect執行 */ export function trigger(target, type, key, value) { const depsMap = targetMap.get(target); // 獲取當前target對應的Map if (!depsMap) { // 若是該對象沒有收集依賴 console.log("該對象還未收集依賴"); // 好比修改值的時候,沒有調用過effect return; } const effects = new Set(); // 存儲依賴的effect const add = (effectsToAdd) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { effects.add(effect); }); } }; // const run = (effect) => { // effect(); // 當即執行effect // } // 修改run方法,若是是計算屬性的effect則執行其scheduler方法 + const run = (effect) => { + if (effect.options.scheduler) { // 若是是計算屬性的effect則執行其scheduler()方法 + effect.options.scheduler(); + } else { // 若是是普通的effect則當即執行effect方法 + effect(); + } + } /** * 對於effect中使用到的數據,那確定是響應式對象中已經存在的key,當數據變化後確定能經過該key拿到對應的依賴, * 對於新增的key,咱們也不須要通知effect執行。 * 可是對於數組而言,若是給數組新增了一項,咱們是須要通知的,若是咱們仍然以key的方式去獲取依賴那確定是沒法獲取到的, * 由於也是屬於新增的一個索引,以前沒有對其收集依賴,可是咱們使用數組的時候會使用JSON.stringify(arr),此時會取length屬性, * 索引會收集length的依賴,數組新增元素後,其length會發生變化,咱們能夠經過length屬性去獲取依賴 */ if (key !== null) { add(depsMap.get(key)); // 對象新增一個屬性,因爲沒有依賴故不會執行 } if (type === "add") { // 處理數組元素的新增 add(depsMap.get(Array.isArray(target)? "length": "")); } // 遍歷effects並執行 effects.forEach(run); }