写点什么

Vue3 源码 | 深入理解响应式系统上篇 -reactive

用户头像
梁龙先森
关注
发布于: 2021 年 03 月 07 日
Vue3源码 | 深入理解响应式系统上篇-reactive

Vue3 优点

在深入阅读理解 Vue3 响应式系统前,我们首先要知道的是 Vue3 是通过 Proxy 进行数据双向绑定,而 Vue2 采用的是 Object.defineProperty 。那 Proxy 相对于 Object.defineProperty 特性存在的优劣性对比如下:

  1. Proxy 是对整个对象的代理,而 Object.defineProperty 只能代理某个属性。

  2. 对象上新增属性,Proxy 可以监听到,Object.defineProperty 不能。

  3. 数组新增修改,Proxy 可以监听到,Object.defineProperty 不能。

  4. 若对象内部属性要全部递归代理,Proxy 可以只在调用的时候递归,而 Object.definePropery 需要一次完成所有递归,性能比 Proxy 差。

  5. Proxy 不兼容 IE,Object.defineProperty 不兼容 IE8 及以下

  6. Proxy 使用上比 Object.defineProperty 方便多。


如果你对上面两个用法不了解,可以先阅读下这篇文章:Proxy与Object.defineProperty的用法与区别

接下来,正式阅读 Vue3 的响应式相关实现的代码。


PS:近期正在阅读 Vue3 源码,将陆续推出序列文章,感兴趣,点个关注,一起学习~

源码目录说明

这是从 GitHub 上下载下来的 Vue3 源码目录结构,画红色边框的 reactivity 目录是实现响应式的代码包,它可以被单独构建作为独立的包使用。

下面对 reactivity 目录主要文件进行说明:

  1. __tests__:描述的是代码包相关的测试用例。

  2. index.ts:用于导出包相关实现方法。

  3. reactive.ts:描述的是采用 Proxy 去代理对象,并进行劫持操作。

原理是:用 Proxy 创建代理对象,并在 Proxy 代理对象执行 get 陷阱函数读值的时候进行 track 操作,执 行 set 陷阱函数写值的时候进行 trigger 操作。注意这里是只针对对象的代理,不能对基本数据类型。

  1. refs.ts:主要描述的是如何解决基本数据类型代理的问题。

原理是:利用对象本身的 get | set 函数,并在 get 函数进行 track 操作,set 函数进行trigger操作。

  1. computed.ts:描述的是计算属性的实现。实际是带有 lazy 属性的 effect

  2. effect.ts:描述如何跟踪属性的变化并执行回调函数。

  3. baseHandlers.ts:对 Object、Array 数据类型进行代理指定的自定义拦截行为。

  4. collectionHandlers.ts:对 Set, Map, WeakMap, WeakSet 数据类型进行代理指定的自定义拦截行为。

使用案例

这里来看两种场景,一种直接使用 vue3 库,另一种使用 reactivity 目录单独构建的包。

场景一:Vue3 库
// 直接使用vue3库import { reactive, effect } from 'vue3.js'
const obj = reactive({ x: 1 })
effect(() => { patch()})
setTimeout(() => { obj.x = 2}, 1000)
function patch() { document.body.innerText = obj.x}
复制代码

很显然,1s 后,页面显示内容变为 2。说明 reactive 对 obj 对象的 setter 方法进行了劫持,当赋新值的时候触发了 effect 函数。

场景二:reactivity 包

构建 reactivity 库,可以下载 vue-next 的 vue3 源码,然后在根目录执行如下命令,这里我采用的是 yarn。

// 安装依赖yarn install // 执行此命令开启构建,构建完成,会在reactivity包下生成dist目录,里面是reactivity.glob.js文件// 该文件暴露了全局的VueReactivity对象,里面集成了包暴露的所有方法以及配置对象// ps:为了方便查看targetMap是什么,我自个新增了该对象的暴露yarn dev reactivity 
复制代码

下面看看案例代码,实现效果与场景一是一样的。

// VueReactivity 是 reactivity目录单独构建暴露的全局变量const { effect, track, trigger, targetMap } = VueReactivityvar obj = {  x: 1}
effect(() => { patch(); // 这里 track 只能放在effect里面,track需要使用effect内的activeEffect track(obj, 'get', 'x'); console.log(targetMap, 'targetMap')})
setTimeout(() => { obj.x = 2; trigger(obj, 'set', 'x')}, 1000)
function patch() { document.body.innerText = obj.x}
复制代码

reactive 源码分析

reactive 用于创建响应式对象,原理是,通过 Proxy 对目标对象 target 进行代理,进行数据操作劫持。下面看下源码,具体实现:

// 枚举了一些响应式对象的类型标识,不同类型采用不同的劫持方式export const enum ReactiveFlags {  // 标志SKIP,则此对像永远不会被转为代理  SKIP = '__v_skip',    // 是reactive处理的代理对象  IS_REACTIVE = '__v_isReactive',  // 只读代理  IS_READONLY = '__v_isReadonly',  // 原始对象  RAW = '__v_raw',  REACTIVE = '__v_reactive',  READONLY = '__v_readonly'}// 对外暴露的创建响应式对象的APIexport function reactive(target: object) {  // 如果是只读代理,则直接返回  if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {    return target  }  // 创建响应式对象  return createReactiveObject(    target,    false,    mutableHandlers,    mutableCollectionHandlers  )}// 只读代理,这里不过多解读export function readonly<T extends object>(  target: T): DeepReadonly<UnwrapNestedRefs<T>> {  return createReactiveObject(    target,    true,    readonlyHandlers,    readonlyCollectionHandlers  )}
复制代码


/** 返回对象是否可以观察:* 1. 标志了SKIP状态的对象,用于不会被转换为代理,对应API是,markRaw* 2. 对象类型属于其中一种:Object,Array,Map,Set,WeakMap,WeakSet* 3. 对象非冻结,冻结的对象无法被修改的*/const canObserve = (value: Target): boolean => {  return (    !value[ReactiveFlags.SKIP] &&    isObservableType(toRawType(value)) &&    !Object.isFrozen(value)  )}
// 这里会根据不同的参数创建不同类型的代理function createReactiveObject( target: Target, isReadonly: boolean, baseHandlers: ProxyHandler<any>, collectionHandlers: ProxyHandler<any>) { // 非对象不能返回,不能进行代理劫持,直接返回 if (!isObject(target)) { return target }
// 已经是代理,直接返回,不能重复劫持 if ( target[ReactiveFlags.RAW] && !(isReadonly && target[ReactiveFlags.IS_REACTIVE]) ) { return target } // 已经具有相应的代理[reactive/readonly],则返回 const reactiveFlag = isReadonly ? ReactiveFlags.READONLY : ReactiveFlags.REACTIVE if (hasOwn(target, reactiveFlag)) { return target[reactiveFlag] }
// 判断目标是否可进行代理劫持 if (!canObserve(target)) { return target } // 目标对象设置代理 const observed = new Proxy( target, // 如果是Set, Map, WeakMap, WeakSet集合数据类型,走collectionHandlers // 基础数据类型Object/Array 走 baseHandlers collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers ) def(target, reactiveFlag, observed) // 返回代理对象 return observed}
复制代码

到此处的代码,主要讲述的是通过 reactive 创建响应式对象,对入参的必要满足条件,总结下有如下几点:

  1. 非对象不能进行代理劫持。

  2. 目标已经被劫持,则返回,不能重复劫持。

  3. 只读劫持。

  4. 对象__v_skip 属性非真,且数据类型属于Object,Array,Map,Set,WeakMap,WeakSet其中一种,并且对象非冻结状态,因为对象被冻结后不能被修改。

  5. 根据不同数据类型选择不同的劫持方式。

集合类型Set, Map, WeakMap, WeakSet,collectionHandlers

基础数据类型Object,Array baseHandlers


能够传递一个正确的对象,下面看看如何通过代理对对象进行数据劫持操作,才能达到响应式的效果。这里以 baseHandlers 为例。

  1. 首先 baseHandlers 是对接口 ProxyHandler 的实现,先看看接口都定义了什么。


interface ProxyHandler<T extends object> { getPrototypeOf? (target: T): object | null; setPrototypeOf? (target: T, v: any): boolean; isExtensible? (target: T): boolean; preventExtensions? (target: T): boolean; getOwnPropertyDescriptor? (target: T, p: PropertyKey): PropertyDescriptor | undefined; has? (target: T, p: PropertyKey): boolean; get? (target: T, p: PropertyKey, receiver: any): any; set? (target: T, p: PropertyKey, value: any, receiver: any): boolean; deleteProperty? (target: T, p: PropertyKey): boolean; defineProperty? (target: T, p: PropertyKey, attributes: PropertyDescriptor): boolean; ownKeys? (target: T): PropertyKey[]; apply? (target: T, thisArg: any, argArray?: any): any; construct? (target: T, argArray: any, newTarget?: any): object;}
复制代码

其实这里包含的是 Proxy 基本所有的陷阱函数。 Proxy 可以拦截 JavaScript 引擎内部目标的底层对象操作,这些操作被拦截后会触发响应特定操作的陷阱函数。

  1. 看看 mutableHandlers 具体实现,挑选基本的 get/和 set 理解

const set = /*#__PURE__*/ createSetter()const get = /*#__PURE__*/ createGetter()
// 劫持的处理程序export const mutableHandlers: ProxyHandler<object> = { get, set, deleteProperty, has, ownKeys}
// 主要用于追踪收集数据变化,并放入到targetMapfunction createGetter(isReadonly = false, shallow = false) { return function get(target: object, key: string | symbol, receiver: object) { // 标志状态相关处理,并没有进行收集 if (key === ReactiveFlags.IS_REACTIVE) { return !isReadonly } else if (key === ReactiveFlags.IS_READONLY) { return isReadonly } else if ( key === ReactiveFlags.RAW && receiver === (isReadonly ? (target as any)[ReactiveFlags.READONLY] : (target as any)[ReactiveFlags.REACTIVE]) ) { return target } // 数组相关处理 const targetIsArray = isArray(target) if (targetIsArray && hasOwn(arrayInstrumentations, key)) { return Reflect.get(arrayInstrumentations, key, receiver) } // 通过反射获取对象的默认行为(属于语言内部的方法),不管Proxy怎么改都没用 const res = Reflect.get(target, key, receiver) // key是Symbol类型,原型链,或者是Ref引用类型标志,则返回,不追踪 if ( isSymbol(key) ? builtInSymbols.has(key) : key === `__proto__` || key === `__v_isRef` ) { return res } // 追踪属性变化,这里会放入到targetMap,在effect文件里面声明的存储追踪数据的weakMap变化 if (!isReadonly) { track(target, TrackOpTypes.GET, key) } // 浅层代理(如,shallowReactive),不做递归,并且ref引用没有做拆包,因为直接return。 if (shallow) { return res } // 引用对象,如果是数组,则返回数组,如果是对象,则返回值。 // 非浅层代理(reactive),则做了ref的拆包 if (isRef(res)) { return targetIsArray ? res : res.value } // 如果是对象,则递归进行响应式处理 if (isObject(res)) { return isReadonly ? readonly(res) : reactive(res) }
return res }}
// 用于触发依赖,从targetMap获取存在数据,执行对应的effectfunction createSetter(shallow = false) { return function set( target: object, key: string | symbol, value: unknown, receiver: object ): boolean { const oldValue = (target as any)[key] if (!shallow) { value = toRaw(value) // 引用类型Ref的处理 if (!isArray(target) && isRef(oldValue) && !isRef(value)) { oldValue.value = value return true } } else { // 在浅层模式下,不管是否响应式,对象都按原样设置 }
const hadKey = hasOwn(target, key) const result = Reflect.set(target, key, value, receiver) // 如果目标是原型链中的某个东西,就不要触发 if (target === toRaw(receiver)) { // 根据key是否存在,去触发对应的操作 if (!hadKey) { trigger(target, TriggerOpTypes.ADD, key, value) } else if (hasChanged(value, oldValue)) { trigger(target, TriggerOpTypes.SET, key, value, oldValue) } } return result }}
复制代码

看了上面 get|set 两个代码块实现,我们可以总结出,在实现响应式过程中的结论:

  1. 在 get 陷阱函数中,对不同类型属性做对应处理,并进行数据的 track 操作(追踪收集缓存)

  2. 在 set 陷阱函数中,根据 key 类型以及是否存在做对应处理,并执行 trigger 操作,会触发 effect 方法


因为这里的 tracktrigger 方法是在 effect.ts 文件中,那这里就不做解决,放到下一篇再讲。

其他源码说明

1. 响应式对象

我们知道 Vue3 能创建多种类型的响应式对象,这些响应式对象是如何定义的呢。如下可以看出其他类型的响应式对象的定义也是使用采用方法 createReactiveObject ,只是采用了不一样的处理程序。

export function shallowReactive<T extends object>(target: T): T {  return createReactiveObject(    target,    false,    shallowReactiveHandlers,    shallowCollectionHandlers  )}
// 第二个参数,表示的是是否采用浅层shallow模式,// shallow=true,任务属性ref都不会自动解包,具体看代码const shallowGet = /*#__PURE__*/ createGetter(false, true)// 第一个参数,表示的是是否采用浅层shallow模式,const shallowSet = /*#__PURE__*/ createSetter(true)
复制代码


  1. shallowReactive

创建一个响应式对象,只能跟踪自身属性的变化,不对嵌套对象进行响应式处理。与 reactive 不同,使用的任何属性 ref不会被代理自动解包。因为采用的是shallow = true的模式。

const state = shallowReactive({  foo: 1,  nested: {    bar: 2  }})
// mutating state's own properties is reactivestate.foo++// ...but does not convert nested objectsisReactive(state.nested) // falsestate.nested.bar++ // non-reactive
复制代码


  1. shallowReadonly

创建一个代理,使其自身的属性为只读,但不执行嵌套对象的深度只读转换。与 readonly 不同,使用的任何属性 ref不会被代理自动解包

const state = shallowReadonly({  foo: 1,  nested: {    bar: 2  }})
// mutating state's own properties will failstate.foo++// ...but works on nested objectsisReadonly(state.nested) // falsestate.nested.bar++ // works
复制代码
2. 其他 API

直接看下 API 源码实现。

// 检测对象是否是由reactive创建的export function isReactive(value: unknown): boolean {  if (isReadonly(value)) {    return isReactive((value as Target)[ReactiveFlags.RAW])  }  return !!(value && (value as Target)[ReactiveFlags.IS_REACTIVE])}// 检测对象是否是只读代理export function isReadonly(value: unknown): boolean {  return !!(value && (value as Target)[ReactiveFlags.IS_READONLY])}// 检测对象是否代理export function isProxy(value: unknown): boolean {  return isReactive(value) || isReadonly(value)}// 返回reactive或readonly代理的原始对象export function toRaw<T>(observed: T): T {  return (    (observed && toRaw((observed as Target)[ReactiveFlags.RAW])) || observed  )}// 标记一个对象,使其永远不会转换为代理export function markRaw<T extends object>(value: T): T {  def(value, ReactiveFlags.SKIP, true)  return value}
复制代码

总结

至此大概阅读了下 reactive 主要核心的相关源码,这里以简易代码描述下其核心实现过程:

const reactive = (target){	 // 代理数据  return new Proxy(target, {    get(target, prop) {      // 执行追踪,数据放入targetMap      track(target, prop);      return Reflect.get(target, prop);    },    set(target, prop, newVal) {      Reflect.set(target, prop, newVal);      // 触发effect      trigger(target, prop);      return true;    }  })}
复制代码

解读如有不正确的地方,欢迎指正。下一篇预告:解读 track、trigger 和 effect。


发布于: 2021 年 03 月 07 日阅读数: 67
用户头像

梁龙先森

关注

脚踏V8引擎的无情写作机器 2018.03.17 加入

还未添加个人简介

评论

发布
暂无评论
Vue3源码 | 深入理解响应式系统上篇-reactive