回顾
上面介绍完 mountComponent 函数的,创建组实例和初始化组件两个关键步骤,接下来分析组件渲染 setupRenderEffect 函数。
setupRenderEffect
const setupRenderEffect: SetupRenderEffectFn = (
instance,
initialVNode,
container,
anchor,
parentSuspense,
isSVG,
optimized
) => {
const componentUpdateFn = () => {
/* 组件渲染相关逻辑 */
}
// 创建渲染 reactive effect
const effect = (instance.effect = new ReactiveEffect(
componentUpdateFn, // 渲染逻辑
() => queueJob(instance.update),
instance.scope // track it in component's effect scope
))
// 初始化渲染函数 update
const update = (instance.update = effect.run.bind(effect) as SchedulerJob)
// 确认渲染函数 id = 组件实例 uid
update.id = instance.uid
// ...
// 执行渲染函数
update()
}
上述代码涉及到了 vue3 的响应模式 effect,这个后续分析。
先看下渲染相关的函数 componentUpdateFn
componentUpdateFn
const componentUpdateFn = () => {
if (!instance.isMounted) {
// 未被渲染执行下面代码
// ...
if (el && hydrateNode) {
// ... ssr
} else {
// render start
// ...
const subTree = (instance.subTree = renderComponentRoot(instance))
// ...
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// ...
initialVNode.el = subTree.el
}
// ...
} else {
// 组件更新执行下面代码
// ...
}
}
此函数组要是执行了两个函数 renderComponentRoot 和 patch,首先,看看 renderComponentRoot 函数,入参是组件实例(instance)
renderComponentRoot
export function renderComponentRoot(
instance: ComponentInternalInstance
): VNode {
const {
type: Component,
vnode,
proxy,
withProxy,
props,
propsOptions: [propsOptions],
slots,
attrs,
emit,
render,
renderCache,
data,
setupState,
ctx,
inheritAttrs
} = instance
let result
let fallthroughAttrs
// ...
try {
if (vnode.shapeFlag & ShapeFlags.STATEFUL_COMPONENT) {
const proxyToUse = withProxy || proxy
result = normalizeVNode(
render!.call(
proxyToUse,
proxyToUse!,
renderCache,
props,
setupState,
data,
ctx
)
)
fallthroughAttrs = attrs
} else {
// ...
}
} catch (err) {
// ...
}
// ...
return result
}
首先,执行了 render!.call(proxyToUse, ...) 函数,简单回顾下 render 函数。
function render(_ctx/* withProxy */, _cache/* [] */) {
with (_ctx) {
const { toDisplayString: _toDisplayString, openBlock: _openBlock, createElementBlock: _createElementBlock } = _Vue
return (_openBlock(), _createElementBlock("div", null, _toDisplayString(msg), 1 /* TEXT */))
}
}
此函数,执行了两个函数 openBlock 和 createElementBlock,其中 openBlock 函数,用来生成存放子 vnode 对象的数组,代码如下:
export const blockStack: (VNode[] | null)[] = []
let currentBlock: VNode[] | null = null
// openBlock 函数定义
export function openBlock(disableTracking = false) {
blockStack.push((currentBlock = disableTracking ? null : []))
}
剩下的 createElementBlock 函数,用来生成 vnode,其中,需要注意的是 _toDisplayString(msg) 中的 msg,会触发 render 函数中,_ctx(withProxy)的 has 拦截。
get(target: ComponentRenderContext, key: string) {
// 判断属性名是否等于 Symbol.unscopables
// 此时 key = msg
if ((key as any) === Symbol.unscopables) {
return
}
// 因此执行 PublicInstanceProxyHandlers.get 函数
return PublicInstanceProxyHandlers.get!(target, key, target)
},
has(_: ComponentRenderContext, key: string) {
// 判断属性名称 key 值不是以 '_' 开头的,并且不是特定的一些字符串,类似 Object、Boolean 等
const has = key[0] !== '_' && !isGloballyWhitelisted(key)
// ...
return has
}
因为,return has = true,所以,执行 \_ctx.msg,触发 get 拦截,执行 PublicInstanceProxyHandlers.get 函数
// PublicInstanceProxyHandlers内部get钩子函数的具体实现
get({ _: instance }: ComponentRenderContext, key: string) {
// ...
let normalizedProps
// 判断属性名 key(msg) 不是以'$'开头
if (key[0] !== '$') {
// 不存在于 instance 的 accessCache 缓存对象中
const n = accessCache![key]
// ...
} else if (setupState !== EMPTY_OBJ && hasOwn(setupState, key)) {
accessCache![key] = AccessTypes.SETUP // 0
// setupState 对象是 setup 函数返回值的 Proxy 对象,所以执行 setupState[message] 时会触发 get 钩子函数
return setupState[key]
}
// ...
}
}
export function unref<T>(ref: T): T extends Ref<infer V> ? V : T {
return isRef(ref) ? (ref.value as any) : ref
}
const shallowUnwrapHandlers: ProxyHandler<any> = {
get: (target, key, receiver) => unref(Reflect.get(target, key, receiver))
}
这里涉及双向数据绑定逻辑,不细说,以后会详细分析。总之,返回具体值('Hello World')
执行 _createElementBlock("div", null, _toDisplayString(msg), 1 /* TEXT */) 等价于 createElementBlock("div", null, 'Hello World', 1 /* TEXT */)
export function createElementBlock(
type: string | typeof Fragment,
props?: Record<string, any> | null,
children?: any,
patchFlag?: number,
dynamicProps?: string[],
shapeFlag?: number
) {
return setupBlock(
// createBaseVNode 会将 vnode push 进 currentBlock 中,如上文中 openBlock 函数所说
// createBaseVNode return vnode 结构如下
/*
__v_isVNode: true,
__v_skip: true,
type,
props,
key: props && normalizeKey(props),
ref: props && normalizeRef(props),
scopeId: currentScopeId,
slotScopeIds: null,
children,
component: null,
suspense: null,
ssContent: null,
ssFallback: null,
dirs: null,
transition: null,
el: null,
anchor: null,
target: null,
targetAnchor: null,
staticCount: 0,
shapeFlag,
patchFlag,
dynamicProps,
dynamicChildren: null,
appContext: null
*/
createBaseVNode(
type, // div
props, // null
children, // Hello World
patchFlag, // 1
dynamicProps,
shapeFlag,
true /* isBlock */
)
)
}
回到最开始 const subTree = (instance.subTree = renderComponentRoot(instance)) 中,renderComponentRoot() 函数,做了一下几件事:
-
执行 instance.render,返回组件 vnode;
-
标准化 vnode,normalizeVNode(vnode),返回 vnode。
patch
patch(
null,
subTree, // 组件 vnode
container, // querySelector('#app')
anchor, // null
instance, // 组件实例
parentSuspense,
isSVG
)
接着执行 patch 函数
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// ...
const { type, ref, shapeFlag } = n2
switch (type) {
// ...
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
// ...
}
// ..
}
这次执行 patch 函数会执行 processElement 函数
processElement
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
// 挂载 element
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 和更新有关,这个函数后,可能会执行 diff 算法
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
上面的代码对于第一次渲染来说,会执行 mountElement 函数
mountElement
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
slotScopeIds: string[] | null,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, patchFlag, dirs } = vnode
if (
!__DEV__ &&
vnode.el &&
hostCloneNode !== undefined &&
patchFlag === PatchFlags.HOISTED
) {
// ...
} else {
// document.createElement / document.createElementNS
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// el.textContent = text
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
// ...
}
// ...
}
// ...
// parent.insertBefore(child, anchor || null)
// parent = querySelector('#app')
hostInsert(el, container, anchor)
// ...
}
到这里首次渲染就完成了