前言
文件总览
在正式进入源码之前,我们先总览一下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对象了),如果已经缓存过,就直接返回之前创建的proxyexport 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。