Vue 源码阅读

Vue 源码阅读

最近公司项目逐渐从 Vue2 迁到 Vue3,我也趁机补一波干货,下面的一些篇幅将会记录我阅读 Vue3 源码的过程和一些思考。

环境搭建

首先下载源码,Vue3 尤大大切了一个新的仓库去写,仓库名是 vue-next

1
git clone https://github.com/vuejs/vue-next.git

首先看了一下项目结构,很明显是一个 Monorepo 组织的项目:

1
2
3
4
5
6
7
8
9
10
11
12
13
├── packages
│   ├── compiler-core // 编译器核心
│   ├── compiler-dom // 编译DOM
│   ├── compiler-sfc // 编译Vue单文件
│   ├── compiler-ssr // 编译服务端渲染
│   ├── reactivity // 响应式代码
│   ├── runtime-core // 运行时核心
│   ├── runtime-dom // 运行时DOM
│   ├── runtime-test // 内部测试代码
│   ├── server-renderer // 服务端渲染
│   ├── shared // 共享的工具库
│   ├── vue // 主入口文件
│   ├── vue-compat // 提供兼容Vue2的能力

接着,我执行了一下 npm install 发现报错了:

1
2
3
4
5
6
$ npm i

> @3.1.0-beta.4 preinstall /Users/vincent/Workspace/vue-next
> node ./scripts/checkYarn.js

This repository requires Yarn 1.x for scripts to work properly.

看了下 scripts,原来是 preinstall 的时候执行了 checkYarn.js 这个脚本去判断包管理工具,所以我继续执行 yarn 安装依赖。然后执行 yarn dev 开发环境打包试了一下,编译结果放在 ./vue/dist/vue.global.js,但是没有生成 sourcemap,如果没有 sourcemap 后面就很不方便调试了。看了下打包脚本 dev.js,发现是有 sourcemap 参数的:

./scripts/dev.js
1
2
const sourceMap = args.sourcemap || args.s
// ...

于是试了一下执行下面指令:

1
$ npx node scripts/dev.js -s

发现成功生成 vue.global.js.map,于是我在项目根目录,新建了一个 html:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Demo</title>
</head>
<body>
<div id="app">
<p>{{count}}</p>
</div>

<script src="./packages/vue/dist/vue.global.js"></script>
<script>
const vm = Vue.createApp({
data() {
return { count: 0 }
}
}).mount('#app');
</script>
</body>
</html>

然后新建了 launch 脚本 (需要安装 Debugger for Chrome VSCode 插件):

./.vscode/launch.json
1
2
3
4
5
6
7
8
9
10
11
12
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Chrome Debug",
"file": "index.html",
"webRoot": "${workspaceFolder}"
}
]
}

我在入口文件中第一个执行的函数 registerRuntimeCompiler 上下了个断点:

./packages/vue/src/index.ts
1
2
> registerRuntimeCompiler(compileToFunction)
// ...

然后执行调试 (F5),可以捕获到断点,至此 Vue3 的源码调试环境搭建完成。

入口点分析

首先从入口文件 ./packages/vue/src/index.ts 分析:

1
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
84
85
86
87
88
89
90
91
92
93
94
95
import { initDev } from './dev'
import { compile, CompilerOptions, CompilerError } from '@vue/compiler-dom'
import { registerRuntimeCompiler, RenderFunction, warn } from '@vue/runtime-dom'
import * as runtimeDom from '@vue/runtime-dom'
import { isString, NOOP, generateCodeFrame, extend } from '@vue/shared'
import { InternalRenderFunction } from 'packages/runtime-core/src/component'

if (__DEV__) {
initDev()
}

// 全局编译缓存
const compileCache: Record<string, RenderFunction> = Object.create(null)

function compileToFunction(
template: string | HTMLElement,
options?: CompilerOptions
): RenderFunction {
if (!isString(template)) {
if (template.nodeType) {
// 如果 template 是 HTMLElement,转为字符串
template = template.innerHTML
} else {
__DEV__ && warn(`invalid template option: `, template)
return NOOP
}
}

const key = template
const cached = compileCache[key]
if (cached) {
// 命中缓存
return cached
}

if (template[0] === '#') {
// 如果 template 是 element id,查询 HTMLElement,后转为字符串
const el = document.querySelector(template)
if (__DEV__ && !el) {
warn(`Template element not found or is empty: ${template}`)
}
// __UNSAFE__
// Reason: potential execution of JS expressions in in-DOM template.
// The user must make sure the in-DOM template is trusted. If it's rendered
// by the server, the template should not contain any user data.
// 这里意思是说使用 element id 不安全,template 可能会执行不安全的 js
template = el ? el.innerHTML : ``
}

// 调用 @vue/compiler-dom 的 compile 函数编译模板,返回结果是用于渲染 DOM 的源码
const { code } = compile(
template,
extend(
{
hoistStatic: true,
onError: __DEV__ ? onError : undefined,
onWarn: __DEV__ ? e => onError(e, true) : NOOP
} as CompilerOptions,
options
)
)

function onError(err: CompilerError, asWarning = false) {
const message = asWarning
? err.message
: `Template compilation error: ${err.message}`
const codeFrame =
err.loc &&
generateCodeFrame(
template as string,
err.loc.start.offset,
err.loc.end.offset
)
warn(codeFrame ? `${message}\n${codeFrame}` : message)
}

// 把刚才 compiler 编译后的代码封装成函数,处理了全局 Vue 引入的情况
const render = (__GLOBAL__
? new Function(code)()
: new Function('Vue', code)(runtimeDom)) as RenderFunction

// mark the function as runtime compiled
;(render as InternalRenderFunction)._rc = true

// 缓存编译结果并输出 render 函数
return (compileCache[key] = render)
}

// 注册编译函数,跳进去能发先就是吧参数 compileToFunction 函数放到全局
registerRuntimeCompiler(compileToFunction)

export { compileToFunction as compile }

// 暴露 runtime 方法,例如 createApp 等等
export * from '@vue/runtime-dom'

入口点的逻辑很简单,就是封装 @vue/compiler-dom 的 compile 函数,处理模板,添加缓存,注册编译函数。

createApp

当注册完编译函数后,紧接着就是调用 Vue.createApp 来创建 Vue 实例了,继续跟代码,在 @vue/runtime-dom 中找到 createApp 的定义。

./packages/runtime-dom/src/index.ts
1
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
function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

export const createApp = ((...args) => {
// 构建 app 实例,最终会 return 出去
const app = ensureRenderer().createApp(...args)

if (__DEV__) {
injectNativeTagCheck(app)
injectCompilerOptionsCheck(app)
}

const { mount } = app

// 重载 app.mount 方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 初始化容器
// 例如 app.mount('#app'),container
const container = normalizeContainer(containerOrSelector)
if (!container) return

const component = app._component

// 此处省略一些兼容性代码
// ...

// 挂载前清空容器中的内容
container.innerHTML = ''

// 调用 @vue/runtime-core 中的 mount 的方法得到一个 Proxy 对象
const proxy = mount(container, false, container instanceof SVGElement)
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}

return app
}) as CreateAppFunction<Element>

代码中首先间接调用 @vue/runtime-core 里的 createRenderer 函数创建全局 renderer 实例,然后调用它的 createApp 方法创建 app 实例,而后对返回的 app 实例的 mount 方法进行二次封装,开发模式注入了两个函数,一些兼容 Vue2 的代码等,这些就不深入去看了,所以其实核心都在 core 包里。

./packages/runtime-core/src/renderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}

function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 此处省略 2000 行代码
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}

继续跟进 createRenderer,它会调用 baseCreateRenderer,然后返回三个方法:

  • render
  • hydrate
  • createApp

其中 hydrate 主要跟服务端渲染相关,先跳过,而 createApp 是通过 createAppAPI 创建的,我们继续跟进去看看。

./packages/runtime-core/apiCreateApp.ts
1
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
export interface App<HostElement = any> {
version: string
config: AppConfig
use(plugin: Plugin, ...options: any[]): this
mixin(mixin: ComponentOptions): this
component(name: string): Component | undefined
component(name: string, component: Component): this
directive(name: string): Directive | undefined
directive(name: string, directive: Directive): this
mount(
rootContainer: HostElement | string,
isHydrate?: boolean,
isSVG?: boolean
): ComponentPublicInstance
unmount(): void
provide<T>(key: InjectionKey<T> | string, value: T): this

// internal, but we need to expose these for the server-renderer and devtools
_uid: number
_component: ConcreteComponent
_props: Data | null
_container: HostElement | null
_context: AppContext

/**
* v2 compat only
*/
filter?(name: string): Function | undefined
filter?(name: string, filter: Function): this

/**
* @internal v3 compat only
*/
_createRoot?(options: ComponentOptions): ComponentPublicInstance
}

export interface AppContext {
app: App // for devtools
config: AppConfig
mixins: ComponentOptions[]
components: Record<string, Component>
directives: Record<string, Directive>
provides: Record<string | symbol, any>
/**
* Flag for de-optimizing props normalization
* @internal
*/
deopt?: boolean
/**
* HMR only
* @internal
*/
reload?: () => void
/**
* v2 compat only
* @internal
*/
filters?: Record<string, Function>
}

基本上从 interface 就能猜出 app 实例有那些功能,首先它会创建一个上下文对象 context,比如我们 use components,mixin 等等就会挂载 app context 的实例上。

mount

然后我们重点看一下 mount 函数,看一下 Vue3 的首次渲染都做了什么:

./packages/runtime-core/apiCreateApp.ts
1
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
mount(
rootContainer: HostElement,
isHydrate?: boolean,
isSVG?: boolean
): any {
if (!isMounted) {
// 创建根节点
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 把 app 的 context 存在根节点上
vnode.appContext = context

// 热更新
if (__DEV__) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG)
}
}

if (isHydrate && hydrate) {
// SSR 相关
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else {
// 渲染根节点到根容器
render(vnode, rootContainer, isSVG)
}

// 挂载完成
isMounted = true

// 跟容器挂在 app 实例对象上
app._container = rootContainer

// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app

if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
devtoolsInitApp(app, version)
}
return vnode.component!.proxy
} else if (__DEV__) {
warn(
`App has already been mounted.\n` +
`If you want to remount the same app, move your app creation logic ` +
`into a factory function and create fresh app instances for each ` +
`mount - e.g. \`const createMyApp = () => createApp(App)\``
)
}
}

render

上面 mount 中的调用的 render 函数,其实就是之前提到的 baseCreateRenderer 函数中返回的 render。

./packages/runtime-core/src/renderer.ts
1
2
3
4
5
6
7
8
9
10
11
12
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 调用 patch 方法挂载 VNode 到 容器
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}

我们发现,render 其实调用的是 patch 函数,或者说这就是老生常谈的 diff,其实 baseRenderer 那两千多行的代码其实都是为了 patch 服务的。

./packages/runtime-core/src/renderer.ts
1
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
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
const patch: PatchFn = (
n1, // oldNode,当旧节点为 null 时表现为 mount
n2, // newNode
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = false
) => {
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}

if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}

const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG)
} else if (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}

// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}

跟踪代码得知,首次渲染,n1 旧节点为 null,n2 为待渲染的节点,所以 patch 表现为挂载节点,后面替换节点的时候依然靠的是这个 patch 方法。相比于 Vue2 的 full diff,Vue3 显得智能很多,原因在 Vue3 会根据节点的类型分别调用不同的 process 函数,然后根据节点不同的 patchFlags,调用对应的 patch 方法。

reactive

响应式原理应该老生常谈了,Vue3 把响应式 API 单独抽出来一个 reactive 的包,并且改用 Proxy 作为它响应式的核心。记得之前写 Vue2 的时候最讨厌的就是碰到无法触发响应的数据要自己写一边 this.$set 方法,原因在于 defineProperty 对于类型支持的不完善,因此魔改了很多特殊数据类型的函数,比如 push、pop、slice 等,改成 Proxy 就舒服多了。下面我简单实现了一下 reactive 这个函数:

1
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
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Reactive Demo</title>
</head>
<body>
<div id="app"></div>

<script>
const data = reactive({
a: { b: { c: 0 }}
});

setInterval(() => {
data.a.b.c++
}, 100);

function effectFn() {
document.getElementById('app').innerHTML = data.a.b.c;
}

const targetMap = new Map();

function reactive(target) {
return new Proxy(target, {
get(target, key, receiver) {
const ret = Reflect.get(target, key, receiver);
track(target, key);
if (typeof ret !== 'object') return ret
return reactive(ret);
},
set(target, key, receiver) {
const ret = Reflect.set(target, key, receiver);
trigger(target, key);
return ret
}
})
}

function track(target, key) {
let depsMap = targetMap.get(target);
if (!depsMap) {
depsMap = new Map();
targetMap.set(target, depsMap);
}
let deps = depsMap.get(key);
if (!deps) {
deps = new Set();
depsMap.set(key, deps);
}
deps.add(effectFn)
}

function trigger(target, key) {
const depsMap = targetMap.get(target);
if (depsMap) {
const deps = depsMap.get(key);
if (deps) {
deps.forEach(effect => effect());
}
}
}
</script>
</body>
</html>
Posted on

2021-05-26

Updated on

2021-10-13

Licensed under

Comments