前言
本文将阅读源码、简化源码从而分析vue响应式的原理。
请知悉:
- 本文源码经过了一定的简化,去掉了部分干扰的代码逻辑。
- 这是我阅读源码分析得出的结论,不一定100%准确。
reactive
在vue的官方文档中ref的介绍部分有这么一句话。
如果将一个对象赋值给 ref,那么这个对象将通过reactive转为具有深层次响应式的对象
如果 const demo=ref({})内部实际上是调用了reactive。因此需要先了解一下reactive。
ok那么走进源码。
// 假设需要新建一个名为formData的reactive数据
const formData=reactive({
name:'张三',
});
export function reactive(target: object) {
const proxy = new Proxy(
target,
new MutableReactiveHandler(),
);
return proxy;
}
// 去掉一堆干扰的代码之后只剩下一个很简单的新建了一个Proxy代理对象,然后把它return了出去
// formData = new Proxy({name:"张三"}, new MutableReactiveHandler());
所以这里得出一个非常简单的结论,reactive的核心实际上就是Proxy。
如果你对Proxy不了解,请看
Proxy - ECMAScript 6入门 (ruanyifeng.com)然后再接着阅读。
在了解Proxy是什么了之后接着阅读,看看new MutableReactiveHandler()是什么。
// 这里使用了es6的class语法,如果遇到不懂的写法、语法也可以查阅上面proxy链接的资料。
class MutableReactiveHandler {
get(target: Target, key: string | symbol, receiver: object) {
// Reflect同样是es6新增的与Proxy配合用的东西,如果不懂也请查阅上面资料
// 通过Reflect读目标对象的指定属性
const res = Reflect.get(target, key, receiver);
// 请记住这里触发track,暂时不去想track里面干了什么
track(target, TrackOpTypes.GET, key)
return res;
}
set(target: object,key: string | symbol,value: unknown,receiver: object) {
// 简单判断了一下数组和对象的情况。数组key则是下标,对象key则是属性key
const hadKey = isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 通过Reflect给目标对象指定key设置value
const result = Reflect.set(target, key, value, receiver);
// 请记住这里触发了trigger,暂时不去想trigger里面干了什么
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
return result;
}
// 这里省略一些其他deleteProperty、has、ownKeys等函数
}
// 所以看到这里实际上干了这么一个事情
const formData = new Proxy({name:"张三"}, {
get:()=>{
// 通过Reflect取值
// 触发track函数
},
set:()=>{
// 通过Reflect设置值
// 触发trigger函数
}
});
// 在了解Proxy的用法之后你就会了解,假如我
console.log(formData.name);
// 则会触发get函数,通过Reflect向{name:"张三"}取出key为name的value"张三"
// 然后触发了一个track函数
formData.name="李四";
// 则会触发set函数,通过Reflect向{name:"张三"}设置key为name的value为李四
// 然后触发了一个trigger函数
所以到目前为止reactive响应式的原理是:创建了一个被Proxy代理的对象,Proxy里面代理了各种操作,在读取的时候触发track函数,在写入的时候触发trigger函数。
咱们先不去想track函数和trigger函数,因为ref里面同样也用了,ref讲完后再继续往更深的地方分析。
ref
// 就是简单的创实例化了RefImpl
export function ref(value?: unknown) {
return new RefImpl(rawValue, false)
}
class RefImpl<T> {
private _value: T // 当前的value,也是ref.value所访问的值
private _rawValue: T // 原始值
public dep?: Dep = undefined // 这个ref关联的东西
constructor(
value: T,
public readonly __v_isShallow: boolean,
) {
this._rawValue = value;
// 如果是object就内部调用reactive
// 这里关联一下前面提到的ref内部中如果遇到是对象则创建了一个reactive
this._value = isObject(value) ? reactive(value) : value;
}
// es6的getter方法 const a=new RefImpl(); a.value时会触发这个函数
get value() {
trackRefValue(this); // get的时候触发track
return this._value
}
set value(newVal) {
// 判断一下新老value是否改变
if (hasChanged(newVal, this._rawValue)) {
const oldVal = this._rawValue
this._rawValue = newVal
this._value = newVal
// set的时候触发trigger
triggerRefValue(this, DirtyLevels.Dirty, newVal, oldVal)
}
}
}
所以ref的本质就是实例化了RefImpl类得到了一个对象,访问这个对象的value属性时触发track,设置这个对象的value属性时触发trigger
ref 和 reactive不同
对于ref而言响应式的关键在于自定义class的getter和setter(为什么ref要.value,因为ref返回的是一个对象,它有一个value属性指向原始值而已)
reactive则是Proxy
track和trackRefValue
经过上面的分析,我们知道了对ref或reactive进行访问时会触发track,接下来一起看看track是什么
// 请注意:这个全局的map非常重要
// targetMap key是reactive的对象,value是一个depMap
// 例如: const formData=reactive({}); key则是formData
// depMap是一个map,里面存放的是依赖这个reactive的相关回调
const targetMap = new WeakMap<object, KeyToDepMap>();
// 整个targetMap的结构应该是
targetMap={
// formData不是字符串,formData这里代指实例化的reactive;const formData=reactive({name:'123'});
formData:{
// name是字符串,用get的key作为depsMap的key
name:{
// effect也不是字符串,用effect作为depMap的key
effect:7, // value是一个number, 是effect的_trackId
}
}
}
export function track(target: object, type: TrackOpTypes, key: unknown) {
// 先看看这个reactive没有已经绑定好的depMap,有就取出来,没有就新建一个
// target是例子中的formData
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap)
}
// 然后再看depMap里面有没有这个key相关的map,有就取出来,没有就新建一个
// key是例子中的name
let dep = depsMap.get(key);
if (!dep) {
dep = new Map();
dep.cleanup = () => depsMap!.delete(key);
depsMap.set(key, dep)
}
// 这里的activeEffect是如何构建的先暂时无视
trackEffect(
activeEffect,
dep,
void 0,
)
}
// effect大概长这样
effect={
deps = []
_depsLength = 0
_trackId = 0
fn ; // fn存放的是getter函数
scheduler // 调度器执行函数
}
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
dep.set(effect, effect._trackId);
}
在track之前全局已经构建好了全局的effect对象, 然后在track中把effct对象放入全局的targetMap。
trackRefValue
export function trackRefValue(ref: RefBase<any>) {
ref = toRaw(ref);
ref.dep ??= createDep(() => (ref.dep = undefined),ref);
trackEffect(
activeEffect,
ref.dep,
void 0,
)
}
export function trackEffect(
effect: ReactiveEffect,
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
// 所以ref和reactive的区别在于dep的存放位置,reactive是全局的
// ref存放在ref对象中的dep属性中
dep.set(effect, effect._trackId)
}
// 为什么reactive是全局,ref存在自己的属性中?
// 我认为是因为reactive的proxy对象,proxy对象无法额外添加属性,但是又不能直接改原对象,只能这样搞了。
trigger和triggerRefValue
经过上面的分析,我们知道了对ref或reactive进行set操作时会触发trigger,接下来一起看看trigger是什么
trigger
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>,
) {
// 从全局的targetMap取出depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
let deps: (Dep | undefined)[] = []
// 根据读的key去取出dep放入数组中
deps.push(depsMap.get(key))
pauseScheduling() // 暂停调度
for (const dep of deps) {
if (dep) {
triggerEffects(
dep,
DirtyLevels.Dirty,
void 0,
)
}
}
resetScheduling() // 恢复调度
}
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
// 遍历effect
for (const effect of dep.keys()) {
// 把调度器放入队列中
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
}
}
resetScheduling()
}
export function resetScheduling() {
pauseScheduleStack--
while (!pauseScheduleStack && queueEffectSchedulers.length) {
// 把每个调度器取出来执行
queueEffectSchedulers.shift()!()
}
}
对于trigger而言,其实就是从全局的map中找到当前reactive的map,然后再找到当前set的key的map,然后取出effct来,然后把effect的调度器放入队列中,然后清空队列挨着执行
triggerRefValue
export function triggerRefValue(
ref: RefBase<any>,
dirtyLevel: DirtyLevels = DirtyLevels.Dirty,
newVal?: any,
oldVal?: any,
) {
ref = toRaw(ref)
// 直接从ref的属性中取dep
const dep = ref.dep
if (dep) {
triggerEffects(
dep,
dirtyLevel,
void 0,
)
}
}
triggerRefValue也是如此,只不过取的位置是直接从自己的dep属性中取出effct来。
简单总结一下:
ref会在get操作时触发track,把当前的effect给放入自己的dep属性中保存;
会在set操作时触发trigger,把自己dep中保存的effect给取出来,然后执行;
结构如下
// ref
const name = ref('123');
ref = {
value:'123' // 原始值
dep : [effect1,effect2]
}
reactive get时则是把effect放入全局map中保存;
然后在set操作时读取effect,然后执行;
结构如下
// reactive
// 全局map
const formData = reactive({ name:'张三', });
targetMap = {
[formData] : {
name : {
[effect1] : id,
[effect2] : id,
}
}
}
Effect
响应式的关键离不开effect。我们用watch的源码来看看effect是如何创建的
const name = ref('123');
// 一个常见的watch
watch(name,()=>{
console.log(name.value);
});
export function watch<T = any, Immediate extends Readonly<boolean> = false>(
source: T | WatchSource<T>,
cb: any,
options?: WatchOptions<Immediate>,
): WatchStopHandle {
let getter: () => any
// 如果watch的第一个参数是ref
// getter实际上就是 ()=> name.vale;
if (isRef(source)) {
getter = () => source.value
}
let oldValue: any
// 重点
const job: SchedulerJob = () => {
// 执行一次effect的run ,effect.run实际上就是执行了一次getter获取了一次最新的ref值
const newValue = effect.run()
// 把新旧value当做参数调用一次cb
callWithAsyncErrorHandling(
cb,
instance,
ErrorCodes.WATCH_CALLBACK,
[newValue, oldValue === INITIAL_WATCHER_VALUE? undefined: isMultiSource &&oldValue[0] === INITIAL_WATCHER_VALUE ? []: oldValue,onCleanup,]
)
oldValue = newValue
}
let scheduler: EffectScheduler = () => queueJob(job)
// 创建了effect
const effect = new ReactiveEffect(getter, NOOP, scheduler)
if (cb) {
if (immediate) {
job() // 如果配置了immediate立马执行一次
} else {
oldValue = effect.run()
}
}
const unwatch = () => {
effect.stop()
}
return unwatch
}
export class ReactiveEffect<T = any> {
// fn是getter函数
// 重点关注调度器,这里把调度器保存了一下
constructor(
public fn: () => T,
public trigger: () => void,
public scheduler?: EffectScheduler,
) {
// 去除无用代码
}
// run其实就是执行了一次getter函数,拿到了ref的最新值
run() {
// 在run的时候把全局的activeEffect指向了当前的effect
// 这里对应了之前track时候的activeEffect
activeEffect = this;
// this.fn = ()=>name.value
// 这里触发了一个get操作,则会触发ref的track
// track中就把当前的全局变量activeEffect给放入了ref的deps中
this.fn()
}
}
// 假设现在name.value = '456',则会触发trgger函数,走到triggerEffects
export function triggerEffects(
dep: Dep,
dirtyLevel: DirtyLevels,
debuggerEventExtraInfo?: DebuggerEventExtraInfo,
) {
pauseScheduling()
for (const effect of dep.keys()) {
// effect的调度器里面存放的是job
// job里面执行了一次getter拿到了ref的最新值
// 然后把最新值和老的值当做参数执行了一次watch的回调函数
if (effect.scheduler) {
queueEffectSchedulers.push(effect.scheduler)
}
}
resetScheduling()
}
// 开始调度
export function resetScheduling() {
pauseScheduleStack--
while (!pauseScheduleStack && queueEffectSchedulers.length) {
// 把每个调度器取出来执行
queueEffectSchedulers.shift()!()
// 这里执行的是之前的job函数
}
}
// 重新贴job函数过来方便理解
// 重新取一次最新值,然后执行一次回调函数,是不是就跟watch对应起来啦?
// 当name.value产生变化后,取得最新的456的值,然后把123的旧值一起传入给watch的回调函数执行
const job: SchedulerJob = () => {
// 执行一次effect的run ,effect.run实际上就是执行了一次getter获取了一次最新的ref值
const newValue = effect.run()
// 把新旧value当做参数调用一次cb
callWithAsyncErrorHandling(
cb,
instance,
ErrorCodes.WATCH_CALLBACK,
[newValue, oldValue === INITIAL_WATCHER_VALUE? undefined: isMultiSource &&oldValue[0] === INITIAL_WATCHER_VALUE ? []: oldValue,onCleanup,]
)
oldValue = newValue
}
所以看到这里整个链路就清晰了
对于响应式的数据,在用到它的地方会构建effect对象,effect在get的时候会创建关联,响应式的数据会创建dep存放effct。其他地方执行了set操作,触发了trigger后会取出dep中的effect来,然后放入调度器队列,最后清空队列全部执行。