简介
vue的响应式如下图所示,一句话概括就是在对被代理的对象访问/修改的过程中完成组件的更新,触发patch方法 完成diff找到需要更新的dom并完成更新。
定义响应式对象
ref
官网对ref的说明是 接受一个内部值,返回一个响应式的、可更改的 ref 对象,此对象只有一个指向其内部值的属性 `.value`
接下来让咱们一起看一下vue源码中ref的实现吧, 核心代码如下: 点我查看源码
// 代码对应目录 packages/reactivity/src/ref.ts
// ref的内部是通过createRef来常见的 createRef大致只做了饭回了这个RefImpl类的实现 这里不展开createRef 有兴趣可自行查看 shadowRef的实现也是如此
class RefImpl<T> {
private _value: T
private _rawValue: T
public dep?: Dep = undefined
public readonly __v_isRef = true
constructor(
value: T,
public readonly __v_isShallow: boolean
) {
this._rawValue = __v_isShallow ? value : toRaw(value)
// 这里其实是对value的类型做了判断 如果是obkect 则内部调用了reactive
this._value = __v_isShallow ? value : toReactive(value)
}
get value() {
// 依赖收集
trackRefValue(this)
return this._value
}
set value(newVal) {
const useDirectValue =
this.__v_isShallow || isShallow(newVal) || isReadonly(newVal)
newVal = useDirectValue ? newVal : toRaw(newVal)
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal
this._value = useDirectValue ? newVal : toReactive(newVal)
// 依赖出发
triggerRefValue(this, newVal)
}
}
}
从代码中可以看到, ref其实就是将原始值包裹为一个对象, 通过对value的get和set去收集和触发依赖
reactive
reactive和ref不同的是 他所接收的值必须是一个object类型(ref的object内部也是reactive实现), 这是因为proxy的代理是代理的对象
以下是vue中reactive的部分实现点我查看源码
// 代码对应目录 packages/reactivity/src/reactive.ts
// 如上方的createRef一样 reactive内部也是用这个方法实现的
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>,
proxyMap: WeakMap<Target, any>
) {
// 省略前置代码
// 这里出现了代理方式的不一致 其实主要是对map weakMap set weakSet的代理做了单独的劫持 这里不做展示 以普通对象为主 感兴趣可自行观看
const proxy = new Proxy(
target,
targetType === TargetType.COLLECTION ? collectionHandlers : baseHandlers
)
return proxy
}
// 代码对应目录 packages/reactivity/src/baseHandlers.ts
// 由于baseHandlers是继承了基类ProxyHandler 所以为了方便将他们合成一个对象
// 本次只分析get/set 其余的实现大同小异 带入理解即可
baseHandlers: {
get(target: Target, key: string | symbol, receiver: object) {
// ...省略部分代码
// 这个判断单独拿出来讲是因为在针对数组的情况下做了一些特殊的处理 我会在最后去详细的介绍这个方法
//感兴趣的也可以自行查看packages/reactivity/src/baseHandlers.ts里的createArrayInstrumentations方法
const targetIsArray = isArray(target)
if (!isReadonly) {
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
if (key === 'hasOwnProperty') {
return hasOwnProperty
}
}
// ... 省略部分代码
// 当不是只读时进行依赖收集(因为只读的Object不可更改 收集他的依赖其实没啥用)
if (!isReadonly) {
track(target, TrackOpTypes.GET, key)
}
// ... 省略部分代码
return Reflect.get(target, key, receiver)
},
set(target: object, key: string | symbol, value: unknown, receiver: object): boolean {
// ... 省略部分代码
// 判断数组的add/set
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
if (target === toRaw(receiver)) {
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
reactive和ref的工作大同小异, get收集依赖,set触发依赖, 唯一有区别的就是劫持了数组的一些方法。
effect
上面讲了vue收集和触发依赖的过程, 接下来讲一下vue收集的依赖,所谓的依赖 在vue的文档中被称为副作用函数, 是指在某个响应式对象的值发生变化时执行的函数, 我将effect分为两种, 一种是组件effect, 一种为数据effect, 之前看过一些vue2的源码解析, 组件effect被称为渲染watcher, 我觉得这两种叫法相差不大, 如有差别请指出。
- 首先先说一下组件的effect, 组件的effect其实就是组件内部定义的update方法, 在依赖触发之后对组件进行patch->diff->rerender, 完成页面数据的更新。
- 接下来说一下数据的effect, 这个其实就是我们日常开发中用到的API(watch、watchEffect,computed等), 显示的指定一个副作用函数, 在相关依赖触发之后去调用该函数. 那么接下来让我们一起看一下effect的实现吧点我查看源码
// 代码对应目录 packages/reactivity/src/effect.ts
// effect的核心实现是ReactiveEffect类
class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
/**
* Can be attached after creation
* @internal
*/
computed?: ComputedRefImpl<T>
/**
* @internal
*/
allowRecurse?: boolean
/**
* @internal
*/
private deferStop?: boolean
onStop?: () => void
// dev only
onTrack?: (event: DebuggerEvent) => void
// dev only
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
// 执行副作用函数, 设置当前effect为激活的effect, 方便函数内的依赖收集
run() {
if (!this.active) {
return this.fn()
}
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
try {
this.parent = activeEffect
activeEffect = this
shouldTrack = true
trackOpBit = 1 << ++effectTrackDepth
if (effectTrackDepth <= maxMarkerBits) {
initDepMarkers(this)
} else {
cleanupEffect(this)
}
return this.fn()
} finally {
if (effectTrackDepth <= maxMarkerBits) {
finalizeDepMarkers(this)
}
trackOpBit = 1 << --effectTrackDepth
activeEffect = this.parent
shouldTrack = lastShouldTrack
this.parent = undefined
if (this.deferStop) {
this.stop()
}
}
}
stop() {
// stopped while running itself - defer the cleanup
if (activeEffect === this) {
this.deferStop = true
} else if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
ReactiveEffect的实现还是比较简单的, 其中内置了两个方法,run和stop, 顾名思义就是运行和停止副作用函数, 其中需要注意的是这个activeEffect, 拿watchEffect这个Api举例, watchEffect接受一个函数, 会立即运行run方法, 此时会访问某个响应式对象, 触发get拦截器, 然后在track函数中收集activeEffect, 也就是watchEffect的函数参数, 这也就是watchEffect为什么会自动收集依赖的原因了。
依赖的收集和触发
收集
在上面ref和reactive的实现中可以看到, vue的依赖收集重要是拦截get, 然后通过trackRefValue/track函数实现依赖收集
// 代码对应目录 packages/reactivity/src/effect.ts
// 由于track和trackRefValue的实现并无太大差别, 且最终实现都是trackEffects方法, 所以此处直接分析trackEffects
export function trackEffects(dep: Dep,debuggerEventExtraInfo?:DebuggerEventExtraInfo) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
// 这个是在判断当前副作用有没有在dep数组中
if (!newTracked(dep)) {
dep.n |= trackOpBit // set newly tracked
shouldTrack = !wasTracked(dep)
}
} else {
// Full cleanup mode.
shouldTrack = !dep.has(activeEffect!)
}
if (shouldTrack) {
dep.add(activeEffect!)
activeEffect!.deps.push(dep)
if (__DEV__ && activeEffect!.onTrack) {
activeEffect!.onTrack(
extend(
{
effect: activeEffect!
},
debuggerEventExtraInfo!
)
)
}
}
}
在上面代码中可以看到, trackEffects本质上就是将activeEffects加入到了自己的依赖收集数组中, 这也正对应了上面的watchEffect的自动依赖收集的过程。
触发
触发和收集有一点不同, reactive的触发设置了三种不同的触发类型(add, set, delete),接下来一一分析各自的简单实现。点击看源码
// 代码对应目录 packages/reactivity/src/ref.ts
// ref的依赖触发
function triggerRefValue(ref: RefBase<any>, newVal?: any) {
ref = toRaw(ref)
const dep = ref.dep
if (dep) {
triggerEffects(dep)
}
}
// 代码对应目录 packages/reactivity/src/effect.ts
// reactive的依赖触发
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) {
// never been tracked
return
}
let deps: (Dep | undefined)[] = []
if (type === TriggerOpTypes.CLEAR) {
// collection being cleared
// trigger all effects for target
deps = [...depsMap.values()]
} else if (key === 'length' && isArray(target)) {
const newLength = Number(newValue)
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= newLength) {
deps.push(dep)
}
})
} else {
// schedule runs for SET | ADD | DELETE
if (key !== void 0) {
deps.push(depsMap.get(key))
}
// also run for iteration key on ADD | DELETE | Map.SET
switch (type) {
case TriggerOpTypes.ADD:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
} else if (isIntegerKey(key)) {
// new index added to array -> length changes
deps.push(depsMap.get('length'))
}
break
case TriggerOpTypes.DELETE:
if (!isArray(target)) {
deps.push(depsMap.get(ITERATE_KEY))
if (isMap(target)) {
deps.push(depsMap.get(MAP_KEY_ITERATE_KEY))
}
}
break
case TriggerOpTypes.SET:
if (isMap(target)) {
deps.push(depsMap.get(ITERATE_KEY))
}
break
}
}
if (deps.length === 1) {
if (deps[0]) {
triggerEffects(deps[0])
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
triggerEffects(createDep(effects))
}
}
// 代码目录 packages/reactivity/src/effect.ts
function triggerEffects(dep: Dep | ReactiveEffect[], debuggerEventExtraInfo?: DebuggerEventExtraInfo) {
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
if (effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
for (const effect of effects) {
if (!effect.computed) {
triggerEffect(effect, debuggerEventExtraInfo)
}
}
}
function triggerEffect(effect: ReactiveEffect) {
if (effect !== activeEffect || effect.allowRecurse) {
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
可以看到在trigger方法中, triggerType被分成了三种类型, 大部分都是对数组和map做了一些处理,这里的细节大家可以直接看源码, 这里就不展开讲了。
组件effect的收集
组件effect的依赖收集主要发生在setup调用的时候, steup调用的时候会将activeEffect设置为当前组件的effect, 然后在render的时候 模板里的变量会被访问, 此时就会完成响应式数据对组件effect的收集, 这也就是为什么数据更新之后组件也会随之更新.
在上图的调用堆栈中可以看到组件的挂载中会执行一次组件的effect,然后在steup函数中完成了对组件effect的依赖收集。
一个小问题(或者说疑惑)
- toRefs的问题
上面这张图是使用toRefs之后 open变量的依赖收集情况
下面的是我修改完之后直接返回state之后 open变量的依赖收集情况
这样的话我们是不是可以定义多个响应式对象然后可以分别返回呢, 这样的话会减少一部分的依赖收集情况
- toRefs除了造成多个get拦截之外, 还会将一些定义在响应式对象内但是不需要给模板使用的变量的依赖添加组件的effect, 造成不必要的渲染(这点还未验证, 只是一个猜想, 希望有懂的朋友一起交流一下啊)
数组的劫持
最后一个分类讲一下arrayInstrumentations, 这个是对数组的一些方法进行了劫持, 在调用这些方法的时候去选择性的收集依赖。点击查看源码
- 在vue3中, 对数组的includes、indexOf、findLastIndex进行了劫持, 对他们的下标进行了依赖收集, 这个也很好理解, 可以直接操作数组的下标对某一项进行赋值,从而触发依赖。
- 除此之外, 还对push、pop、shift、unshift、splice这些可以改变数组长度的方法进行了劫持,并且在这些方法执行的时候暂停了依赖的收集, 这其实是因为proxy代理数组的话, 通过这些方法修改数组的话会多触发一次set劫持, 劫持的key为length, 所有vue只处理了key为length的情况, 在调用这些方法时选择手动暂停了依赖收集. 这有一个proxy代理数组的demo 大家可以试一下看看触发了几次
写在最后
以上就是我对响应式原理的一些简单理解了, 希望可以对你有一些帮助, 如果有别的意见或建议请在评论区指出, 大家一起讨论一下,以上就是本期的文章了, 等下期随缘更新咯。