vue3渲染器的设计
渲染器的作用是把虚拟 DOM 渲染为特定平台上的真实元素。在浏览器平台上,渲染器会把虚拟 DOM vdom渲染为真实 DOM 元素。
渲染器把虚拟 DOM 节点渲染为真实 DOM 节点的过程叫作挂载,通常用英文 mount 来表达。例如 Vue.js 组件中的 mounted 钩子就会在挂载完成时触发。这就意味着,在 mounted 钩子中可以访问真实DOM 元素。理解这些名词有助于我们更好地理解框架的 API 设计。
渲染器把真实 DOM 挂载到哪里呢?渲染器通常需要接收一个挂载点作为参数,用来指定具体的挂载位置。
export function createRenderer<
HostNode = RendererNode,
HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
function baseCreateRenderer() {
function render(vnode, container, namespace) {
// ...
}
return render
}
当首次调用 renderer.render 函数时,只需要创建新的 DOM 元素即可,这个过程只涉及挂载。而当多次在同一个 container 上调用 renderer.render 函数进行渲染时,渲染器除了要执行挂载动作外,还要执行更新动作。
节点常用操作
export function createRenderer<
HostNode = RendererNode,
export const nodeOps: Omit<RendererOptions<Node, Element>, 'patchProp'> = {
insert: (child, parent, anchor) => { //新增节点
parent.insertBefore(child, anchor || null)
},
remove: child => { // 删除节点
const parent = child.parentNode
if (parent) {
parent.removeChild(child)
}
},
createElement: (tag, namespace, is, props): Element => { // 创建节点
const el =
namespace === 'svg'
? doc.createElementNS(svgNS, tag)
: namespace === 'mathml'
? doc.createElementNS(mathmlNS, tag)
: is
? doc.createElement(tag, { is })
: doc.createElement(tag)
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, '')
},
// __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, namespace, 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 =
namespace === 'svg'
? `<svg>${content}</svg>`
: namespace === 'mathml'
? `<math>${content}</math>`
: content
const template = templateContainer.content
if (namespace === 'svg' || namespace === 'mathml') {
// remove outer svg/math 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!,
]
},
}
HostElement = RendererElement,
>(options: RendererOptions<HostNode, HostElement>) {
return baseCreateRenderer<HostNode, HostElement>(options)
}
function baseCreateRenderer() {
function render(vnode, container, namespace) {
// ...
}
return render
}
对比属性方法
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
namespace,
parentComponent,
) => {
const isSVG = namespace === 'svg'
if (key === 'class') {
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, parentComponent)
// #6007 also set form state as attributes so they work with
// <input type="reset"> or libs / extensions that expect attributes
// #11163 custom elements may use value as an prop and set it as object
if (
!el.tagName.includes('-') &&
(key === 'value' || key === 'checked' || key === 'selected')
) {
patchAttr(el, key, nextValue, isSVG, parentComponent, key !== 'value')
}
} 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)
}
}
操作类名
// 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)[vtcKey]
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
}
}
操作样式
export function patchStyle(el: Element, prev: Style, next: Style) {
const style = (el as HTMLElement).style
const isCssString = isString(next)
let hasControlledDisplay = false
if (next && !isCssString) {
if (prev) {
if (!isString(prev)) {
for (const key in prev) {
if (next[key] == null) {
setStyle(style, key, '')
}
}
} else {
for (const prevStyle of prev.split(';')) {
const key = prevStyle.slice(0, prevStyle.indexOf(':')).trim()
if (next[key] == null) {
setStyle(style, key, '')
}
}
}
}
for (const key in next) {
if (key === 'display') {
hasControlledDisplay = true
}
setStyle(style, key, next[key])
}
} else {
if (isCssString) {
if (prev !== next) {
// #9821
const cssVarText = (style as any)[CSS_VAR_TEXT]
if (cssVarText) {
;(next as string) += ';' + cssVarText
}
style.cssText = next as string
hasControlledDisplay = displayRE.test(next)
}
} else if (prev) {
el.removeAttribute('style')
}
}
// indicates the element also has `v-show`.
if (vShowOriginalDisplay in el) {
// make v-show respect the current v-bind style display when shown
el[vShowOriginalDisplay] = hasControlledDisplay ? style.display : ''
// if v-show is in hidden state, v-show has higher priority
if ((el as VShowElement)[vShowHidden]) {
style.display = 'none'
}
}
}
操作事件
export function patchEvent(
el: Element & { [veiKey]?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | unknown,
instance: ComponentInternalInstance | null = null,
) {
// vei = vue event invokers
const invokers = el[veiKey] || (el[veiKey] = {})
const existingInvoker = invokers[rawName] // 是否缓存过
if (nextValue && existingInvoker) {
// patch
existingInvoker.value = (nextValue as EventValue)
} else {
const [name, options] = parseName(rawName)
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(
(nextValue as EventValue),
instance,
))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove
removeEventListener(el, name, existingInvoker, options)
invokers[rawName] = undefined
}
}
}
在绑定事件的时候,绑定一个伪造的事件处理函数invoker,把真正的事件处理函数设置为invoker.value属性的值
操作属性
export function patchAttr(
el: Element,
key: string,
value: any,
isSVG: boolean,
instance?: ComponentInternalInstance | null,
isBoolean = isSpecialBooleanAttr(key),
) {
if (isSVG && key.startsWith('xlink:')) {
if (value == null) {
el.removeAttributeNS(xlinkNS, key.slice(6, key.length))
} else {
el.setAttributeNS(xlinkNS, key, value)
}
} else {
if (__COMPAT__ && compatCoerceAttr(el, key, value, instance)) {
return
}
// note we are only checking boolean attributes that don't have a
// corresponding dom prop of the same name here.
if (value == null || (isBoolean && !includeBooleanAttr(value))) {
el.removeAttribute(key)
} else {
// attribute value is a string https://html.spec.whatwg.org/multipage/dom.html#attributes
el.setAttribute(
key,
isBoolean ? '' : isSymbol(value) ? String(value) : value,
)
}
}
}
创建渲染器
const renderer = createRenderer()
// 挂载:首次渲染
renderer.render(oldVDom, document.querySelector('#app'))
// 更新:第二次渲染
renderer.render(newVDom, document.querySelector('#app'))
// ...
// 卸载:传递了 null 作为新 vnode
renderer.render(newVDom, document.querySelector('#app'))
在这种情况下,渲染器会使用 newVNode 与上一次渲染的 oldVNode 进行比较,试图找到并更新变更点。这个过程叫作“打补丁”(或更新),英文通常用patch 来表达。实际上,初次挂载也可以用patch 来表达,也就是旧vDom不存在的情况。卸载也同理。
// 多次调用render 会进行虚拟节点的比较,在进行更新
const render: RootRenderFunction = (vnode, container, namespace) => {
if (vnode == null) {
// 移除当前容器中的dom元素
if (container._vnode) {
unmount(container._vnode, null, null, true)
}
} else {
// 将虚拟节点变成真实节点进行渲染
patch(
container._vnode || null,
vnode,
container,
null,
null,
null,
namespace,
)
}
container._vnode = vnode
}
虚拟DOM更新实现原理
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
namespace = undefined,
slotScopeIds = null,
optimized = !!n2.dynamicChildren,
) => {
// 两次渲染同一个元素直接跳过即可
if (n1 === n2) {
return
}
// 直接移除老的dom元素,初始化新的dom元素
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, namespace)
}
break
case Fragment:
// 处理 Fragment 元素
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT) {
// 处理普通 DOM 元素
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.COMPONENT) {
// 处理组件
processComponent(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else if (shapeFlag & ShapeFlags.TELEPORT) {
// 处理 TELEPORT
;(type as typeof TeleportImpl).process(
n1 as TeleportVNode,
n2 as TeleportVNode,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
} else if (__FEATURE_SUSPENSE__ && shapeFlag & ShapeFlags.SUSPENSE) {
// 处理 SUSPENSE
;(type as typeof SuspenseImpl).process(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
internals,
)
}
}
// set ref
if (ref != null && parentComponent) {
// n2 是dom 还是 组件 还是组件有expose
setRef(ref, n1 && n1.ref, parentSuspense, n2 || n1, !n2)
}
}
处理文本节点
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor,
)
} else {
const el = (n2.el = n1.el!)
if (n2.children !== n1.children) {
hostSetText(el, n2.children as string)
}
}
}
处理注释节点
const processCommentNode: ProcessTextOrCommentFn = (
n1,
n2,
container,
anchor,
) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateComment((n2.children as string) || '')),
container,
anchor,
)
} else {
// there's no support for dynamic comments
n2.el = n1.el
}
}
处理静态节点
const mountStaticNode = (
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
namespace: ElementNamespace,
) => {
// static nodes are only present when used with compiler-dom/runtime-dom
// which guarantees presence of hostInsertStaticContent.
;[n2.el, n2.anchor] = hostInsertStaticContent!(
n2.children as string,
container,
anchor,
namespace,
n2.el,
n2.anchor,
)
}
处理 Fragment 元素
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren, slotScopeIds: fragmentSlotScopeIds } = n2
// check if this is a slot fragment with :slotted scope ids
if (fragmentSlotScopeIds) {
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(
// #10007
// such fragment like `<></>` will be compiled into
// a fragment which doesn't have a children.
// In this case fallback to an empty array
(n2.children || []) as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
if (
patchFlag > 0 &&
patchFlag & PatchFlags.STABLE_FRAGMENT &&
dynamicChildren &&
// #2715 the previous fragment could've been a BAILed one as a result
// of renderSlot() with no valid children
n1.dynamicChildren
) {
// a stable fragment (template root or <template v-for>) doesn't need to
// patch children order, but it may contain dynamicChildren.
patchBlockChildren(
n1.dynamicChildren,
dynamicChildren,
container,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
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.
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.
patchChildren(
n1,
n2,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
}
处理普通DOM元素
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
if (n2.type === 'svg') {
namespace = 'svg'
} else if (n2.type === 'math') {
namespace = 'mathml'
}
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
} else {
patchElement(
n1,
n2,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
处理组件
const processComponent = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
) => {
n2.slotScopeIds = slotScopeIds
if (n1 == null) {
if (n2.shapeFlag & ShapeFlags.COMPONENT_KEPT_ALIVE) {
;(parentComponent!.ctx as KeepAliveContext).activate(
n2,
container,
anchor,
namespace,
optimized,
)
} else {
mountComponent(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
optimized,
)
}
} else {
updateComponent(n1, n2, optimized)
}
}
处理 TELEPORT
export const TeleportImpl = {
name: 'Teleport',
__isTeleport: true,
process(
n1: TeleportVNode | null,
n2: TeleportVNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
internals: RendererInternals,
) {
const {
mc: mountChildren,
pc: patchChildren,
pbc: patchBlockChildren,
o: { insert, querySelector, createText, createComment },
} = internals
const disabled = isTeleportDisabled(n2.props)
let { shapeFlag, children, dynamicChildren } = n2
// #3302
// HMR updated, force full diff
if (__DEV__ && isHmrUpdating) {
optimized = false
dynamicChildren = null
}
if (n1 == null) {
// insert anchors in the main view
const placeholder = (n2.el = __DEV__
? createComment('teleport start')
: createText(''))
const mainAnchor = (n2.anchor = __DEV__
? createComment('teleport end')
: createText(''))
insert(placeholder, container, anchor)
insert(mainAnchor, container, anchor)
const target = (n2.target = resolveTarget(n2.props, querySelector))
const targetAnchor = prepareAnchor(target, n2, createText, insert)
if (target) {
// #2652 we could be teleporting from a non-SVG tree into an SVG tree
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace === 'mathml' || isTargetMathML(target)) {
namespace = 'mathml'
}
} else if (__DEV__ && !disabled) {
warn('Invalid Teleport target on mount:', target, `(${typeof target})`)
}
const mount = (container: RendererElement, anchor: RendererNode) => {
// Teleport *always* has Array children. This is enforced in both the
// compiler and vnode children normalization.
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
mountChildren(
children as VNodeArrayChildren,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
)
}
}
if (disabled) {
mount(container, mainAnchor)
} else if (target) {
mount(target, targetAnchor)
}
} else {
// update content
n2.el = n1.el
n2.targetStart = n1.targetStart
const mainAnchor = (n2.anchor = n1.anchor)!
const target = (n2.target = n1.target)!
const targetAnchor = (n2.targetAnchor = n1.targetAnchor)!
const wasDisabled = isTeleportDisabled(n1.props)
const currentContainer = wasDisabled ? container : target
const currentAnchor = wasDisabled ? mainAnchor : targetAnchor
if (namespace === 'svg' || isTargetSVG(target)) {
namespace = 'svg'
} else if (namespace === 'mathml' || isTargetMathML(target)) {
namespace = 'mathml'
}
if (dynamicChildren) {
// fast path when the teleport happens to be a block root
patchBlockChildren(
n1.dynamicChildren!,
dynamicChildren,
currentContainer,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
)
// even in block tree mode we need to make sure all root-level nodes
// in the teleport inherit previous DOM references so that they can
// be moved in future patches.
traverseStaticChildren(n1, n2, true)
} else if (!optimized) {
patchChildren(
n1,
n2,
currentContainer,
currentAnchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
false,
)
}
if (disabled) {
if (!wasDisabled) {
// enabled -> disabled
// move into main container
moveTeleport(
n2,
container,
mainAnchor,
internals,
TeleportMoveTypes.TOGGLE,
)
} else {
// #7835
// When `teleport` is disabled, `to` may change, making it always old,
// to ensure the correct `to` when enabled
if (n2.props && n1.props && n2.props.to !== n1.props.to) {
n2.props.to = n1.props.to
}
}
} else {
// target changed
if ((n2.props && n2.props.to) !== (n1.props && n1.props.to)) {
const nextTarget = (n2.target = resolveTarget(
n2.props,
querySelector,
))
if (nextTarget) {
moveTeleport(
n2,
nextTarget,
null,
internals,
TeleportMoveTypes.TARGET_CHANGE,
)
} else if (__DEV__) {
warn(
'Invalid Teleport target on update:',
target,
`(${typeof target})`,
)
}
} else if (wasDisabled) {
// disabled -> enabled
// move into teleport target
moveTeleport(
n2,
target,
targetAnchor,
internals,
TeleportMoveTypes.TOGGLE,
)
}
}
}
updateCssVars(n2)
},
remove(
vnode: VNode,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
{ um: unmount, o: { remove: hostRemove } }: RendererInternals,
doRemove: boolean,
) {
const {
shapeFlag,
children,
anchor,
targetStart,
targetAnchor,
target,
props,
} = vnode
if (target) {
hostRemove(targetStart!)
hostRemove(targetAnchor!)
}
// an unmounted teleport should always unmount its children whether it's disabled or not
doRemove && hostRemove(anchor!)
if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) {
const shouldRemove = doRemove || !isTeleportDisabled(props)
for (let i = 0; i < (children as VNode[]).length; i++) {
const child = (children as VNode[])[i]
unmount(
child,
parentComponent,
parentSuspense,
shouldRemove,
!!child.dynamicChildren,
)
}
}
},
move: moveTeleport,
hydrate: hydrateTeleport,
}
处理 SUSPENSE
export const SuspenseImpl = {
name: 'Suspense',
__isSuspense: true,
process(
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
namespace: ElementNamespace,
slotScopeIds: string[] | null,
optimized: boolean,
// platform-specific impl passed from renderer
rendererInternals: RendererInternals,
) {
if (n1 == null) {
mountSuspense(
n2,
container,
anchor,
parentComponent,
parentSuspense,
namespace,
slotScopeIds,
optimized,
rendererInternals,
)
} else {
if (
parentSuspense &&
parentSuspense.deps > 0 &&
!n1.suspense!.isInFallback
) {
n2.suspense = n1.suspense!
n2.suspense.vnode = n2
n2.el = n1.el
return
}
patchSuspense(
n1,
n2,
container,
anchor,
parentComponent,
namespace,
slotScopeIds,
optimized,
rendererInternals,
)
}
},
hydrate: hydrateSuspense,
normalize: normalizeSuspenseChildren,
}