前言
在 2019.10.05 Vue3 的源代码正式发布了,来自官方的消息:
我自己也是在阅读源码,而且提了几个 PR:
本篇文章算是 Vue3 源码解析系列-响应式系统原理的开篇吧,后续会给大家带来整个响应式系统原理的源码解析以及流程图
仓库
响应式系统的源码是在仓库的 packages/reactivity
中,看源码第一步就是去看仓库的 README,翻译后大致意思如下:
这个包会被嵌入到面向框架使用者的渲染器中,例如
@vue/runtime-dom
,但是也可以单独作为一个独立的包(@vue/reactivity
)使用。 而且这个独立的包不应该和面向框架使用者的渲染器一起使用,也就是说不要这个文件是从@vue/reactivity
导入响应式 API,另一个文件就从@vue/runtime-dom
里导入,而应该统一从渲染器里再导入响应式的所有 API 来使用
所以,从 README 里我们能获取到什么信息呢?
- 响应式系统是独立与编译或者渲染的
- 单独使用响应式包来实现一些状态管理之类的功能
然后先来撸一眼响应式系统核心目录:
packages/reactivity/src
├── baseHandlers.ts // Proxy handler,针对于常规对象(Array,Object)
├── collectionHandlers.ts // Proxy handler,针对于 Set,Map,WeakMap,WeakSet 这类集合数据
├── computed.ts // 功能同 vue2 的 computed
├── effect.ts // 数据变动响应的处理,以及依赖收集
├── index.ts // 入口文件,导出 API
├── lock.ts // 锁控制(其实控制 只读的响应式变量的 set 和 delete 操作)
├── operations.ts // 对数据操作类型的枚举
├── reactive.ts // 响应式入口,针对于对象,下方会有介绍
└── ref.ts // 响应式入口,针对于基本类型,下方会有介绍
Api 概览
对于不熟悉新的响应式 API 的同学,可以直接看下面的示例:
import { reactive, ref, computed, effect } from '@vue/reactivity'
const original = { foo: 0 }
const observed = reactive(original)
const oRef = ref(0)
const getter = computed(() => observed.foo + oRef.value)
effect(() => {
console.log('trigger effect', getter.value)
})
// 打印 trigger effect 0
observed.foo++ // 打印 trigger effect 1
oRef.value++ // 打印 trigger effect 2
根据上面的目录以及 API 示例,其实响应式系统中主要包含了 4 个 关键API,分别是:
reactive
:响应式关键入口 API,作用同 Vue2 组件的data
选项ref
:响应式关键入口 API,作用同reactivity
,不过是用于针对基本数据类型的响应式包装,因为基本数据类型在对象结构或者函数参数传递时会丢失引用computed
:响应式计算 API,同 Vue2 的computed
选项effect
:作用同 Vue2 的watcher
,是用来进行依赖收集的 API,computed
和 后续文章的watch
API都是基于effect
的
介绍完 4 个关键的 API 后,先简单地给出它们之间的联系:
接下来就对这几个 API 以及辅助工具的源码以及原理来进行解析
大家如果不熟悉 API,也可以先从单测的测试用例开始看起,比如相老板的 Vue3响应式系统源码解析-单测篇
reactive
核心入口
话不多说,我们先来看下核心的 reactive
的源码,先看下有哪些依赖:
// 工具方法,isObject 是判断是否是对象,toTypeString 获取数据类型
import { isObject, toTypeString } from '@vue/shared'
// Proxy 的 handlers
// mutableHandlers:可变数据的 handler
// readonlyHandlers:只读数据的 handler
import { mutableHandlers, readonlyHandlers } from './baseHandlers'
import {
mutableCollectionHandlers, // 可变集合数据的 handler
readonlyCollectionHandlers // 只读集合数据的 handler
} from './collectionHandlers'
// effect 泛型类型
import { ReactiveEffect } from './effect'
// ref 泛型类型
import { UnwrapRef, Ref } from './ref'
// 工具方法,将字符串转化成 Map,返回 function 来判断是否 这个Map 上包含所给的 key
// 这个在 vue2 里也有
import { makeMap } from '@vue/shared'
可以很清楚的看到,重点依赖项就是那一堆 handler
了,其他都是一些工具方法和泛型类型
接下来的源码里是一堆变量的定义,不过我们先跳过,先来看下 reactive
的方法和类型:
// 上面还有一大坨变量定义,很关键,但是我们先跳过,先看下有哪些方法,类型是什么样的
// 依赖集合类型
export type Dep = Set<ReactiveEffect>
// 看名字就知道,是 key 和 Dep 集合的对应关系集合
// key 其实就是我们响应式数据上的 key,Dep 则是有哪些地方依赖到了这个 key
// 比如 const a = { foo: 1 },如果在其他两处都用到了 a.foo,那么
// 这里的 key 就是 foo,Dep 就是这两处的 依赖集合
export type KeyToDepMap = Map<string | symbol, Dep>
// 判断对象能不能被观察的
const canObserve = (value: any): boolean
// only unwrap nested ref
// 解套 Ref 类型
type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRef<T>
// 响应式入口方法,入参是泛型,继承 object,返回值就是上面的解套 Ref 类型
export function reactive<T extends object>(target: T): UnwrapNestedRefs<T>
// 跟 reactive 作用相同,只不过返回值是 Readonly 的
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>>
// 创建响应式对象的关键方法,reactive 和 only 都调用了这个方法
function createReactiveObject(
target: any,
toProxy: WeakMap<any, any>,
toRaw: WeakMap<any, any>,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
): any
// 是否是响应式数据
export function isReactive(value: any): boolean
// 是否是只读的响应式数据
export function isReadonly(value: any): boolean
// 将响应式数据转化为原始数据
export function toRaw<T>(observed: T): T
// 将 value 标记为 Readonly,在 reactive 方法里会判断是否是 Readonly 的原始数据
export function markReadonly<T>(value: T): T
// 将 value 标记为不可响应数据,这个将会影响 canObserve 方法的判断
export function markNonReactive<T>(value: T): T
看完方法和类型,大致有以下几个问题:
Dep
依赖是如何追踪的?UnwrapRef
是如何展开嵌套的响应式数据类型的(俗称解套),比如reactive({ name: reactive(ref('Jooger')) })
- 如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?
- 如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?
问题 1,后面的 effect
会讲,这里先不讨论
问题 2,后面的 ref
会讲,这里先不讨论
问题 3,4 就需要看下我刚才跳过的一堆变量的定义了:
// The main WeakMap that stores {target -> key -> dep} connections.
// Conceptually, it's easier to think of a dependency as a Dep class
// which maintains a Set of subscribers, but we simply store them as
// raw Sets to reduce memory overhead.
// target 和 KeyToDepMap 的映射关系集合
// 一句话理解,有多个 target,每个 target 上有多个 key,每个 key 都有多个依赖
// 至于为什么要把映射关系存到 WeakMap 里,根据上面注释所述,是为了减少内存开销
// 这个在后续的 effect 部分会讲
export const targetMap = new WeakMap<any, KeyToDepMap>()
// WeakMaps that store {raw <-> observed} pairs.
// 下面这四个变量就是为了解答 问题 3 和 4 的
// 根据上面的原英文注释,这四个变量是 raw 和 observed 的对应关系集合
// raw 是原始数据,observed 则是响应式数据
// 原始数据 -> 响应式数据
const rawToReactive = new WeakMap<any, any>()
// 响应式数据 -> 原始数据
const reactiveToRaw = new WeakMap<any, any>()
// 原始数据 -> 只读响应式数据
const rawToReadonly = new WeakMap<any, any>()
// 只读响应式数据 -> 原始数据
const readonlyToRaw = new WeakMap<any, any>()
// WeakSets for values that are marked readonly or non-reactive during
// observable creation.
// 前面提到过的 markReadonly 和 markNonReactive 方法用到的
// 用来存储我们标记的特定数据,以便在创建响应式数据是来检查是否被上面两个方法标记过
const readonlyValues = new WeakSet<any>()
const nonReactiveValues = new WeakSet<any>()
// 判断是否是集合类型(Set, Map, WeakMap, WeakSet)
// 因为集合类型的代理 handler 和普通对象是不同的,需要特殊处理
const collectionTypes = new Set<Function>([Set, Map, WeakMap, WeakSet])
// 判断是否是可观察类型,有以下 6 类,在 canObserve 方法里会用到
const isObservableType = /*#__PURE__*/ makeMap(
['Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet']
.map(t => `[object ${t}]`)
.join(',')
)
看完上面这些变量定义,我们来解答一下问题 3,4:
问题 3:如何判断是否是(只读的)响应式数据?往响应式数据上面挂载一个标记位来标记?还是用一个对象来存储响应式数据?
用 readonlyToRaw
来存储只读响应式数据的,参见下面代码:
export function isReadonly(value: any): boolean {
return readonlyToRaw.has(value)
}
问题 4:如何将响应式数据转化为原始数据?proxy 数据如何转化成 object?
用 reactiveToRaw
和 readonlyToRaw
来存储响应式数据 -> 原始数据
的映射关系,然后:
export function toRaw<T>(observed: T): T {
return reactiveToRaw.get(observed) || readonlyToRaw.get(observed) || observed
}
总的来讲,就是利用各种集合来存储原始数据和响应式数据的映射关系,以便快速根据这种映射关系拿到对应的数据,至于为什么用 WeakMap
,上面说过了是为了减少内存开销,不清楚原理的同学可以移步 MDN WeakMap
再回头看下 canObserve
方法,来看看到底有哪些数据是可以观察的:
const canObserve = (value: any): boolean => {
return (
// Vue 实例不可观察,目前库里还没有 _isVue 的逻辑,不过猜测应该是内部在 setup 方法中挂载
!value._isVue &&
// virtual dom 不可观察
!value._isVNode &&
// 'Object', 'Array', 'Map', 'Set', 'WeakMap', 'WeakSet' 类型以外的不可观察
isObservableType(toTypeString(value)) &&
// 已经标记为不可响应数据的不可观察
!nonReactiveValues.has(value)
)
}
相比于 Vue2
的是否可观察判断,则少了很多条件:
// 我就不解析 Vue2 中的这段判断代码了
// 相比于 Vue2,少了 __ob__ ,ssr 以及 Object.isExtensible 的判断
// 这都是得益于 Proxy
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
ob = new Observer(value)
}
接下来就讲一下重点的 reactive
和 readonly
这两个核心方法的实现:
// 前面讲过返回值就是上面的解套 Ref 类型
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果是一个只读响应式数据,直接返回,因为已经是响应式的了
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
// 如果曾经被标记为只读数据,直接调用 readonly 方法生成只读响应式对象
if (readonlyValues.has(target)) {
return readonly(target)
}
// 创建响应式对象
return createReactiveObject(
target, // 原始数据
rawToReactive, // raw -> observed
reactiveToRaw, // observed -> raw
mutableHandlers, // 可变数据的 proxy handle
mutableCollectionHandlers // 可变集合数据的 proxy handler
)
}
export function readonly<T extends object>(
target: T
): Readonly<UnwrapNestedRefs<T>> {
// value is a mutable observable, retrieve its original and return
// a readonly version.
// 如果是响应式数据,那么获取原始数据来进行观察
if (reactiveToRaw.has(target)) {
target = reactiveToRaw.get(target)
}
// 创建响应式数据,同样
return createReactiveObject(
target, // 原始数据
rawToReadonly, // raw -> readonly observed
readonlyToRaw, // readonly ovserved -> raw
readonlyHandlers, // 只读数据的 proxy handler
readonlyCollectionHandlers // 只读集合数据的 proxy handler
)
}
其实我们看 Vue3
的源码会发现,很多入口方法都变得短小精简,不像 Vue2
里的一些 exposed function 那样写的很长,这两个核心方法也一样,逻辑很简单,主要是进行一些原始数据检查和转换,核心实现逻辑都是放在 createReactiveObject
里的
下面继续看下核心实现方法 createReactiveObject
:
// 创建响应式对象
function createReactiveObject(
target: any, // 原始数据
toProxy: WeakMap<any, any>, // raw -> (readonly) observed
toRaw: WeakMap<any, any>, // (readonly) observed -> raw
baseHandlers: ProxyHandler<any>, // 只读/可变 数据的 proxy handler
collectionHandlers: ProxyHandler<any> // 只读/可变 集合数据的 proxy handler
) {
// 如果不是对象,则直接返回自身,包括 null,reactive(null) => null
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target already has corresponding Proxy
// 如果原始数据已经被观察过,直接通过 raw -> observed 映射,返回响应式数据
let observed = toProxy.get(target)
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 如果原始数据本身就是响应式的,直接返回自身
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
// 如果是不可观察对象,直接返回自身
if (!canObserve(target)) {
return target
}
// 判断是采用基础数据(object|array)handler 还是集合数据 handler
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers
// Proxy 创建代理对象,即响应式对象
observed = new Proxy(target, handlers)
// 创建后,设置好 raw <-> observed 的双向映射关系
toProxy.set(target, observed)
toRaw.set(observed, target)
// 上面讲到了 targetMap 的作用,这里是创建默认依赖追踪集合
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}
我们可以看到:
- 目前来看
reactive
和readonly
的区别仅有两点:映射关系存储集合不同 andproxy handler
不同 object``array
和集合类型Set``Map``WeakSet``WeakMap
的proxy handler
是不同的
所以下面再来依次看下响应式核心中的核心 - 各种 proxy handler
baseHandler
方法跟上面一样,先看依赖:
// 上面讲过,不过现在来看感觉像是是在 get set 这些 trap 方法里会调用
import { reactive, readonly, toRaw } from './reactive'
// 操作类型枚举,对应于 proxy handler 里的 trap 方法
import { OperationTypes } from './operations'
// 依赖收集和触发依赖回调
import { track, trigger } from './effect'
// 全局锁,用来禁止 set 和 delete
import { LOCKED } from './lock'
// 工具方法,类型判断
import { isObject, hasOwn, isSymbol } from '@vue/shared'
// 判断是否是 ref,后面会讲到
import { isRef } from './ref'
这里有两个疑问:
track
和trigger
的实现LOCKED
的作用?为什么会有这个全局锁?
问题 1 在后面的 effect
部分会讲到,现在只需要知道是用来追踪依赖和触发依赖回调方法就行
问题 2 现在我也不是特别了解,只知道是在组件 mount
和 update
的时候会对组件的 props
的代理进行修改,因为我们都知道单向数据流中,子组件内部是不能更改 props
的,但是子组件更新,进行 vnode patch 后需要更新子组件的 props
,包括一些动态 props
再来看下变量和方法概览:
// JS 内部语言行为描述符集合,比如 Symbol.iterator 这些,在 get 里会用到
const builtInSymbols = new Set(
Object.getOwnPropertyNames(Symbol)
.map(key => (Symbol as any)[key])
.filter(isSymbol)
)
function createGetter(isReadonly: boolean) {}
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean
function deleteProperty(target: any, key: string | symbol): boolean
function has(target: any, key: string | symbol): boolean
function ownKeys(target: any): (string | number | symbol)[]
export const mutableHandlers: ProxyHandler<any> = {
get: createGetter(false),
set,
deleteProperty,
has,
ownKeys
}
export const readonlyHandlers: ProxyHandler<any> = {
get: createGetter(true),
set(target: any, key: string | symbol, value: any, receiver: any): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Set operation on key "${String(key)}" failed: target is readonly.`,
target
)
}
return true
} else {
return set(target, key, value, receiver)
}
},
deleteProperty(target: any, key: string | symbol): boolean {
if (LOCKED) {
if (__DEV__) {
console.warn(
`Delete operation on key "${String(
key
)}" failed: target is readonly.`,
target
)
}
return true
} else {
return deleteProperty(target, key)
}
},
has,
ownKeys
}
可以看到,mutableHandlers
和 readonlyHandlers
都是定义了 5 个 trap 方法:get
、set
、deleteProperty
、has
、ownKeys
,前 3 个不用多家介绍,has
trap 针对与 in
操作符,而 ownKeys
针对于 for in
和 Object.keys
这些遍历操作的
而 readonlyHandlers
相比于 mutableHandlers
其实只是在 get
、set
和 deleteProperty
这三个 trap 方法里有区别,而对于可能改变数据的 set
和 deleteProperty
方法,则是利用 LOCKED
来锁定,不让修改数据,这个变量我在上面也提了一下
下面来一个一个的看下各个 trap 方法
get
// 创建 get trap 方法
// 如果是可变数据, isReadonly 是 false
// 如果是只读数据,那么 isReadonly 就是 true
function createGetter(isReadonly: boolean) {
return function get(target: any, key: string | symbol, receiver: any) {
// 利用 Reflect 反射来获取原始值
const res = Reflect.get(target, key, receiver)
// 如果是 JS 内置方法,不进行依赖收集
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
// 如果是 ref 类型数据,则直接返回其 value
// TODO 后面 ref 部分我们会讲到,ref(target) 其实在 get value 的时候做了依赖收集了,
// 就不需要下面重复收集依赖
if (isRef(res)) {
return res.value
}
// get 类型操作的依赖收集
track(target, OperationTypes.GET, key)
// 这里其实很简单就是递归返回响应式对象
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}
看完 get trap
其实很简单,但是也会有写疑问:
1. 为什么用 Reflect.get
,而不是直接 target[key]
返回呢?
看 MDN Reflect.get 我们可以看它的第三个参数:
receiver:如果target对象中指定了getter,receiver则为getter调用时的this值
举个例子:
const target = {
foo: 24,
get bar () {
return this.foo
}
}
const observed = reactive(target)
此时,如果不用 Reflect.get
,而是 target[key]
,那么 this.foo
中的 this
就指向的是 target
,而不是 observed
,此时 this.foo
就不能收集到 foo
的依赖了,如果 observed.foo = 20
改变了 foo 的值,那么是无法触发依赖回调的,所以需要利用 Reflect.get
将 getter 里的 this
指向代理对象
2. 为什么在结尾 return 的时候还要调用 reactive
或者 readoonly
呢?
原注释是这样写的:
need to lazy access readonly and reactive here to avoid circular dependency 翻译过来是:需要延迟地使用 readonly 和 readtive 来避免循环引用
为什么这样说呢?这里不得不说一下 Proxy
的特性:只能代理一层,对于嵌套的深层对象,如果不按源码中的方法,那就需要一层层递归来代理劫持对象,即每次递归都判断是否是对象,如果是对象,那么再调用 reactive
来响应式化
但是问题又来了,JS 里是有循环引用这个概念的,就像下面这样:
const a = {
b: {}
}
a.b.c = a
这样的话,如果每次递归调用 reactive
的话,会造成调用栈溢出 Maximum call stack size exceeded
,但是我们只需要加上一个判断条件即可解决,在上面解析的 createReactiveObject
方法里我们知道如果原始数据已经被观察过,则直接返回对应的响应式数据,那么我们可以在递归调用 reactive
的时候判断 toProxy.get(target)
是否存在,如果存在就不往下递归了,我写了一个例子代码:
// 循环引用对象
const target = { b: { c: 1 }}
target.b.d = target
// 这个就是上面讲的 原始数据 -> 响应式数据的集合
const rawToReactive = new WeakMap()
function reactive (data) {
const observed = new Proxy(data, {
get (target, key, receiver) {
const res = Reflect.get(target, key, receiver)
const observed = rawToReactive.get(res)
return observed || res
}
})
rawToReactive.set(data, observed)
for (let key in data) {
const child = data[key]
// 这里判断如果没有被观察过,那么继续 reactive 递归观察
if (typeof child === 'object' && !rawToReactive.get(child)) {
reactive(child)
}
}
return observed
}
const p = reactive(target)
console.log(p.b.c) // 1
console.log(p.b.d.b) // Proxy {c: 1, d: {…}}
// 我试了一下。跟源码里的 reactive 的 get 结果是一样的
可以去看下我在 vue3响应式源码解析-Reactive篇 这篇文章下的评论部分
而源码中的 lazy access
方式很取巧,只代理一层,当用到某个属性值对象时,再进行响应式观察这一层
所以相比于初始化时递归劫持,延迟访问劫持的方式更能提升初始化性能,也有利于对数据劫持做更细的控制,特别是针对于数据对象比较大时(比如接口返回数据嵌套过深),有些数据并非需要劫持,所以按需劫持代理我们用到的数据这种方式更好
set
// set trap 方法
function set(
target: any,
key: string | symbol,
value: any,
receiver: any
): boolean {
// 如果是观察过响应式数据,那么获取它映射的原始数据
value = toRaw(value)
// 获取旧值
const oldValue = target[key]
// 如果旧值是 ref 类型数据,而新的值不是 ref,那么直接赋值给 oldValue.value
// 因为 ref 数据在 set value 的时候就已经 trigger 依赖了,所以直接 return 就行
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
// 对象上是否有这个 key,有则是 set,无则是 add
const hadKey = hasOwn(target, key)
// 利用 Reflect 来执行 set 操作
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 如果 target 原型链上的数据,那么就不触发依赖回调
if (target === toRaw(receiver)) {
/* istanbul ignore else */
if (__DEV__) {
// 开发环境操作,只比正式环境多了个 extraInfo 的调试信息
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (value !== oldValue) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
// 上面讲过,有这个 key 则是 set,无则是 add
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (value !== oldValue) {
// 只有当 value 改变的时候才触发
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}
同样,set trap
看起来也很简单,但是同时也会有一些问题:
1. target === toRaw(receiver)
是什么鬼逻辑?
首先看下 MDN handler.set() 的关于第三个参数的说明:
最初被调用的对象。通常是proxy本身,但handler的set方法也有可能在原型链上或以其他方式被间接地调用(因此不一定是proxy本身)。 比如,假设有一段代码执行 obj.name = "jen",obj不是一个proxy且自身不含name属性,但它的原型链上有一个proxy,那么那个proxy的set拦截函数会被调用,此时obj会作为receiver参数传进来
上面已经给出例子了,这里我再写一下:
const child = { name: 'child' }
const parent = new Proxy({ name: 'parent' }, {
set (target, key, value, receiver) {
console.log(target, receiver)
return Reflect.set(target, key, value, receiver)
}
})
Object.setPrototypeOf(child, parent)
child.foo = 1
// 会打印出
// {name: "parent"} {name: "child"}
这里有两个先决条件:
- child 的原型链是一个 Proxy
- child 在设置值的时候,本身不包含 key 的
可以看到,当满足上面两个条件的时候,设置 child 的值,会触发原型链上的 set trap
方法,并且 target
是原型链数据,而 receiver
则是真实数据
所以,源码中的那个条件逻辑也就不难看懂了,当满足上述两个条件时,我们当然不希望触发 parent 的 set trap
了
2. 像数组的 unshift
,splice
这些操作是如何触发 set trap
方法的呢?
// 在 set 里加上这么一个 log
console.log(!hadKey ? 'add' : value !== oldValue ? 'set' : 'unknow', target, key, value, oldValue)
然后
a = reactive([1, 2, 3])
a.unshift(0)
// 会打印
// add [1, 2, 3, 3] 3 3 undefined
// set [1, 2, 2, 3] 2 2 3
// set [1, 1, 2, 3] 1 1 2
// set [0, 1, 2, 3] 0 0 1
// unknow [0, 1, 2, 3] length 4 4
一共打印了 5 次,根据打印内容我们可以看到 unshift
的实际操作过程,即把数组的每一项依次都往后移动一位,然后再把首位设置成 0
,至于为什么这么操作,ECMA-262 Array.property.unshift 标准中有原理介绍,我就不赘述了,还有像 shift
和 splice
也是一样的操作步骤
可以看到 unshift
或者 splice
是会带来多次的 trigger
的,当然这些会有批量跟新优化的,有时间我再展开讲一下
细心的同学可能会发现,还触发了 length
属性的 set,而且 value
和 oldValue
是一样的,那么根据源码所示,就不会触发 set 类型的回调了呀,那我们如果在 template 里用到了 a.length
那也不会更新了么?
肯定是会更新的,解决办法就在 trigger
这个方法里,后续 effect
部分会讲到,先简单说一下,对于会导致数组 length
改变的操作,比如 add 和 delete,在 effect
的 trigger
方法里会单独处理,来触发 length
属性的依赖回调的
其他 trap 方法
还有 deleteProperty
、has
和 ownKeys
这几个 trap,代码不多,都很简单,直接看下面的源码就能明白,我就不在赘述了
// deleteProperty trap 方法
function deleteProperty(target: any, key: string | symbol): boolean {
const hadKey = hasOwn(target, key)
const oldValue = target[key]
const result = Reflect.deleteProperty(target, key)
if (result && hadKey) {
/* istanbul ignore else */
if (__DEV__) {
trigger(target, OperationTypes.DELETE, key, { oldValue })
} else {
trigger(target, OperationTypes.DELETE, key)
}
}
return result
}
// has trap 方法
function has(target: any, key: string | symbol): boolean {
const result = Reflect.has(target, key)
track(target, OperationTypes.HAS, key)
return result
}
// ownKey trap 方法
function ownKeys(target: any): (string | number | symbol)[] {
track(target, OperationTypes.ITERATE)
return Reflect.ownKeys(target)
}
总结
baseHandler
是针对于数组和对象类型的数据的 proxy handler- 每个
trap
方法都是用Reflect
来反射到原始数据上的 - 对于
get
、has
和ownKeys
这一类读操作,会进行track
来收集依赖,而对于set
和deleteProperty
这类写操作,则是会进行trigger
来触发依赖回调 - 响应式数据的读取是
lazy
的,即初始化的时候不会对嵌套对象全盘观察,而是只有用到了每个值才会生成对应的响应式数据
collectionHandler
还记得我们在看 reactive
方法那里有个 collectionTypes
的判断对吧,collectionHandler
就是专门来处理 Set|Map|WeakSet|WeakMap
这类集合类型数据的
这里可以参考相学长的vue3响应式源码解析-Reactive篇 collectionHandler 这篇文章,写的很详细,我这里也不再赘述了
总结
最开始阅读 reactive
源码时,总体的逻辑是比较清晰的,但是仍然有几个地方当时有疑惑:
rawToReactive|rawToReadonly
等这几个变量是干嘛的?targetMap
的是干什么的?为什么是WeakMap<any, KeyToDepMap>
类型LOCKED
是用来干嘛的?baseHandler
的get trap
为什么又返回了一个reactive(res)
?collectionHandler
里的 trap 方法为什么只有get
?为什么跟baseHandler
不一样?
在读完源码后,除了 LOCKED
那个疑惑,其他几个问题我都已经找到答案,并且也在上面解惑了,我相信大家看完这篇文章后也应该都有自己的答案了
最后再来个源码里的知识点总结吧:
reactive
是利用Proxy
来进行数据观察,Reflect
相关操作来反射到原始数据的,并且数据的访问是一个lazy reactive
方式,即按需观察- 普通对象、数组和集合类型数据的代理 handler 是不同的,这是因为
Proxy
的一些限制,参考Proxy limitations - 利用几个
WeakMap
来存储原始数据 <-> 响应式数据的双向映射关系,以便在响应式入口方法里判断是否原始数据已经被观察过,这个相比于 Vue2 的直接在原始数据上挂载__ob__
要少一些冗余数据,并且由于WeakMap
的 GC 特性,在运行时会有一定的内存优化 - 响应式数据的读操作会
track
来收集依赖,写操作则是会trigger
来触发依赖回调
整个 reactive|readonly
的流程如下:
后续文章将会介绍 ref
、computed
、effect
这几个核心 API,以及将响应式系统流程串联起来,文章里的几个 TODO 后续都会讲到
参考文章
关联博客文章:Vue3 源码解析系列 - 响应式原理(reactive 篇)
也欢迎关注我的博客:Jooger.me