关注
若川视野
, 回复”
” 领取资料,回复”
1
“,可加群长期交流学习
刘崇桢,微医云服务团队前端工程师,左手抱娃、右手持家的非典型码农。
9 月初
Vue.js 3.0
正式发布,代号
"One Piece"
。大秘宝都摆到眼巴前了,再不扒拉扒拉就说不过去了。那我们就从初始化开始。
目标:
-
弄清楚
createApp(App).mount("#app")
到底做了什么 -
弄清楚
Vue3.0
的初始化渲染是怎么样的过程
能收获到什么:
-
了解
Vue3.0
的初始化过程 -
介绍一个阅读
Vue3.0
源码的入口和方向
先跑起来
将
vue-next
代码克隆到本地,打开
package.json
将
scripts dev
末尾加上
--sourcemap
。
然后
yarn dev
,
vue
目录下的
dist
打包出了一份
vue.global.js
和相应的
sourcemap
文件。这样方便我们一步一步调试代码,查看程序在
call Stack
中的每一步调用。
查看
vue
官方给出的 demo,发现
vue
的使用分为
classic
和
composition
,我们先用
classic
方式,实现一个最简单的 demo。
const app = {
data () {
return {
counter: 1
}
}
}
Vue.createApp(app).mount("#app")
ok,页面跑起来了。我们就在这段代码打个断点,然后一步一步的调试,观察
createApp(App).mount("#app")
到底做了什么,了解
Vue3.0
的初始化过程。
在这之前,简单了解一下整体的背景,我们这次主要涉及到
runtime
运行时的代码。
runtime-dom
我们先跟着代码进入:
createApp(App).mount("#app");
这个
createApp()
来自
runtime-dom
,我们通过这个图可以看到他大致做的事情:
return
了一个注册了
mount
方法
app
。这样我们的 demo 至少能跑起来不报错。
createApp
调用了
ensureRenderer
方法,他确保你能得到一个
renderer
渲染器。
renderer
是通过调用创建渲染器的
createRenderer
来生成的,这个
createRenderer
来自于
runtime-core
,后面我们会看到。
而这个
rendererOptions
是什么呢?
const rendererOptions = extend({ patchProp, forcePatchProp }, nodeOps);
export const nodeOps: Omit<RendererOptions<Node, Element>, "patchProp"> = {
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null);
},
remove,
createElement,
createText,
// ...
};
是不是就是一些
DOM API
的高阶封装,这个在
vue
的生态中,叫
平台特性
。vue 源码中的平台特性就是针对 web 平台的。如果开发者想要在别的平台上运行 vue,比如 mpvue、weex,就不需要 fork 源码库改源码了,直接把
nodeOps
中的方法按着平台的特性逐一实现就可以了。这也是
createRenderer
等跨平台的代码放到
runtime-core
中的原因。
当然
runtime-dom
远远不只图中这些东西,我们先大致过一下初始化过程,以对
vue3.0
有一个大致的了解。
runtime-core
紧接着,进入
runtime-core
,创建渲染器
我们注意
baseCreateRenderer
这个
fn
,2000 多行的代码量,里面的东西都是渲染的核心代码,从平台特性
options
取出相关 API,实现了 patch、处理节点、处理组件、更新组件、安装组件实例等等方法,最终返回了一个对象。这里我们看到了【2】中渲染器调用的
createApp
方法,他是通过
createAppAPI
创建的。代码进入
createAppAPI
。
这里我们又看见了熟悉的
Vue2.x
中的 API,挂载在
app
上面。
至此,
Vue.createApp(app).mount("#app")
,创建 app 实例的流程,终于在【7】中
return app
告一段落,我们拿到了【2】中的
app
实例。
大致瞄一眼
app
,我们可以在
apiCreateApp.ts
中找到其实现
初次渲染
.mount("#app")
.mount("#app")
上面的介绍中,其实有两处
.mount
的实现,一处是在
runtime-dom
【2】中的
mount
,我们叫他
dom-mount
。一处是【7】中的
mount
,我们叫他
core-mount
。
dom-mount
的实现:
const { mount } = app; // 先暂存'core-mount'
app.mount = (containerOrSelector: Element | string): any => {
const container = normalizeContainer(containerOrSelector); // #app dom 节点
if (!container) return;
const component = app._component;
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML; // 平台特性的逻辑
}
// clear content before mounting
container.innerHTML = "";
const proxy = mount(container); // 执行'core-mount'
container.removeAttribute("v-cloak");
return proxy;
};
dom-mount
并不是重写
core-mount
,而是提取了平台特性的逻辑。比如上面如果
component
不是
function
,又没有
render
、
template
,就读取 dom 节点内部的 html 作为渲染模板。
然后再执行
core-mount
,
mount(container)
。
代码很简单,就两步:
-
创建根组件的
vnode
-
渲染这个
vnode
创建根组件的
vnode
vnode
创建
vnode
,是一个初始化
vnode
的过程,这个阶段中,下面的这些属性被初始化为具体的值(还有很多属性没有罗列,都是初始值)。
当
vnode
描述不同的事物时,他的属性值也各不相同,这些在
vnode
初始化阶段确定的属性在渲染组件时,能带来非常重要的效率提升。
-
type
,标识
VNode
的种类
-
html 标签的描述,type 属性就是一个字符串,即标签的名字
-
组件的描述,type 属性就是引用组件类(或函数)本身
-
文本节点的描述,type 属性就是 null
-
patchFlag
,标识组件变化的地方 -
shapeFlag
,
VNode
的标识,标明
VNode
属于哪一类,demo 中的
shapeFlag
是
4
:
STATEFUL_COMPONENT
,有状态的组件。
在
packages/shared/src/shapeFlags.ts
中,定义了这些通过将十进制数字
1
左移不同的位数得来的枚举值。
export const enum ShapeFlags {
ELEMENT = 1, // 1 - html/svg 标签
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 = 1 << 6, // 64
SUSPENSE = 1 << 7, // 128
COMPONENT_SHOULD_KEEP_ALIVE = 1 << 8, // 256 - 需要被 keepAlive 的有状态组件
COMPONENT_KEPT_ALIVE = 1 << 9, // 512 - 已经被 keepAlive 的有状态组件
COMPONENT = ShapeFlags.STATEFUL_COMPONENT | ShapeFlags.FUNCTIONAL_COMPONENT // 组件
}
为什么为
VNode
标识这些枚举值呢?在
Vue2.x
的
patch
过程中,代码通过
createElm
区分
VNode
是 html 还是组件或者 text 文本。
所以
Vue2.x
的
patch
是一个试错过程,在这个阶段是有很大的性能损耗的。
Vue3.0
把对
VNode
的判断放到了创建的时候,这样在
patch
的时候就能避免消耗性能的判断。
最终,我们看一下 vnode 的结构
export interface VNode<
HostNode = RendererNode,
HostElement = RendererElement,
ExtraProps = { [key: string]: any }
> {
/**
* @internal
*/
__v_isVNode: true // 一个始终为 true 的值,有了它,我们就可以判断一个对象是否是 VNode 对象
/**
* @internal 内部属性
*/
[ReactiveFlags.SKIP]: true
type: VNodeTypes
props: (VNodeProps & ExtraProps) | null
key: string | number | null
ref: VNodeNormalizedRef | null
scopeId: string | null // SFC only
children: VNodeNormalizedChildren
component: ComponentInternalInstance | null
dirs: DirectiveBinding[] | null
transition: TransitionHooks<HostElement> | null
// DOM 相关
el: HostNode | null
anchor: HostNode | null // fragment anchor
target: HostElement | null // teleport target
targetAnchor: HostNode | null // teleport target anchor
staticCount: number // number of elements contained in a static vnode
// suspense 支持 suspense 的属性
suspense: SuspenseBoundary | null
ssContent: VNode | null
ssFallback: VNode | null
// optimization only 优化模式中使用的属性
shapeFlag: number
patchFlag: number
dynamicProps: string[] | null
dynamicChildren: VNode[] | null
// application root node only
appContext: AppContext | null
}
渲染这个
vnode
vnode
ok,书接上回,我们拿到
根组件的 VNode
,接下来执行到
render
函数。
render
的核心逻辑就是
patch
函数。
patch 函数
patch 有两种含义: 1)整个虚拟 dom 映射到真实 dom 的过程;2)patch 函数。我们这里讲的是函数。
patch
就是
render
渲染组件的关键逻辑,【5】中
baseCreateRenderer
2000 行左右的代码,主要是为了
patch
服务的。
// patching & not same type, unmount old tree
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
// 对于前后节点类型不同的,vue 是直接卸载之前的然后重新渲染新的,不会考虑可能的子节点复用。
...
const { type, ref, shapeFlag } = n2
switch (type) { // 根据节点类型 type 分发到不同的 process
case Text:
processText(n1, n2, container, anchor)
break
case Comment:
processCommentNode(n1, n2, container, anchor)
break
case Static:
...
case Fragment:
...
default: // 根据不同的节点标识 shapeFlag 分发到不同的 process
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(...)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(...)
...
patch
根据节点
VNode
(4.1 创建的根组件的 vnode) 的
type
和
shapeFlags
执行不同的
process
。
-
type
:Text 文本 -
type
:Comment 注释 -
type
:Static 静态标签 -
type
:Fragment 片段:VNode 的类型是 Fragment,就只需要把该 VNode 的子节点渲染到页面。有了他,就没有只能有一个根节点的限制,也可以做到组件平级递归 -
shapeFlags
:ShapeFlags.ELEMENT 原生节点,html/svg 标签 -
shapeFlags
:ShapeFlags.COMPONENT 组件节点 -
shapeFlags
:ShapeFlags.TELEPORT 传送节点,将组件渲染的内容传送到制定的 dom 节点中 -
shapeFlags
:ShapeFlags.SUSPENSE 挂起节点(异步渲染)
Vue3 新增组件 – Fragment、Teleport、Suspense,可见此链接 (https://www.yuque.com/hugsun/vue3/component)
我们的 demo 中的根组件
VNode
的
shapeFlag
是
4(0100)
,
ShapeFlags.COMPONENT(0110)
,按位与后结果为非零,代码会进入
processCompoent
。
processXXX
processXXX 是对挂载(mount)和更新(update)补丁的统一操作入口。
processXXX
会根据节点是否是初次渲染,进行不同的操作。
-
如果没有老的 VNode,就挂载组件(mount)。首次挂载,递归创建真实节点。
-
如果有老的 VNode,就更新组件(update)。更新补丁的的渲染系统的介绍放到下下篇来介绍。
挂载
创建组件内部实例
内部实例也会暴露一些实例属性给其他更高级的库或工具使用。组件实例属性很多很重要也能帮助理解,可以在
packages/runtime-core/src/component.ts
查看实例的接口声明
ComponentInternalInstance
。很壮观啊,啪的一下 100 多行属性的定义,主要包括基本属性、响应式 state 相关、suspense 相关、生命周期钩子等等
安装组件实例
-
初始化 props 和 slots
-
安装有状态的组件,这里会初始化组件的响应式
【15】
setupStatefulComponent
,调用了
setup(props, setupContext)
。
如果没有
setup
时会调用
applyOptions
,应用
vue2.x
的
options API
,最终对
data()
的响应式处理也是使用
vue3.0
的
reactive
。
上面讲过,安装组件实例触发响应式初始化就发生在这里,具体怎么触发的,这块又是一个千层套路,放到下一篇中。
【16】主要是根据
template
拿到组件的
render
渲染函数和应用
vue2.x
的
options API
。
我们看一下
template
模板编译后生成的
render
函数。
我们大致看下生成的 render 函数,有几点需要注意
-
这里的
render
函数执行后的返回是组件的
VNode
-
_createVNode
函数,用于创建
VNode
-
_createVNode
函数的入参,
type
、
patchFlags
、
dynamicProps
等
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT, // type,标识 VNode 的种类
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0, // 标记节点动态变化的地方
dynamicProps: string[] | null = null, // 动态 props
isBlockNode = false
): VNode { ... }
createVNode
在创建根节点的时候就出现过,用于创建虚拟 DOM。这个是内部使用的 API,面向用户的 API 还是
h
函数。
export function h(type: any, propsOrChildren?: any, children?: any): VNode { ... }
h
的实现也是调用
createVNode
,但是没有
patchFlag
、
dynamicProps
、
isBlockNode
这三个参数。也就是 h 是没有
optimization
的,应该是因为这三个参数,让用户自己算容易出错。
看来这个
patchFlags
有点意思,
标识组件变化的地方,用于 patch 的 diff 算法优化
。
export const enum PatchFlags {
TEXT = 1, // 动态文字内容
CLASS = 1 << 1, // [2]动态 class 绑定
STYLE = 1 << 2, // [4]动态样式
PROPS = 1 << 3, // [8]动态 props,不是 class 和 style 的动态 props
FULL_PROPS = 1 << 4, // [16]有动态的 key,也就是说 props 对象的 key 不是确定的。key 变化时,进行一次 full diff
HYDRATE_EVENTS = 1 << 5, // [32]
STABLE_FRAGMENT = 1 << 6, // [64]children 顺序确定的 fragment
KEYED_FRAGMENT = 1 << 7, // [128]children 中有带有 key 的节点的 fragment
UNKEYED_FRAGMENT = 1 << 8, // [256]没有 key 的 children 的 fragment
NEED_PATCH = 1 << 9, // [512]
DYNAMIC_SLOTS = 1 << 10, // [1024]动态的插槽
// SPECIAL FLAGS -------------------------------------------------------------
// 以下是特殊的 flag,负值
HOISTED = -1, // 表示他是静态节点,他的内容永远不会改变
BAIL = -2, // 用来表示一个节点的 diff 应该结束
}
之所以使用位运算,是因为
-
用
|
来进行复合,
TEXT | PROPS
得到
0000 1001
,即十进制 9。标识他既有动态文字内容,也有动态 props。 -
用
&
进行 check,
patchFlag & TEXT
,
0000 1001
&
0000 0001
,得到
0000 0001
,只要结果大于 0,就说明属性命中。 -
方便扩展、计算更快…
patchFlag
被赋值到
VNode
的属性中,他在后面更新节点时会被用到。为了配合代码的正常流转,先放一放,代码继续
F10
。如果你去调试代码,会发现这真的是千层套路啊,一直
shift + F11
跳出代码到怀疑人生,才终于回到
mountComponent
…
总结一下
setupComponent
安装组件实例,主要做了什么事情:initProps、initSlots、响应式初始化、得到模板的
render
函数等等。
回顾前文,跳出到【13】,setup 安装组件实例后,下一步是
setupRenderEffect
激活渲染函数的副作用
激活渲染函数的副作用
setupRenderEffect
setupRenderEffect
实现基于【21】,
effect
副作用,意味着响应式数据变化后引起的变更。
effect
源自
reactive
,传入一个
fn
得到一个
reactiveEffect
。
effect
的入参
componentEffect
是一个命名函数,会立即执行。
componentEffect
执行过程中,触发响应式数据的
getter
拦截,会在
全局数据响应关系仓库
记录当前
componentEffect
。在响应式对象发生改变时,派发更新,执行
componentEffect
。
回到
componentEffect
function componentEffect() {
if (!instance.isMounted) {
let vnodeHook: VNodeHook | null | undefined
const { el, props } = initialVNode
const { bm, m, parent } = instance
// beforeMount hook 生命周期钩子函数
if (bm) {
invokeArrayFns(bm)
}
...
// subTree 根节点的 subTree,通过 renderComponentRoot 根据 render 生成的 vnode
//大家回忆一下 render 是什么?是不是根组件的 template 编译后得到的好多_createVNode 的渲染器函数?
const subTree = (instance.subTree = renderComponentRoot(instance))
...
// 更新
patch(null, subTree, container, ...)
...
if (m) { // parent 的 mounted 执行之前,先执行 subTree 的 patch
queuePostRenderEffect(m, parentSuspense)
}
...
instance.isMounted = true // 标志实例已挂载
} else { ... }
}
执行前面编译后得到的渲染函数 render,生成
subTree: vnode
最后执行
patch
,上文中渲染根节点的
vnode
时执行过
patch
,这里就进入了一个
大循环
,根据组件的
children
的
type
和
shapeFlag
,
baseCreateRenderer
会继续进行各种
processXXX
处理,直至基于
平台特性
的
DOM 操作
挂载到各自的父节点中。
这个顺序是深度遍历的过程,子节点的
patch
完成之后再进行父节点的
mounted
。
patch 循环 && subTree 一览
// subTree 的 模板 template
<div id="app">
<h1>composition-api</h1>
<p @click="add" :attr-key="counter">{{counter}}</p>
<p :class="{'counter': counter % 2}">{{doubleCounter}}</p>
</div>
// patchFlag: 64
// STABLE_FRAGMENT = 1 << 6, // 64 表示:children 顺序确定的 fragment
// shapeFlag: 16
// ARRAY_CHILDREN = 1 << 4, // 16
观察上面这个模板,
Vue2.x
中的模板只能有一个根元素,
Vue3.0
的这个 demo 中有三个根元素,这得益于新增的
fragment
组件。
vnode
标识出来
patchFlag:64
,表示 children 顺序确定的
fragment
;
vnode
标识出来
shapeFlag:16
,表示当前节点是一个孩子数组。
vnode
标识出来
dynamicChildren
,标识动态变化的孩子节点。显然是两个
p
标签,可以想象这个数组的元素也是当前呈现的
vnode
,只不过具体属性值不同罢了
等等,还有 4 吗,我不知道…
当然还有,
processxxx
中一般都会判断是挂载还是更新,更新的时候就会用到
patchFlag
,比如
patchElement
… 下次一定
等等,还有 5 吗,我不知道…
当然还有,第五层我就已经裂开了啊…
あ:あげない あ:不给你哦~ ????????????
い:いらない, い:不要了啦~ ????????????
う:うごけない う:动不了了~ ????????????
え:えらべない え:不会选嘛~ ????????????
お:おせない お:按不到耶~ [裂开][裂开][裂开]
刚看源码不久,只能靠
F11
、参考其他文档,凭自己的理解写出这样的文章,肯定有很多理解不对的地方,希望得到批判指正。
附录
-
Vue3初始化.drawio (https://www.yuque.com/office/yuque/0/2020/drawio/441847/1605880555730-4e18923f-c087-4082-af06-ec51986ba658.drawio?from=https%3A%2F%2Fwww.yuque.com%2Fdocs%2Fshare%2F64bd5cdc-3086-4154-a447-04032d161830%3F%23)
推荐阅读
我在阿里招前端,我该怎么帮你?(现在还可以加模拟面试群)
如何拿下阿里巴巴 P6 的前端 Offer
如何准备阿里P6/P7前端面试–项目经历准备篇
大厂面试官常问的亮点,该如何做出?
如何从初级到专家(P4-P7)打破成长瓶颈和有效突破
若川知乎问答:2年前端经验,做的项目没什么技术含量,怎么办?
若川知乎高赞:有哪些必看的 JS库?
末尾
你好,我是
若川,江湖人称菜如若川,历时一年只写了一个学习源码整体架构系列
~(点击蓝字了解我)
-
关注
若川视野
,回复”pdf” 领取优质前端书籍pdf,回复”1″,可加群长期交流学习 -
我的博客地址:https://lxchuan12.gitee.io 欢迎收藏
-
觉得文章不错,可以点个
在看
呀^_^另外欢迎
留言
交流~
小提醒:若川视野公众号面试、源码等文章合集在菜单栏中间
【源码精选】
按钮,欢迎点击阅读,也可以星标我的公众号,便于查找