Vue3源码阅读(一)
这里笔者以最新的3.4.27版本为例,记录一下Vue3的源码阅读。这篇文章会先梳理一个Vue项目在创建时内部工作的大体脉络。
一般来说,我们通过这样的方式来创建一个Vue3应用。
ts
<template>
<div id="app"></div>
</template>
import { createApp } from 'vue'
const app = createApp({
/* 根组件选项 */
})
app.mount('#app')
可以看到runtime-dom下的index.ts,注释掉一些与主流程无关的代码。
ts
export const createApp = ((...args) => {
// 创建App对象实例
const app = ensureRenderer().createApp(...args)
const { mount } = app
// 重写mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
// ...
}
return app
}) as CreateAppFunction<Element>
这里主要做了两个事情:
- 创建了一个实例对象app
- 重写mount方法
创建Vue实例对象app
其中,ensureRenderer函数最终返回了一个对象
ts
return {
render,// 渲染函数,核心
hydrate,
createApp: createAppAPI(render, hydrate),// 创建app实例方法
}
定位到createAppAPI函数
ts
export function createAppAPI (render,hydrate?) {
return function createApp(rootComponent, rootProps = null) {
if (!isFunction(rootComponent)) {
rootComponent = extend({}, rootComponent)
}
// 创建上下文对象
const context = createAppContext()
const installedPlugins = new WeakSet()
let isMounted = false
// 初始化app对象并挂到context.app下
const app: App = (context.app = {
_uid: uid++, // 绑定唯一id
_component: rootComponent as ConcreteComponent,// 根组件
_props: rootProps, // 根属性
_container: null,
_context: context,
_instance: null,
version,
// ...初始化 use、mixin、component、directive、mount、unmount、provide
})
return app
}
}
重写mount方法
在初始化的时候,内部还重写了app的mount方法,代码如下
ts
const { mount } = app
// 重写mount方法
app.mount = (containerOrSelector: Element | ShadowRoot | string): any => {
const container = normalizeContainer(containerOrSelector)
if (!container) return
const component = app._component
if (!isFunction(component) && !component.render && !component.template) {
component.template = container.innerHTML
}
container.innerHTML = ''
const proxy = mount(container, false, resolveRootNamespace(container))
if (container instanceof Element) {
container.removeAttribute('v-cloak')
container.setAttribute('data-v-app', '')
}
return proxy
}
// 实例内部的mount方法
mount(
rootContainer: HostElement,
isHydrate?: boolean,
namespace?: boolean | ElementNamespace,
): any {
const vnode = createVNode(rootComponent, rootProps)
vnode.appContext = context
if (namespace === true) {
namespace = 'svg'
} else if (namespace === false) {
namespace = undefined
}
// 调用Renderer返回的渲染函数
render(vnode, rootContainer, namespace)
isMounted = true
app._container = rootContainer
// for devtools and telemetry
;(rootContainer as any).__vue_app__ = app
return getExposeProxy(vnode.component!) || vnode.component!.proxy
}
这一步主要做了两个事情
- 调用createVNode生成vnode对象;
- 调用render函数,将vnode渲染成真实dom挂载到根节点;
生成vnode对象
ts
function _createVNode(
type: VNodeTypes | ClassComponent | typeof NULL_DYNAMIC_COMPONENT,
props: (Data & VNodeProps) | null = null,
children: unknown = null,
patchFlag: number = 0,
dynamicProps: string[] | null = null,
isBlockNode = false,
): VNode {
if (isVNode(type)) {
const cloned = cloneVNode(type, props, true /* mergeRef: true */)
if (children) {
normalizeChildren(cloned, children)
}
if (isBlockTreeEnabled > 0 && !isBlockNode && currentBlock) {
if (cloned.shapeFlag & ShapeFlags.COMPONENT) {
currentBlock[currentBlock.indexOf(type)] = cloned
} else {
currentBlock.push(cloned)
}
}
cloned.patchFlag |= PatchFlags.BAIL
return cloned
}
if (isClassComponent(type)) {
type = type.__vccOpts
}
// 初始化style和class属性
if (props) {
props = guardReactiveProps(props)!
let { class: klass, style } = props
if (klass && !isString(klass)) {
props.class = normalizeClass(klass)
}
if (isObject(style)) {
if (isProxy(style) && !isArray(style)) {
style = extend({}, style)
}
props.style = normalizeStyle(style)
}
}
// 设置vnode类型,以根节点为例,shapeFlag为4
const shapeFlag = isString(type)
? ShapeFlags.ELEMENT
: __FEATURE_SUSPENSE__ && isSuspense(type)
? ShapeFlags.SUSPENSE
: isTeleport(type)
? ShapeFlags.TELEPORT
: isObject(type)
? ShapeFlags.STATEFUL_COMPONENT
: isFunction(type)
? ShapeFlags.FUNCTIONAL_COMPONENT
: 0
const vnode = {...} as VNode // 初始化一系列属性
// 处理子集,若子集存在,创建vnode对象挂载到vnode.children上。并修改到对应的shapeFlag
normalizeChildren(vnode, children)
return vnode
}
这一步做了哪些事情呢?
- 处理当前已是vnode对象的情况,先拷贝,再生成children的vnode
- 初始化style和class属性
- 定义shapeFlag
- 创建基础VNode对象
- 根据子集情况,为子集创建vnode对象,并挂到当前vnode.children上。例如根组件执行时是没有children的
render函数
ts
const render: RootRenderFunction = (vnode, container, namespace) => {
// 若新vnode为null
if (vnode == null) {
// 旧vnode存在,卸载组件
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else { // 新vnode存在,执行patch
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
}
ts
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren,
) => {
... // 经过判断,分情况处理
// 根组件进入了下面这个判断
else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
render函数内部做了什么事情呢?
- 调用patch函数
- patch 函数通过判断vnode类型作出处理,如shapeFlag为4时调用processComponent函数处理
- 初始化时旧节点为null,所以调用mountElement 函数,否则调用patchElement函数
- mountElement 函数内部调用了setupComponent 函数,initProps、solts等属性,完成后继续调用setupRenderEffect函数
- 内部创建ReactiveEffect 实例对象effect,并绑定上componentUpdateFn 函数,随后调用update函数渲染出html代码,调用hostInsert插入到父节点中
- 如监测到数据更新,会继续触发update函数更新dom
至此,完成了根组件项目的初始化。由于这里逻辑非常多,笔者会另外解析。如有理解错误的地方,欢迎大佬指正啊。
总结
在Vue.createApp函数调用后,内部通过渲染器来创建一个app实例对象,随后生成上下文,重写mount方法等完成app的初始化。
在执行app.mount函数后,第一步创建vnode对象,然后调用render函数、patch函数将vnode更新成真实的dom渲染到页面上。