从我最开始讲createApp
的时候,里面就有组件挂载的逻辑,但是我并没有讲,今天我们就来讲一下组件挂载的逻辑。
在createApp
中,我们使用mount
方法来将组件挂载到DOM
上,这里我们先回顾一下mount
方法的实现:
function mount(rootContainer, isHydrate, isSVG) {
// 判断是否已经挂载
if (!isMounted) {
// 这里的 #5571 是一个 issue 的 id,可以在 github 上搜索,这是一个在相同容器上重复挂载的问题,这里只做提示,不做处理
// #5571
if ((process.env.NODE_ENV !== 'production') && rootContainer.__vue_app__) {
warn(`There is already an app instance mounted on the host container.\n` +
` If you want to mount another app on the same host container,` +
` you need to unmount the previous app by calling \`app.unmount()\` first.`);
}
// 通过在 createApp 中传递的参数来创建虚拟节点
const vnode = createVNode(rootComponent, rootProps);
// store app context on the root VNode.
// this will be set on the root instance on initial mount.
// 上面有注释,在根节点上挂载 app 上下文,这个上下文会在挂载时设置到根实例上
vnode.appContext = context;
// HMR root reload
// 热更新
if ((process.env.NODE_ENV !== 'production')) {
context.reload = () => {
render(cloneVNode(vnode), rootContainer, isSVG);
};
}
// 通过其他的方式挂载,这里不一定指代的是服务端渲染,也可能是其他的方式
// 这一块可以通过创建渲染器的源码可以看出,我们日常在客户端渲染,不会使用到这一块,这里只是做提示,不做具体的分析
if (isHydrate && hydrate) {
hydrate(vnode, rootContainer);
}
// 其他情况下,直接通过 render 函数挂载
// render 函数在 createRenderer 中定义,传递到 createAppAPI 中,通过闭包缓存下来的
else {
render(vnode, rootContainer, isSVG);
}
// 挂载完成后,设置 isMounted 为 true
isMounted = true;
// 设置 app 实例的 _container 属性,指向挂载的容器
app._container = rootContainer;
// 挂载的容器上挂载 app 实例,也就是说我们可以通过容器找到 app 实例
rootContainer.__vue_app__ = app;
// 非生产环境默认开启 devtools,也可以通过全局配置来开启或关闭
// __VUE_PROD_DEVTOOLS__ 可以通过自己使用的构建工具来配置,这里只做提示
if ((process.env.NODE_ENV !== 'production') || __VUE_PROD_DEVTOOLS__) {
app._instance = vnode.component;
devtoolsInitApp(app, version);
}
// 返回 app 实例,这里不做具体的分析
return getExposeProxy(vnode.component) || vnode.component.proxy;
}
// 如果已经挂载过则输出提示消息,在非生产环境下
else if ((process.env.NODE_ENV !== 'production')) {
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)\``);
}
}
在我讲createApp
的时候其实已经知道了,组件的挂载其实就是通过render
函数来挂载的,上面的mount
方法中其实也可以看出来是使用render
函数来挂载的,简化之后的mount
方法如下:
function mount(rootContainer, isHydrate) {
// createApp 中传递的参数在我们这里肯定是一个对象,所以这里不做创建虚拟节点的操作,而是模拟一个虚拟节点
const vnode = {
type: rootComponent,
children: [],
component: null,
}
// 通过 render 函数渲染虚拟节点
render(vnode, rootContainer);
// 返回 app 实例
return vnode.component
}
上面的代码以及解释来自:【源码&库】在调用 createApp 时,Vue 为我们做了那些工作?
这次我们直接进入正题,看一下render
函数的实现,如果没有看过我上面写的createApp
的话,这里建议可以去看看;
因为render
函数是在baseCreateRenderer
中定义的,这一篇不会讲怎么找baseCreateRenderer
已经它的实现,如果初看可能找不到render
函数。
render
render
函数的实现如下:
/**
*
* @param vnode 虚拟节点
* @param container 容器
* @param isSVG 是否是 svg
*/
const render = (vnode, container, isSVG) => {
// 如果 vnode 不存在,说明是卸载
if (vnode == null) {
// 如果容器上有 vnode 才执行卸载操作,和最后一行对应
if (container._vnode) {
// 卸载
unmount(container._vnode, null, null, true);
}
} else {
// patch 核心,等会主讲
patch(container._vnode || null, vnode, container, null, null, null, isSVG);
}
// 触发一些需要在数据更新之前执行的回调函数
flushPreFlushCbs();
// 触发一些需要在数据更新之后执行的回调函数
flushPostFlushCbs();
// 将 vnode 赋值给容器的 _vnode 属性
container._vnode = vnode;
};
render
函数的实现很简单,就是判断vnode
是否存在,如果存在则执行patch
函数,如果不存在则执行unmount
函数。
unmount
函数不是我们这章主讲的,我们直接来看patch
函数。
patch
patch
函数的实现如下:
/**
*
* @param n1 上一个虚拟节点
* @param n2 当前虚拟节点
* @param container 容器
* @param anchor 锚点
* @param parentComponent
* @param parentSuspense
* @param isSVG
* @param optimized
*/
const patch = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = (process.env.NODE_ENV !== 'production') && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 相同的虚拟节点,直接返回
if (n1 === n2) {
return;
}
// patching & not same type, unmount old tree
// 如果 n1 存在,且 n1 和 n2 不是同一类型的虚拟节点,则卸载 n1
if (n1 && !isSameVNodeType(n1, n2)) {
// 用 n1 的下一个兄弟节点作为锚点
anchor = getNextHostNode(n1);
// 卸载 n1
unmount(n1, parentComponent, parentSuspense, true);
n1 = null;
}
// 如果确定 n2 这个节点没有动态内容
if (n2.patchFlag === -2 /* PatchFlags.BAIL */) {
// 没有动态内容,没必要再进行优化了
optimized = false;
// 将动态子节点置空,避免不必要的操作
n2.dynamicChildren = null;
}
// 获取 n2 的类型
// type 表示当前节点的类型,可以是字符串、标签名、组件对象、函数组件等
// ref 表示当前节点的 ref 属性,如果定义了 ref 属性,则 ref 指向这个节点的实例
// shapeFlag 表示当前节点的标识,是一个二进制数
const {type, ref, shapeFlag} = n2;
switch (type) {
// 文本节点
case Text:
processText(n1, n2, container, anchor);
break;
// 注释节点
case Comment:
processCommentNode(n1, n2, container, anchor);
break;
// 静态节点
case Static:
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
} else if ((process.env.NODE_ENV !== 'production')) {
patchStaticNode(n1, n2, container, isSVG);
}
break;
// Fragment 节点
case Fragment:
processFragment(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
break;
// 其他节点
default:
// 如果是元素节点
if (shapeFlag & 1 /* ShapeFlags.ELEMENT */) {
processElement(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
// 如果是组件节点
else if (shapeFlag & 6 /* ShapeFlags.COMPONENT */) {
processComponent(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
// 如果是 Teleport 组件包裹的节点
else if (shapeFlag & 64 /* ShapeFlags.TELEPORT */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
}
// 如果是 Suspense 组件包裹的节点
else if (shapeFlag & 128 /* ShapeFlags.SUSPENSE */) {
type.process(n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized, internals);
}
// 不是以上类型的节点,报错
else if ((process.env.NODE_ENV !== 'production')) {
warn('Invalid VNode type:', type, `(${ typeof type })`);
}
}
// set ref
// 如果 n2 定义了 ref 属性,则调用 setRef 函数
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2);
}
};
patch
函数的参数比较多,代码也很多,不要着急,我们一步一步来看。
首先我们目的学习组件是如何挂载的,那么我们只需要关注switch
语句中的代码即可。
switch
语句中一共有八种情况:
- 文本节点
- 注释节点
- 静态节点
- Fragment 节点
- 元素节点
- 组件节点
- Teleport 组件包裹的节点
- Suspense 组件包裹的节点
这其中Teleport
和Suspense
组件包裹的节点,我们暂时不用关注,因为这两个组件还没有讲到,我们先来看看前面的几种情况。
文本节点的挂载
文本节点挂载是通过processText
函数来实现的,processText
函数的代码如下:
/**
* 处理文本节点
* @param n1 旧的虚拟节点
* @param n2 新的虚拟节点
* @param container 容器
* @param anchor 锚点
*/
const processText = (n1, n2, container, anchor) => {
// n1 不存在,说明是新节点,直接创建文本节点
if (n1 == null) {
hostInsert((n2.el = hostCreateText(n2.children)), container, anchor);
} else {
// n1 存在,说明是更新节点,复用旧节点
const el = (n2.el = n1.el);
// 如果新旧节点的文本内容不一致,则更新文本内容
if (n2.children !== n1.children) {
hostSetText(el, n2.children);
}
}
};
processText
的实现还是比较简单的,这里主要关心的是hostInsert
、hostCreateText
和hostSetText
这三个函数。
但是这三个函数是在createApp
函数实现的时候定义的,又说到了createApp
函数,还是建议没看过我之前的文章的同学先去看看。
注释节点的挂载
注释节点挂载是通过processCommentNode
函数来实现的,processCommentNode
函数的代码如下:
const processCommentNode = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert((n2.el = hostCreateComment(n2.children || '')), container, anchor);
} else {
// there's no support for dynamic comments
n2.el = n1.el;
}
};
内部逻辑和processText
函数类似,这里代码量也不多,主要是将hostCreateText
换成了hostCreateComment
,可以自己花个几秒钟对比一下;
静态节点的挂载
静态节点挂载在switch
语句中的代码不同于文本节点和注释节点,因为静态节点代表的就是不会发生变化的节点,所以我们不需要每次都去更新节点,只需要在第一次挂载的时候创建节点即可。
而在开发环境下,我们可能会手动修改静态节点的内容,这时候我们需要重新渲染静态节点,所以在开发环境下,会执行patchStaticNode
函数,而在生产环境下,会执行processText
函数。
case Static:
// 直接挂载静态节点
if (n1 == null) {
mountStaticNode(n2, container, anchor, isSVG);
}
// 在开发环境下,如果静态节点发生了变化,则打补丁
else if ((process.env.NODE_ENV !== 'production')) {
patchStaticNode(n1, n2, container, isSVG);
}
mountStaticNode
和patchStaticNode
函数的代码如下:
const mountStaticNode = (n2, container, anchor, isSVG) => {
[n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG, n2.el, n2.anchor);
};
/**
* Dev / HMR only
*/
const patchStaticNode = (n1, n2, container, isSVG) => {
// static nodes are only patched during dev for HMR
if (n2.children !== n1.children) {
const anchor = hostNextSibling(n1.anchor);
// remove existing
removeStaticNode(n1);
[n2.el, n2.anchor] = hostInsertStaticContent(n2.children, container, anchor, isSVG);
} else {
n2.el = n1.el;
n2.anchor = n1.anchor;
}
};
代码也很简单,这里一个细节就是通过解构赋值的方式将hostInsertStaticContent
函数的返回值赋值给n2.el
和n2.anchor
;
就是不知道为啥patchStaticNode
函数中的else
没有用解构赋值的方式,而是直接赋值的。
Fragment 节点的挂载
Fragment 节点的挂载是通过processFragment
函数来实现的,processFragment
函数的代码如下:
const processFragment = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
// 如果旧的虚拟节点不存在,则创建一个文本节点作为开始和结束的锚点标记
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''));
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''));
// 获取新节点(n2)中的 patchFlag、dynamicChildren、fragmentSlotScopeIds
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2;
// 只有在开发环境下,且是 HMR 更新或者是根节点的 Fragment 才会执行
if ((process.env.NODE_ENV !== 'production') &&
// #5523 dev root fragment may inherit directives
(isHmrUpdating || patchFlag & 2048 /* PatchFlags.DEV_ROOT_FRAGMENT */)) {
// HMR updated / Dev root fragment (w/ comments), force full diff
patchFlag = 0;
optimized = false;
dynamicChildren = null;
}
// check if this is a slot fragment with :slotted scope ids
// 检查这是否是一个具有 :slotted 作用域 ID 的插槽片段
if (fragmentSlotScopeIds) {
// 如果 slotScopeIds 存在,则将 fragmentSlotScopeIds 和 slotScopeIds 进行合并
slotScopeIds = slotScopeIds
? slotScopeIds.concat(fragmentSlotScopeIds)
: fragmentSlotScopeIds;
}
// 如果没有旧节点就直接挂载
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor);
hostInsert(fragmentEndAnchor, container, anchor);
// a fragment can only have array children
// since they are either generated by the compiler, or implicitly created
// from arrays.
// 一个片段只能有数组子节点,因为它们要么由编译器生成,要么隐式地从数组中创建。
mountChildren(n2.children, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
else {
if (patchFlag > 0 &&
patchFlag & 64 /* PatchFlags.STABLE_FRAGMENT */ &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
// #2715 之前的片段可能是一个 BAILed 的片段,因为 renderSlot() 没有有效的子节点
n1.dynamicChildren) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
// 一个稳定的片段(模板根或 <template v-for>)不需要修补子节点顺序,但它可能包含 dynamicChildren。
patchBlockChildren(n1.dynamicChildren, dynamicChildren, container, parentComponent, parentSuspense, isSVG, slotScopeIds);
if ((process.env.NODE_ENV !== 'production') && parentComponent && parentComponent.type.__hmrId) {
traverseStaticChildren(n1, n2);
}
else if (
// #2080 if the stable fragment has a key, it's a <template v-for> that may
// get moved around. Make sure all root level vnodes inherit el.
// #2134 or if it's a component root, it may also get moved around
// as the component is being moved.
// #2080 如果稳定片段有一个 key,那么它是一个 <template v-for> 可能会被移动。确保所有根级 vnode 继承 el。
// #2134 或者如果它是一个组件根,它也可能会被移动,因为组件正在被移动。
n2.key != null ||
(parentComponent && n2 === parentComponent.subTree)) {
traverseStaticChildren(n1, n2, true /* shallow */);
}
}
else {
// keyed / unkeyed, or manual fragments.
// for keyed & unkeyed, since they are compiler generated from v-for,
// each child is guaranteed to be a block so the fragment will never
// have dynamicChildren.
// 对于有 key 和无 key 的,因为它们是从 v-for 编译器生成的,所以每个子节点都保证是一个块,因此片段永远不会有 dynamicChildren。
patchChildren(n1, n2, container, fragmentEndAnchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
}
};
processFragment
函数中的代码比较多,而且注释也比较多,所以这里就不一一解释了,大家可以自己去捉摸着看看;
Element 节点的挂载
Element
节点的挂载是通过processElement
函数来实现的,processElement
函数的代码如下:
这里会涉及到
shapeFlag
的使用,shapeFlag
就是在createVNode
函数中,通过判断type
的类型来赋值的,这里只做提示,具体的可以看看createVNode
函数;
const processElement = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
isSVG = isSVG || n2.type === 'svg';
if (n1 == null) {
mountElement(n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized);
}
};
还是老样子,如果没有旧节点就直接挂载,如果有旧节点就进行更新,这里主要讲的是组件的挂载,所以就暂时先不跟进去详细介绍了;
组件的挂载
组件的挂载是通过processComponent
函数来实现的,processComponent
函数的代码如下:
const processComponent = (n1, n2, container, anchor, parentComponent, parentSuspense, isSVG, slotScopeIds, optimized) => {
n2.slotScopeIds = slotScopeIds;
// 如果没有旧节点就直接挂载
if (n1 == null) {
// 如果是 keep-alive 组件,就调用父组件的 activate 方法
if (n2.shapeFlag & 512 /* ShapeFlags.COMPONENT_KEPT_ALIVE */) {
parentComponent.ctx.activate(n2, container, anchor, isSVG, optimized);
}
// 否则就直接挂载
else {
mountComponent(n2, container, anchor, parentComponent, parentSuspense, isSVG, optimized);
}
} else {
// 更新组件
updateComponent(n1, n2, optimized);
}
};
看了上面那么多类型的节点挂载,其实发现组件的挂载和上面的节点挂载都差不多,主要还是看mountComponent
函数和updateComponent
函数的实现;
mountComponent
mountComponent
函数的代码如下:
/**
* @param initialVNode 组件的 vnode
* @param container 容器
* @param anchor 锚点
* @param parentComponent 父组件
* @param parentSuspense 父 suspense
* @param isSVG 是否是 svg
* @param optimized 是否优化
*/
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// 创建组件实例
const instance = (initialVNode.component = createComponentInstance(initialVNode, parentComponent, parentSuspense));
// inject renderer internals for keepAlive
// 为 keepAlive 注入 renderer
if (isKeepAlive(initialVNode)) {
instance.ctx.renderer = internals;
}
// resolve props and slots for setup context
// 为 setup 上下文解析 props 和 slots
setupComponent(instance);
// setup() is async. This component relies on async logic to be resolved
// before proceeding
// setup() 是异步的。在挂载之前需要等待异步逻辑完成
if (instance.asyncDep) {
// 如果有 parentSuspense,就注册依赖
parentSuspense && parentSuspense.registerDep(instance, setupRenderEffect);
// Give it a placeholder if this is not hydration
// TODO handle self-defined fallback
// 如果这不是"hydration"(首次渲染直接输出HTML而非先输出一个空的HTML再用 JS 进行“hydration”),那么给它一个占位符。
// TODO 处理自定义的 fallback
// 如果没有 el 属性,也就是说当前组件不存在,那么就创建一个占位符
if (!initialVNode.el) {
// 通过注释节点来创建一个占位符
const placeholder = (instance.subTree = createVNode(Comment));
processCommentNode(null, placeholder, container, anchor);
}
return;
}
// 执行组件的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
上面的代码移除了开发环境下的一些代码,后面的代码讲解也会默认去掉开发环境下的代码;
上面的代码其实并不多,抛开边界情况,只执行了三个函数,createComponentInstance
、setupComponent
、setupRenderEffect
;
createComponentInstance
createComponentInstance
函数的代码如下:
// 在 createApp 的文章中讲到过 createAppContext 函数
const emptyAppContext = createAppContext();
// 组件实例的 uid
let uid = 0;
function createComponentInstance(vnode, parent, suspense) {
const type = vnode.type;
// inherit parent app context - or - if root, adopt from root vnode
// 继承父应用上下文 - 或者 - 如果是根节点,就从根 vnode 中继承
const appContext = (parent ? parent.appContext : vnode.appContext) || emptyAppContext;
// 组件实例
const instance = {
uid: uid++,
vnode,
type,
parent,
appContext,
root: null,
next: null,
subTree: null,
effect: null,
update: null,
scope: new EffectScope(true /* detached */),
render: null,
proxy: null,
exposed: null,
exposeProxy: null,
withProxy: null,
provides: parent ? parent.provides : Object.create(appContext.provides),
accessCache: null,
renderCache: [],
// local resolved assets
components: null,
directives: null,
// resolved props and emits options
propsOptions: normalizePropsOptions(type, appContext),
emitsOptions: normalizeEmitsOptions(type, appContext),
// emit
emit: null,
emitted: null,
// props default value
propsDefaults: EMPTY_OBJ,
// inheritAttrs
inheritAttrs: type.inheritAttrs,
// state
ctx: EMPTY_OBJ,
data: EMPTY_OBJ,
props: EMPTY_OBJ,
attrs: EMPTY_OBJ,
slots: EMPTY_OBJ,
refs: EMPTY_OBJ,
setupState: EMPTY_OBJ,
setupContext: null,
// suspense related
suspense,
suspenseId: suspense ? suspense.pendingId : 0,
asyncDep: null,
asyncResolved: false,
// lifecycle hooks
// not using enums here because it results in computed properties
isMounted: false,
isUnmounted: false,
isDeactivated: false,
bc: null,
c: null,
bm: null,
m: null,
bu: null,
u: null,
um: null,
bum: null,
da: null,
a: null,
rtg: null,
rtc: null,
ec: null,
sp: null
};
// 将组件实例挂载到 ctx 上
instance.ctx = { _: instance };
// 根组件的实例
instance.root = parent ? parent.root : instance;
// 组件实例的 emit 函数,将 this 指向组件实例
instance.emit = emit.bind(null, instance);
// apply custom element special handling
// 应用自定义元素特殊处理
if (vnode.ce) {
vnode.ce(instance);
}
// 返回组件实例
return instance;
}
这里就做了一些组件的实例初始化的工作,初始化完成之后,将组件实例返回;
setupComponent
setupComponent
函数的代码如下:
// 是否在 SSR 组件的 setup 中
let isInSSRComponentSetup = false;
/**
* @param instance 组件实例
* @param isSSR 是否是 SSR
* @return {*}
*/
function setupComponent(instance, isSSR = false) {
isInSSRComponentSetup = isSSR;
// 获取组件的 props、children
const { props, children } = instance.vnode;
// 是否是有状态的组件
const isStateful = isStatefulComponent(instance);
// 初始化 props
initProps(instance, props, isStateful, isSSR);
// 初始化 slots
initSlots(instance, children);
// 有状态的组件调用 setupStatefulComponent 函数
const setupResult = isStateful
? setupStatefulComponent(instance, isSSR)
: undefined;
isInSSRComponentSetup = false;
// 返回 setup 函数的返回值
return setupResult;
}
setupComponent
函数的作用是初始化组件的 props
、slots
,然后调用 setupStatefulComponent
函数,setupStatefulComponent
函数的作用是调用组件的 setup
函数;
isStatefulComponent
就是通过组件的shapeFlag
来判断组件是否是有状态的组件;
initProps
initProps
函数的代码如下:
function initProps(instance, rawProps, isStateful, // result of bitwise flag comparison
isSSR = false) {
const props = {};
const attrs = {};
// 通过 Object.defineProperty 将 InternalObjectKey (__vInternal)设置为 1,并且不可枚举
def(attrs, InternalObjectKey, 1);
// 通过 Object.create(null) 创建一个空对象,这个对象没有原型链
instance.propsDefaults = Object.create(null);
// 设置组件的 props、attrs
setFullProps(instance, rawProps, props, attrs);
// ensure all declared prop keys are present
// 确保所有声明的 prop key 都存在
for (const key in instance.propsOptions[0]) {
if (!(key in props)) {
props[key] = undefined;
}
}
// 有状态的组件
if (isStateful) {
// stateful
// 将 props 设置为响应式的
instance.props = isSSR ? props : shallowReactive(props);
}
else {
// 当函数式组件没有声明 props 时,将 attrs 设置为组件的 props
if (!instance.type.props) {
// functional w/ optional props, props === attrs
// 当组件没有声明 props 时,传递给组件的 porps 会自动设置为 attrs,这个时候 props 和 attrs 是等价的
instance.props = attrs;
}
else {
// functional w/ declared props
// 函数式组件声明了 props,保存 props
instance.props = props;
}
}
// 设置组件的 attrs
instance.attrs = attrs;
}
这里的关键点是通过 setFullProps
函数将 rawProps
转换为 props
和attrs
,然后通过 shallowReactive
函数将 props
设置为响应式的;
initSlots
initSlots
函数的代码如下:
const initSlots = (instance, children) => {
// 如果 vnode 的 shapeFlag 为 32,对应是 SLOTS_CHILDREN
if (instance.vnode.shapeFlag & 32 /* ShapeFlags.SLOTS_CHILDREN */) {
// “_” 代表的是组件的实例,在 createComponentInstance 中有出现
const type = children._;
if (type) {
// users can get the shallow readonly version of the slots object through `this.$slots`,
// we should avoid the proxy object polluting the slots of the internal instance
// 用户可以通过 this.$slots 获取 slots 对象的浅只读版本,
// 我们应该避免代理对象污染内部实例的 slots
instance.slots = toRaw(children);
// make compiler marker non-enumerable
// 将编译器标记设置为不可枚举
def(children, '_', type);
}
else {
// 如果 type 不存在,说明没有定义 slots,直接将 children 赋值给 instance.slots
normalizeObjectSlots(children, (instance.slots = {}));
}
}
else {
// 如果 vnode 的 shapeFlag 不为 32,说明没有定义 slots,直接将 instance.slots 赋值为空对象
instance.slots = {};
// 如果 children 存在,说明有默认插槽,需要进行插槽的规范化
if (children) {
normalizeVNodeSlots(instance, children);
}
}
// 将 InternalObjectKey 设置为 1,并且设置为不可枚举,这个在上面的 initProps 函数中有出现
def(instance.slots, InternalObjectKey, 1);
};
这里主要是对 slots
进行初始化,如果 vnode
的 shapeFlag
为 32
,则说明定义了 slots
,否则说明没有定义 slots
;
如果定义了 slots
,则会将 children
赋值给 instance.slots
,否则将 instance.slots
赋值为空对象;
如果 children
存在,说明有默认插槽,需要进行插槽的规范化;
这里不过多的深入 props 和 slots 的实现,这里有个大概的意思就可以了,后面会有专门的章节来讲解 props 和 slots 的实现;
setupStatefulComponent
setupStatefulComponent
函数的代码如下:
function setupStatefulComponent(instance, isSSR) {
var _a;
// instance.type 就是组件的定义,比如 App.vue
const Component = instance.type;
// 0. create render proxy property access cache
// 0. 创建 render 代理属性访问缓存
instance.accessCache = Object.create(null);
// 1. create public instance / render proxy
// also mark it raw so it's never observed
// 1. 创建公共实例 / render 代理
// 同时将其标记为原始的,以便永远不会被观察到
instance.proxy = markRaw(new Proxy(instance.ctx, PublicInstanceProxyHandlers));
// 2. call setup()
// 2. 调用 setup()
const { setup } = Component;
if (setup) {
// 获取 setupContext 对象
const setupContext = (instance.setupContext =
setup.length > 1 ? createSetupContext(instance) : null);
// 设置当前实例
setCurrentInstance(instance);
// 暂停追踪依赖
pauseTracking();
// 调用 setup 函数并获取返回值
const setupResult = callWithErrorHandling(setup, instance, 0 /* ErrorCodes.SETUP_FUNCTION */, [instance.props, setupContext]);
// 恢复追踪依赖
resetTracking();
// 解绑当前实例
unsetCurrentInstance();
// 如果返回值是 Promise
if (isPromise(setupResult)) {
// 等待 Promise 执行完毕就解绑当前实例
setupResult.then(unsetCurrentInstance, unsetCurrentInstance);
// 如果是服务端渲染
if (isSSR) {
// return the promise so server-renderer can wait on it
// 返回 Promise,以便服务器渲染器可以等待它
return setupResult
.then((resolvedResult) => {
handleSetupResult(instance, resolvedResult, isSSR);
})
.catch(e => {
handleError(e, instance, 0 /* ErrorCodes.SETUP_FUNCTION */);
});
}
else {
// async setup returned Promise.
// bail here and wait for re-entry.
// 异步 setup 返回 Promise。
// 在这里中止并等待重新进入。
instance.asyncDep = setupResult;
}
}
else {
// 同步 setup 返回值
handleSetupResult(instance, setupResult, isSSR);
}
}
else {
// 没有定义 setup 函数,直接调用 finishComponentSetup
finishComponentSetup(instance, isSSR);
}
}
这里主要是对 setup
函数进行调用,如果 setup
函数返回的是 Promise
,则会等待 Promise
执行完毕,否则直接调用 finishComponentSetup
函数;
finishComponentSetup
而finishComponentSetup
函数执行的主要是对template
或者 render
函数的编译,代码如下:
function finishComponentSetup(instance, isSSR, skipOptions) {
// 实例上的 type 就是组件的定义,比如 App.vue
const Component = instance.type;
// template / render function normalization
// could be already set when returned from setup()
// template / render 函数规范化
// 可能已经在从 setup() 返回时设置了
if (!instance.render) {
// only do on-the-fly compile if not in SSR - SSR on-the-fly compilation
// is done by server-renderer
// 只有在 SSR 时才进行即时编译 - 即时编译 SSR 是由服务器渲染器完成的
if (!isSSR && compile && !Component.render) {
// 获取 template,如果当前组件没有定义 template,则会通过合并实例中的属性获取 template
// 这里的 template 则可能是 mixins 中的 template
const template = Component.template ||
resolveMergedOptions(instance).template;
// 如果 template 存在,则进行编译
if (template) {
// 获取全局配置
const { isCustomElement, compilerOptions } = instance.appContext.config;
// 获取组件的 delimiters 和 compilerOptions
const { delimiters, compilerOptions: componentCompilerOptions } = Component;
const finalCompilerOptions = extend(extend({
isCustomElement,
delimiters
}, compilerOptions), componentCompilerOptions);
// 编译 template,得到 render 函数
Component.render = compile(template, finalCompilerOptions);
}
}
// 获取 render 函数
instance.render = (Component.render || NOOP);
// for runtime-compiled render functions using `with` blocks, the render
// proxy used needs a different `has` handler which is more performant and
// also only allows a whitelist of globals to fallthrough.
// 对于使用 `with` 块的运行时编译的 render 函数,使用的 render 代理需要一个不同的 `has` 处理程序,该处理程序更高效,并且只允许白名单的全局变量通过。
if (installWithProxy) {
installWithProxy(instance);
}
}
// support for 2.x options
// 2.x 选项的支持
if (__VUE_OPTIONS_API__ && !(false )) {
setCurrentInstance(instance);
pauseTracking();
applyOptions(instance);
resetTracking();
unsetCurrentInstance();
}
}
这里主要是对 template
或者 render
函数的编译,如果 template
存在,则会进行编译,得到 render
函数;
代码中的配置项,例如:
isCustomElement
、compilerOptions
、delimiters
、compilerOptions
等,可以在官网中查到,这里不提供链接了,大家可以自行查阅。
到这里,setup
函数的调用就结束了,接下来就是对 render
函数的调用了;
setupRenderEffect
render
函数的调用,主要是在 mountComponent
函数中,在mountComponent
函数中,最后一行代码如下:
const mountComponent = (initialVNode, container, anchor, parentComponent, parentSuspense, isSVG, optimized) => {
// ...
// 执行组件的渲染函数
setupRenderEffect(instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized);
};
setupRenderEffect
函数的代码如下:
const setupRenderEffect = (instance, initialVNode, container, anchor, parentSuspense, isSVG, optimized) => {
// 组件的更新函数
const componentUpdateFn = () => {
// ...
};
// create reactive effect for rendering
// 创建用于渲染的响应式 effect
const effect = (instance.effect = new ReactiveEffect(componentUpdateFn, () => queueJob(update), instance.scope // track it in component's effect scope
));
// 组件的更新钩子函数
const update = (instance.update = () => effect.run());
// 组件uid,在上面出现过
update.id = instance.uid;
// allowRecurse
// #1801, #2043 component render effects should allow recursive updates
// 允许递归
// #1801,#2043 组件渲染效果应该允许递归更新
toggleRecurse(instance, true);
// 执行组件的更新函数
update();
};
这里主要是创建了一个 effect
函数,而这个ReactiveEffect
就是在我之前讲的响应式核心中提到的 ReactiveEffect
,巧了不是?这一下直接把之前的文章都串起来了;
这里首先通过 new ReactiveEffect
创建了一个 effect
函数,里面执行的副作用函数就是 componentUpdateFn
;
后面传了一个调度器,这个调度器就是queueJob(update)
,这个queueJob
函数在讲nextTick
时出现过,这一下全都串起来了;
最后执行了 update
函数,然后就会触发依赖收集,当我们修改了响应式数据时,就会触发依赖更新,从而触发 effect
函数,从而执行 componentUpdateFn
函数;
componentUpdateFn
componentUpdateFn
函数的内部实现主要有两个部分,一个是组件挂载的逻辑,一个是组件更新的逻辑;
我们先来看组件挂载的逻辑:
const componentUpdateFn = () => {
// isMounted 表示组件是否已经挂载
if (!instance.isMounted) {
let vnodeHook;
// initialVNode 是组件的虚拟节点
const { el, props } = initialVNode;
// 获取组件的 beforeMount、mounted 钩子函数 和 父组件
const { bm, m, parent } = instance;
// 是否是异步组件
const isAsyncWrapperVNode = isAsyncWrapper(initialVNode);
// 不允许递归调用
toggleRecurse(instance, false);
// beforeMount hook
// 执行组件的 beforeMount 钩子函数
if (bm) {
invokeArrayFns(bm);
}
// onVnodeBeforeMount
// 执行组件的 onVnodeBeforeMount 钩子函数
if (!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parent, initialVNode);
}
// 允许递归调用
toggleRecurse(instance, true);
// 执行 hydrateNode
if (el && hydrateNode) {
// vnode has adopted host node - perform hydration instead of mount.
// vnode 已经接管了宿主节点 - 执行 hydration 而不是 mount
const hydrateSubTree = () => {
instance.subTree = renderComponentRoot(instance);
hydrateNode(el, instance.subTree, instance, parentSuspense, null);
};
// 如果是异步组件,则执行异步加载
if (isAsyncWrapperVNode) {
initialVNode.type.__asyncLoader().then(
// note: we are moving the render call into an async callback,
// which means it won't track dependencies - but it's ok because
// a server-rendered async wrapper is already in resolved state
// and it will never need to change.
// 注意:我们将渲染调用移动到异步回调中,
// 这意味着它不会跟踪依赖关系 - 但这是可以的,因为
// 服务器渲染的异步包装器已经处于解决状态
// 它永远不会需要改变
() => !instance.isUnmounted && hydrateSubTree());
}
else {
hydrateSubTree();
}
}
// 否则就直接执行 patch 函数进行挂载
else {
// 挂载的是组件的子树
const subTree = (instance.subTree = renderComponentRoot(instance));
patch(null, subTree, container, anchor, instance, parentSuspense, isSVG);
initialVNode.el = subTree.el;
}
// mounted hook
// 执行组件的 mounted 钩子函数
if (m) {
queuePostRenderEffect(m, parentSuspense);
}
// onVnodeMounted
// 执行组件的 onVnodeMounted 钩子函数
if (!isAsyncWrapperVNode &&
(vnodeHook = props && props.onVnodeMounted)) {
const scopedInitialVNode = initialVNode;
queuePostRenderEffect(() => invokeVNodeHook(vnodeHook, parent, scopedInitialVNode), parentSuspense);
}
// activated hook for keep-alive roots.
// #1742 activated hook must be accessed after first render
// since the hook may be injected by a child keep-alive
// keep-alive 根的激活钩子
// #1742 激活钩子必须在第一次渲染后访问
// 由于钩子可能由子 keep-alive 注入
if (initialVNode.shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */ ||
(parent &&
isAsyncWrapper(parent.vnode) &&
parent.vnode.shapeFlag & 256 /* ShapeFlags.COMPONENT_SHOULD_KEEP_ALIVE */)) {
instance.a && queuePostRenderEffect(instance.a, parentSuspense);
}
// 设置组件已经挂载
instance.isMounted = true;
// #2458: deference mount-only object parameters to prevent memleaks
// #2458:延迟挂载仅对象参数以防止内存泄漏
initialVNode = container = anchor = null;
}
};
可以看到在挂载的过程中,会执行一系列相关的钩子函数,比如 beforeMount
、mounted
、onVnodeBeforeMount
、onVnodeMounted
等等;
这里可能有疑问的是onVnodeBeforeMount
和onVnodeMounted
这两个钩子函数是啥?其实这是Vue
内部使用的钩子函数,我们可以在Vue
的源码中找到它们的定义:
// core/packages/runtime-core/src/vnode.ts 91-113
type VNodeMountHook = (vnode: VNode) => void
type VNodeUpdateHook = (vnode: VNode, oldVNode: VNode) => void
export type VNodeHook =
| VNodeMountHook
| VNodeUpdateHook
| VNodeMountHook[]
| VNodeUpdateHook[]
// https://github.com/microsoft/TypeScript/issues/33099
export type VNodeProps = {
key?: string | number | symbol
ref?: VNodeRef
ref_for?: boolean
ref_key?: string
// vnode hooks
onVnodeBeforeMount?: VNodeMountHook | VNodeMountHook[]
onVnodeMounted?: VNodeMountHook | VNodeMountHook[]
onVnodeBeforeUpdate?: VNodeUpdateHook | VNodeUpdateHook[]
onVnodeUpdated?: VNodeUpdateHook | VNodeUpdateHook[]
onVnodeBeforeUnmount?: VNodeMountHook | VNodeMountHook[]
onVnodeUnmounted?: VNodeMountHook | VNodeMountHook[]
}
至于具体是用于干啥的我并没有找到相关的内容,后续找到了再补充。
这里需要注意的是,挂载最后还是执行的patch
函数,挂载的是组件的子树,这里的子树是通过renderComponentRoot
函数生成的;
总结
这一章主要是介绍了Vue3
的组件挂载过程,其实内容到这里有点仓促结束了,因为这一章的内容比较多,但是通过这一章我们关联了很多东西;
组件的挂载过程中,会使用ReactiveEffect
来执行render
函数,而这个是我们之前花了很多时间来讲的;
同时还是会使用queueJob
来执行updateComponent
函数,这个也是在之前讲过的,可以看看nextTick
的实现那一章;
最后组件挂载是通过patch
函数来完成的,而挂载到最后还是绕回到了patch
函数,后续我们会继续深入的讲解patch
函数的实现;
宣传
大家好,这里是田八的【源码&库】系列,
Vue3
的源码阅读计划,Vue3
的源码阅读计划不出意外每周一更,欢迎大家关注。如果想一起交流的话,可以点击这里一起共同交流成长
系列章节:
本文正在参加「金石计划」