上文主要解析了执行render函数生成VNode对象,本文将详细解析patch方法如何将VNode对象转化为真正的DOM
patch方法解析VNode入口
由上文解析知,在renderComponentRoot函数中,通过执行render!.call(...)生成并返回VNode对象,renderComponentRoot函数最终也是返回VNode。回归到setupRenderEffect函数中,起初是执行componentEffect方法时执行了renderComponentRoot函数(关于setupRenderEffect方法中effect函数的执行过程,可以看下"全局依赖activeEffect收集")。下面解析下setupRenderEffect函数的后续操作
if (el && hydrateNode) {/*...*/}
else {
// ...
patch(
null,
subTree,
container,
anchor,
instance,
parentSuspense,
isSVG
)
// ...
initialVNode.el = subTree.el
}
后续就是调用patch方法,传参的subTree就是VNode对象,container就是#app节点的DOM对象(关于调用setupRenderEffect函数传入的container参数,可以看下"入口函数-mount挂载",#app节点的DOM对象是如何生成的,以及如何通过调用链传递给setupRenderEffect函数),本例为<div id='app'></div>,下面解析下patch方法的内部实现
const patch: PatchFn = (
n1,
n2,
container,
anchor = null,
parentComponent = null,
parentSuspense = null,
isSVG = false,
optimized = false
) => {
if (n1 && !isSameVNodeType(n1, n2)) {/*...初始挂载时n1为null*/}
if (n2.patchFlag === PatchFlags.BAIL/*2*/) {/*...*/}
const { type, ref, shapeFlag } = n2
switch (type) {
case Text:
processText(n1, n2, container, anchor)
break
case Fragment:
processFragment(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
break
default:
if (shapeFlag & ShapeFlags.ELEMENT/* 1 */) {
processElement(
n1,
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
}
patch方法解析根VNode对象
首先解构出VNode对象的type属性,本例解析的VNode.type为Symbol('Fragment')(根VNode对象的生成过程可以看下"createBlock创建根vnode对象"),所以执行processFragment函数
// processFragment方法定义
const processFragment = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
const fragmentStartAnchor = (n2.el = n1 ? n1.el : hostCreateText(''))!
const fragmentEndAnchor = (n2.anchor = n1 ? n1.anchor : hostCreateText(''))!
let { patchFlag, dynamicChildren } = n2
if (patchFlag > 0) {
optimized = true
}
// ...
if (n1 == null) {
hostInsert(fragmentStartAnchor, container, anchor)
hostInsert(fragmentEndAnchor, container, anchor)
mountChildren(
n2.children as VNodeArrayChildren,
container,
fragmentEndAnchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {}
}
app元素插入两个空文本元素
processFragment方法内部首先创建VNode的el和anchor属性,当n1不存在时(初始化挂载的时候老的VNode对象是不存在的),就是hostCreateText('')的返回值(在baseCreateRenderer函数开头部分定义了一系列的操作DOM的方法,这里不一一列举,遇到再详细解析),hostCreateText函数就是createText方法,而createText在nodeOps对象中对应的方法如下(关于调用baseCreateRenderer方法的options对象,就是调用createRenderer函数传入的rendererOptions对象,其中就包含操作DOM方法的nodeOps对象,这部分的执行流程,可以看下"入口函数-app对象生成"):
// 校验运行环境中是否存在document属性
const doc = (typeof document !== 'undefined' ? document : null);
// 定义createText方法
createText: text => doc.createTextNode(text)
所以createText方法就是调用createTextNode方法创建文本节点,所以n2的el和anchor属性都是文本节点并且内容为空,之后调用hostInsert方法,hostInsert方法在options对象中对应的是insert方法,而insert在nodeOps对象中对应的方法如下
insert: (child, parent, anchor) => {
parent.insertBefore(child, anchor || null)
}
所以hostInsert方法的作用就是在container节点(#app的DOM节点)的最后插入fragmentStartAnchor和fragmentEndAnchor两个空的文本节点。
mountChildren方法解析VNode子对象
再调用mountChildren方法解析VNode对象的子对象(children数组)
const mountChildren: MountChildrenFn = (
children,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized,
start = 0
) => {
for (let i = start; i < children.length; i++) {
const child = (children[i] = optimized
? cloneIfMounted(children[i] as VNode)
: normalizeVNode(children[i]))
patch(
null,
child,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
}
}
mountChildren方法主要是循环children数组,将每一个子VNode对象继续调用patch方法。
processText解析动态文本对象
本例上文解析出来的VNode对象,children数组中有两个元素,第一个是type为Symbol(Text)的文本对象,回归到patch方法中,switch-case根据type的值调用processText方法
const processText: ProcessTextOrCommentFn = (n1, n2, container, anchor) => {
if (n1 == null) {
hostInsert(
(n2.el = hostCreateText(n2.children as string)),
container,
anchor
)
} else {
// ...
}
}
因为n1==null,调用hostInsert方法插入元素,第一个参数是hostCreateText方法的返回值,在本文上面解析了hostCreateText方法就是创建文本节点的,这里创建的文本节点的内容是n2.children,也就是第一个子节点对象的children值(本例为字符串"测试数据 "),之后调用hostInsert方法将"测试数据 "文本节点插入到container(#app根DOM节点)的空文本节点(fragmentEndAnchor)之前。
此时
#app的根DOM节点下新增了一个文本节点,<div id='app'>测试数据 </div>,页面上也由空白新增了"测试数据 "文案
processElement解析HTML标签元素对象
处理完第一个children元素回归到mountChildren方法中,继续调用patch方法解析第二个button元素的VNode对象,此时type为"button"字符串,回归到patch方法中,根据type类型switch-case进入default分支,再根据shapeFlag的值(9),调用processElement方法处理元素VNode对象
const processElement = (
n1: VNode | null,
n2: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
isSVG = isSVG || (n2.type as string) === 'svg'
if (n1 == null) {
mountElement(
n2,
container,
anchor,
parentComponent,
parentSuspense,
isSVG,
optimized
)
} else {
patchElement(n1, n2, parentComponent, parentSuspense, isSVG, optimized)
}
}
processElement函数中根据n1==null,继续调用mountElement方法
const mountElement = (
vnode: VNode,
container: RendererElement,
anchor: RendererNode | null,
parentComponent: ComponentInternalInstance | null,
parentSuspense: SuspenseBoundary | null,
isSVG: boolean,
optimized: boolean
) => {
let el: RendererElement
let vnodeHook: VNodeHook | undefined | null
const { type, props, shapeFlag, transition, scopeId, patchFlag, dirs } = vnode
if (!__DEV__ && vnode.el && hostCloneNode !== undefined && patchFlag === PatchFlags.HOISTED) {}
else {
el = vnode.el = hostCreateElement(
vnode.type as string,
isSVG,
props && props.is
)
if (shapeFlag & ShapeFlags.TEXT_CHILDREN) {
hostSetElementText(el, vnode.children as string)
}
// ... dirs
if (props) {
for (const key in props) {
if (!isReservedProp(key)) {
hostPatchProp(el, key, null, props[key], isSVG, vnode.children as VNode[], parentComponent, parentSuspense, unmountChildren)
}
}
// ...
}
// scopeId
}
if (__DEV__ || __FEATURE_PROD_DEVTOOLS__) {
Object.defineProperty(el, '__vnode', {
value: vnode,
enumerable: false
})
Object.defineProperty(el, '__vueParentComponent', {
value: parentComponent,
enumerable: false
})
}
if (dirs) {/*...*/}
const needCallTransitionHooks = (!parentSuspense || (parentSuspense && !parentSuspense.pendingBranch)) && transition && !transition.persisted
if (needCallTransitionHooks) {/*...*/}
hostInsert(el, container, anchor)
// ...异步的执行队列,v-show进入此分支
}
createElement创建HTML标签DOM元素
mountElement函数中首先调用hostCreateElement创建元素,hostCreateElement在options中对应的是createElement,createElement在nodeOps对象中对应的方法如下
createElement: (tag, isSVG, is): Element =>
isSVG
? doc.createElementNS(svgNS, tag)
: doc.createElement(tag, is ? { is } : undefined)
因为isSVG为false,所以调用document.createElement方法,而props.is是不存在的,所以参数为(tag, undefined),从而创建了一个button的DOM节点并赋值给了vnode的el属性。
setElementText设置元素文本内容
之后因为shapeFlag=8,所以调用hostSetElementText方法。hostSetElementText在options中对应的是
setElementText,setElementText在nodeOps对象中对应的方法如下
setElementText: (el, text) => {
el.textContent = text
}
就是设置el节点的文本内容,根据传参是给button节点设置vnode.children内容。vnode.children是button子节点对象的子节点,本例为"修改数据"字符串。
addEventListener添加元素的事件监听
后续再判读VNode对象的props属性是否存在,如果存在则调用hostPatchProp方法为button元素添加属性(这里会通过isReservedProp方法判断是否是特定的属性值,例如:key,ref等等,本例为onClick)。hostPatchProp函数在options
对象中对应的patchProp,调用baseCreateRenderer方法传入的rendererOptions对象,除了nodeOps对象,还合并了{ patchProp, forcePatchProp },看下patchProp的具体实现
// isOn定义
const onRE = /^on[^a-z]/
export const isOn = (key: string) => onRE.test(key)
// patchProp定义
export const patchProp: DOMRendererOptions['patchProp'] = (
el,
key,
prevValue,
nextValue,
isSVG = false,
prevChildren,
parentComponent,
parentSuspense,
unmountChildren
) => {
switch (key) {
default:
if (isOn(key)) {
// ignore v-model listeners
if (!isModelListener(key)) {
patchEvent(el, key, prevValue, nextValue, parentComponent)
}
// else-if
break
}
}
}
patchProp方法中根据key的值选择switch-case分支,本例key=onClick,进入default分支,调用isOn方法(以on开头并且后一个字符不是小写字母a-z)判断是否为事件监听属性,本例返回true。再调用isModelListener方法(属性名key以"onUpdate:"开头),本例返回为false。所以调用patchEvent函数
export function patchEvent(
el: Element & { _vei?: Record<string, Invoker | undefined> },
rawName: string,
prevValue: EventValue | null,
nextValue: EventValue | null,
instance: ComponentInternalInstance | null = null
) {
// vei = vue event invokers
const invokers = el._vei || (el._vei = {})
const existingInvoker = invokers[rawName]
if (nextValue && existingInvoker) {/*...*/}
else {
const [name, options] = parseName(rawName)
if (nextValue) {
// add
const invoker = (invokers[rawName] = createInvoker(nextValue, instance))
addEventListener(el, name, invoker, options)
} else if (existingInvoker) {
// remove ...
}
}
}
patchEvent函数中,首先因为el._vei是{}空对象,所以existingInvoker=undefined,然后调用parseName方法解析属性名
const optionsModifierRE = /(?:Once|Passive|Capture)$/
function parseName(name: string): [string, EventListenerOptions | undefined] {
let options: EventListenerOptions | undefined
if (optionsModifierRE.test(name)) {/*...*/}
return [hyphenate(name.slice(2)), options]
}
parseName方法首先判断属性名称是否是Once|Passive|Capture,否则返回一个数组,数组的第一个元素是获取属性名称的第二位到最后一位,本例是onClick,所以name.slice(2)=click。第二个元素是options=undefined,最终此方法返回[click, undefined]。
回归到patchEvent方法中,因为nextValue是存在的,就是属性值,所以调用createInvoker函数
function createInvoker(
initialValue: EventValue,
instance: ComponentInternalInstance | null
) {
const invoker: Invoker = (e: Event) => {/* ...定义invoker方法 */}
invoker.value = initialValue
invoker.attached = getNow()
return invoker
}
createInvoker函数内部定义了invoker方法,将invoker.value设置为props的属性值,本例为modifyMessage方法,最后返回invoker函数。回归到patchEvent方法中,继续调用addEventListener函数
export function addEventListener(
el: Element,
event: string,
handler: EventListener,
options?: EventListenerOptions
) {
el.addEventListener(event, handler, options)
}
addEventListener方法就是给el(创建的button元素),添加event(click点击事件)事件监听,事件的回调函数就是invoker函数。然后回归到mountElement方法中,继续调用hostInsert将el(button元素)插入到container(#app的DOM元素)的anchor(fragmentEndAnchor空文本节点)文本元素之前。
回归到mountChildren方法中,以本模版为例,根VNode下的children已经全部循环解析并生成了DOM元素。
此时的
#app的DOM元素为:<div id='app'>测试数据 <button>修改数据</button></div>。button按钮上有click事件监听
至此patch方法解析VNode对象生成DOM元素已经完成了。后续的关键点在于button按钮的事件监听上,点击按钮触发事件监听的回调函数,执行modifyMessage方法,修改message.value值,触发set钩子函数,重新生成VNode。
总结
接上文生成VNode对象,本文主要解析patch方法如何解析VNode对象生成DOM元素,首先是解析根VNode对象,在#app元素上添加两个空文本节点,然后循环解析子元素(VNode.children数组),依次插入到第二个空文本DOM节点之前。然后对于HTML标签元素(依本例的button标签为例),创建button元素,添加元素的文本内容(按钮内容),之后在button元素上添加click事件监听。后续会解析,点击按钮时执行回调函数,修改数据,如何触发DOM的更新。