上文主要解析了执行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
的更新。