本地调试源码

  • 拷贝一份源码
  • 安装依赖yarn install
  • 执行yarn dev -s生成dist文件夹,里面有打包后的文件和sourceMap文件。
  • 写一个.html文件,引用生成的打包文件,/packages/vue/dist/vue.global.js
  • 此时就可以开始debug

    目录结构

    此为package下面的目录结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.
├── compiler-core // 顾名思义,核心中的核心,抽象语法树和渲染桥接实现
├── compiler-dom // Dom的实现
├── compiler-sfc // Vue单文件组件(.vue)的实现
├── compiler-ssr
├── global.d.ts // 声明文件
├── reactivity // 响应式模块
├── runtime-core
├── runtime-dom // 入口文件
├── runtime-test
├── server-renderer // 服务端渲染实现
├── shared // package 之间共享的工具库
├── size-check
├── template-explorer
└── vue

先从入口函数来分析,从组件到DOM的过程

入口

1.创建App对象

demo.html

1
2
3
4
5
6
7
8
9
const { ref } = Vue
const vm = Vue.createApp({
setup() {
const count = ref(1);
return {
count
}
}
}).mount('#app');

当我们调用Vue.createApp时候,会执行

1
2
3
4
5
6
7
8
9
10
// /vue-next/packages/runtime-dom/src/index.ts
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args)
// 重写mount方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
}
return app
})

ensureRenderer() 来延时创建渲染器,好处是当用户只依赖响应式包的时候,就不会创建渲染器,因此可以通过 tree-shaking 的方式移除核心渲染逻辑相关的代码。渲染器是为跨平台渲染做准备。

1
2
3
4
5
6
7
8
9
// lazy create the renderer - this makes core renderer logic tree-shakable
// in case the user only imports reactivity utilities from Vue.
let renderer: Renderer<Element> | HydrationRenderer

function ensureRenderer() {
// 如果 renderer 有值的话,那么以后都不会初始化了
// 创建一个渲染器对象,跨平台渲染做准备的,包含平台渲染核心逻辑的 JavaScript 对象。
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

ensureRenderer()会调用createRenderer()会返回baseCreateRenderer(),这个函数有2000多行,也是Vue渲染的主要逻辑,重点关注最终返回的createApp()

1
2
3
4
5
6
7
8
9
function baseCreateRenderer(options: RendererOptions,  // options是用来创建当前DOM的方法,传入的好处是在不同平台可以传入对应的方法
createHydrationFns?: typeof createHydrationFunctions){
......
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}

createAppAPI是我们在调用Vue.createApp()真正被调用到的方法,上面这几个方法大致调用如下

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
// demo.html
const app = Vue.createApp(App)

// 入口函数
export const createApp = ((...args) => {
const app = ensureRenderer().createApp(...args) // 这里就是调用createAppAPI的地方
}

function ensureRenderer() {
return renderer || (renderer = createRenderer<Node, Element>(rendererOptions))
}

export function createRenderer(options: RendererOptions<HostNode, HostElement>){
return baseCreateRenderer(options)
}


function baseCreateRenderer(){
......
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}

里面的第一个rootComponent参数就是传入的配置对象。
当我们在调用Vue.create(App),会把App组件对象传入createApp中的rootComponent对象。
createApp 内部就创建了一个 app 对象,它会提供 mount 方法,这个方法是用来挂载组件的。
这里通过函数柯里化返回createApp,避免大量的if else,也为了可以跨平台和参数复用

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
// 文件位置 runtime-core/src/apiCreateApp.ts
export function createAppAPI<HostElement>(render: RootRenderFunction,
hydrate?: RootHydrateFunction){
// createApp createApp 方法接受的两个参数:根组件的对象和 prop
/**
* rootComponent:传入的配置对象,实际上就是根组件对象
* {
* data(){},
* setup(){},
* ...
* }
*/
// rootProps App组件创建时候,可以传入一个Props
return function createApp(rootComponent, rootProps = null){
if (rootProps != null && !isObject(rootProps)) {
// 根App创建也可以传入props,props必须是一个对象
__DEV__ && warn(`root props passed to app.mount() must be an object.`)
rootProps = null
}
let isMounted = false
// 创建AppContext默认的上下文
const context = createAppContext()
const installedPlugins = new Set()
// 这里的_component是传入的配置项
const app: App = context.app = {
_uid: uid++,
// 这里的_component是传入的配置项
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,

version,

get config() {
return context.config
},

set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use() {...},
mixin() {...},
component() {...},
directive() {...},
mount(rootContainer) {
// 创建根组件的 vnode
const vnode = createVNode(rootComponent, rootProps)
// 利用渲染器渲染 vnode
render(vnode, rootContainer)
app._container = rootContainer
return vnode.component.proxy
}
unmount() {...},
}
return app
}
}

createAppContext创建AppContext默认的上下文对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export function createAppContext(): AppContext {
return {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
}

创建好的app对象

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
{
component: component(name, component),
config: (),
directive: directive(name, directive),
mixin:mixin(mixin),
mount: mount(rootContainer, isHydrate, isSVG) {},
provide:provide(key, value),
unmount: unmount(),
use: use(plugin, ...options),
version: "3.1.0-beta.3",
_component: {data: (){}, mounted: (){}, setup: (){}},
_container: null,
_context: {app: {…}, config: {…}, mixins: Array(), components: {}, directives: {}},
_props: null,
_uid: 0,
get config: ƒ config(),
set config: ƒ config(v)
}

最终入口函数返回app就是上面的app对象

1
2
3
4
5
6
7
8
9
10
export const createApp = ((...args) => {
// 这里的app就是上面的app组件对象
const app = ensureRenderer().createApp(...args)
// 重写mount方法
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
...
}
return app
})

2.mount重写和挂载

可以看到,createApp入口函数中的app对象中的mount方法被重写。
返回的app.mount被重写,在执行Vue.create().mount('#app')会执行这里的挂载函数,通过normalizeContainer获取到挂载的dom节点,如果没有render,template字符串等,就获取容器的innerHTML作为模板挂载到component.template,这点有点像Vue2.x,最后调用mount做真正的挂载

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
const { mount } = app
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 获取传入的dom或者字符串或者选择器
const container = normalizeContainer(containerOrSelector)
if (!container) return
// _component是用户传入的配置项,比如setup,data等
const component = app._component
// 没有写render函数,也没有template字符串时候
if (!isFunction(component) && !component.render && !component.template) {
// 获取container.innerHTML,就是当前的template模板,和Vue2有点类似
component.template = container.innerHTML
// 2.x compat check
if (__COMPAT__ && __DEV__) {
for (let i = 0; i < container.attributes.length; i++) {
const attr = container.attributes[i]
if (attr.name !== 'v-cloak' && /^(v-|:|@)/.test(attr.name)) {
compatUtils.warnDeprecation(
DeprecationTypes.GLOBAL_MOUNT_CONTAINER,
null
)
break
}
}
}
}
// 挂在前清空内容
container.innerHTML = ''
// 做真正的挂载
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

可以看到,这里最终返回了app对象,也就是我们在应用里面最终返回的app

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
const App = {
setup() {
const a = Vue.ref(1)
return {
a
}
},
}
/**
{
component: component(name, component),
config: (),
directive: directive(name, directive),
mixin:mixin(mixin),
mount: mount(rootContainer, isHydrate, isSVG) {},
provide:provide(key, value),
unmount: unmount(),
use: use(plugin, ...options),
version: "3.1.0-beta.3",
_component: {data: (){}, mounted: (){}, setup: (){}},
_container: null,
_context: {app: {…}, config: {…}, mixins: Array(), components: {}, directives: {}},
_props: null,
_uid: 0,
get config: ƒ config(),
set config: ƒ config(v)
}
*/
const app = Vue.createApp(App)
app.mount('#app')

为什么要重写mount方法?

因为Vue不仅仅是支持web端。而是支持跨平台,app内部的mount方法是一个标准的跨平台流程。

创建vnode和渲染vnode

Vue2.x中,没有render函数情况下,当获取完dom中的template,就会开始将template编译为vnodeVue3也类似

上面创建好app对象,获取到容器的innetHTML作为template,调用了app.mount方法,看一下mount函数的实现

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
export function createAppAPI<HostElement>(render: RootRenderFunction){
return function createApp(rootComponent, rootProps = null) {
_uid: uid++,
// 这里的_component是传入的配置项
_component: rootComponent as ConcreteComponent,
_props: rootProps,
_container: null,
_context: context,

version,

get config() {
return context.config
},

set config(v) {
if (__DEV__) {
warn(
`app.config cannot be replaced. Modify individual options instead.`
)
}
},
use() {...},
mixin() {...},
component() {...},
directive() {...},
// 这里是挂载的地方
mount(
// dom
rootContainer: HostElement,
// 是否是服务端渲染
isHydrate?: boolean,
// 是否是SVG
isSVG?: boolean
): any {
// 是否已经被挂载
if (!isMounted) {
// 执行createVNode创建当前App跟组件转换为vnode
// rootComponent就是我们传入的选项,App根组件对象
// 基于App根选项创建根vnode
/**
* {
* rootComponent,就是我们传入的选项,App根组件对象
* setup: ƒ setup()
template: "\n {{a}}\n "
}
*/
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// 将当前的context(app对象)缓存在vnode.appContext中
vnode.appContext = context
// 将isMounted设置为true
isMounted = true
// 将挂载的容器节点保存在app._container中
app._container = rootContainer
return vnode.component!.proxy
}
}
}
}

可以看到,核心就是将当前的rootComponent,也就是当前的App 组件对象通过createVNode转换为vnodetemplate是之前调用被重写的mount()方法而来的。基于App根选项创建根vnode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
if (!isMounted) {
// 执行createVNode创建当前App跟组件转换为vnode
// rootComponent就是我们传入的选项,App根组件对象
// 基于App根选项创建根vnode
/**
* {
* rootComponent,就是我们传入的选项,App根组件对象
* setup: ƒ setup()
template: "\n {{a}}\n "
}
*/
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
}

createVNode

其最终调用的是_createVNode方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// vue-next/packages/runtime-core/src/vnode.ts
export const createVNode = (__DEV__? createVNodeWithArgsTransform: _createVNode) as typeof _createVNode

function _createVNode( type, NULL_DYNAMIC_COMPONENT,props = null,children = null,
// 靶向更新标记
patchFlag = 0,
// 自己定义的属性
dynamicProps = null,
// block,是否为动态节点
isBlockNode = false)
{
...

}

其中有几个关键的步骤
第一个是格式化组件props中的classstyle

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function _createVNode(){
// 处理props 标准化class 和 style,组件上面有写class或者style的情况
if (props) {
// for reactive or proxy objects, we need to clone it to enable mutation.
// 如果proxy是被代理或者是只读,克隆一份
if (isProxy(props) || InternalObjectKey in props) {
// extend是Object.assign
props = extend({}, props)
}
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
// reactive state objects need to be cloned since they are likely to be
// mutated
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
}

第二个是对当前的type对象做判断,创建vnode会用到,用来标志当前的vnode是哪个类型

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function _createVNode(){
if (props) {
// ...
}
// 对 vnode 类型信息编码,下面创建vnode会用到
// suspense和teleport的type会有_suspense,_teleport标记
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT // 1
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE // "异步组件" 1 << 7 128
: isTeleport(type)
? ShapeFlags.TELEPORT // "瞬移组件" 1<<6 64
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT // 盲猜这里的"有状态组件"应该是普通组件,返回4
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT // 函数式组件1 << 1 2
: 0 // 不是以上会返回0
}

这里需要额外注意shapeFlags,是通过或位于运算实现的,通过或位于运算判断当前符合类型的shape,权限判断也可以这样去做。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// packages\shared\src\shapeFlags.ts
export const enum ShapeFlags {
// dom节点
ELEMENT = 1, // 00000000001
// 函数组件
FUNCTIONAL_COMPONENT = 1 << 1, // 2
STATEFUL_COMPONENT = 1 << 2, // 4
TEXT_CHILDREN = 1 << 3,// 8
ARRAY_CHILDREN = 1 << 4,// 16
SLOTS_CHILDREN = 1 << 5,// 32
// teleport
TELEPORT = 1 << 6,// 64
// suspense
SUSPENSE = 1 << 7,// 128
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8,// 256
COMPONENT_KEPT_ALIVE = 1 << 9, // 512
// 组件,相当于 1 << 2 | 1 << 1 结果为6也就是00000000110
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT
}

如果判断一个节点是不是符合上面的类型,只需要 将 ·未知节点·&上面其中一个节点 , 结果大于0就满足。按钮级别权限可以用这个方法

后面很多地方都会有这个判断,比如

1
2
3
if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
...
}

这就判断当前的shapeFlag是不是属于/包含ShapeFlags.STATEFUL_COMPONENT

第三个是创建vnode

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 _createVNode(){
if (props) {
// ...
}
// 对 vnode 类型信息编码,下面创建vnode会用到
// suspense和teleport的type会有_suspense,_teleport标记
const shapeFlag = ...
// 创建vnode
const vnode: VNode = {
// 标记当前是一个vnode
__v_isVNode: true,
// 无需响应对象,vnode不需要响应式
__v_skip: true,
// 传入的dom对象
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId, // currentScopeId 首次默认为null
slotScopeIds: null,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
// 传入的type是什么类型的type
shapeFlag,
// 靶向更新标记
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
}
}

最后是normalizeChildren之后返回vnode

1
2
3
4
5
6
7
8
9
// 格式化子节点 
normalizeChildren(vnode, children)
// 如果是suspense组件,格式化suspense的children
if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
const { content, fallback } = normalizeSuspenseChildren(vnode)
vnode.ssContent = content
vnode.ssFallback = fallback
}
return vnode

最后生成的vnode大致如下

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
const vnode: VNode = {
// 标记当前是一个vnode
__v_isVNode: true,
// 无需响应对象,vnode不需要响应式
__v_skip: true,
// 传入的组件对象
type:{
data: data()
mounted: mounted()
setup: setup()
template: "\n <p>\n </p><div>\n 454654\n </div>\n <p></p>\n <p></p>\n "
},
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId, // currentScopeId 首次默认为null
slotScopeIds: null,
children: null,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
// 传入的type是什么类型的type
shapeFlag:4,
// 靶向更新标记
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: {
app: null as any,
config: {
isNativeTag: NO,
performance: false,
globalProperties: {},
optionMergeStrategies: {},
errorHandler: undefined,
warnHandler: undefined,
compilerOptions: {}
},
mixins: [],
components: {},
directives: {},
provides: Object.create(null)
}
}

render 渲染VNode

再回到上面的mount方法,在调用mount时候,返回了创建好vnode,之后还将app对象缓存在了vnode.appContext

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
function createAppAPI<HostElement>(){
return function createApp(rootComponent, rootProps = null) {
mount(
// dom
rootContainer: HostElement,
// 是否是服务端渲染
isHydrate?: boolean,
// 是否是SVG
isSVG?: boolean){
const vnode = createVNode(
// rootComponent的template是节点模板字符串和组件对象
rootComponent as ConcreteComponent,
rootProps
)
vnode.appContext = context
// 开始执行render,将创建好的VNode渲染到页面中
/**
* vnode:创建好的vnode
* rootContainer:当前的dom
* isSVG: 是否是svg
*/
// rootContainer #app的容器
render(vnode, rootContainer, isSVG)
// 将isMounted设置为true
isMounted = true
// 将挂载的容器节点保存在app._container中
app._container = rootContainer
}
}
}

创建好之后,准备开始执行关键的render函数。在Vue2源码中,我们知道,是通过_render()调用createElement创建的Vnode,通过_update()dom patch到页面中,来看下Vue3中的实现

1
2
3
4
5
6
7
// 开始执行render
/**
* vnode:创建好的vnode
* rootContainer:当前的dom
* isSVG: 是否是svg
*/
render(vnode, rootContainer, isSVG)

这里的render方法就是那个开头见到的2000多行的函数中,也是createApp调用的地方baseCreateRenderer

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
// vue-next\packages\runtime-core\src\renderer.ts

function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
...省略2000
// 创建好的vnode,当前的容器dom,是否是svg
// 用来做渲染的入口函数
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// vnode存在 去patch
// 这几个参数对应的是旧的vnode 新的vnode dom容器
// 首次渲染container._vnode是undefined
// 创建或者更新组件
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// 将当前的vnode缓存在dom节点上
container._vnode = vnode
}

return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}

首先判断vnode不存在,执行销毁,这部分有些像Vue2patch函数,也是首先判断是不是销毁,然后执行patch,看一下patch函数的实现,patch函数的主要是用来挂载和更新dom的。在当前函数482

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// patch函数实现,主要功能为挂载dom和更新dom
// 初始化的App组件是一个组件vnode
const patch: PatchFn = (
// 旧的vnode,n1不存在表示挂载,初次渲染为空
n1,
// 新的vnode
n2,
// DOM容器,vnode生成dom之后的挂载节点
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
// 是否采用优化模式
optimized = false
) => {
...
}

在这个里面,再出现了patchFlag概念,第一次是在创建vnode时候

patchFlag

patchFlag是用来做什么的?简单来讲,就是通过对vnode进行标记,采用不同的patch方法,在Vue3里面,diff有两种模式

  • 优化模式-不同的vnode不同的patch方法(optimized mode)
  • 普通模式-对vnode进行全量diff,比如render函数,renderSlot(),手动克隆vnode

ShapeFlag是具有形状的元素,作用是帮助 Rutime 时的 render 的处理。patchFlag是标记vnode应该怎样进行diff

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
// vue-next\packages\shared\src\patchFlags.ts
export const enum PatchFlags {
// 动态文本元素 1
TEXT = 1,
// 动态class绑定 2
CLASS = 1 << 1,

// 动态style绑定 4
STYLE = 1 << 2,

// 不包含class、style绑定的props 8
PROPS = 1 << 3,

// 动态props和有key绑定的元素 16
FULL_PROPS = 1 << 4,

// 有事件监听的元素 32
HYDRATE_EVENTS = 1 << 5,

// fragment元素,子元素顺序确定的 64
STABLE_FRAGMENT = 1 << 6,

// 自己或者子元素带有key绑定的fragment元素 128
KEYED_FRAGMENT = 1 << 7,


// fragment 没有key绑定的children 256
UNKEYED_FRAGMENT = 1 << 8,

//只有非props需要patch的,比如`ref` 或者指令 521
NEED_PATCH = 1 << 9,

// // 动态的插槽 1024
DYNAMIC_SLOTS = 1 << 10,

// 根节点的fragment 2048
DEV_ROOT_FRAGMENT = 1 << 11,

// 内置特殊的flag
// 纯静态元素
HOISTED = -1,

// 应该跳出diff的优化模式(optimized mode)进行全量diff,比如renderSlot(),手写的render,手动克隆vnode等
BAIL = -2
}

理解完patchFlag,继续往下看patch过程。

patch

patch函数的主要是用来挂载和更新dom的。
首先判断了是否存在同时存在新旧节点。如果都存在并且不是一个sameVnode,则销毁旧的节点,一个元素由UL变成了div,那么就会直接删除原来的节点,直接去做挂载。之后,会根据传入的type判断当前流程
patch参数有很多,其中的意思是:
n1是旧的vnode,n1旧节点不存在表示挂载,初次渲染为空
n2是新的vnode,后续会根据这个vnode执行不同的处理逻辑
container是DOM容器,vnode生成dom之后的挂载节点
optimized表示是否采用优化模式
patchFlag的概念,用来标记一个节点应该如何进行diff更新策略,没有patchFlag,执行全量diff

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
const patch = (
// n1是旧的vnode,n1旧节点不存在表示挂载,初次渲染为空
n1,
// n2是新的vnode,后续会根据这个vnode执行不同的处理逻辑
n2, container, anchor = null, parentComponent = null, parentSuspense = null, isSVG = false, optimized = false) => {
// 存在新旧vnode,并且新旧vnode不相同,说明是一次更新过程,不考虑子节点复用,先销毁旧节点
// isSameVNodeType 对比vnode的type和key
// 如果说,一个元素由UL变成了div,那么就会直接删除原来的节点,直接去做挂载
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// patchFlag的概念,用来标记一个节点应该如何进行diff更新策略
// 没有patchFlag,执行全量diff
/**
* 如果新的vnode patchFlag是-2,表示退出优化模式。进行全量diff
*/
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
const { type, shapeFlag } = n2
/** type:第一次是用户写的内容,是一个Object,走default的流程
* data: data()
mounted: mounted()
setup: setup(const count = ref(1)...)
template: "\n <p>\n </p>"
*/
switch (type) {
case Text:
// 处理文本节点
break
case Comment:
// 处理注释节点
break
case Static:
// 处理静态节点
break
case Fragment:
// 处理 Fragment 元素
break
default:
if (shapeFlag & 1 /* ELEMENT */) {
// 处理普通 DOM 元素
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 6 /* COMPONENT */) {
// 处理组件
// 处理组件,刚开始渲染的App.vue也是一个组件对象,经过转换再次执行会变成普通dom
// 一般情况下,第一次渲染的就是一个组件对象,然后执行里面的组件挂载流程
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else if (shapeFlag & 64 /* TELEPORT */) {
// 处理 TELEPORT组件
// ...
}
else if (shapeFlag & 128 /* SUSPENSE */) {
// 处理 SUSPENSE组件
// ...
}
}
}

这里主要关注对普通元素的处理processElement()和组件的处理processComponent()

组件patch

刚开始我们的App是一个组件,组件vnode会通过processComponent方法来处理,经过转换再次执行会变成普通dom, 一般情况下,第一次渲染的就是一个组件对象,然后执行里面的组件挂载流程。

函数逻辑为如果n1为空,那么意味着没有旧的vnode,直接挂载,否则执行更新逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const processComponent = (
// 旧的 vnode
n1,
// 新的 vnode 节点
n2,
// dom容器挂载节点
container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 没有旧的vnode,表示是一次组件挂载的过程
if (n1 == null) {
// 挂载组件
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized)
}
else {
// 否则表示是一次更新的过程
updateComponent(n1, n2, parentComponent, optimized)
}
}