大家知道吗,Vue3.0中通过Proxy代理数据的方式替换了vue2.x中通过Object.defineProperty挟持对象属性的方式实现了响应式;这篇文章,我们将仿照Reactive API 中的reactive和watchEffect,结合vue3.0源码手写代码,研究Vue3.0如何通过Proxy代理数据实现响应式
下面这篇文章,主要是通过手写 丐版的vue3.0响应式代码,去掉部分非核心的边界条件和除了reactive外其他响应式API的影响(比如readonly,shallowReactive,shallowReadonly等),一步步带领大家从零开始了解和掌握Vue3.0 响应式核心原理;
回顾Vue 2.x响应式:
vue2.X使用Object.defineProperty,挟持对象的属性的get和set,收集依赖和触发依赖响应;
Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: () => { var value = getter ? getter.call(obj) : val; if (Dep.target) { dep.depend(); if (childOb) { childOb.dep.depend(); if (Array.isArray(value)) { dependArray(value); } } } return value; }, set: newVal => { // ...省略 childOb = !shallow && observe(newVal); dep.notify(); }});
我们看一个例子:
<template> {{ obj.c }}</template><script> export default { data: { obj: { a: 1 } }, mounted() { this.obj.c = 3 } }</script>
这个例子中,我们对obj上原本不存在的c属性进行了一个赋值,但是在Vue2中,这是不会触发响应式的。
这是因为Object.defineProperty必须对于确定的key值进行响应式的定义,
这就导致了如果data在初始化的时候没有c属性,那么后续对于c属性的赋值都不会触发Object.defineProperty中的劫持。
在Vue2中,这里只能用一个额外的api Vue.set来解决。
Vue 3.0 ----Proxy代理:
const raw = {}const data = new Proxy(raw, { get(target, key) { }, set(target, key, value) { }})
proxy代理实现代码如上,对整个raw对象实现访问代理,可以看出来,Proxy在定义的时候并不用关心key值;只要你定义了get方法,那么后续对于data上任何属性的访问(哪怕是不存在的),都会触发get的劫持,set也是同理。
这样Vue3中,对于需要定义响应式的值,初始化时候的要求就没那么高了,只要保证它是个可以被Proxy接受的对象或者数组类型即可。
-
Object.defineProperty只能劫持对象的属性,而Proxy是直接代理对象; -
Object.defineProperty对新增属性需要手动进行Observe; -
vue2.x无法监控到数组下标的变化,因为vue2放弃了这个特性; -
Proxy支持13种拦截操作,这是
defineProperty所不具有的; -
Proxy支持对Set,WeakSet,Map,weakMap的数据代理
当然Proxy也是有缺陷的,IE适配问题是它最大的缺陷,所有版本的IE浏览器都不能用。好在IE马上要光荣退出浏览器舞台了,微软宣布2021年停止支持IE浏览器,edge浏览器已经接过IE的大旗。
基于Proxy的实现:
基本思路:
-
定义某个数据为
响应式数据,它会拥有收集访问它的函数的能力。 -
定义观察函数,在这个函数内部去访问
响应式数据。
要实现的代码如下:
// 响应式数据const counter = reactive({ num: 0 });// 观察函数observe(() => console.log(counter.num));
这已经一目了然了,
-
用
reactive包裹的数据叫做响应式数据, -
在
observe内部执行的函数叫观察函数。
观察函数默认定义后会立即执行:
访问时:observe函数会帮你去执行 console.log(counter.num),这时候 proxy的get拦截到了对于counter.num的访问,这时候又可以知道访问者是 () => console.log(counter.num)这个函数,那么就把这个函数作为 num这个key值的观察函数收集在一个地方。修改时:下次对于
counter.num修改的时候,去找num这个key下所有的观察函数,轮流执行一遍。这样就实现了响应式模型。 ``
1.reactive()
在看reactive函数之前,以下是需要提前掌握的知识点:
// 键值对: proxy => target 的 weakMapexport const proxyToRaw = new WeakMap<ReactiveProxy, Raw>();// 键值对: target => proxy 的 weakMapexport const rawToProxy = new WeakMap<Raw, ReactiveProxy>();export function canObserve(val: any): val is object { return typeof val === "object" && val !== null;}
reactive() 的作用主要是将目标转化为响应式的 proxy 实例。例如:
export function reactive(target: Raw) { if (!canObserve(target)) { console.warn(`value cannot be made reactive: ${String(target)}`); return target; } // 已经被定义成响应式proxy了 或者传入的是内置对象 就直接原封不动的返回 if (proxyToRaw.has(target)) { return target; } // 如果这个原始对象已经被定义过响应式 就返回存储的响应式proxy const existProxy = rawToProxy.get(target); if (existProxy) { return existProxy; } // 新建响应式proxy return createReactive(target);}
reactive 函数的处理逻辑:
-
如果target不是可以代理的对象,返回target;
-
如果target已经是proxy实例,返回target;
-
如果target已经存在proxy实例,返回其proxy实例;
-
调用createReactive函数,新建响应式proxy;
其中1.需要单独说一下:
为什么要调用canObserve函数?
答案:通过canObserve函数判断target类型是否是可观察的对象,target 的类型必须为下列值之一 Object,Array,Map,Set,WeakMap,WeakSet 才可被监听。
2.createReactive()
在看createReactive函数之前,以下是需要提前掌握的知识点:
export enum TargetType { INVALID = 0, COMMON = 1, COLLECTION = 2,}function targetTypeMap(rawType: string) { switch (rawType) { case "Object": case "Array": return TargetType.COMMON; case "Map": case "Set": case "WeakMap": case "WeakSet": return TargetType.COLLECTION; default: return TargetType.INVALID; }}export function getTargetType(value: Raw) { return targetTypeMap(toRawType(value));}export const toRawType = (value: unknown): string => { // extract "RawType" from strings like "[object RawType]" return Object.prototype.toString.call(value).slice(8, -1);};
createReactive() 函数根据target类型生成不同handler处理对象的Proxy实例:
function createReactive(target: Raw) { // 获取当前taget的TargetType类型 const targetType = getTargetType(target); const reactive = new Proxy( target, targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers, ); // 双向存储原始值和响应式proxy的映射 rawToProxy.set(target, reactive); proxyToRaw.set(reactive, target); // 建立一个映射 // 原始值 -> 存储这个原始值的各个key收集到的依赖函数的Map storeObservable(target); // 返回响应式proxy return reactive;}
createReactive函数的处理逻辑:
-
根据getTargetType返回的类型 ,创建不一样的handler参数;
-
创建 target和row 互为k,v的weakMap存储;
-
通过storeObservable函数创建一个key -> ReactionForRaw(观察者的map)的映射;
-
返回 proxy实例;
步骤3 中生成的connectionStore ,这个connectionStore实际上是target收集key的依赖函数的映射;如下图
/** * connectionStore 类型 WeakMap * key值是target:传入的需要 * key -> ReactionForRaw(观察者的map)的映射 * **/const connectionStore = new WeakMap<Raw, ReactionForRaw>();// reactionForRaw的key为对象key值 value为这个key值收集到的Reaction集合export type ReactionForRaw = Map<Key, ReactionForKey>;// key值收集到的Reaction集合export type ReactionForKey = Set<ReactionFunction>;
这里面重点要提的是1:
创建的proxy实例,这里有两种传递的参数,一个是collectionHandlers,一个是baseHandlers;这个三元表达式targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers,如果是普通的对象 Object 或 Array,处理器对象就使用 baseHandlers;如果是 Set, Map, WeakMap, WeakSet 中的一个,就使用 collectionHandlers。
collectionHandlers 和 baseHandlers 是从 collectionHandlers.ts 和 baseHandlers.ts 处引入的。
baseHandlers对象
处理器对五种操作进行了拦截,分别是:
-
get 属性读取
-
set 属性设置
-
deleteProperty 删除属性
-
ownKeys
其中 get、ownKeys 操作会收集依赖,set、deleteProperty 操作会触发依赖。
export const baseHandlers = { get, set, ownKeys, deleteProperty,};
其中 ownKeys 可拦截以下操作:
-
Object.getOwnPropertyNames() -
Object.getOwnPropertySymbols() -
Object.keys() -
Reflect.ownKeys()
get函数:
/** 劫持get访问 收集依赖 */function get(target: Raw, key: Key, receiver: ReactiveProxy) { //... 此处忽略对数组的内置方法的特殊处理; 'includes', 'indexOf', 'lastIndexOf'和'push', 'pop', 'shift', 'unshift', 'splice' const result = Reflect.get(target, key, receiver); // 内置的Symbol不观察 if ( (typeof key === "symbol" && wellKnownSymbols.has(key)) || key === "__proto__" ) { return result; } // 收集依赖 track({ target, key, receiver, type: "get" }); // 如果访问的是对象 则返回这个对象的响应式proxy // 如果没有就重新调用reactive新建一个proxy const reativeResult = rawToProxy.get(result); if (canObserve(result)) { if (reativeResult) { return reativeResult; } return reactive(result); } return result;}
大体的流程:
-
Reflect.get()获取 key对应的值; -
wellKnownSymbols.has(key)为 true 或原型对象返回result,不收集依赖; -
track收集依赖;
-
查看Reflect返回的result是否存可以observe;
-
查看result是否存在Proxy实例,存在返回reativeResult,不存在调用reactive响应化result;
为什么要用Reflect,用target[key]行不行?
track我们后面会讲!
set函数:
/** 劫持set访问 触发收集到的观察函数 */function set(target: Raw, key: Key, value: any, receiver: ReactiveProxy) { // 确保原始值里不要被响应式对象污染 if (canObserve(value)) { value = proxyToRaw.get(value) || value; } // 先检查一下这个key是不是新增的 const hadKey = hasOwnProperty.call(target, key); // 拿到旧值 const oldValue = target[key]; // 设置新值 const result = Reflect.set(target, key, value, receiver); if (!hadKey) { // 新增key值时以type: add触发观察函数 trigger({ target, key, value, receiver, type: "add" }); } else if (value !== oldValue) { // 如果新旧值不相等,才触发依赖 // 什么时候会有新旧值相等的情况?例如监听一个数组,执行 push 操作,会触发多次 setter // 第一次 setter 是新加的值 第二次是由于新加的值导致 length 改变 // 但由于 length 也是自身属性,所以 value === oldValue trigger({ target, key, value, oldValue, receiver, type: "set", }); } return result;}
大致流程是:
-
如果value是一个Proxy实例,那么获取他的target赋值给value;
-
Reflect.set设置新值; -
如果key之前不存在,trigger 触发key值的响应,type为add;
-
如果key存在 且 旧值不等于新值,trigger 触发key值的响应,type为set;
-
返回 Reflect.set 的返回值result;
observe.ts 文件(observe, trick, trigger)
observe函数
/** * 观察函数 * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖 * 下次访问的key发生变化的时候 就会重新运行这个函数 */export function observe(fn: Function): ReactionFunction { if (fn[IS_REACTION]) { fn = (fn as ReactionFunction).raw; } // reaction是包装了原始函数只后的观察函数 // 在runReactionWrap的上下文中执行原始函数 可以收集到依赖。 const reaction: ReactionFunction = (...args: any[]) => { return runReactionWrap(reaction, fn, this, args); }; // 先执行一遍reaction reaction(); reaction[IS_REACTION] = true; reaction.raw = fn; // 返回出去 让外部也可以手动调用 return reaction;}
大致流程是:
-
如果fn已经是ReactionFunction,fn赋值为ReactionFunction.raw;
-
创建reaction函数,内部执行并返回
runReactionWrap(reaction, fn, this, args); -
执行一遍reaction函数 并返回;
runReactionWrap
runReactionWrap观察当前ReactionFunction的状态,符合条件执行最后一步:清空当前ReactionFunction的依赖和清空依赖此的key的reactionsForKey依赖函数集合,将ReactionFunction入栈,执行并返回用户传入的函数fn,通过触发Proxy实例的get,track收集观察函数, ReactionFunction出栈;
首先看下ReactionFunction的类型:
export const IS_REACTION = Symbol("is reaction");export type Key = string | number | symbol;// 需要定义响应式的原值export type Raw = object;// 定义成响应式后的proxyexport type ReactiveProxy = object;// 收集响应依赖的的函数export type ReactionFunction = Function & { cleaners?: ReactionForKey[]; unobserved?: boolean; raw?: Function; [IS_REACTION]?: boolean;};
/** 把函数包裹为观察函数 */export function runReactionWrap( reaction: ReactionFunction, fn: Function, context: any, args: any[],) { // 已经取消观察了 就直接执行原函数 if (reaction.unobserved) { return Reflect.apply(fn, context, args); } // 如果观察函数是已经在运行 直接返回 if (isRunning(reaction)) { return; } // 把上次收集到的依赖清空 重新收集依赖 // 这点对于函数内有分支的情况很重要 // 保证每次收集的都是确实能访问到的依赖 releaseReaction(reaction); try { // 把当前的观察函数推入栈内 开始观察响应式proxy reactionStack.push(reaction); // 运行用户传入的函数 这个函数里访问proxy就会收集reaction函数作为依赖了 return Reflect.apply(fn, context, args); } finally { // 运行完了永远要出栈 reactionStack.pop(); }}/** * 把上次收集到的观察函数清空 重新收集观察函数 * 这点对于函数内有分支的情况很重要 * 保证每次收集的都是确实能访问到的观察函数 */export function releaseReaction(reaction: ReactionFunction) { if (reaction.cleaners) { // 把key -> reaction的set里相应的观察函数清楚掉 reaction.cleaners.forEach((reactionsForKey: ReactionForKey) => { reactionsForKey.delete(reaction); }); } // 重置队列 reaction.cleaners = [];}
其中 releaseReaction(effect) 的作用是让 reactionFunction关联下的所有 的cleaners清空ReactionForKey,即清除这个依赖函数。
执行 track() 收集的依赖就是 ReactionFunction。趁热打铁,现在我们再来看一下 track() 和 trigger() 函数。
回顾下上面提到的connectionStore ,它是一个weakMap类型的集合:
/** * connectionStore 类型 WeakMap * key值是target:传入的需要 * key -> ReactionForRaw(观察者的map)的映射 * **/const connectionStore = new WeakMap<Raw, ReactionForRaw>();// reactionForRaw的key为对象key值 value为这个key值收集到的Reaction集合export type ReactionForRaw = Map<Key, ReactionForKey>;// key值收集到的Reaction集合export type ReactionForKey = Set<ReactionFunction>;
WeakMap 和Map的区别是什么??
WeakMap 对象是一组键/值对的集合,其中的键是弱引用的。其键必须是对象,而值可以是任意的。
track
/** 依赖收集栈 */const reactionStack: ReactionFunction[] = [];/** 依赖收集 在get操作的时候要调用 */export function track(operation: Operation) { const runningReaction = getRunningReaction(); if (runningReaction) { registerReactionForOperation(runningReaction, operation); }}
/** •• * 把对响应式对象key的访问与观察函数建立关联 * 后续就可以在修改这个key的时候 找到响应的观察函数触发 */export function registerReactionForOperation( reaction: ReactionFunction, { target, key, type }: Operation,) { if (type === "iterate") { key = Array.isArray(target) ? "length" : ITERATION_KEY; } // 拿到原始对象 -> 观察者的map const reactionsForRaw = connectionStore.get(target); // 拿到key -> 观察者的set let reactionsForKey = reactionsForRaw.get(key); if (!reactionsForKey) { // 如果这个key之前没有收集过观察函数 就新建一个 reactionsForKey = new Set(); // set到整个value的存储里去 reactionsForRaw.set(key, reactionsForKey); } if (!reactionsForKey.has(reaction)) { // 把这个key对应的观察函数收集起来 reactionsForKey.add(reaction); // 把key收集的观察函数集合 加到cleaners队列中 便于后续取消观察 reaction.cleaners.push(reactionsForKey); }}
trigger
/** 值更新时触发观察函数 */export function trigger(operation: Operation) { getReactionsForOperation(operation).forEach((reaction) => reaction());}
/** * 根据key,type和原始对象 拿到需要触发的所有观察函数 */export function getReactionsForOperation({ target, key, type }: Operation) { // 拿到原始对象 -> 观察者的map const reactionsForTarget = connectionStore.get(target); const reactionsForKey: ReactionForKey = new Set(); if (type === "clear") { reactionsForTarget.forEach((_, key) => { addReactionsForKey(reactionsForKey, reactionsForTarget, key); }); } else { // 把所有需要触发的观察函数都收集到新的set里 addReactionsForKey(reactionsForKey, reactionsForTarget, key); } // add和delete的操作 需要触发某些由循环触发的观察函数收集 // observer(() => rectiveProxy.forEach(() => proxy.foo)) if (type === "add" || type === "delete" || type === "clear") { // ITERATION_KEY: // 如果proxy拦截到的ownKeys的操作 就会用ITERATION_KEY作为观察函数收集的key // 比如在观察函数里通过Object.keys()访问了proxy对象 就会以这个key进行观察函数收集 // 那么比如在delete操作的时候 是要触发这个观察函数的 因为很明显Object.keys()的值更新了 // length: // 遍历一个数组的相关操作都会触发对length这个属性的访问 // 所以如果是数组 只要把访问length时收集到的观察函数重新触发一下就可以了 // 如observe(() => proxyArray.forEach(() => {})) const iterationKey = Array.isArray(target) ? "length" : ITERATION_KEY; addReactionsForKey(reactionsForKey, reactionsForTarget, iterationKey); } return reactionsForKey;}
function addReactionsForKey( reactionsForKey: ReactionForKey, reactionsForTarget: ReactionForRaw, key: Key,) { const reactions = reactionsForTarget.get(key); reactions && reactions.forEach((reaction) => { reactionsForKey.add(reaction); });}
流程大体清洗后,我们再看下那些触发track或trigger的操作:
export const enum TrackOpTypes { GET = 'get', HAS = 'has', ITERATE = 'iterate'}export const enum TriggerOpTypes { SET = 'set', ADD = 'add', DELETE = 'delete', CLEAR = 'clear'}
总结以上,我们可以得出下面的数据响应式流程图,reactive作响应式数据注册,初始化并返回proxy实例,effect(或observe)作为观察函数,触发proxy和观察函数收集依赖;当proxy数据改变,会触发响应的effect(或observe)观察函数再次执行;
大家了解了vue3响应式的原理之后可以去学习下vue3关于reactive这一块的源码;
git仓库地址:《vue-next-master》
参考: