准备读源码
去github把vue3的代码仓库拉下来。
在package.json中给dev脚本加上sourcemap参数,然后执行脚本。
"dev": "node scripts/dev.js --sourcemap",
然后可以在下面这个路径中新建一个文件打开,在浏览器中进行断掉调试了。
core/packages/vue/examples/composition/test.html
首次渲染过程
要渲染出下面这个界面,vue要做出哪些步骤呢?
<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
// 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方法。
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的内部方法
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里且同级的方法:
// 调用它
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
// --->
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
mountComponent方法内部
const mountComponent: MountComponentFn = (
initialVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
) => {
// ...
// 初始化组件更新函数,创建数据更新副作用。
setupRenderEffect(
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
)
// ...
}
setupRenderEffect方法内部
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类
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方法会去执行哪些方法呢?
会执行以下这些方法:
// patch方法经过判断,会去执行renderer渲染器的
processElement()
// -->
mountElement()
// -->
mountChildren() // 因为内部有多个标签元素,会循环执行mountElement方法
// -->
mountElement()