本文为原创文章,未获授权禁止转载,侵权必究!
本篇是 Vue3 源码解析系列第 8 篇,关注专栏
前言
runtime
文中我们了解到,Vue 通过 h
函数生成 VNode
对象,再通过 render
函数将 VNode
对象渲染为真实 DOM
。由于 render
函数涉及到 DOM
的渲染、更新、删除等,本篇我们先来看下 render
函数是如何实现 DOM
渲染的。
案例
首先引入 h
、 render
两个函数,之后通过 h
函数生成一个 vnode
对象,最后将 vnode
对象通过 render
函数渲染为真实 DOM
。
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
<script src="../../../dist/vue.global.js"></script>
</head>
<body>
<div id="app"></div>
<script>
const { h, render } = Vue
const vnode = h(
'div',
{
class: 'test'
},
'hello render'
)
render(vnode, document.querySelector('#app'))
</script>
</body>
</html>
render 实现
render
函数定义在 packages/runtime-core/src/renderer.ts
文件下,大致在 2341
行:
ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 省略
const render: RootRenderFunction = (vnode, container, isSVG) => {
if (vnode == null) {
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
container._vnode = vnode
}
// 省略
return {
render,
hydrate,
createApp: createAppAPI(render, hydrate)
}
}
render
函数实际是 baseCreateRenderer
函数暴露出来的一个对象方法,但在我们使用时是直接调用 render
函数,那 Vue 是如何导出该方法的呢?
我们知道 Vue 中 runtime-core
文件夹是运行时的核心部分,主要对虚拟 DOM 的处理等,是与平台(例如浏览器、服务器端渲染)无关的部分,它可以在不同的平台上运行,只需要配合相应的渲染器(比如 runtime-dom
)来实现具体的渲染逻辑。
而 runtime-dom
文件夹是针对浏览器环境的具体渲染器,包含了与浏览器环境相关的操作,比如处理 DOM 元素、事件处理、属性更新等。
所以 render
函数导出实际被定义在 packages/runtime-dom/src/index.ts
文件中:
ts
export const render = ((...args) => {
ensureRenderer().render(...args)
}) as RootRenderFunction<Element | ShadowRoot>
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
可以看出 render
函数实际执行的是 ensureRenderer().render(...args)
,而 ensureRenderer
函数实际执行的是 createRenderer
方法,该方法定义在 packages/runtime-core/src/renderer.ts
文件下:
ts
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
可想而知,render
函数真正执行的是 baseCreateRenderer
函数返回对象中的 render
方法。那么理解完 render
函数的调用和导出,我们再回过来看下 render
函数的实现逻辑:
ts
const render: RootRenderFunction = (vnode, container, isSVG) => {
// vnode 不存在
if (vnode == null) {
// 存在旧节点
if (container._vnode) {
// 卸载旧节点
unmount(container._vnode, null, null, true)
}
} else {
// 更新节点
patch(container._vnode || null, vnode, container, null, null, null, isSVG)
}
flushPostFlushCbs()
// _vnode 赋值旧节点
container._vnode = vnode
}
根据案例,当前 vnode
存在,直接走 patch
方法,该方法也是被定义在 baseCreateRenderer
函数中:
ts
const patch: PatchFn = (
n1, // 旧节点
n2, // 新节点
container, // 容器
anchor = null, // 锚点
parentComponent = null,
parentSuspense = null,
isSVG = false,
slotScopeIds = null,
optimized = __DEV__ && isHmrUpdating ? false : !!n2.dynamicChildren
) => {
// 新旧节点是否相同
if (n1 === n2) {
return
}
// patching & not same type, unmount old tree
// 存在旧节点 且 新旧节点类型是否相同
if (n1 && !isSameVNodeType(n1, n2)) {
anchor = getNextHostNode(n1)
unmount(n1, parentComponent, parentSuspense, true)
n1 = null
}
if (n2.patchFlag === PatchFlags.BAIL) {
optimized = false
n2.dynamicChildren = null
}
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 (__DEV__) {
patchStaticNode(n1, n2, container, isSVG)
}
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized,
internals
)
} else if (__DEV__) {
warn('Invalid VNode type:', type, `(${typeof type})`)
}
}
// set ref
if (ref != null && parentComponent) {
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
我们只需关注前四个参数:n1 旧节点
、n2 新节点
、container 容器
、anchor 锚点
。由于第一次渲染,所以此时 n1 为 null
,这里的 锚点
也比较关键, 具体逻辑我们稍后分析。之后根据 n2 新节点
类型走不同的逻辑,当前新节点 type
类型为 div
,所以走 default
逻辑。
接着执行判断逻辑 shapeFlag & ShapeFlags.ELEMENT
,当前 shapeFlag
为 9, ShapeFlags.ELEMENT
为 1,按位与
运算结果为 1,if(1)
为真,执行 processElement
方法。
这里拓展下 &
按位与,和之前 按位或
相似,都是转为二进制后计算:
ts
// 1 ShapeFlags.ELEMENT
00000000 00000000 00000000 00000001
// 9 shapeFlag
00000000 00000000 00000000 00001001
// 与 运算 上下为 1 则为 1 否则为 0
// 结果 1
00000000 00000000 00000000 00000001
我们再看下 processElement
方法,该方法也是被定义在 baseCreateRenderer
函数中:
ts
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) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
} else {
// 更新
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
isSVG,
slotScopeIds,
optimized
)
}
}
由于当前旧节点不存在,直接走 mountElement
方法:
ts
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
) {
// If a vnode has non-null el, it means it's being reused.
// Only static vnodes can be reused, so its mounted DOM nodes should be
// exactly the same, and we can simply do a clone here.
// only do this in production since cloned trees cannot be HMR updated.
el = vnode.el = hostCloneNode(vnode.el)
} else {
// 执行 createElement 方法
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is,
props
)
// mount children first, since some props may rely on child content
// being already rendered, e.g. `<select value>`
// 挂载子节点
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
// setElementText 方法
hostSetElementText(el, vnode.children as string)
} else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
vnode.children as VNodeArrayChildren,
el,
null,
parentComponent,
parentSuspense,
isSVG && type !== 'foreignObject',
slotScopeIds,
optimized
)
}
if (dirs) {
invokeDirectiveHook(vnode, null, parentComponent, 'created')
}
// props
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
// 执行 runtime-dom/src/patchProp.ts中的 patchProp 方法
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
/**
* Special case for setting value on DOM elements:
* - it can be order-sensitive (e.g. should be set *after* min/max, #2325, #4024)
* - it needs to be forced (#1471)
* #2353 proposes adding another renderer option to configure this, but
* the properties affects are so finite it is worth special casing it
* here to reduce the complexity. (Special casing it also should not
* affect non-DOM renderers)
*/
if ('value' in props) {
hostPatchProp(el, 'value', null, props.value)
}
if ((vnodeHook = props.onVnodeBeforeMount)) {
invokeVNodeHook(vnodeHook, parentComponent, vnode)
}
}
// scopeId
setScopeId(el, vnode, vnode.scopeId, slotScopeIds, parentComponent)
}
// 省略
// 插入到 container 中
hostInsert(el, container, anchor)
// 省略
}
由于此时 vnode.el
不存在,直接走 el = vnode.el = hostCreateElement()
对其赋值,我们再看下 hostCreateElement
方法:
ts
function baseCreateRenderer(
options: RendererOptions,
createHydrationFns?: typeof createHydrationFunctions
): any {
// 省略
const {
insert: hostInsert,
remove: hostRemove,
patchProp: hostPatchProp,
createElement: hostCreateElement,
createText: hostCreateText,
createComment: hostCreateComment,
setText: hostSetText,
setElementText: hostSetElementText,
parentNode: hostParentNode,
nextSibling: hostNextSibling,
setScopeId: hostSetScopeId = NOOP,
cloneNode: hostCloneNode,
insertStaticContent: hostInsertStaticContent
} = options
// 省略
}
该方法是通过传入的 options
参数解构得到的,我们知道 render
函数执行的是 ensureRenderer().render(...args)
,而 ensureRenderer
执行的是 createRenderer
,等同于执行 baseCreateRenderer
方法,而参数是在执行 createRenderer
时传入的 rendererOptions
:
ts
const rendererOptions = /*#__PURE__*/ extend({ patchProp }, nodeOps)
export const render = ((...args) => {
ensureRenderer().render(...args)
}) as RootRenderFunction<Element | ShadowRoot>
function ensureRenderer() {
return (
renderer ||
(renderer = createRenderer<Node, Element | ShadowRoot>(rendererOptions))
)
}
可以看出 rendererOptions
参数实际是 { patchProp }, nodeOps
合并后的对象,我们主要看下 nodeOps
对象,它被定义在 packages/runtime-dom/src/nodeOps.ts
文件中:
ts
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => {
// 将 child 插入 锚点之前
// 执行完 页面会渲染完成
parent.insertBefore(child, anchor || null)
},
remove: child => {
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
createText: text => doc.createTextNode(text),
createComment: text => doc.createComment(text),
setText: (node, text) => {
node.nodeValue = text
},
setElementText: (el, text) => {
el.textContent = text
},
parentNode: node => node.parentNode as Element | null,
nextSibling: node => node.nextSibling,
querySelector: selector => doc.querySelector(selector),
setScopeId(el, id) {
el.setAttribute(id, '')
},
cloneNode(el) {
const cloned = el.cloneNode(true)
// #3072
// - in `patchDOMProp`, we store the actual value in the `el._value` property.
// - normally, elements using `:value` bindings will not be hoisted, but if
// the bound value is a constant, e.g. `:value="true"` - they do get
// hoisted.
// - in production, hoisted nodes are cloned when subsequent inserts, but
// cloneNode() does not copy the custom property we attached.
// - This may need to account for other custom DOM properties we attach to
// elements in addition to `_value` in the future.
if (`_value` in el) {
;(cloned as any)._value = (el as any)._value
}
return cloned
},
// __UNSAFE__
// Reason: innerHTML.
// Static content here can only come from compiled templates.
// As long as the user only uses trusted templates, this is safe.
insertStaticContent(content, parent, anchor, isSVG, start, end) {
// <parent> before | first ... last | anchor </parent>
const before = anchor ? anchor.previousSibling : parent.lastChild
// #5308 can only take cached path if:
// - has a single root node
// - nextSibling info is still available
if (start && (start === end || start.nextSibling)) {
// cached
while (true) {
parent.insertBefore(start!.cloneNode(true), anchor)
if (start === end || !(start = start!.nextSibling)) break
}
} else {
// fresh insert
templateContainer.innerHTML = isSVG ? `<svg>${content}</svg>` : content
const template = templateContainer.content
if (isSVG) {
// remove outer svg wrapper
const wrapper = template.firstChild!
while (wrapper.firstChild) {
template.appendChild(wrapper.firstChild)
}
template.removeChild(wrapper)
}
parent.insertBefore(template, anchor)
}
return [
// first
before ? before.nextSibling! : parent.firstChild!,
// last
anchor ? anchor.previousSibling! : parent.lastChild!
]
}
}
nodeOps
对象主要定义了一些浏览器相关的方法,比如 DOM 处理、事件处理、属性更新等。我们回过来再看下 hostCreateElement
方法,实际执行的是 createElement
方法:
ts
createElement: (tag, isSVG, is, props): Element => {
const el = isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
if (tag === 'select' && props && props.multiple != null) {
;(el as HTMLSelectElement).setAttribute('multiple', props.multiple)
}
return el
},
当前 tag
为传入的 vnode.type
即 div
,通过 document.createElement
创建了一个 div
元素赋值给 el
并返回。
此时 vnode.el
就挂载 div
元素:
接着执行 shapeFlag & ShapeFlags.TEXT_CHILDREN
判断,shapeFlag
为 9,ShapeFlags.TEXT_CHILDREN
为 8,按位与
运算得出结果是 8,if(8)
结果为真,执行 hostSetElementText
方法,可以看出所有前缀 host
方法都是浏览器相关操作,都被定义在 nodeOps
对象中,该方法实际执行的是 setElementText
方法:
ts
setElementText: (el, text) => {
el.textContent = text
},
而 el
参数为之前创建的 div
元素,而 text
参数为传入的 vnode.children
即 hello render
,所以此时 div
元素的 innderHTML
、innderText
被赋值为 hello render
:
子节点挂载完毕,之后挂载 prop
属性,当前 props
为 { class: 'test' }
:
ts
if (props) {
for (const key in props) {
if (key !== 'value' && !isReservedProp(key)) {
// 执行 runtime-dom/src/patchProp.ts中的 patchProp 方法
hostPatchProp(
el,
key,
null,
props[key],
isSVG,
vnode.children as VNode[],
parentComponent,
parentSuspense,
unmountChildren
)
}
}
// 省略
}
hostPatchProp
方法实际执行的是 patchProp
,它被定义在 packages/runtime-dom/src/patchProp.ts
文件中:
ts
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
if (key === 'class') {
// runtime-dom/src/modules/class.ts 中
patchClass(el, nextValue, isSVG)
} else if (key === 'style') {
patchStyle(el, prevValue, nextValue)
} else if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
} else if (
key[0] === '.'
? ((key = key.slice(1)), true)
: key[0] === '^'
? ((key = key.slice(1)), false)
: shouldSetAsProp(el, key, nextValue, isSVG)
) {
patchDOMProp(
el,
key,
nextValue,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
)
} else {
// special case for <input v-model type="checkbox"> with
// :true-value & :false-value
// store value as dom properties since non-string values will be
// stringified.
if (key === 'true-value') {
;(el as any)._trueValue = nextValue
} else if (key === 'false-value') {
;(el as any)._falseValue = nextValue
}
patchAttr(el, key, nextValue, isSVG, parentComponent)
}
}
我们主要关注前四个参数,el
为当前 div
元素,key
为 class
,prevValue
为 null
,nextValue
为 props[key]
即 test
,由于当前 key
为 class
,执行 patchClass
方法,该方法定义在 packages/runtime-dom/src/modules/class.ts
文件中:
ts
export function patchClass(el: Element, value: string | null, isSVG: boolean) {
// directly setting className should be faster than setAttribute in theory
// if this is an element during a transition, take the temporary transition
// classes into account.
const transitionClasses = (el as ElementWithTransition)._vtc
if (transitionClasses) {
value = (
value ? [value, ...transitionClasses] : [...transitionClasses]
).join(' ')
}
if (value == null) {
el.removeAttribute('class')
} else if (isSVG) {
el.setAttribute('class', value)
} else {
el.className = value
}
}
该方法就是通过 DOM
的方法、属性来设置或移除 class
,所以此时 vnode.el
赋值为带有 class
为 test
的 div
元素:
之后执行 hostInsert(el, container, anchor)
方法,该方法实际执行的是 insert
方法:
ts
insert: (child, parent, anchor) => {
// 将 child 插入 锚点之前
// 执行完 页面会渲染完成
parent.insertBefore(child, anchor || null)
},
而之前提到的 anchor
锚点,就是为了将子节点插入到锚点之前 。当前 child
为 div.test
元素,parent
为 div#app
元素,执行完 parent.insertBefore(child, anchor || null)
页面渲染完成。
元素挂载渲染完毕,render
函数执行 container._vnode = vnode
,将新节点 vnode
赋值到旧节点_vnode
上,render
函数执行完毕。
总结
render
函数触发patch
方法,在patch
方法中根据新节点的type
和shapeFlag
值决定当前哪一种类型的节点挂载。再根据新旧节点
决定是挂载
还是更新
,取决于旧节点是否存在。- 挂载过程分为四大步:
a. 创建div
标签,即hostCreateElement()
b. 生成标签里的text
,即hostSetElementText()
c. 处理prop
属性,即hostPatchProp()
d. 插入DOM
,即hostInsert()
- 最后挂载旧节点
_vnode