源码解读 vue3 的初始化渲染流程
准备读源码
去 github 把 vue3 的代码仓库拉下来。
在 package.json 中给 dev 脚本加上 sourcemap 参数,然后执行脚本。
js
"dev": "node scripts/dev.js --sourcemap",然后可以在下面这个路径中新建一个文件打开,在浏览器中进行断掉调试了。
js
core/packages/vue/examples/composition/test.html首次渲染过程
要渲染出下面这个界面,vue 要做出哪些步骤呢?
html
<script src="../../dist/vue.global.js"></script>
<div id="app">
<h1>
{{count}}
</h1>
</div>
<script>
Vue.createApp({
data() {
return {
count: 0
}
}
}).mount("#app") // 在这里打上断点
</script>然后进行调试
- 执行渲染器的 createApp 方法,返回 app 实例
- 解构出 app 实例的原生 mount 方法
- 对 app 实例的 mount 方法进行扩展,主要是
获取宿主节点
定义根组件的引用常量 component
将宿主节点的 innerHTML 赋值到根组件的 template 属性上。
处理 vue2/vue3 的兼容性问题,如果数组节点上有非 v-cloak 指令,就打印警告,它主要想说明的是:
// vue2 是通过 outerHtml 将宿主节点整个都给替换为 vue 应用。 // 而 vue3 是通过 innerHtml 将 vue 应用作为子节点插入到宿主元素的内部。 // ===2.x compat check // 检查宿主节点的 attribute,如果包含非 v-cloak 指令,发出警告 // vue2 通过 outerHtml,将整个应用替换掉宿主节点。 // vue3 通过 innerHtml,将整个应用作为子节点插入到宿主元素中。
- 在首次挂载之前清空宿主节点的 innerHtml
- 执行扩展过后的 mount 方法,返回 proxy 常量
- 最后移除宿主节点上的 v-cloak 指令
- 返回 proxy
js
// vue 的初始化渲染流程,会去执行一个 createApp 方法,在它内部
export const createApp = ((...args) => {
// 1. 首先执行渲染器的 createApp 方法,并返回 app 实例
const app = ensureRenderer().createApp(...args)
// ...
// 2. 结构出 app 实例的元素 mount 方法
const { mount } = app
// 3. 对 app 实例的 mount 方法进行扩展,主要是
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// 3.1 获取宿主节点
const container = normalizeContainer(containerOrSelector)
if (!container) return
// _component 是从哪里来的?在执行 createApp 的时候以_componnent 属性添加在 app 实例上
// 3.2 定义根组件的引用常量 component
const component = app._component
if (!isFunction(component) && !component.render && !component.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.
// 3.3 将宿主节点的 innerHTML 赋值到根组件的 template 属性上。
component.template = container.innerHTML
// 3.4 处理 vue2/vue3 的兼容性问题,如果数组节点上有非 v-cloak 指令,就打印警告,它主要想说明的是:
// vue2 是通过 outerHtml 将宿主节点整个都给替换为 vue 应用。
// 而 vue3 是通过 innerHtml 将 vue 应用作为子节点插入到宿主元素的内部。
// ===2.x compat check
// 检查宿主节点的 attribute,如果包含非 v-cloak 指令,发出警告
// vue2 通过 outerHtml,将整个应用替换掉宿主节点。
// vue3 通过 innerHtml,将整个应用作为子节点插入到宿主元素中。
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
}
}
}
}
// clear content before mounting
// 4. 在首次挂载之前清空宿主节点的innerHtml
container.innerHTML = ''
// 5. 执行扩展过后的mount方法,返回proxy常量
const proxy = mount(container, false, container instanceof SVGElement)
// 6. 最后移除宿主节点上的v-cloak指令
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
// 7 返回proxy
return proxy
}
return app
}) as CreateAppFunction<Element>原生 mount 方法定义的位置
原生 mount 方法会去执行 render 方法。
js
function createAppApi(render, hydrate) {
return function createApp(rootComponent, rootProps) {
const app = {
use() {/** */},
mixin() {/** */},
component() {/** */},
directive() {/** */},
mount() {
if (!isMounted) {
// ...创建根组件的虚拟 dom
const vnode = createVNode(
rootComponent as ConcreteComponent,
rootProps
)
// ...
if (isHydrate && hydrate) { // 如果是服务器渲染
hydrate(vnode as VNode<Node, Element>, rootContainer as any)
} else { // 会走这步
// 执行 renderer.ts 里的 render 方法
render(vnode, rootContainer, isSVG)
}
isMounted = true
// ...
} 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)\``
)
}
},
unmount() {/** */},
provide() {/** */}
}
return app;
}
}renderer.ts 里定义的名为 render 的内部方法
js
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 调用同样定义在 renderer.ts 文件里的名为 patch 的内部方法
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
// 这三行代码是干什么的不知道
flushPreFlushCbs()
flushPostFlushCbs()
container._vnode = vnode
}patch 方法内部会经过一系列判断,执行以下定义在 renderer.ts 里且同级的方法:
js
// 调用它
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// --->
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)mountComponent 方法内部
js
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// ...
// 初始化组件更新函数,创建数据更新副作用。
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
// ...
}setupRenderEffect 方法内部
js
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
// 组件更新函数
const componentUpdateFn = () => {
if (!instance.isMounted) { // 如果没有被挂载
if (el && hydrateNode) {
// ...
} else {
// ...
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// ...
}
} else { // 更新
// ...
patch(
prevTree,
nextTree,
// parent may have changed if it's in a teleport
hostParentNode(prevTree.el!)!,
// anchor may have changed if it's in a fragment
getNextHostNode(prevTree),
instance,
parentSuspense,
isSVG
)
// ...
}
}
// 创建响应式副作用,传入参数组件更新函数
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn,
() => queueJob(update),
instance.scope // track it in component's effect scope
))
const update: SchedulerJob = (instance.update = () => effect.run())
// effect.run() 方法最终其实执行的是 componentUpdateFn 方法。
update()
}查看 ReactiveEffect 类
js
export class ReactiveEffect<T = any> {
constructor(
public fn: () => T,
public scheduler: EffectScheduler | null = null,
scope?: EffectScope
) {
// ...
}
run() {
try {
Effect(this)
return this.fn()
} finally {
// ...
}
}
stop() {
}
}回归正题,接下来会执行 componentUpdateFn 方法,而它的主要作用是去执行 patch 方法。
咦,这里第二次出现了 patch 方法。第一次 patch 方法是在哪里被执行的呢?
没错,是原生 mount 方法里的 render 方法里,执行了 patch 方法。
那么这一会 patch 方法会去执行哪些方法呢?
会执行以下这些方法:
js
// patch 方法经过判断,会去执行 renderer 渲染器的
processElement()
// -->
mountElement()
// -->
mountChildren() // 因为内部有多个标签元素,会循环执行 mountElement 方法
// -->
mountElement()