赞
踩
在 vue 中 patch 函数的作用是在渲染的过程中,比较新旧节点的变化,通过打补丁的形式,进行新增、删除、移动或替换操作,此过程避免了大量的 dom 操作,提升了运行的性能。
patch 函数整体流程比较长,函数内部包含很多分支用于处理不同的节点(Text、ELEMENT、COMPONENT)。
为了便于理解,文章中的代码皆为简化之后的代码:
/** * * @param n1 上一次渲染的 vnode * @param n2 当前需要渲染的 vnode * @param container 容器 * @param anchor 锚点, 用来标记插入的位置 * @returns */ const patch = (n1, n2, container, anchor = null) => { // 没变化不用对比,跳过此处节点 if (n1 === n2) { return } // 如果type以及key值不一样,则删除此就节点 if (n1 && !isSameVNodeType(n1, n2)) { const parent = n1.el.parentNode if (parent) { parent.removeChild(n1.el) // 删除元素 } n1 = null // 删除之后重新加载 } // 通过节点的shapeFlag(描述该组件的类型)进行不同处理 const { shapeFlag, type } = n2 if (type === Text) { // 文本 console.log('文本') processText(n1, n2, container) } else if (shapeFlag & ShapeFlags.ELEMENT) { // 元素 console.log('元素') processElement(n1, n2, container, anchor) } else if (shapeFlag & ShapeFlags.STATEFUL_COMPONENT) { // 组件 console.log('组件') processComponent(n1, n2, container) } } // 对比新旧的type值以及key值 const isSameVNodeType = (n1, n2) => { return n1.type === n2.type && n1.key === n2.key }
processText 函数用于处理文本节点的更新。
函数根据新旧 vnode 节点的情况,创建新的文本节点或更新现有的文本节点的内容,以确保文本内容正确地显示在 DOM 中。
const processText = (n1, n2, container) => {
const textNode = document.createTextNode(n2.children)
// 旧的 vnode 节点不存在
if (n1 == null) {
// 创建文本节点并将内容设置为新节点的文本内容
insert(createText(n2.children), container)
} else {
// 更新现有的文本节点的内容
n1.el.textContent = textNode
}
}
processElement 函数用于处理元素节点的更新。
函数根据新旧 vnode 节点的差异,对元素节点进行更新,并处理元素节点的属性、样式、事件等方面的变化。
const processElement = (n1, n2, container, anchor) => {
if (n1 == null) { // 旧的 vnode 节点不存在
// 创建新的元素节点
mountElement(n2, container, anchor)
} else { // 更新现有的元素节点
console.log('同一元素的比对')
patchElement(n1, n2, container, anchor)
}
}
mountElement 用于挂载新的元素节点到 DOM 中。
const mountElement = (vnode, container, anchor) => { const { type, props, shapeFlag, children } = vnode // 创建一个新的元素节点 el,并将其设置为 vnode节点的 el 属性,便于在后续的更新中可以引用到该元素节点 const el = vnode.el = document.createElement(type) if (props) { // 添加属性 // 遍历设置元素节点的属性 for (const key in props) { el.setAttribute(key, props[key]) } } // 处理children if (children) { if (shapeFlag & ShapeFlags.TEXT_CHILDREN) { // 子组件是纯文本 // 创建文本元素 setElementText(el, children) } else if (shapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 子组件是数组列表 // 递归挂载子节点 mountChildren(el, children) } } // 将元素节点挂载到容器中 container.insertBefore(el, anchor || null) }
mountChildren 函数的作用是将一组子节点挂载到指定的父节点中。
const mountChildren = (el, children) => {
for (let i = 0; i < children.length; i++) {
const child = normalizeVNode(children[i]) // 对当前子节点进行规范化处理,确保它是一个虚拟节点对象
patch(null, child, el) // 因为旧的节点为null(表示之前没有任何节点),所以会直接创建并挂载新的子节点到 el 中
}
}
// 规范化虚拟节点,确保它们都是预期的对象格式
const normalizeVNode = (child) => {
if (isObject(child)) { // 已经是一个规范的 vnode
return child
} else {
return createVNode(Text, null, String(child)) // 创建一个文本类型的 vnode
}
}
patchElement 用于更新已存在的元素节点。
函数的作用是根据新旧 vnode 节点的差异,对已存在的元素节点进行更新。
const patchElement = (n1, n2, container, anchor) => { const el = (n2.el = n1.el) // 获取真实节点 const oldProps = n1.props || {} const newProps = n2.props || {} for (const key in newProps) { const oldValue = oldProps[key] const newValue = newProps[key] if (oldValue !== newValue) { el.setAttribute(key, newValue) } } // 比对children patchChildren(n1, n2, el) } const patchChildren = (n1, n2, el) => { const c1 = n1.children const c2 = n2.children const prevShapeFlag = n1.shapeFlag const nextShapeFlag = n2.shapeFlag if (nextShapeFlag & ShapeFlags.TEXT_CHILDREN) { // 文本类型 // 直接设置元素的文本内容为新的子节点内容 setElementText(el, c2) } else { if (prevShapeFlag & ShapeFlags.ARRAY_CHILDREN) { // 新旧子节点都是数组 patchKeyedChildren(c1, c2, el) // diff算法 使用双指针策略来更新子节点 } else { // 如果旧的子节点是文本,但新的子节点是一个数组 setElementText(el, '') // 首先,清空旧元素的文本内容 mountChildren(el, c2) // 然后,挂载新的子节点到DOM元素上 } } }
这里的 patchKeyedChildren 是 patch 函数最为重要的内容。函数内部实现了核心的 diff 算法,在后面的文章中会重点介绍。
processComponent 用于处理组件类型的虚拟 DOM 节点。
函数的作用是处理组件类型的虚拟 DOM 节点,包括创建组件实例、挂载组件、更新组件等操作。
const processComponent = (n1, n2, container) => {
if (n1 === null) { // 首次挂载
mountComponent(n2, container)
} else { // 更新
updateComponent(n1, n2, container)
}
}
mountComponent 函数用于挂载组件类型的虚拟 DOM 节点。
函数的作用是创建组件实例、解析组件实例对象并设置渲染效果。
// 组件挂载
const mountComponent = (initialVNode, container) => {
const instance = initialVNode.component = createComponentInstance(
initialVNode) // 初始化一个组件的实例对象
setupComponent(instance) // 解析组件的实例对象
setupRenderEffect(instance, container) // 创建一个effect
}
createComponentInstance: 初始化一个组件的实例对象,添加相关属性(vnode、type、props、attrs、ctx、proxy)。
setupComponent: 函数来解析组件的实例对象。这个函数会处理组件的 props、slots、attrs 等属性,并设置响应式数据等。它会在组件实例上创建一些与组件相关的属性和方法。
setupRenderEffect: 创建一个 effect,用于在组件状态变化时触发重新渲染。该 effect 依赖于组件实例的响应式数据。当响应式数据发生变化时,会触发该 effect,进而调用组件实例的 render 方法重新渲染组件,并将渲染结果插入到 container 容器中。
updateComponent 函数用于更新组件类型的虚拟 DOM 节点。
函数根据新旧节点的变化情况,更新组件的状态和属性,并触发组件的生命周期钩子函数。通过更新组件的 props、slots 等属性,执行 beforeUpdate 生命周期钩子函数。执行组件的更新操作,最后执行 updated 生命周期钩子函数,完成了组件的更新过程。
function updateComponent(n1, n2, container) {
const instance = n2.component = n1.component // 组件实例引用进行传递,确保组件实例的一致性
if (shouldUpdateComponent(n1, n2)) { // 判断是否需要更新组件
// 更新组件的 props、slots 等属性
instance.props = n2.props
instance.slots = n2.slots
// 触发 beforeUpdate 生命周期钩子函数
callBeforeUpdateHooks(instance)
// 执行组件的更新操作
instance.update()
// 触发 updated 生命周期钩子函数
callUpdatedHooks(instance)
}
}
shouldUpdateComponent: 函数判断是否需要更新组件。该函数会比较新旧节点的属性、事件等是否发生了变化,以确定是否需要进行组件的更新操作。如果判断为需要更新组件,则执行下面的操作;否则,不进行任何更新操作。
callBeforeUpdateHooks: 函数触发组件的 beforeUpdate 生命周期钩子函数。该函数会在组件更新之前执行一些预处理或清理操作。
update: 执行组件的更新操作,包括重新渲染组件。
callUpdatedHooks: 函数触发组件的 updated 生命周期钩子函数。该函数会在组件更新完成后执行一些操作,如更新后的 DOM 操作、发送请求等。
为了便于理解我把以上代码流程流程图的形式展示出来了,希望通过本篇文章可以帮助读者更好的了解 vue 的渲染过程。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。