- Vue3源码01 : 代码管理策略-monorepo
- Vue3源码02: 项目构建流程和源码调试方法
- Vue3源码03: Vue3响应式核心原理
- Vue3源码04: Vue3响应式系统源码实现1/2
在前面的文章中,我们分析了reactive
、effect
、mutableHandlers
之间的相互协作关系。本文会重点分析effect.ts
中的每一个API及相关代码的实现细节,因为响应式原理的核心是Proxy
代理机制,还有一个特别重要的就是对依赖关系的管理,而依赖关系的管理主要逻辑在effect.ts
文件中完成,同时还会带着大家阅读computed
的源码实现。鉴于涉及了响应式系统的很多实现细节,这是一篇比较长的文章,文字加代码超过2万
个字符,请大家在耐心和时间上做好准备,阅读完本文相信会让大家对Vue3
响应式系统有深刻的理解。
依赖收集及触发更新
在前面两篇文章中,我们知道了,响应式系统的核心,就是依赖收集和触发更新。在代理对象属性值被使用的时候,需要保存数据属性与依赖函数的关系;当代理对象的属性的只被修改的时候,需要将保存的该属性对应的依赖函数进行遍历并执行。可以用下面这张图来进一步理解这个基本原理:
图中的第一步,将普通对象处理成代理对象,是eactive
、shallowReactive
等函数来实现的,这些函数内部会创建Proxy
实例,同时会为这些Proxy
实例设置处理器,处理器中又会进行依赖收集和触发更新。而依赖收集和触发更新的具体实现是在effect.ts
文件中完成。对于依赖的管理可以说是响应式系统的基石,下面我们就来看看effect.ts
中对外暴露的那些函数的具体实现。
effect
代码语言:javascript复制// 代码片段1
export function effect<T = any>(
fn: () => T,
options?: ReactiveEffectOptions
): ReactiveEffectRunner {
// 分析点1:参数处理
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
// 分析点2: ReactiveEffect
const _effect = new ReactiveEffect(fn)
if (options) {
extend(_effect, options)
// 分析点3: ReactiveScope
if (options.scope) recordEffectScope(_effect, options.scope)
}
if (!options || !options.lazy) {
_effect.run()
}
// 分析点4: 作用域
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
}
上篇文章中只分析了effect
函数的核心逻辑,这里我们来探索其中的实现细节。
分析点1: 参数处理
代码语言:javascript复制// 代码片段2
if ((fn as ReactiveEffectRunner).effect) {
fn = (fn as ReactiveEffectRunner).effect.fn
}
这里的处理很巧妙,为了防止传入的函数fn
本身就是effect
函数,这里规避了对函数重复包裹。
分析点2: ReactiveEffect
下面先分析类ReactiveEffect
中几个属性值的含义,接着分析run
、stop
等方法。
几个属性的含义和用途
代码语言:javascript复制// 代码片段3
export class ReactiveEffect<T = any> {
active = true
deps: Dep[] = []
parent: ReactiveEffect | undefined = undefined
computed?: ComputedRefImpl<T>
allowRecurse?: boolean
onStop?: () => void
onTrack?: (event: DebuggerEvent) => void
onTrigger?: (event: DebuggerEvent) => void
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
recordEffectScope(this, scope)
}
// 此处省略其他代码...
}
fn
属性
基础不扎实的朋友可能会疑惑,因为没看见有一个属性叫做fn
,只有一个构造函数的函数参数是fn
,事实上这是一个TypeScript
中和构造函数参数相关的语法,可以查阅相关文档理解。之所以先解释fn
这个属性,是因为它太重要了。对于依赖收集而言,本质上收集的就是这个函数,虽然在实际代码中存储的依赖关系中,保存的是ReactiveEffect
对象。ReactiveEffect
实例存在的意义其实就是方便管理这个fn
函数。事实上,依赖收集完成,触发更新的时候,就是触发这里的fn
函数执行。
active
属性
这里的active
属性是用于标识该active
属性对应的ReactiveEffect
实例是否还应该参与正常的依赖收集活动。具体含义在本文run
方法分析的部分进行解释。这里需要知道,正常情况下,active
的值都是true
,就目前响应式源码而言,只有人为干预才会将active
的值变为false
,比如用户手动调用了ReactiveEffect
实例的stop
方法,或者EffectScope
实例调用了自己的stop
方法,触发了自己所关联的ReactiveEffect
实例的stop
方法。至于EffectScope
的相关内容在本文其他部分会有讲解。
deps
属性
这个属性也特别重要,为什么呢?我们常常讲依赖收集,讲得大概是保存这样的对应关系:
代码语言:javascript复制// 代码片段4
// 假设有对象target,target有属性,prop1、prop2;
// target有对应代理对象proxyTarget;
// proxyTarget.prop1被函数fn1、fn2使用;
// proxyTarget.prop2被函数fn2、fn3使用
// 则对应关系可表示为如下形式:
{
target:{
prop1:[fn1, fn2],// 这里用数组表示,实际上是用Set集合保存
prop2:[fn2, fn3]
}
}
“注意:下面关于
deps
的描述和源码实现是不一致的,但是其本质思想确实是相似的 ”
这与上面的示意图的描述是一致的,但是,假如函数fn2
在某种情况下不希望被依赖收集,不希望在proxyTarget.prop1
或proxyTarget.prop2
值改变后自动执行fn2
怎么办呢?这里的deps
就发挥了作用,利用deps
可以保存这样一组关系:
//代码片段5
{
fn2:[target.prop1, target.prop2]
}
当然,我们前面说过依赖函数实际上是包裹在了ReactiveEffect
实例中,所以可以这样描述这组关系:
// 代码片段6
{
fn2所在的ReactiveEffect实例:[target.prop1, target.prop2]
}
有了这组关系,就可以讲fn2
和proxyTarget.prop1
、proxyTarget.prop2
进行解绑,解绑方式很简单,就是找到代码片段4中的对应关系,从对应的属性对应的函数集合删除自己,最终讲代码片段4中的对应关系,修改成下面这种状态:
// 代码片段7
{
target:{
prop1:[fn1],
prop2:[fn3]
}
}
这样,当proxyTarget.prop1
、proxyTarget.prop2
发生变化的时候,就不会再触发fn2
重新执行了。
我们要知道,所谓的依赖收集,是指保存这些函数与其所依赖的代理对象的属性之间的关系。也就是说函数是依赖方,代理对象的属性是被依赖方。代理对象属性的改变会触发依赖这些属性的函数重新执行。但是这些依赖函数自己,也保存了其所依赖的对象属性,在需要的时候,再根据保存的自己所依赖的对象属性,删除这组依赖关系。
上面关于deps
的描述和源码实现是不一致的,但是有了这个基础,理解源码实现就很简单了,在代码片段6进行改造如下:
{
fn2所在的ReactiveEffect实例:[target.prop1所对应的依赖函数集合, target.prop2所对应的依赖函数的集合]
}
再进一步改造:
代码语言:javascript复制ReactiveEffect实例.deps = [target.prop1所对应的依赖函数集合, target.prop2所对应的依赖函数的集合]
当然这里的"target.prop1所对应的依赖函数集合"、“target.prop2所对应的依赖函数的集合”都包含ReactiveEffect实例自身,这样和源码实现就是一致的了。
parent
属性
个人觉得parent
这个属性名在这里不太好,因为实际表达的是上一个处于活动中的ReactiveEffect
实例。以理解为就像是一个栈
,先处于活跃状态的实例在最底层,后处于活跃状态的实例处在上层,栈顶是当前活跃的ReactiveEffect
实例。当然实际代码中只是通过变量维护了一个链式的关系。但理解为栈
在程序运行流程上是没有太大差别的。事实上,在Vue3
之前的版本,本身就是通过栈的形式来维护这种关系,为了性能上的提升,改为如今这种链式的方式维护,这也体现了Vue3
框架作者们追求极致的精神。在下文run
方法的部分,还会具体涉及这一块内容。
computed
属性
和计算属性相关,此处暂不做解释,下文相关的地方会提及。
allowRecurse
属性
允许递归调用,下文在分析函数triggerEffects
时会提及。
onStop
属性
一个回调函数,在调用ReactiveEffect
实例的stop
方法时,如果该实例onStop
有对应的函数值,则调用该函数。相当于一个普通的回调函数,用户可以在该函数中处理一些个性化的需求。
onTrack
、onTrigger
属性
这两个属性对应的是两个函数值,但只和开发阶段有关,在某些时机会调用这两个函数(如果传入的参数有对应的值的话),本文不对其进行分析。
scheduler
属性
这个属性比较重要,如果用户传入参数给scheduler
赋予了一个函数值,则不会执行该ReactiveEffect
实例的run
方法。
stop方法
请先阅读代码:
代码语言:javascript复制// 代码片段8
export class ReactiveEffect<T = any> {
// 此处省略很多代码...
stop() {
if (this.active) {
cleanupEffect(this)
if (this.onStop) {
this.onStop()
}
this.active = false
}
}
}
function cleanupEffect(effect: ReactiveEffect) {
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i ) {
deps[i].delete(effect)
}
deps.length = 0
}
}
这里的逻辑比较简单,上文提到的active
属性的值在该函数置为false
,而onStop
函数在上面已经解释过,至于cleanupEffect
函数,实际上在对deps
属性的解释中,已经回答了这里为什么要这么实现。
run方法
我们来先看看run
方法中的代码实现:
// 代码片段9
export class ReactiveEffect<T = any> {
// 此处省略很多代码...
run() {
// 关键点1
if (!this.active) {
return this.fn()
}
// 关键点2
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
// 关键点3
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
}
}
// 此处省略很多代码...
}
关键点1
对应代码如下:
代码语言:javascript复制// 代码片段10
if (!this.active) {
return this.fn()
}
前面讲过,如果active
属性值为false
,那么就不会参与正常的依赖收集活动,但是我们发现如果为false
仍然会执行该active
属性对应的ReactiveEffect
实例下的函数fn
,然而我们知道在执行函数fn
的时候,仍然会进行依赖收集。这里是矛盾的吗?答案是并不矛盾。至于具体原因,我们在代码片段9的关键点3部分的分析中进行解释。
关键点2
对应代码如下:
代码语言:javascript复制// 代码片段11
let parent: ReactiveEffect | undefined = activeEffect
let lastShouldTrack = shouldTrack
while (parent) {
if (parent === this) {
return
}
parent = parent.parent
}
这样如果一下看不出来,如果换回栈的描述就清楚了,上面的代码可以这样描述:
代码语言:javascript复制// 代码片段12
// effectStack保存了所有的活跃过的依赖函数
if (effectStack.includes(this)) {
return
}
上面代码片段11的while
循环,目的就是看当前的ReactiveEffect
实例,是不是已经处在这个活跃ReactiveEffect
实例的链条中。这个时候可能有朋友问,为什么一定要保存这个链式的关系呢?在回答这个问题之前,我们先看看代码片段11中的变量parent
被赋值为activeEffect
,这个activeEffect
又是什么呢?activeEffect
是effect.ts
文件中定义的一个全局变量。这个activeEffect
又有什么用呢?还记得本文开始画的依赖收集的示意图吗,依赖收集就是保存对象属性和ReactiveEffect
实例的关系。那这个ReactiveEffect
实例从哪里获取呢,就是从这里的全局变量activeEffect
。假如有下面示意的effect实例调用run
方法的执行顺序:
// 代码片段13
effect(()=>{}); // effect1
effect(()=>{}); // effect2
effect(()=>{}); // effect3
执行完代码片段13中的代码,那么effect3
就是activeEffect
。由于JavaScript
程序是单线程,那么执行effect1
的时候,此时effect1
就是activeEffect
,依赖收集的对应关系就关联到effect1
,执行完effect1
再执行effect2
,依赖关系就关联到effect2
,依此类推。按照这样看起来似乎没有问题,甚至不用维护一个链式关系也没问题。但是,如果是下面的执行逻辑呢?
// 代码片段14
effect(()=>{ // effect1
console.log(proxyTarget.name);
setTimeout(()=>{
console.log(proxyTarget.city);
}, 3000);
effect(()=>{ // effect2
proxyTarget.age;
});
})
如果activeEffect
没有链式关系,在依赖收集的过程中,proxyTarget.city
对应的ReactiveEffect
实例就是effect2
,而事实上,proxyTarget.city
的依赖函数是effect1
,出现了错乱的状况。这时候可能朋友们还会问,有activeEffect
链式的关系又怎样呢,也不会自动解决这个问题吧,没错,需要做些相应的处理。我们接下来,看代码片段9中所示的关键点3处的代码:
关键点3
代码语言:javascript复制// 代码片段15
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
}
为了解决代码片段14所示的问题,Vue3
的作者们,设计了上文提到过的,维护activeEffect
的链式关系。同时,如代码片段15所示,我们会发现,在执行fn
函数执行有这样的代码:
// 代码片段16
this.parent = activeEffect
activeEffect = this
而在执行完fn
函数之后,有这样两行代码:
// 代码片段17
activeEffect = this.parent
this.parent = undefined
也就是说,在fn
函数执行完成后,activeEffect
会恢复到上一级的状态。这就完美的解决了代码片段14所描述的问题。也回答了为什么要维护一个activeEffect
的链式关系,总之,就是链式关系的存在就是为了解决嵌套的问题。
在代码片段15中还有几个点值得我们注意。这里涉及两个比较特殊的变量:effectTrackDepth
和trackOpBit
。effectTrackDepth
表示当前effect
函数的嵌套层数。但是,这个层数也不是无限的,而是常量maxMarkerBits
所表示的数量30
,至于为什么是这么一个数字,Vue
作者们在源码中给出了注释:
// 代码片段18
/**
* The bitwise track markers support at most 30 levels of recursion.
* This value is chosen to enable modern JS engines to use a SMI on all platforms.
* When recursion depth is greater, fall back to using a full cleanup.
*/
const maxMarkerBits = 30
我们要重点了解下代码片段15中的变量trackOpBit
。在函数fn
执行前:
// 代码片段19
trackOpBit = 1 << effectTrackDepth
在函数fn
执行完成后:
// 代码片段20
trackOpBit = 1 << --effectTrackDepth
从这里可以看出,trackOpBit
某种意义上代表的是activeEffect
嵌套的深度。那这个变量trackOpBit
在哪里使用呢?抛开超过嵌套层数限制的逻辑,我们看在函数fn
执行前后还执行了那些代码:
// 代码片段21
// 函数fn执行前
initDepMarkers(this)
代码语言:javascript复制// 代码片段22
// 函数fn执行后
finalizeDepMarkers(this)
那我们再来看看initDepMarkers
和finalizeDepMarkers
这两个函数的实现:
// 代码片段23,所属文件:core/reactivity/src/dep.ts
export const initDepMarkers = ({ deps }: ReactiveEffect) => {
if (deps.length) {
for (let i = 0; i < deps.length; i ) {
deps[i].w |= trackOpBit // set was tracked
}
}
}
export const finalizeDepMarkers = (effect: ReactiveEffect) => {
const { deps } = effect
if (deps.length) {
let ptr = 0
for (let i = 0; i < deps.length; i ) {
const dep = deps[i]
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr ] = dep
}
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
}
deps.length = ptr
}
}
会发现,在代码片段23中,发现了上面提到的变量trackOpBit
。从上文的内容我们知道,ReactiveEffect
实例用变量deps
保存了依赖收集中,该ReactiveEffect
实例所在的所有依赖函数集合。initDepMarkers
在这里给集合中所有的ReactiveEffect
实例都通过一个属性w
的值来做标记,表示该实例已经被收集过。而这个标记很特别,是一个二进制数据来表示,比如:
// 代码片段24,因为最深嵌套为30层,所以下面二进制数据只显示30位
000000000000000000000000000001 // 表示第一层嵌套
000000000000000000000000001000 // 表示第四层嵌套,因为计算方式,是数字1左移对应嵌套层数对应的数字
deps[i].w |= trackOpBit // 如果deps[i].w默认值是0,trackOpBit为2,则deps[i].w的值为下面表示:
000000000000000000000000000010 // 如果deps[i].w默认值是2,trackOpBit为3,则deps[i].w的值为下面表示:
000000000000000000000000000110
我们再来看看finalizeDepMarkers
中关于trackOpBit
相关的代码:
// 代码片段25
// clear bits
dep.w &= ~trackOpBit
dep.n &= ~trackOpBit
因为trackOpBit
的默认值是1
,后续随着effect
函数的嵌套深度增加或减少而递增或递减,无论怎样都不会比1
小,也就是说这是一个正数。而~
符号是非运算符,按位取反,对于有符号数来讲,正数会变为负数,而这里的&
运算符,由于符号位不同,最终计算结果肯定依然是负值。到了这里,也就解释了函数finalizeDepMarkers
完成的一项重要工作,就是清除该实例已经在收集依赖过程中被收集过的标记。至于属性dep.n
是用来标记是不是刚刚被收集依赖。之所以要用二进制的方式进行标记,一方面可以提高性能,另一方面可以方便计算,具体为什么可以方便运算,上面的&
、|
、~
就是很好的例子,其实还有更多体现,将来的文章在分析runtime-core
相关内容的时候会详细解释。Vue3
在不断发展变化,对性能的追求从未停止,而对位运算的精准应用在源码中不少地方都有体现。目前关于位运算标记暂时先了解这些,在分析trackEffects
的时候还会有所涉及。
函数finalizeDepMarkers
还完成了另一项重要的工作,由下面代码完成:
// 代码片段26
if (wasTracked(dep) && !newTracked(dep)) {
dep.delete(effect)
} else {
deps[ptr ] = dep
}}
如果该ReactiveEffect
实例已经被依赖收集过,而且不是最近被收集的ReactiveEffect
实例,那么就从依赖收集中删除ReactiveEffect
实例。事实上正常情况下是不应该出现这种情况的,因为就如函数finalizeDepMarkers
在代码片段25所处理的那样,执行完run
方法,相应的状态都进行了重置。
分析点3: EffectScope
EfffectScope
是Vue3.2
版本提供的一个高级特性,就日常开发来讲几乎用不到。但是对于一些库的作者就比较常用了。我们知道,我们在setup
中调用reactive
、ref
或者其他响应式API
之后,依赖收集和解除这种依赖关系,是Vue
组件内部自己完成的。但是如果在某些场景下,手动控制这种响应式的依赖关系呢?这时候EffectScope
就派上用场了。我们先来看看类EffectScope
的构造函数:
// 代码片段27
export class EffectScope {
active = true
effects: ReactiveEffect[] = []
cleanups: (() => void)[] = []
parent: EffectScope | undefined
scopes: EffectScope[] | undefined
/**
* track a child scope's index in its parent's scopes array for optimized
* removal
*/
private index: number | undefined
constructor(detached = false) {
if (!detached && activeEffectScope) {
this.parent = activeEffectScope
this.index =
(activeEffectScope.scopes || (activeEffectScope.scopes = [])).push(
this
) - 1
}
}
run<T>(fn: () => T): T | undefined {
if (this.active) {
try {
activeEffectScope = this
return fn()
} finally {
activeEffectScope = this.parent
}
} else if (__DEV__) {
warn(`cannot run an inactive effect scope.`)
}
}
// 此处省略许多其他代码...
}
如果构造函数没有传递特别的参数来控制,那么默认情况下,就会建立一种链式的关系。这种链式关系也借助了一个全局变量activeEffectScope
来实现,这和上文的ReactiveEffect
实例的链式关系的建立有相似之处。这时这种链式关系的维护背后有共同的工程实践意义,那就是解决嵌套的问题,同时有了这种链式关系,也为EffectScope
中的stop
方法中的实现打下了基础。注意构造函数中还用到了一个index
属性,这个index
属性代表了该EffectScope
实例在其父EffectScope
实例维护的子EffectScope
实例数组中所处的位置,在stop
方法中会用到。
EffectScope
的run
方法逻辑很简单,维护链式关系,同时执行传入的fn
函数。逻辑比较丰富比较巧妙的是其stop
方法。
// 代码片段28
stop(fromParent?: boolean) {
if (this.active) {
let i, l
for (i = 0, l = this.effects.length; i < l; i ) {
this.effects[i].stop()
}
for (i = 0, l = this.cleanups.length; i < l; i ) {
this.cleanups[i]()
}
if (this.scopes) {
for (i = 0, l = this.scopes.length; i < l; i ) {
this.scopes[i].stop(true)
}
}
// nested scope, dereference from parent to avoid memory leaks
if (this.parent && !fromParent) {
// optimized O(1) removal
const last = this.parent.scopes!.pop()
if (last && last !== this) {
this.parent.scopes![this.index!] = last
last.index = this.index!
}
}
this.active = false
}
}
代码片段28所示的stop
方法可以概括为做了4件事情:首先,将EffectScope
实例所关联的ReactiveEffect
实例数组进行遍历,依次执行ReactiveEffect
实例的stop
方法。其次,遍历this.cleanups
数组,并依次调用相关函数,this.cleanups
来源于下文将会讲解的onScopeDispose
函数。再次,将关联的子EffectScope
实例数组进行遍历,依次执行这些EffectScope
实例的stop
方法。最后,解除和父级EffectScope
实例的关联,需要注意的是这里的解除关联的方式很巧妙:
// 代码片段29
// nested scope, dereference from parent to avoid memory leaks
if (this.parent && !fromParent) {
// optimized O(1) removal
const last = this.parent.scopes!.pop()
if (last && last !== this) {
this.parent.scopes![this.index!] = last
last.index = this.index!
}
}
仔细观察这段代码,其最元素的需求是,子EffectScope
实例从父级EffectScope
实例维护的子EffectScope
实例数组中删除自身。但是这里没有按照常规的先查找再删除。而是直接将数组末尾的元素放在了该子EffectScope
实例原来所在的位置。将复杂度降到了最低,再次体现了Vue3
作者们追求极致的精神。
下面对几个对外暴露的和EffectScope
相关的函数。
effectScope
代码语言:javascript复制// 代码片段30
export function effectScope(detached?: boolean) {
return new EffectScope(detached)
}
逻辑很简单,只是创建了一个EffectScope
实例,从功能上讲这个函数存在的意义不太大,但是这方便了创建对象,不需要手动进行new
操作。
recordEffectScope
代码语言:javascript复制// 代码片段31
export function recordEffectScope(
effect: ReactiveEffect,
scope: EffectScope | undefined = activeEffectScope
) {
if (scope && scope.active) {
scope.effects.push(effect)
}
}
将某个ReactiveEffect
实例和该EffectScope
实例进行关联,当然关联之后该EffectScope
实例就可以调用该ReactiveEffect
实例的stop
等方法。
getCurrentScope
代码语言:javascript复制// 代码片段32
export function getCurrentScope() {
return activeEffectScope
}
返回当前处于活跃状态的EffectScope
实例。
onScopeDispose
代码语言:javascript复制// 代码片段33
export function onScopeDispose(fn: () => void) {
if (activeEffectScope) {
activeEffectScope.cleanups.push(fn)
} else if (__DEV__) {
warn(
`onScopeDispose() is called when there is no active effect scope`
` to be associated with.`
)
}
}
将某些函数fn
和activeEffectScope
进行关联,当调用activeEffectScope
的stop
方法的时候,会触发这些函数。
小结
上面对函数effect
进行了介绍,先是引出了类ReactiveEffect
并进入了ReactiveEffect
的run
方法中,分析了其主要逻辑,而后由引出了类EffectScope
,并介绍了其实现细节。到目前为止,我们对effect
函数背后的含义应该有了比较清晰的认知。接下来我们分析effect.ts
文件中对外暴露的其他函数。
stop
代码语言:javascript复制// 代码片段34
export function stop(runner: ReactiveEffectRunner) {
runner.effect.stop()
}
这里的runner
就是effect
函数执行完成后的返回值。对应effect
函数的这些代码:
// 代码片段35
const runner = _effect.run.bind(_effect) as ReactiveEffectRunner
runner.effect = _effect
return runner
不难发现,runner
是一个函数,只不过该函数有一个effect
属性,该属性的值就是一个ReactiveEffect
实例。
pauseTracking、enableTracking、resetTracking
代码语言:javascript复制// 代码片段36
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
和一个boolean
数组,来控制当前是否需要进行依赖收集。最直接的体现,就是track
函数中有下面的代码:
// 代码片段37
if (shouldTrack && activeEffect) {
// 省略其他代码... 这里在做依赖收集的具体工作
}
track、trackEffects
从前面的文章我们已经知道,依赖收集的具体触发点,在Proxy
对象实例的get
属性被访问的时候,具体触发的动作就是调用track
函数,在track
函数内部又调用了函数trackEffects
。我们先看看函数track
的内部实现:
// 代码片段38
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (shouldTrack && activeEffect) {
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
const eventInfo = __DEV__
? { effect: activeEffect, target, type, key }
: undefined
trackEffects(dep, eventInfo)
}
}
其实track
函数的逻辑,在本文开头的依赖收集示意图中已经有直接的体现,其实就是在内存中保存了一组对象和对象属性及这些属性和对应依赖函数集合的对应关系。接下来我们看看trackEffects
的内部实现:
// 代码片段39
export function trackEffects(
dep: Dep,
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
let shouldTrack = false
if (effectTrackDepth <= maxMarkerBits) {
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(
Object.assign(
{
effect: activeEffect!
},
debuggerEventExtraInfo
)
)
}
}
}
trackEffects
函数中有几个我们值得注意的地方:
首先,前面提到过的变量trackOpBit
,这里执行了dep.n |= trackOpBit
的语句,相当于给dep
所对应的ReactiveEffect
实例集合初次收集activeEffect
对应的ReactiveEffect
实例。加上条件判断if (!newTracked(dep))
最大的好处是提升性能,如果ReactiveEffect
实例的fn
函数中,多次使用了同一个代理对象的同一个属性,有了这个条件判断可以直接避免多次收集。
其次,关于执行语句shouldTrack = !wasTracked(dep)
,在触发更新的时候会执行ReactiveEffect
实例的run
方法,会将该实例对应的deps
所有依赖集合做上该实例已经被依赖收集的标记。
最后,上面的逻辑之所以能够正常运转,最重要的原因是trackOpBit
变量采用的是二进制方式来记录,可以轻松的确认是哪一级嵌套的依赖关系,因为依赖收集中的一个ReactiveEffect
实例集合可能在不同嵌套层中重复出现,有了这种二进制机制的区分,解决了很多潜在的问题,不得不说这很巧妙,但同时代码的可读性下降了很多。所以我们在日常业务开发中需要进行取舍,代码可读性的优先级可能会更高一点,框架的性质决定了对性能的要求会更高,所以有必要追求极致的性能,哪怕是损失一些代码可读性也是值得的。
trigger、triggerEffects
从前面的文章我们已经知道,触发更新的具体触发点,在Proxy
对象实例的set
属性被访问的时候,具体触发的动作就是调用trigger
函数,在trigger
函数内部又调用了函数triggerEffects
。我们先看看函数trigger
的内部实现,整个trigger
函数的逻辑被可以分成几部分,我们从上至下开始看代码:
// 代码片段40
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()]
}
if(type === TriggerOpTypes.CLEAR)
处理的事集合对象调用clear
方法后的逻辑,这里需要所有用到了该集合对象的地方都触发更新。
// 代码片段41
if (key === 'length' && isArray(target)) {
depsMap.forEach((dep, key) => {
if (key === 'length' || key >= (newValue as number)) {
deps.push(dep)
}
})
}
代码片段41处理的是修改了数组的length
属性值的逻辑,当key
为length
或者索引大于新赋予的值,则触发相应的依赖更新。
// 代码片段42
// 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
}
代码片段42,先通过if (key !== void 0)
来保证depsMap.get(key)
取值正常,进而确保给对象设置值、添加值、删除值的时候,会触发所有相关依赖更新。紧接着,通过类型来区分,进行相应条件判断,保证后续能触发相应的依赖更新。trigger
函数的最后一部分逻辑如下:
// 代码片段43
if (deps.length === 1) {
if (deps[0]) {
if (__DEV__) {
triggerEffects(deps[0], eventInfo)
} else {
triggerEffects(deps[0])
}
}
} else {
const effects: ReactiveEffect[] = []
for (const dep of deps) {
if (dep) {
effects.push(...dep)
}
}
if (__DEV__) {
triggerEffects(createDep(effects), eventInfo)
} else {
triggerEffects(createDep(effects))
}
}
这里逻辑不多,对deps.length === 1
单独处理,做了一定程度的优化。最终是调用triigerEffects
函数:
// 代码片段44
export function triggerEffects(
dep: Dep | ReactiveEffect[],
debuggerEventExtraInfo?: DebuggerEventExtraInfo
) {
// spread into array for stabilization
for (const effect of isArray(dep) ? dep : [...dep]) {
if (effect !== activeEffect || effect.allowRecurse) {
if (__DEV__ && effect.onTrigger) {
effect.onTrigger(extend({ effect }, debuggerEventExtraInfo))
}
if (effect.scheduler) {
effect.scheduler()
} else {
effect.run()
}
}
}
}
逻辑比较简单,触发对应的ReactiveEffect
实例的run
方法。需要注意的是,如果ReactiveEffect
实例有scheduler
属性,则执行该属性对应的函数值,而不再执行run
方法。scheduler
属性值是用户调用effect
函数时候传递的参数,本文后面还会提及scheduler
的作用。
reactive、ref相关API
我暂时不打算对reactive.ts
、ref.ts
中的每一个api进行介绍。有了本文上半部分的基础,加之前面的文章也对reactive.ts
进行过主要逻辑的介绍,此时朋友们如果再回过头阅读reative.ts
、ref.ts
相关代码,会发现比较简单,如果读者朋友们发现还是很有必要对reative.ts
、ref.ts
的实现细节进行讲解,可以留言说明具体困难,如果有必要我再单独出一篇文章补充这部分内容,目前篇幅已经实在是太长了。
函数computed
我们先看看computed
函数的具体实现:
// 代码片段45
export function computed<T>(
getter: ComputedGetter<T>,
debugOptions?: DebuggerOptions
): ComputedRef<T>
export function computed<T>(
options: WritableComputedOptions<T>,
debugOptions?: DebuggerOptions
): WritableComputedRef<T>
export function computed<T>(
getterOrOptions: ComputedGetter<T> | WritableComputedOptions<T>,
debugOptions?: DebuggerOptions,
isSSR = false
) {
let getter: ComputedGetter<T>
let setter: ComputedSetter<T>
const onlyGetter = isFunction(getterOrOptions)
if (onlyGetter) {
getter = getterOrOptions
setter = __DEV__
? () => {
console.warn('Write operation failed: computed value is readonly')
}
: NOOP
} else {
getter = getterOrOptions.get
setter = getterOrOptions.set
}
const cRef = new ComputedRefImpl(getter, setter, onlyGetter || !setter, isSSR)
if (__DEV__ && debugOptions && !isSSR) {
cRef.effect.onTrack = debugOptions.onTrack
cRef.effect.onTrigger = debugOptions.onTrigger
}
return cRef as any
}
从代码片段45中可以看出computed
函数,最核心的逻辑就是新建了一个ComputedRefImpl
实例,并将该实例作为结果返回。我们来看看类ComputedRefImpl
的代码实现:
// 代码片段46
export class ComputedRefImpl<T> {
public dep?: Dep = undefined
private _value!: T
public readonly effect: ReactiveEffect<T>
public readonly __v_isRef = true
public readonly [ReactiveFlags.IS_READONLY]: boolean
public _dirty = true
public _cacheable: boolean
constructor(
getter: ComputedGetter<T>,
private readonly _setter: ComputedSetter<T>,
isReadonly: boolean,
isSSR: boolean
) {
this.effect = new ReactiveEffect(getter, () => {
if (!this._dirty) {
this._dirty = true
triggerRefValue(this)
}
})
this.effect.computed = this
this.effect.active = this._cacheable = !isSSR
this[ReactiveFlags.IS_READONLY] = isReadonly
}
get value() {
// the computed ref may get wrapped by other proxies e.g. readonly() #3376
const self = toRaw(this)
trackRefValue(self)
if (self._dirty || !self._cacheable) {
self._dirty = false
self._value = self.effect.run()!
}
return self._value
}
set value(newValue: T) {
this._setter(newValue)
}
}
对于这个类,我们可以从下面几个方面来分析:
首先,该类有value
属性,底层相当于用了Object.defineProperty
(所以如果说Vue3
的响应式系统是建立在Proxy
的基础上的,这句话是不严谨的),如果用户没有传入_setter
参数,则整个计算属性相当于一个只读的属性。当用户使用value
属性的时候,会进行依赖收集,从这里可以看出进行依赖收集之后马上执行来effect
属性的run
方法。
其次,我们知道effect
属性的run
方法内部会执行用户传入的fn
函数,这里的getter
函数就是那个fn
,因为在构造函数中,初始化了一个ReactiveEffect
实例,传入的正是这个用户传入的getter
函数。内部执行了getter
函数就会进行依赖收集,getter
函数中的代理对象发生变化后就会进行触发更新操作。需要注意的是,由于这里给ReactiveEffect
构造函数传入了第二个参数,也就是前面说的scheduler
函数,那么触发更新的时候就不会再次执行getter
,而是执行triggerRefValue(this)
。
最后,triggerRefValue(this)
与get value()
中的trackRefVale(self)
遥相呼应,当computed
中涉及到的代理对象属性发生了变化,就会触发更新。