Vue3 源码目录结构和启动调试
# Vue3 目录结构
Vue3 源码内的目录结构
├──packages
├── compiler-core
├── compiler-dom
├── compiler-sfc
├── compiler-ssr
├── reactivity
├── runtime-core
├── runtime-dom
├── vue
├── shared
├── ...
2
3
4
5
6
7
8
9
10
11
介绍一些模块功能
- compiler-core: 与平台无关的编译模块,例如基础的 baseCompile 编译模版文件, baseParse生成AST
- compiler-dom: 基于compiler-core,专为浏览器的编译模块,可以看到它基于baseCompile,baseParse,重写了complie、parse
- compiler-sfc: 用来编译vue单文件组件
- compiler-ssr: 服务端渲染相关的
- reactivity: vue独立的响应式模块
- runtime-core: 也是与平台无关的基础模块,有vue的各类API,虚拟dom的渲染器
- runtime-dom: 基于runtime-core,针对浏览器的运行时
- vue: 引入导出 runtime-core,还有编译方法
可以看到 Vue3
模块拆分的清晰,模块相对独立,我们可以单独引用 reactivity
这个模块,也可以引用 compiler-sfc
在我们自己开发的 plugin
中去使用它,例如 vue-loader
, vite
都有使用。
# monorepo
这是因为Vue3
采用 monorepo
是管理项目代码的方式。不同于 Vue2
代码管理,它是在一个 repo
中管理多个package
,每个 package
都有自己的类型声明、单元测试。 package
又可以独立发布,总体来说更便于维护、发版和阅读的。
# 源码入口
接下来就从import { createApp } from 'vue';
这句开始源码的学习。
在 源码里 vue
模块,我们可以看到,vue
这个模块其实大致就做了引入导出runtime-dom,complier
。继续在 runtime-dom
中寻找 createApp
。
export const createApp = ((...args) => {
// 可以看到真正的createApp 方法是在渲染器属性上的
const app = ensureRenderer().createApp(...args)
// ...
const { mount } = app
app.mount = (containerOrSelector: Element | string): any => {
// ...
}
return app
}) as CreateAppFunction<Element>
/**
* ensureRenderer 这里是为了执行createApp时,才给renderer渲染器赋值,也是优化的一点。
* 只导入reactive, 没有执行createApp,不会执行 createRenderer,
* 那么打包时 tree-shaking 可以摇掉 runtime-core 这个模块。
* */
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在 runtime-dom/index.ts
里可以发现对 createApp 的定义,里面的实现大致分为三步步,创建 app 应用实例,改写mount
方法, 返回 app 应用实例。
接着我们去导出这个方法的 runtime-core
模块中看看 createRenderer
,并找到真正的 createApp
// 1
export { createRenderer } from './renderer'
// 2
export function createRenderer (options) {
return baseCreateRenderer(options)
}
// 3
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
) {
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
forcePatchProp: hostForcePatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// ...
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
// 4
export function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
// ...
const app: App = (context.app = {
_uid: uid++,
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,
version,
use (plugin) {
// ...
return app
},
mixin(mixin: ComponentOptions) {
// ...
return app
},
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
if (isHydrate && hydrate) {
// ...
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
},
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
- 注:上面代码我省略了很多与创建 pp 实例无关的代码,我觉得理清思路看源码比逐行去阅读要清晰一些。
辗转四次我们终于在 runtime-core/apiCreateApp
里找到 createApp
方法, 以及 app 实例。 可以看到 app 实例 和 Vue2
里 Vue 构造函数上的 API 基本一致的。app 实例上例如 use
、component
等最后都会返回 app 实例,支持链式写法。
从 createApp 方法的调用到 app 实例创建这个过程,其实我们大致可以看到runtime-dom
这个模块怎么基于 runtime-core
构建对于浏览器的虚拟 dom 渲染器。
/**
* 在 runtime-dom 里,调用 runtime-core 的 createRenderer 方法
* 并传入 rendererOptions,这个 rendererOptions 里面其实包含着浏览器的DOM API,props
* 例如 createElement、insertBefore 等,大家可以去 runtime-dom/nodeOps.ts 里面看看。
* **/
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}
/**
* 传入不同环境的 endererOptions,就可以生成不同环境的render
* **/
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
) {
// 这些变量最终都是为 redner 里 patch 服务的
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
// ...
} = options
// 此处生成对应浏览器环境的 render
const render: RootRenderFunction = (vnode, container) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container)
}
flushPostFlushCbs()
container._vnode = vnode
}
return {
render,
hydrate,
// 以参数形式 传入 createApp 中, 最终供 app实例里的 mount 使用。
createApp: createAppAPI(render, hydrate)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
我们继续看看 runtime-core/apiCreateApp
里 createApp
方法
export function createAppAPI(render, hydrate) {
return function createApp(rootComponent, rootProps = null) {
// ...
const app: App = (context.app = {
// ...
/**
* 我们在项目里创建 app实例,再 mount 到某一节点
* 最终会执行到这里,App 组件作为 rootComponent, render是浏览器环境的渲染器。
* **/
mount(rootContainer: HostElement, isHydrate?: boolean): any {
if (!isMounted) {
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
if (isHydrate && hydrate) {
// ...
} else {
render(vnode, rootContainer)
}
isMounted = true
app._container = rootContainer
;(rootContainer as any).__vue_app__ = app
return vnode.component!.proxy
}
}
// ...
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
当然,我们在项目里.mount('#app')
并不是直接执行 app 实例里的 mount,这里 mount 方法 是 runtime-core
里与平台无关。事实上 runtime-dom
有重写过 mount,亦是针对浏览器环境。
但是这块,这篇文章不会继续说下去,因为 mount 过程大致为创建 Vnode,渲染,生成真实 DOM,其中有用到 Vue3
中新的响应式,所以我们可以先看看这个可以独立使用,不会有其他包袱的模块 reactivity
。 (给自己挖个坑,后面会说的 -。-|)
# Proxy 响应式
我们知道在 Vue2
里内部通过 Object.defineProperty
API 劫持数据的变化,深度遍历 data 函数里的对象,给对象里每一个属性设置 getter
、setter
。
触发 getter
会通过 Dep
类做依赖收集操作,收集当前 Dep.target
, 也就是 watcher
。
触发 setter
,会做派发更新操作,执行 dep.notify
通知收集到的各类 watcher
更新,如 computed watcher
、user watcher
、渲染 watcher
。
Vue3
用 Proxy
重构了响应式部分,effect
副作用函数 代替了 watcher
,
Proxy
的 get
handle 里 执行track()
用来跟踪收集依赖(收集 activeEffect
,也就是 effect
),
set
handle 里执行 trigger()
用来触发响应(执行收集的 effect
)
# 独立的响应式
之前提过很多次,reactivity
是可以独立使用,例如我们在 node 中使用。
// index.js
const { effect, reactive } = require('@vue/reactivity');
// reactive 定义响应式数据,也就是用proxy 设置 get、set handle
const obj = reactive({ num: 1 });
// effect 定义副作用函数
effect(() => {
console.log(obj.num);
});
// 修改num, trigger 触发响应,执行 effect
setInterval(() => {
++obj.num;
}, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
node index.js
, 运行这段脚本就可以看到控制台会一直递增打印。
# reactive
那接下来我们就跟着这段代码来看看 @vue/reactivity
里的 reactive
, effect
吧。后面还是只关注我们目前所关心的,这样个方向的主流程走完之后再梳理其余方法。
先简单介绍写 ReactiveFlags
这个枚举,因为后面也会用到
export const enum ReactiveFlags {
SKIP = '__v_skip', // 这个属性值为 true 的对象 都会被跳过代理
IS_REACTIVE = '__v_isReactive', // 获取是否是响应式
IS_READONLY = '__v_isReadonly', // 是否是只读的
RAW = '__v_raw' // 这个属性会应用到原始对象
}
2
3
4
5
6
进入查看 reactive 方法的正题。
export function reactive(target: object) {
// 如果是只读的响应式数据,直接会返回本身哈
if (target && (target as Target)[ReactiveFlags.IS_READONLY]) {
return target
}
return createReactiveObject(
target, // 对象
false, // 是否只读
mutableHandlers, // proxy handle
mutableCollectionHandlers // 集合数据的 proxy handle
)
}
function createReactiveObject(
target: Target,
isReadonly: boolean,
baseHandlers: ProxyHandler<any>,
collectionHandlers: ProxyHandler<any>
) {
if (!isObject(target)) {
// 不是对象 直接返回
return target
}
// 如果已经是响应式对象则直接返回, 除非是 readonly 作用在这个响应式对象上
if (
target[ReactiveFlags.RAW] &&
!(isReadonly && target[ReactiveFlags.IS_REACTIVE])
) {
return target
}
// 一个缓存map,key是 target对象, value是响应式对象
// 如果这个对象已经创建过响应式对象,则从 缓存map中读出返回
const proxyMap = isReadonly ? readonlyMap : reactiveMap
const existingProxy = proxyMap.get(target)
if (existingProxy) {
return existingProxy
}
// 带有 skip 标记 、被冻结等至不可扩展的,类型不是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
)
// 存入 缓存map 中
proxyMap.set(target, proxy)
return proxy
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
baseHandlers 里面劫持了哪些操作呢?
// reactivity/baseHandlers.ts => mutableHandlers
// 这里我们是选择了普通对象的handlers来看的
export const mutableHandlers: ProxyHandler<object> = {
get, // 访问对象属性的handler
set, // 设置对象属性的handler
deleteProperty, //删除对象属性handler
has, // 针对 in 操作符的handler
ownKeys // 对象上getOwnPropertyNames、getOwnPropertySymbols、keys 等方法的handler
};
2
3
4
5
6
7
8
9
使用 Proxy
优势在于哪,我相信很多人就算没有看过源码,也都了解一些。例如Proxy
弥补了 Object.defineProperty
需要递归对象,给每一个属性设置 setter
、getter
,没法劫持一些其他操作,数组也需要 hack,处理新增属性需要额外的方法,Map、Set 、weakMap 等数据结构无法响应式等不足。
总结
上述一些,是使用 Proxy
带来的优化,后面我们会看看代码实现里又做了哪些其他优化。 至此我们了解了例子中第一句, 先执行 reactive
, 用proxy 代理了我们传入的原始对象,返回了一个这个proxy,就叫做响应式对象吧。 最后我们把返回的响应式对象赋值给 obj
。
# effect
接着上面例子的顺序,再来看这一句
effect(() => {
console.log(obj.num);
});
2
3
我们给 effect
方法里传入了一个函数 () => { console.log(obj.num); }
, 函数里访问了响应式对象 obj 的 num 属性。那我们来看看 effect
的源码吧。
export function effect<T = any>(
fn: () => T,
options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
// 1. 如果 fn 有 effect 函数标示,就指向原始函数,在下面 createReactiveEffect 里就可以看到raw、_isEffect的定义
fn = fn.raw
}
// 2. 创建一个响应式副作用函数
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
// 3. 执行effect, 这个有没有像 computed watcher 的 lazy 属性 ,若为true就不立即执行,
effect()
}
// 4. 返回包裹着 fn 的 effect 函数
return effect
}
let uid = 0
const effectStack = [] // effect栈,记得 Vue2 里面 全局存 Watcher 的栈吗?
function createReactiveEffect<T = any>(
fn: () => T,
options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(): unknown {
if (!effect.active) {
return options.scheduler ? undefined : fn()
}
if (!effectStack.includes(effect)) {
// 这是一个优化,清除的 deps 里所有 dep 里的 effect,配合后面 track 里给 effect deps重新加 dep,相当于清除掉不需要的依赖,后面会详细说到
cleanup(effect)
try {
// 开启允许收集 也就是设这个变量shouldTrack为 true
enableTracking()
// 以下是压栈, 设置activeEffect,执行原始函数
effectStack.push(effect)
activeEffect = effect
return fn() // 这里执行原始函数,就是引用到我们在函数里写的 响应式的对象的值,触发的响应式对象的 get handler
} finally {
// 最终出栈,停止收集,activeEffect 回指上一个effect,这里是对有嵌套关系的effect有作用
effectStack.pop()
resetTracking()
activeEffect = effectStack[effectStack.length - 1]
}
}
} as ReactiveEffect
// 下面是effect的相关属性
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
}
function cleanup(effect: ReactiveEffect) {
// deps 是一个 数组包着 Set集合的数据结构 [Set1(...), Set2(...), ...], 每一个 Set 就是,targetMap里面的dep
const { deps } = effect
if (deps.length) {
for (let i = 0; i < deps.length; i++) {
deps[i].delete(effect)
}
deps.length = 0
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
总结
至此我们知道了,例子中执行 effect(fn),会创建、执行、并返回一个 effect 函数,执行这个 effect 函数时,开启收集开关,压入全局 effectStack
栈中,将全局 activeEffect
指针指向自己,并执行传入的 fn,这时候触发了 obj
的 get handler。执行完 fn 后,执行退栈, 停止收集,activeEffect
指向栈中到的上一个 effect。
# get, 依赖收集
上面说到执行传入的 fn,这时候触发了 obj
的 get handler,那我们看看 get 的源码。
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
}
// 还记得 ReactiveFlags 枚举里面的那些值吗, 都是通过上面那些判断来获取到相应的值,也因为都是私有属性,得到值直接返回即可,没必要往下走了
const targetIsArray = isArray(target)
// ['includes', 'indexOf', 'lastIndexOf'] 数组改变,这方法的结果可能也会发生改变,所以 get 里面做了特殊处理
// 例如 执行arr.includes('xx') 时,会跟踪 arr 数组每一个下标
if (targetIsArray && hasOwn(arrayInstrumentations, key)) {
return Reflect.get(arrayInstrumentations, key, receiver)
}
// 利用Reflect映射返回值
const res = Reflect.get(target, key, receiver)
if (
isSymbol(key)
? builtInSymbols.has(key as symbol)
: key === `__proto__` || key === `__v_isRef`
) {
// 判断原生方法等,直接返回,不track了
return res
}
if (!isReadonly) {
// track 依赖收集操作
track(target, TrackOpTypes.GET, key)
}
if (shallow) {
// 这是表示浅响应的,比如 shallowReactive 方法,后面也会简单介绍下
return res
}
if (isRef(res)) {
// 这里 ref 是 reactivily 里面另一个api,实现相对简单,可以对基本类型创建个响应式对象,例如 num = ref(0) 会创建一个 value为0的响应式对象, isRef 就是判断是不是 ref 值
const shouldUnwrap = !targetIsArray || !isIntegerKey(key)
return shouldUnwrap ? res.value : res
}
if (isObject(res)) {
// 可以看到哈,子对象也是需要递归的去劫持的,但这里相比 Vue2 有个优化的点,
// Vue2里面如果属性仍是个对象 数组等,则立即遍历子对象去做劫持
// 而 Vue3 则是在访问到这个属性,发现值是对象再去转为响应式对象
return isReadonly ? readonly(res) : reactive(res)
}
return res
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
上述呢是完整的 get
handler了,可以发现get操作里面先是对 ReactiveFlags
这个枚举里面的值做了劫持,接着对数组里面特殊方法单独求值、处理,然后使用 Reflect
这个好搭档来求值,再 track
来做依赖收集相关的事情,最后返回结果。 那我们来看一看 track。
// ./effect.ts
// 先看两个使用到的变量
activeEffect // 类似于 Vue2 里的 Dep.target, 一个watcher。 这里表示当前激活的effect
shouldTrack // 判断当前是否应该收集 ,effect函数内部执行一开始设这个变量为true
targetMap // 以原始对象为健 ,值也是一个weakMap,map以属性名为key,effect集合为value
export function track(target: object, type: TrackOpTypes, key: unknown) {
if (!shouldTrack || activeEffect === undefined) {
return
}
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 以上这一堆判断,缓存,最终会形成 targetMap 样的数据结构
/**
* targetMap = {
* [target]: {
* [key]: new Set([ effect,... ])
* }
* }
**/
if (!dep.has(activeEffect)) { // effect 执行时,将 activeEffect 指向自己
dep.add(activeEffect)
// 当前激活的 effect 也会存储 dep集合,这其实是配合 effect 里面 cleanup 方法 清除不需要的依赖
activeEffect.deps.push(dep)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
为什么说 activeEffect.deps.push(dep) 是配合 cleanup 清除不需要的依赖,来看下面例子
// 例2: 多了一个 count 属性
const obj = reactive({num: 1, count: 0});
effect(() => {
if (obj.num === 1) {
++obj.num
// 我们只有在 num 为 1 的是时候再去访问 count
console.log('count', obj.count)
}
console.log('num', obj.num)
},);
setTimeout(() => {
// 因为第一次访问到了 count, 所以 我们修改 count 会去执行 effect
console.log('第一次修改 count')
obj.count = 2
}, 1000);
setTimeout(() => {
// 上一次执行effect的时候,cleanup清除了所有依赖,但因为 num 为 2
// 函数内部不会访问到 count ,并未去 track count,也就不会有重新 activeEffect.deps.push(dep) 这个操作
// 所以修改 count 并不会执行 effect
console.log('第二次修改 count')
obj.count = 3
}, 1000);
setTimeout(() => {
// num 每次都有访问到,所以正常触发响应式
console.log('第三次修改 num')
obj.num = 3
}, 1000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
从这个例子来说第一次执行 effect,是自动执行的,effect.deps 值就会是 [dep, dep]
,分别是 num 的 dep 集合,以及 count 的 dep 集合。
接着修改 count 触发执行 effect,先执行 cleanup
,会清除 num和count dep 集合里的effect,
执行 fn 时候,因为只有访问 num,num 的 dep 又会加上 effect,但 count 的 dep 不会了。而且 effect.deps 只会 push num 的 dep。
所以之后修改 count 的时候不会 就不会在触发 effect了。
总结
最后回归主流程 ,在最开始的例子中我们在传入的 fn 中访问量 obj.num
,触发 get handler,它会通过 Reflect.get 得到值 1,接着 track 咱们的 num 属性,收集effect,最后 targetMap 的结构会如下
targetMap = {
[obj]: {
'num' : new Set([effect])
}
}
2
3
4
5
# set, 派发更新
上一节依赖收集流程以及走完。其中我们说到修改 num 会触发 effect 执行, 其实就是派发更新,也就是 执行 set handler。 来看看 set handler 源码
// ./baseHandlers.ts
function createSetter(shallow = false) {
return function set(
target: object,
key: string | symbol,
value: unknown,
receiver: object
): boolean {
// 1. 先取old值
const oldValue = (target as any)[key]
if (!shallow) {
value = toRaw(value) // toRaw 就是取原始对象,这里如果value是响应式对象,则一直取到原始对象
if (!isArray(target) && isRef(oldValue) && !isRef(value)) {
// 如果老的值是 Ref 响应对象,而更新的值不是,那更新老的响应式对象的 value 属性值即可,不需要执行这里的trigger
oldValue.value = value
return true
}
} else {
// in shallow mode, objects are set as-is regardless of reactive or not
}
// 2. 判断当前 set 的 key 存不存在与 target上
const hadKey =
isArray(target) && isIntegerKey(key)
? Number(key) < target.length
: hasOwn(target, key)
// 3. Reflect.set 求值
const result = Reflect.set(target, key, value, receiver)
// 这里原始数据上原型链上数据操作,Reflect.set修改后,会再进来,所以有了这判断
if (target === toRaw(receiver)) {
// 4. 通过有没有当前 key 是不是已存在来决定是 add 的 triger,还是 set 的 trigger,set会多一个oldvalue
if (!hadKey) {
trigger(target, TriggerOpTypes.ADD, key, value)
} else if (hasChanged(value, oldValue)) {
trigger(target, TriggerOpTypes.SET, key, value, oldValue)
}
}
return result
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
set的逻辑看完了,注释里面大致标注为4步,来看看最后一步里的 trigger
export function trigger(
target: object,
type: TriggerOpTypes,
key?: unknown,
newValue?: unknown,
oldValue?: unknown,
oldTarget?: Map<unknown, unknown> | Set<unknown>
) {
// 从之前 track 里面存储的 targetMap 里取出对应 depsMap
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有依赖,直接返回,不会触发后面effect的执行
return
}
const effects = new Set<ReactiveEffect>()
const add = (effectsToAdd: Set<ReactiveEffect> | undefined) => {
if (effectsToAdd) { // 这是包含 effects 的 Set集合
effectsToAdd.forEach(effect => {
if (effect !== activeEffect || effect.allowRecurse) {
// add 方法是要把 effect 统一 收集到 effects 这个集合里
effects.add(effect)
}
})
}
}
// ...
add(depsMap.get(key)) // 把dep集合放到effects里
// ...
const run = (effect: ReactiveEffect) => {
if (effect.options.scheduler) {
// 一个调度器,可以去做排序、去重、放入 nexttick 中异步执行
// 这个调度器我们也是可以自定义的
effect.options.scheduler(effect)
} else {
// 否则直接执行
effect()
}
}
// 开始执行
effects.forEach(run)
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
总结
至此,我们了解派发更新这块基础的流程,按照最开始的例子, 我们修改 num ,触发 set handler,把 targetMap 里 num 对应的 dep 取出来。通过 add 方法把 dep 里的 effect 加入到 effects 这个大的集合里。最后执行 run 方法遍历执行 effects 里的 effect。