前言
文件总览
在正式进入源码之前,我们先总览一下src
目录下有那些文件
├── baseHandlers.ts
├── collectionHandlers.ts
├── computed.ts
├── effect.ts
├── index.ts // 主入口,暴露所有方法
├── operation.ts // 包含TrackOpTypes和TriggerOpTypes
├── reactive.ts
└── ref.ts
复制代码
根据这些文件,我们可以把响应式模块分为这几部分:effect
、reactive
、ref
、computed
、handlers
我们知道Vue3跟Vue2在响应式模块的实现区别就在于:从defineproperty
变成了proxy
,而handlers
就是对应创建proxy
对象时传入的handler
注意点
我们知道Vue响应式的核心原理就是:依赖收集和触发更新。通过学习effect
、reactive
、handlers
这几部分,你就能明白这大概是怎么回事了。
而在这几个模块中有几个点需要你留心,比如:
effect
中的track
和trigger
,这两个函数是依赖收集和触发更新的核心函数,是重中之重。在一开始看到这两个函数时,你可能会不理解其作用,但当你继续阅读其他模块,你就会恍然大悟。handlers
中的get
和set
,在这两个函数的内部做了许多事情,也是依赖收集和触发更新的主要入口
effect
effect
我们平常在使用effect
时,都会传入一个回调函数,在函数内部中对响应式数据进行监听,每次当响应式数据更新了,这个回调函数就会执行。如:
const num = ref(1) // 响应式数据
effect(() => console.log(num.value)) // effect
// 当响应式数据更新,则会触发回调函数
num.value = 2 // console.log(2)
复制代码
其内部是怎么实现的呢,现在让我们来看看effect
的源码
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
// 如果fn是effect则取出原始值
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options) // 调用createReactiveEffect创建effect
// 如果不为lazy,则立即执行effect
if (!options.lazy) {
effect()
}
return effect
}
复制代码
可以看到该函数接收的参数主要为两个,一个是回调函数(fn),一个是options
// options
export interface ReactiveEffectOptions {
lazy?: boolean // 是否延迟触发effect,正常是当数据还没更新之前都会触发一次
scheduler?: (job: ReactiveEffect) => void // 调度函数
onTrack?: (event: DebuggerEvent) => void // 监听track
onTrigger?: (event: DebuggerEvent) => void // 监听trigger
onStop?: () => void // 停止监听时触发
allowRecurse?: boolean
}
复制代码
effect
内部主要做了这些事:
- 判断
fn
,如果以及是effect
了,则取出其原始值 - 调用
createReactiveEffect
创建effect
- 如果
options
中没有设置lazy
,则立即执行effect
函数,最后返回effect
函数
我们可以看到其核心在于调用createReactiveEffect
创建effect
现在就让来看看这个函数
function createReactiveEffect<T = any>(
fn: () => T, // 之前传入的回调函数
options: ReactiveEffectOptions // 之前的options
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
// 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,
// 那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
try {
enableTracking() // 即可以追踪,用于后续触发 track 的判断
effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
activeEffect = effect // 为了收集依赖
return fn() // 执行回调函数,进行依赖收集
} finally {
effectStack.pop() // 弹出栈,退出收集依赖的状态
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 恢复原状
}
}
} as ReactiveEffect
// 挂载属性
effect.id = uid++
effect.allowRecurse = !!options.allowRecurse // 允许递归
effect._isEffect = true
effect.active = true // 是否激活
effect.raw = fn
effect.deps = [] // 持有当前 effect 的依赖(dep)数组
effect.options = options
return effect
}
复制代码
createReactiveEffect
的参数还是两个,与effect
相同。
其内部主要做了这些事:
- 定义一个
effect
函数 - 在这个
effect
函数上挂载属性 - 返回这个
effect
函数,即const effect = createReactiveEffect(fn, options)
的左边
让我们先来看看其挂载的属性,再看看这个effect
函数内部做了些什么
effect.id = uid++ // uid,标识effect的编号
effect.allowRecurse = !!options.allowRecurse
effect._isEffect = true // 标识是否是effect
effect.active = true // 是否为激活状态
effect.raw = fn // 保存原函数
effect.deps = [] // 持有当前 effect 的依赖(dep)数组
effect.options = options // 保存传入第二个参数options
复制代码
需要重点留心的是effect.deps
,这与接下来要讲的以及track
函数中的内容有密切的关系
让我们再回头看看新定义的effect
函数
const effect = function reactiveEffect(): unknown {
// 如果 effect 不是激活状态,这种情况发生在我们调用了 effect 中的 stop 方法之后,
// 那么先前没有传入调用 scheduler 函数的话,直接调用原始方法fn,否则直接返回。
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
try {
enableTracking() // 即可以追踪,用于后续触发 track 的判断
effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
activeEffect = effect // 为了收集依赖
return fn() // 执行回调函数
} finally {
effectStack.pop() // 弹出栈,退出收集依赖的状态
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // activeEffect恢复原状
}
}
} as ReactiveEffect
复制代码
这个函数内部主要做了这些事:
- 如果当前的
effect
不是激活状态,即已经被主动stop
了,如果之前的options
中没有scheduler
函数,那就直接调用fn
(回调函数),否则直接返回。 - 如果
effectStack
没有包含当前的effect
,就调用cleanup
清除之前的依赖,即effect.deps
function cleanup(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
复制代码
- 然后开始进入准备依赖收集的状态,调用
enableTracking
、将当前的effect
推入effectStack
中,并将当前effect
设置为activeEffect
(这一步是重点,与后面track函数被调用时息息相关),然后调用fn
(依赖收集的实际执行,后面到proxy-handler会再提到) - 最后将当前的
effect
出栈,调用resetTracking
,activeEffect
恢复原状。
我们看到在上面的步骤中调用了enableTracking
和resetTracking
,其作用又是什么呢?
let shouldTrack = true
const trackStack: boolean[] = []
export function pauseTracking() {
trackStack.push(shouldTrack)
shouldTrack = false
}
export function enableTracking() {
trackStack.push(shouldTrack)
shouldTrack = true
}
export function resetTracking() {
const last = trackStack.pop()
shouldTrack = last === undefined ? true : last
}
复制代码
我们可以看到这三个函数中都是在对shouldTrack
和trackStack
进行操作,需要注意的是shouldTrack
的值也是后面进行依赖收集时的重要因素,很快就会讲到。
总结:
effect
函数主要工作就是创建一个effect
函数,这个函数上挂载了许多属性,比较重要的例如deps
、active
、options
。在创建effect
函数的过程中,也对一些情况进行了处理,比如设置了lazy
来避免一开始就调用;还有将effect
函数再次作为回调函数传入。- 在其内部定义的
effect
函数主要都是为了之后进行依赖收集的过程。有一些需要留意的,这些都与依赖收集息息相关:effect.deps
、activeEffect
、shouldTrack
track
在对track
和trigger
两个函数进行阅读时,你可能疑惑,这两个函数并没有在effect
内部出现过,为什么说是核心呢?确实,这两个函数并没有在effect
中出现,其主要调用者是在reactive
创建的proxy
对象中的handler
和ref
内部等等。这是其他模块与effect
进行沟通的渠道。
现在的你在看这些函数的时候可能会一直半解,但当你阅读到proxy
的handler
,即函数的实际调用时,你就会明白了。
现在废话少说,让我们进入track
首先我们还得知道:在effect
模块中,维护了一个targetMap
,用于保存各个对象的依赖
const targetMap = new WeakMap<any, KeyToDepMap>()
复制代码
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 不进行依赖收集
if (!shouldTrack || activeEffect === undefined) {
return
}
// 开始依赖收集
// 获取触发对象的depsmap
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 获取depsmap中对应key的依赖集合
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
if (!dep.has(activeEffect)) {
dep.add(activeEffect) // 将activeEffect加入依赖set中
activeEffect.deps.push(dep) // 将set加入activeEffect的dep中,即deps为effect的依赖数组
// 如果为开发环境以及activeEffect有onTrack函数,则执行onTrack
if (__DEV__ && activeEffect.options.onTrack) {
activeEffect.options.onTrack({
effect: activeEffect,
target,
type,
key
})
}
}
}
复制代码
先来看看参数(target: object, type: TrackOpTypes, key: unknown)
,依次为触发对象、track
操作(分为get/has/iterate
)、触发对象的key(属性)
接下来看看其内部做了些什么:
- 检查
shouldTrack
和activeEffect
,这两个属性我们已经在effect
见到过了。当shouldTrack
为false
或没有activeEffect
,则不进行依赖收集 - 开始获取
target
的depsMap
,如果没有则进行创建 - 从
depsMap
中获取对应key
(属性)的依赖集合(dep),如果没有则进行创建 - 在拥有当前对象的属性的依赖集合后,如果集合中不包含
activeEffect
,则将activeEffect
加入到依赖集合中(dep),再将依赖集合加入activeEffect.deps
- 最后如果为开发环境且
activeEffect.options
中设置了onTrack
,就对其调用
总结:该函数的主要功能就是将effect
加入到目标对象对应属性的依赖集合(dep)之中,以实现依赖收集。
trigger
在track
函数中,我们知道:依赖都会被收集到targetMap
中。当监听的属性更新时,又要怎么对依赖进行通知呢,trigger
函数就是实现这一过程的函数。
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 对象没有被追踪,没有该对象的依赖时直接返回
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
const effects = new Set<ReactiveEffect>() // effects Set
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
// 根据类型来进行不同的操作-->将符合条件的dep添加到effects中
// CLEAR
if (type === TriggerOpTypes.CLEAR) {
depsMap.forEach(add)
} else if (key === 'length' && isArray(target)) {
// 如果target是数组,表示数组长度发生变化(变短)
//添加'length'的和key>=newValue的依赖
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
add(dep)
}
})
} else {
if (key !== void 0) {
add(depsMap.get(key))
}
switch (type) {
// ADD
case TriggerOpTypes.ADD:
// 如果不是数组
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
// 如果是map
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// 数组变长
// new index added to array -> length changes
add(depsMap.get('length'))
}
break
// DELETE
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
add(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
add(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
// SET
case TriggerOpTypes.SET:
if (isMap(target)) {
add(depsMap.get(ITERATE_KEY))
}
break
}
}
const run = (effect: ReactiveEffect) => {
// 如果effect.options中对trigger进行了监听
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
// 如果有调度函数
if (effect.options.scheduler) {
effect.options.scheduler(effect)
} else {
effect() // 执行effect
}
}
effects.forEach(run) // 把收集到的effects全部执行一遍
}
复制代码
我们再来看一下函数接收的参数:target: object, type: TriggerOpTypes, key?: unknown, newValue?: unknown, oldValue?: unknown, oldTarget?: Map<unknown, unknown> | Set<unknown>
其他没啥好说的,让我们看一下type:TriggerOpTypes
,trigger
操作分为clear/set/delete/add
,trigger
的操作方法和track
不同,它有着至关重要的作用,下面就会介绍到。
让我们来对这个函数的功能做一个大致的介绍,然后再仔细分析:
- 维护一个
effects
集合,用于存放要进行派发更新的依赖 - 根据
trigger
操作的不同,往effects
集合中加入符合条件的effect
- 遍历
effects
集合,执行集合内的每一个effect
现在开始仔细分析:
- 我们知道所有的依赖都被存放再
targetMap
中,因此一开始,我们肯定要获取target
的依赖,如果找不到,就表示没有依赖,也就不用进行派发更新了
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
复制代码
- 接下来开始维护一个
effects
集合,还有提供了一个add
函数来往集合内添加依赖
const effects = new Set<ReactiveEffect>() // effects Set
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) {
effectsToAdd.forEach(effect => {
// 如果当前effect不是活跃(不是正在收集依赖)或设置了allowRecurse就添加至effects
if (effect !== activeEffect || effect.allowRecurse) {
effects.add(effect)
}
})
}
}
复制代码
- 根据不同的操作类型来往
effects
中添加依赖:- 当
type
为clear
,target
的depsMap
全部添加 - 当
target
为数组,key
为length
,表示数组长度发生变化,将length
和key
大于新长度的项添加 - 当
type
为add
,如果target
不是数组,将对target
的ITERATE_KEY
的依赖集合加入,并且如果target
是map
,就将对target
的MAP_KEY_ITERATE_KEY
的依赖集合加入。如果target
是数组,如果数组长度变长了,就将对target
的length
依赖的集合加入effects
- 当
type
为delete
,如果target
不是数组,将对target
的ITERATE_KEY
的依赖集合加入,并且如果target
是map
,就将对target
的MAP_KEY_ITERATE_KEY
的依赖集合加入。 - 当
type
为set
,如果target
是map
,将对target
的ITERATE_KEY
的依赖集合加入
- 当
- 在上一步中,已经把需要派发更新的依赖添加完了,现在开始正式派发更新:
effects.forEach(run)
。run
函数的逻辑比较简单,就是执行effect
。
不知道你是否还记得effect
的内容是什么,让我们再回顾一下:
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect) // 清除依赖:当前effect的deps。避免依赖重复收集
try {
enableTracking() // 即可以追踪,用于后续触发 track 的判断
effectStack.push(effect) // 推入栈中,表示处于收集依赖的状态
activeEffect = effect // 为了收集依赖
return fn() // 执行回调函数
} finally {
effectStack.pop() // 弹出栈,退出收集依赖的状态
resetTracking()
activeEffect = effectStack[effectStack.length - 1] // 恢复原状
}
}
} as ReactiveEffect
复制代码
到这里你可能明白了,在派发更新的时候会重新进行依赖收集,fn
函数也会再执行一次
reactive
reactive
我们正常使用reactive
时是这样的const obj = reactive({name:'obj'})
,都是传入一个对象,然后返回一个响应式对象。
现在让我们开始进入源码
export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
// 如果对象只读,则直接返回
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target,
false,
mutableHandlers, // 对普通的引用数据类型的劫持(object/array)
mutableCollectionHandlers // 对集合类型的劫持(set/map/WeakMap/WeakSet)
)
}
复制代码
老惯例,看参数:target:object
。即传入一个对象。
现在看一下函数内部做了什么:
- 首先对传入的对象进行了判断,如果对象是只读的,表示不能被代理就直接返回
- 然后返回调用
createReactiveObject
函数,传入了四个参数。
可见核心实现都在这个函数中了,让我们看看这个函数到底是干什么的。
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
// 如果不是对象则直接返回
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// 对象已经是proxy
// proxy为响应式的(proxy有两种类型,一种为只读,一种为响应式)
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// target already has corresponding Proxy
// 如果对象上已经挂载了proxy(被proxy代理),则返回该proxy
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 对象类型在白名单(object/array/map/set/weakmap/weakset)内才能被劫持
const targetType = getTargetType(target)
if (targetType === TargetType.INVALID) {
return target
}
// 创建proxy
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
proxyMap.set(target, proxy)
return proxy
}
复制代码
看看刚刚传进入的4个参数到底对应什么:
target -> target: Target // 原对象
false -> isReadonly: boolean // 是否为只读
mutableHandlers -> baseHandlers: ProxyHandler<any> // handlers
mutableCollectionHandlers -> collectionHandlers: ProxyHandler<any> // handlers
复制代码
前两个好理解,后面两个handlers
是干嘛的?我们知道new proxy(target,handle)
语法是这样的。传入的两个handlers
即是为了后面创建proxy
所要传入的。关于handler
的具体内容将在后面提到,这也是关键的一环。
让我们正式进入函数内部:
-
首先先对
target
进行判断,如果target
不是对象就直接返回 -
然后如果传入的
target
已经是proxy
,并且不是只读或不是响应式的对象,就直接返回 -
然后根据函数接受的第二个参数
isReadonly: boolean
,去对应的Map
查看当前对象是否已经缓存过了(是否缓存的意思是:这个target
是否已经创建过proxy
对象了),如果已经缓存过,就直接返回之前创建的proxy
export const reactiveMap = new WeakMap<Target, any>() // 正常类型的缓存Map export const readonlyMap = new WeakMap<Target, any>() // 只读类型的缓存Map 复制代码
-
走到这一步,就表示
target
没有创建过proxy
。接下来开始获取target
的类型,因为要**根据类型来传入不同的handler
**来创建proxy
对象,如果类型是不可用则直接返回target
。获取类型的函数如下:const enum TargetType { INVALID = 0, // 不可用 COMMON = 1, // 普通类型,Object/Array COLLECTION = 2 // 集合类型,Map/Set/WeakMap/WeakSet } // 白名单内的对象类型 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 } } // 获取目标对象的类型 function getTargetType(value: Target) { return value[ReactiveFlags.SKIP] || !Object.isExtensible(value) ? TargetType.INVALID : targetTypeMap(toRawType(value)) } 复制代码
-
最后一步,就是根据
target
的类型来传入不同的handlers
来创建proxy
对象。然后进行缓存,返回proxy
对象。
到这里我们直接了解到reactive
是怎么创建一个响应式对象的,总结来说就是针对target
类型来创建不同的proxy
,还有对一些特殊情况进行处理,比如传入的不是对象、传入的对象已经是proxy
。
在这部分内容中没有提到依赖收集和派发更新相关的东西,因为核心奥秘是在于handlers
中。在下一小节中,我们就会揭开handlers
的面纱。
proxy-handler
相信你还记得,reacitve
在创建proxy
对象时,会根据target
的不同类型传入相应的handlers
。而target
主要分为common
(包含Object/Array
)和collection
(包含Map/Set/WeakMap/WeakSet
)。因此对应的handlers
也分成两大类,分别为baseHandlers
和collectionHandlers
baseHandlers
baseHanlers
这个大类中一共有4个handler
,分别为mutableHandlers
、readonlyHandlers
、shallowReactiveHandlers
、shallowReadonlyHandlers
。
我们主要学习的是:mutableHandlers
,因为当我们在调用reactive
时,其第三个参数传入的就是mutableHanlers
。可以返回上一节查看具体代码。
其他三个则是针对reactive
中提供的其他API,比如shallowReactive
、readonly
、shallowReadonly
相关的handler
。
现在让我们来看看mutableHandlers
:
export const mutableHandlers: ProxyHandler<object> = {
get,
set,
deleteProperty,
has,
ownKeys
}
复制代码
可以看到其内容其实是5个相关的操作函数,对比与defineProperty
中只对get
和set
进行重写,proxy
拓展了许多功能。
其实其他三个handlers
包含的内容也大致相同。区别在于:比如get
其实是调用了一个工厂函数来创建的,而不同的handlers
中调用工厂函数时,传入的参数不同:
const get = /*#__PURE__*/ createGetter()
const shallowGet = /*#__PURE__*/ createGetter(false, true)
const readonlyGet = /*#__PURE__*/ createGetter(true)
const shallowReadonlyGet = /*#__PURE__*/ createGetter(true, true)
复制代码
现在让我们具体看看这5个操作。
get
在上面我们看到,get
的创建其实是调用了createGetter
这个函数。那就让我们来看看这个函数:
function createGetter(isReadonly = false, shallow = false) {
return function get(target: Target, 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 ? readonlyMap : reactiveMap).get(target)
) {
return target
}
const targetIsArray = isArray(target) // 判断target是否为数组
// 如果target是数组且非只读,且key为改写过的数组方法,则调用arrayInstrumentations
// arrayInstrumentations是对数组方法的重写,对其获取res
if (!isReadonly && targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
const res = Reflect.get(target, key, receiver) // 获取值
// 如果是symbol类型的属性
// 然后为symbol本身的属性(在该set之中)或key为__proto__/_v_isRef
// 直接返回结果(不进行依赖收集)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: isNonTrackableKeys(key) // key为__proto__/_v_isRef
) {
return res
}
// 非只读情况下,进行依赖收集
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// 如果是浅响应式,直接返回res
if (shallow) {
return res
}
// 如果是ref类型的对象,则返回ref.value
if (isRef(res)) {
// ref unwrapping - does not apply for Array + integer key.
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
// 如果为对象,则递归处理,返回响应式对象
if (isObject(res)) {
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
复制代码
首先看一下参数,(isReadonly = false, shallow = false)
,这两个参数即标记只读和浅响应式。在上面提到的,主要是不同handlers
在创建get
的时候传入的这两个参数的不同。然后再看一下返回值,当调用这个工厂函数时就会返回一个get
函数。
现在让我们重点看看get
函数内部做了些什么。
-
首先对特殊的
key
进行了处理。当要访问的target
对象的key
是ReactiveFlags.IS_REACTIVE
时返回!isReadonly
;是ReactiveFlags.IS_READONLY
时返回isReadonly
;或者是ReactiveFlags.RAW
,且receiver
为map
(来源于reactive
)存储中的target
对象的proxy
时,则返回target
-
然后判断
target
是否为数组。如果是数组,且访问的key
是数组方法时,则从arrayInstrumentations
中获取对应的方法并调用(Reflect.get(arrayInstrumentations, key, receiver)
)。我们来看看arrayInstrumentations
是怎么做到数组方法改写的。const arrayInstrumentations: Record<string, Function> = {} // 一个对象,存放改写的数组方法 // 通过遍历,将方法重写并添加到arrayInstrumentations中。重写的方法内部会触发依赖收集。 ;(['includes', 'indexOf', 'lastIndexOf'] as const).forEach(key => { const method = Array.prototype[key] as any // 保存原方法 // 重写方法,并添加到arrayInstrumentations中 arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) { const arr = toRaw(this) // 获取调用该方法的源数组 for (let i = 0, l = this.length; i < l; i++) { track(arr, TrackOpTypes.GET, i + '') // 对数组的每一项都进行依赖收集 } const res = method.apply(arr, args) // 调用原方法 if (res === -1 || res === false) { return method.apply(arr, args.map(toRaw)) // 如果调用失败,使用原始值进行重新调用 } else { return res } } }) ;(['push', 'pop', 'shift', 'unshift', 'splice'] as const).forEach(key => { const method = Array.prototype[key] as any // 保存原方法 // 重写方法,并添加到arrayInstrumentations中 arrayInstrumentations[key] = function(this: unknown[], ...args: unknown[]) { pauseTracking() // 更改 shouldTrack状态和trackStack,来源于effect const res = method.apply(this, args) // 调用原方法 resetTracking() // 重置 shouldTrack状态和trackStack,来源于effect return res } }) 复制代码
-
继续返回流程。如果当
target
不是数组,且key
不是改写的方法列表中。则进行普通的获取值,const res = Reflect.get(target, key, receiver)
。 -
当获取到值之后,先处理一下特殊情况:如果
key
是一个symbol
且为symbol
本身的属性或者key
是__proto__,__v_isRef,__isVue
,直接返回值。因为当符合这两种情况时,是不需要进行依赖收集的。 -
然后开始针对工厂函数传入的参数来进行操作。如果是非只读时,则调用
track
进行依赖收集。然后如果是浅响应式时,则直接返回值。 -
最后根据
res
的类型来进行处理:当res
是一个ref
时,则返回ref.value
;当res
是一个对象时,则对其进行使用reactive/readonly
包装,并返回(这是实现深度监听的关键);如果不是这两种情况,则正常的返回res
。
触发时机
让我们再回顾一下effect
跟reactive
的正常配合使用。
let obj = reactive({name:'obj'}) // 使用reactive创建一个响应式对象
effect(() => console.log(obj.name)) // 对这个响应式对象添加依赖
复制代码
我们知道reactive
的根本就是创建一个proxy
对象。当对这个对象进行不同的操作时,则会触发不同的handle
。
让我们再回顾一下effect
中的核心代码:
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
enableTracking()
effectStack.push(effect)
activeEffect = effect
return fn() // 执行回调函数
} finally {
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
复制代码
简述一下当调用该函数时,会先清理之前的依赖。然后开启依赖收集的准备工作,然后就调用回调函数fn
,最后恢复原状态。
而且我们知道:在创建响应式对象的时候是没有触发依赖收集的,需要当对响应式的属性进行访问(执行get
)操作时才会触发。而effect
内部中也没有看到有调用track
的地方。那到底是怎么进行依赖收集的呢?
其实重点就在于effect
中fn
函数:上面的示例中调用effect
时传入的回调函数是() => console.log(obj.name)
,在这个函数内部对响应式对象的属性(obj.name
)进行了访问,当effect
中执行fn
时,就触发了响应式对象的对应key
的get
操作。而在get
的内部中又会调用了track
。因此就这样进行了依赖收集。
总结一下:当调用effect
传入的回调函数中与响应式对象的属性有关系时,那么当effect
函数被执行时,回调函数在其内部也会执行,因此触发了响应式对象的get
操作,而在get
操作内部会调用track
。因此就进行了依赖收集。因此实际effect
中的依赖收集是在fn
的调用。
set
set
也和get
一样,都是由一个工厂函数创建的。这个工厂函数为createSetter
function 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)
// 如果target不是数组且旧值为ref类型,新值不为ref类型
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
oldValue.value = value // 将新值赋给旧值(ref类型)的value,让旧值来处理trigger
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 如果target为数组且key为数字类型,则通过下标判断是否有该key,或则通过hasOwn函数
// hadKey:为了后续的触发更新操作,判断是新增或修改
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver) // 设置值
// don't trigger if target is something up in the prototype chain of original
// 当target不是原型链上的值,此时触发trigger。
// 因为当target是原型链上的值时,设置值的操作起作用的是receiver而不是target,因此不应该对target触发更新
if (target === toRaw(receiver)) {
// 如果target中没有该属性(key),则调用trigger触发add,即新增
// 或则调用trigger触发set操作,即修改
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value) // add操作
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue) // set操作
}
}
return result
}
}
复制代码
老规矩,先看看参数(shallow = false)
,跟createGetter
一样,这个参数主要是为了不同handlers
创建get
时能有区别。同样,返回值是一个get
函数。
现在看看其内部做了哪些事:
- 首先对旧值进行了保存。
- 当为非浅响应式时,如果
target
不为数组,然后旧值为ref
类型但新值不为ref
类型时。就将新值赋给旧值(oldValue.value = value
)。相当于让ref
来实现派发更新。 - 然后判断
target
上是否拥有key
来定义hadKey
。这是为了确认操作是为新增还是修改。以便于调用trigger
派发更新时,能够传入正确的操作类型。 - 通过
const result = Reflect.set(target, key, value, receiver)
设置值。 - 接下来开始派发更新。当
target
不是原型链上的值时,根据之前获取hadKey
来进行不同的trigger
。当为新增时:trigger(target, TriggerOpTypes.ADD, key, value)
当为修改时:trigger(target, TriggerOpTypes.SET, key, value, oldValue)
- 返回
result
deleteProperty
function deleteProperty(target: object, key: string | symbol): boolean {
const hadKey = hasOwn(target, key) // 是否为对象本身的属性
const oldValue = (target as any)[key] // 保存旧值
const result = Reflect.deleteProperty(target, key)
// 触发更新
if (result && hadKey) {
trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
}
return result
}
复制代码
跟get/set
不同的是,deleteProperty
不是通过工厂函数生成的。因此我们直接看看它内部做了些什么:
- 先判断
key
是否是存在于target
- 保存旧值
- 调用
const result = Reflect.deleteProperty(target, key)
进行正式的deleteProperty
操作,并储存是否删除成功 - 当删除成功且
key
是target
上的属性时,进行派发更新trigger(target, TriggerOpTypes.DELETE, key, undefined, oldValue)
- 最后返回删除结果
has
function has(target: object, key: string | symbol): boolean {
const result = Reflect.has(target, key)
// 若属性不是symbol或symbol本身的属性,进行依赖收集
if (!isSymbol(key) || !builtInSymbols.has(key)) {
track(target, TrackOpTypes.HAS, key)
}
return result
}
复制代码
如上,直接看看函数内部:
- 直接调用
const result = Reflect.has(target, key)
并保存查询结果 - 如果属性不是
symbol
或者symbol
本身的属性时,调用track
进行依赖收集 - 返回结果
ownKeys
function ownKeys(target: object): (string | number | symbol)[] {
// 依赖收集
track(target, TrackOpTypes.ITERATE, isArray(target) ? 'length' : ITERATE_KEY)
return Reflect.ownKeys(target)
}
复制代码
这个函数很简单,先track
进行依赖收集,然后调用Reflect.ownKeys(target)
返回结果
collectionHandlers
ref
ref
和reactive
一样,ref
方法也是用来创建一个响应式对象。但区别在于,ref
传入的值一般为基本数据类型而不是引用数据类型。且在访问通过ref
创建的响应式对象时,都要通过.value
。
现在我们就来深入源码,看看它的内部奥秘
export function ref(value?: unknown) {
return createRef(value)
}
复制代码
和reactive
一样,其内部很简单,都是调用了另外一个函数来创建对象。让我们来看看createRef
function createRef(rawValue: unknown, shallow = false) {
// 如果value已经是ref了,直接返回
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
复制代码
这个函数也比较简单,首先先对传入的值进行了判断,如果传入的值已经是ref
类型了,就直接返回,否则就调用new RefImpl()
来创建一个新的ref
对象
class RefImpl<T> {
private _value: T
public readonly __v_isRef = true
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue) // 非shallow,调用convert,并传入value
}
get value() {
track(toRaw(this), TrackOpTypes.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), TriggerOpTypes.SET, 'value', newVal) // 触发更新
}
}
}
复制代码
首先我们来看看它的构造函数:
constructor(private _rawValue: T, public readonly _shallow = false) {
this._value = _shallow ? _rawValue : convert(_rawValue)
}
复制代码
构造函数中接受了两个参数:(_rawVlaue, _shallow)
,即初始值和是否为浅响应式。然后构造函数主要就是将传入的初始值赋值给_value
。当为浅响应式时,直接赋值,否则将调用convert(_rawValue)
然后再将返回值赋值给_value
。
const convert = <T extends unknown>(val: T): T =>
isObject(val) ? reactive(val) : val // 如果是对象则用reactive代理
复制代码
convert
比较简单,就是当传入的值是对象时返回用reactive
包裹的值,否则直接返回val
。所以当调用ref
传入对象时,其实内部还是调用了reactive
接下来看看另外的两个方法:
get value() {
track(toRaw(this), TrackOpTypes.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), TriggerOpTypes.SET, 'value', newVal) // 触发更新
}
}
复制代码
可以看到都是对value
的操作,现在你知道为什么方法ref
类型的值时都需要通过.value
的原因了吧。
get
的内部很简单,就是调用track
进行依赖收集,然后返回值set
则是当值进行更新时,将新值赋值给原始值,再重复构造函数中的操作this._value = this._shallow ? newVal : convert(newVal)
。最后调用trigger
进行派发更新。
ref
讲完了,可以知道跟reactive
的区别除了之前提到的那些之外。还有这些区别:
- 用
reactive
创建的对象是一个proxy
,而ref
不是。 reactive
进行依赖收集和派发更新的位置是在handlers
中代理的方法,而ref
则是在get/set
之中。
computed
在正式进入源码之前,我们有必要先看看它的使用方法
const state = reactive({name:'obj',age:18})
const computedAge = computed(() => state.age + 10)
const computedName = computed({
get() {
return state.name + '123'
},
set(val) {
state.name = val
}
})
复制代码
在平常使用中,我们一般是传入一个回调函数,但其实computed
也接受一个包含get
和set
的对象。
现在我们进入源码一探究竟:
export function computed<T>(
(
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
// 根据传入的函数/包含get和set的对象生成getter和setter
if (isFunction(getterOrOptions)) {
getter = getterOrOptions // getter赋值为函数
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
// 创建ComputedRefImpl
return new ComputedRefImpl(
getter,
setter,
isFunction(getterOrOptions) || !getterOrOptions.set // 当传入的是函数时为true
) as any
}
复制代码
首先看参数:(getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>)
,我们在上面提到了,可以接受一个函数或者一个带着get
和set
的对象。
函数内部主要做了两件事:
- 根据传入的参数来生成
getter/setter
。当传入的是函数时,则将函数赋值给getter
;当传入的是对象时,则对应的将get/set
赋值给getter/setter
- 调用
ComputedRefImpl
实例化一个对象并返回。
重点在于computedRefImpl
这个类:
class ComputedRefImpl<T> {
private _value!: T
private _dirty = true // 标记是否缓存
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true; // 标识为是ref对象
public readonly [ReactiveFlags.IS_READONLY]: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
// 调用effect方法对传入的getter进行响应式包装
// 后面的对象是options
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value') // 触发更新,所有computed的依赖都会进行更新
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly // 设置flag
}
get value() {
// 当依赖发生改变时
if (this._dirty) {
this._value = this.effect() // 调用effect函数重写获取值
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value') // 对computed依赖进行收集
return this._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
复制代码
我们先来看看包含的属性:
private _value!: T // 值
private _dirty = true // 标记是否缓存
public readonly effect: ReactiveEffect<T> // 依赖
public readonly __v_isRef = true; // 标识为是ref对象
public readonly [ReactiveFlags.IS_READONLY]: boolean // reactive Flag
复制代码
可以看到属性中出现了我们十分熟悉的:effect
。其实computed
的核心就是在其内部使用了effect
来添加依赖,因此也具有和effect
相同的效果。还有一个**_dirty
属性,这是computed
进行缓存更新的关键之一**。
接下来我们继续看看构造函数:
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean
) {
// 调用effect方法对传入的getter进行响应式包装
// 后面的对象是options
this.effect = effect(getter, {
lazy: true,
scheduler: () => {
if (!this._dirty) {
this._dirty = true
trigger(toRaw(this), TriggerOpTypes.SET, 'value') // 触发更新
}
}
})
this[ReactiveFlags.IS_READONLY] = isReadonly // 设置flag
}
复制代码
参数一共有三个,分别为getter
、settter
、isReadonly
。我们在刚刚的computed
函数中也看到了实际传入的会有两种情况:
- 当传入的
computed
的是一个函数时,则此时传入ComputedRefImpl
构造函数的参数分别对应:回调函数、一个提示函数以及true
- 当传入的是一个带有
get/set
的对象时,传入构造函数的实参则是分别对应:对象中的get
、对象中的set
、false
构造函数做的事很简单:就是创建一个effect
,并赋值给this.effect
,进行缓存(这也是跟effect
的区别之一,computed
会进行缓存)。重点在于创建effect
时,传入的参数。第一个参数是getter
,第二个参数我们知道是一个options
对象,这里包含了lazy
和一个scheduler
调度函数。
function effect<T = any>(): ReactiveEffect<T> {
const effect = createReactiveEffect(fn, options) // 创建effect
// 如果不为lazy,则立即执行effect
if (!options.lazy) {
effect()
}
return effect
}
复制代码
lazy
的效果我们可以从effect
的源码中知道。就是创建完effect
后不会立即执行。而scheduler
则是在trigger
的中的run
函数会被调用。
export function trigger() {
const run = (effect: ReactiveEffect) => {
if (__DEV__ && effect.options.onTrigger) {
effect.options.onTrigger({
effect,
target,
key,
type,
newValue,
oldValue,
oldTarget
})
}
if (effect.options.scheduler) {
effect.options.scheduler(effect) // 调度函数被调用
} else {
effect()
}
}
effects.forEach(run)
}
复制代码
当每次trigger
之后,就会调用scheduler
函数。而在此处设置的scheduler
中内部是主要是对_dirty
进行了设置还有触发trigger
函数(trigger
函数会对computed
的所有依赖进行派发更新)
现在我们来看看 get value()/set value()
:
get value() {
// 当依赖发生改变时
if (this._dirty) {
this._value = this.effect() // 调用effect函数获取值
this._dirty = false
}
track(toRaw(this), TrackOpTypes.GET, 'value') // 依赖收集
return this._value
}
set value(newValue: T) {
this._setter(newValue)
}
复制代码
set
:当对computed
直接设置值时,则会调用setter
。如果调。用computed
传入的是一个函数时,则会调用设置的警告函数。否则则调用传入对象的set
- 而
get
中,首先会对_dirty
属性进行判断。如果_dirty
为true
,则表示依赖发生改变,因此需要调用this.effect
获取最新值,并修改_dirty
。然后调用track
进行依赖收集并返回值
可见重点在于this._dirty
的变化。当getter
依赖的响应式数据更新时,会调用到设置好的scheduler
调度函数,_dirty
会被设置为true
,表示数据发生改变。然后当再次访问computed
的值时,会重新调用this.effect
来获取新值,并将_dirty
的值恢复成false
。