Vue设计与实现:挂载与更新
挂载子节点和元素的属性
子节点除了是字符串还会有节点就说明是vnode,所以需要把vnode.children 定义为数组
const vnode = {
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
也需要在挂载mountElement时判断children类型,如果children是数组需要递归patch,由于在挂载阶段是没有旧的vnode,所以第一个参数传null
function mountElement(vnode, container) {
// 调用 createElement 函数创建元素
const el = createElement(vnode.type)
if (typeof vnode.children === 'string') {
// 调用 setElementText 设置元素的文本节点
setElementText(el, vnode.children)
+ } else if (Array.isArray(vnode.children)) {
+ vnode.children.forEach(child => {
+ patch(null, child, el)
+ })
+ }
+ // 如果 vnode.props 存在才处理它
+ if (vnode.props) {
+ // 遍历 vnode.props
+ for (const key in vnode.props) {
+ // 调用 setAttribute 将属性设置到元素上
+ el.setAttribute(key, vnode.props[key])
+ }
+ }
// 调用 insert 函数将元素插入到容器内
insert(el, container)
}
HTML Attributes 与 DOM Properties
HTML Attributes 指的就是定义在 HTML 标签上的属性,这里指的 就是 id="my-input"、type="text" 和 value="foo"
<input id="my-input" type="text" value="foo" />
DOM Properties是指DOM 对象的属性
很多 HTML Attributes 在 DOM 对象上有与之同名的 DOM Properties,例如 id="my-input" 对 应 el.id,type="text" 对应 el.type,value="foo" 对应 el.value 等
但 DOM Properties 与 HTML Attributes 的名字不总是一 模一样的,class="foo" 对应的 DOM Properties 则是 el.className
一个 HTML Attributes 可能关联多个 DOM Properties,value="foo" 与 el.value 和 el.defaultValue 都有 关联
如果你通过 HTML Attributes 提供的默认值不合法,那么 浏览器会使用内建的合法值作为对应 DOM Properties 的默认值
正确地设置元素属性
浏览器会将该按钮设置为禁用状态
默认传true
<button disabled>Button</button>
但在vue会编译成空字符串
const button = {
type: 'button',
props: {
disabled: ""
},
children: "button"
}
在mountElement中调用 setAttribute 函数设置属性,浏览器会将按钮禁用
el.setAttribute('disabled', '')
传false
<button :disabled="false">Button</button>
const button = {
type: 'button',
props: {
disabled: false
},
children: "button"
}
因为使用 setAttribute 函数设置的值总是会被字符串化,浏览器仍然将按钮禁用
el.setAttribute('disabled', false)
// 等价于
el.setAttribute('disabled', 'false')
可以优先设置 DOM Properties,当disabled是空字符串会失败
由于 el.disabled 是布尔类型的值,所以当我们尝试将它设置为空字符串时,浏览器会将它的值矫正为布尔类型的值,即 false
解决思路:
优先设置元素的 DOM Properties,但当值为空字符串时,要手动将值矫正为 true
- 如果有vnode.props才执行,遍历vnode.props拿到每个prop
- 判断prop是否在dom上,如果存在再判断prop对应的value是否是空字符串,如果是空字符串手动转换为true赋值给el[key],否则把value赋值el[key]
- 如果prop不在dom上,就用setAttribute设置prop
+function shouldSetAsProps(el, key, value) {
+ // 有一些 DOM Properties 是只读的需要return
+ if (key === 'form' && el.tagName === 'INPUT') return false
+ // 兜底
+ return key in el
+}
function mountElement(vnode, container) {
// 调用 createElement 函数创建元素
const el = createElement(vnode.type)
+ if (vnode.props) {
+ for (const key in vnode.props) {
+ patchProps(el, key, null, vnode.props[key])
+ }
+ }
if (typeof vnode.children === 'string') {
// 调用 setElementText 设置元素的文本节点
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
})
}
// 调用 insert 函数将元素插入到容器内
insert(el, container)
}
const renderer = createRenderer({
+ patchProps(el, key, prevValue, nextValue) {
+ if (shouldSetAsProps(el, key, nextValue)) {
+ const type = typeof el[key]
+ // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
+ if (type === 'boolean' && nextValue === '') {
+ el[key] = true
+ } else {
+ el[key] = nextValue
+ }
+ } else {
+ // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
+ el.setAttribute(key, nextValue)
+ }
+ }
})
结果:
class 的处理
class 为一个字符串
<p class="foo bar"></p>
const vnode = {
type: 'p',
props: {
class: 'foo bar'
}
}
class 为一个对象
<p :class="cls"></p>
const vnode = {
type: 'p',
props: {
class: { foo: true, bar: false }
}
}
class 为一个数组
<p :class="arr"></p>
const vnode = {
type: 'p',
props: {
class: [
'foo bar',
{ baz: true }
]
}
}
解决思路:
在设置元素的 class 之前将值归一化为统一的字符串形式
- 检查
classNames
是否为假值(null、undefined 等),如果是,则返回空字符串 - 检查
classNames
是否为字符串类型,如果是,则去除首尾空格后返回 - 如果
classNames
是数组类型,表示传入了一组 class 名称,我们需要递归调用normalizeClass
对其中的每个元素进行处理,并使用空格拼接起来返回 - 最后,如果
classNames
是对象类型,我们会过滤掉值为真的属性,并只返回属性名拼接而成的字符串
function normalizeClass(classNames) {
if (!classNames) {
return '';
}
if (typeof classNames === 'string') {
return classNames.trim();
}
if (Array.isArray(classNames)) {
return classNames.map(normalizeClass).filter(Boolean).join(' ');
}
if (typeof classNames === 'object') {
return Object.entries(classNames)
.filter(([key, value]) => value)
.map(([key]) => key)
.join(' ');
}
return '';
}
由于 class 属性对应的 DOM Properties 是 el.className,所以表达式 'class' in el 的值将会是 false,并且el.className 的性能比setAttribute好
patchProps(el, key, prevValue, nextValue) {
+ // 对 class 进行特殊处理
+ if (key === "class") {
+ el.className = nextValue || ""
+ } else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
el.setAttribute(key, nextValue)
}
}
结果:
卸载操作
卸载操作发生在更新阶段,更新指的是,在初次挂载完成之后,后续渲染会触发更新
// 初次挂载
renderer.render(vnode, document.querySelector('#app'))
// 再次挂载新 vnode,将触发更新
renderer.render(newVNode, document.querySelector('#app'))
null 作为新 vnode
// 初次挂载
renderer.render(vnode, document.querySelector('#app'))
// 新 vnode 为 null,意味着卸载之前渲染的内容
renderer.render(null, document.querySelector('#app'))
在render函数中,当vnode为null并且container._vnode属性存在时会通过innerHTML清空容器,这样有三个缺点
- 旧容器可能是多个子组件组成,需要触发组件的 beforeUnmount、unmounted等生命周期函数
- 有的元素可能会存在自定义指令,需要执行对应的指令钩子函数
- 需要移除绑定在 DOM 元素上的事件处理函数
解决思路:
- 在创建真实dom时保存到vnode.el,这样虚拟dom就跟真实dom关联起来
- 当卸载时通过container._vnode.el拿到对应的真实dom,通过真实dom的父元素移除元素
function mountElement(vnode, container) {
- const el = createElement(vnode.type)
+ // 让 vnode.el 引用真实 DOM 元素
+ const el = vnode.el = createElement(vnode.type)
if (vnode.props) {
for (const key in vnode.props) {
patchProps(el, key, null, vnode.props[key])
}
}
if (typeof vnode.children === 'string') {
// 调用 setElementText 设置元素的文本节点
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
})
}
// 调用 insert 函数将元素插入到容器内
insert(el, container)
}
+ function unmount(vnode) {
+ // 获取 el 的父元素
+ const parent = vnode.el.parentNode
+ // 调用 removeChild 移除元素
+ if (parent) parent.removeChild(el)
+ }
function render(vnode, container) {
if (vnode) {
patch(container._vnode, vnode, container)
} else {
if (container._vnode) {
- container.innerHTML = ''
+ // 调用 unmount 函数卸载 vnode
+ unmount(container._vnode)
}
}
container._vnode = vnode
}
区分 vnode 的类型
一个vnode可以用来描述普通标签,也可以用来描述组件,还可以用来描述Fragment,对于不同类型的 vnode,需要提供不同的挂载或打补丁的处理方式
function patch(oldN, newN, container) {
+ // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
+ if (oldN && oldN.type !== newN.type) {
+ unmount(oldN)
+ oldN = null
+ }
+ // 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
+ const { type } = newN
+ // 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
+ if (typeof type === "string") {
+ // 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
+ if (!oldN) {
+ mountElement(newN, container)
+ } else {
+ patchElement(oldN, newN)
+ }
+ }
}
事件的处理
在 vnode.props 对象中,凡是以字符串 on 开头的属性都视作事件
绑定一种事件
const vnode = {
type: 'p',
props: {
// 使用 onXxx 描述事件
onClick: () => {
alert('clicked')
}
},
children: 'text'
}
解决思路:
- 从 el._vei 中读取对应的 invoker,如果 invoker 不存在, 则将伪造的 invoker 作为事件处理函数,并将它缓存到 el._vei 属性中
- 当更新事件时,由于 el._vei 已经存在了,所以只需要将 invoker.value 的值修改为新的事件处理函数
- 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
patchProps(el, key, prevValue, nextValue) {
+ // 匹配以 on 开头的属性,视其为事件
+ if (/^on/.test(key)) {
+ // 获取为该元素伪造的事件处理函数 invoker
+ const invoker = el._vei
+ // 根据属性名称得到对应的事件名称,例如 onClick ---> click
+ const name = key.slice(2).toLowerCase()
+ if (nextValue) {
+ if (!invoker) {
+ // 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
+ invoker = el._vei = (e) => {
+ // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
+ invoker.value(e)
+ }
+ // 将真正的事件处理函数赋值给 invoker.value
+ invoker.value = nextValue
+ // 绑定事件,nextValue 为事件处理函数
+ el.addEventListener(name, invoker)
+ } else {
+ // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
+ invoker.value = nextValue
+ }
+ }
+ else if (invoker) {
+ // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
+ el.removeEventListener(name, invoker)
+ }
}
else if (key === "class") {
el.className = nextValue || ""
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
el.setAttribute(key, nextValue)
}
}
结果:
绑定多种类型事件
一个元素同时绑定了多种事件,将会出现事件覆盖的现象
const vnode = {
type: 'p',
props: {
onClick: () => {
alert('clicked')
},
onContextmenu: () => {
alert('contextmenu')
}
},
children: 'text'
}
renderer.render(vnode, document.querySelector('#app'))
解决思路:
- 定义 el._vei 为一个对象,保存每个事件对应的invoker
- 一开始每个事件对应的invoker为undefined,所以只执行!invoker的逻辑,只绑定当前遍历到的事件对应的invoker
patchProps(el, key, prevValue, nextValue) {
// 匹配以 on 开头的属性,视其为事件
if (/^on/.test(key)) {
+ // 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
+ const invokers = el._vei || (el._vei = {})
+ //根据事件名称获取 invoker
+ let invoker = invokers[key]
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
- invoker = el._vei = (e) => {
+ invoker = el._vei[key] = (e) => {
// 当伪造的事件处理函数执行时,会执行真正的事件处理函数
invoker.value(e)
}
// 将真正的事件处理函数赋值给 invoker.value
invoker.value = nextValue
// 绑定事件,nextValue 为事件处理函数
el.addEventListener(name, invoker)
} else {
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
invoker.value = nextValue
}
}
else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker)
}
}
else if (key === "class") {
el.className = nextValue || ""
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
el.setAttribute(key, nextValue)
}
}
结果:
绑定同一类型的多个事件
const vnode = {
type: 'p',
props: {
onClick: [
// 第一个事件处理函数
() => {
alert('clicked 1')
},
// 第二个事件处理函数
() => {
alert('clicked 2')
}
]
},
children: 'text'
}
renderer.render(vnode, document.querySelector('#app'))
解决思路:
如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
patchProps(el, key, prevValue, nextValue) {
// 匹配以 on 开头的属性,视其为事件
if (/^on/.test(key)) {
// 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
const invokers = el._vei || (el._vei = {})
//根据事件名称获取 invoker
let invoker = invokers[key]
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const name = key.slice(2).toLowerCase()
if (nextValue) {
if (!invoker) {
// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
invoker = el._vei[key] = (e) => {
+ // 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数
+ if (Array.isArray(invoker.value)) {
+ invoker.value.forEach(fn => fn(e))
+ } else {
+ // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
+ invoker.value(e)
+ }
}
// 将真正的事件处理函数赋值给 invoker.value
invoker.value = nextValue
// 绑定事件,nextValue 为事件处理函数
el.addEventListener(name, invoker)
} else {
console.log(key)
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
invoker.value = nextValue
}
}
else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker)
}
}
else if (key === "class") {
el.className = nextValue || ""
} else if (shouldSetAsProps(el, key, nextValue)) {
const type = typeof el[key]
// 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
if (type === 'boolean' && nextValue === '') {
el[key] = true
} else {
el[key] = nextValue
}
} else {
// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
el.setAttribute(key, nextValue)
}
}
结果:
更新子节点
每种新子节点都有三种旧子节点,三种新子节点就有九种旧子节点,patchElement函数处理新旧子节点
// 没有子节点
vnode = {
type: 'div',
children: null
}
// 文本子节点
vnode = {
type: 'div',
children: 'Some Text'
}
// 其他情况,子节点使用数组表示
vnode = {
type: 'div',
children: [
{ type: 'p' },
'Some Text'
]
}
更新 props
解决思路:
- 拿到旧vnode的el也就是真实dom赋值给新子节点的el,下次更新新vnode的el就是旧vnode的el
- 拿到新旧vnode的props,遍历新props,判断newProps、oldProps的key对应的value是否一样,不一样就以新props为准更新
- 遍历旧props,如果key不存在新props说明已经把oldProps对应的key删了,所以新值为null
function patchElement(oldN, newN) {
const el = newN.el = old.el
const oldProps = oldN.props
const newProps = newN.props
for (const key in newProps) {
// key都在newProps、oldProps,只不过值不一样
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[key], newProps[key])
}
}
for (const key in oldProps) {
// 新的dom上已经把oldProps对应的key删了,所以新值为null
if (!(key in newProps)) {
patchProps(el, key, oldProps[key], null)
}
}
// 更新 children
patchChildren(oldN, newN, el)
}
结果:
更新 children
新子节点是文本节点
解决思路:
- 先判断新vnode的子节点是不是文本节点,如果是再判断旧vnode的子节点是不是数组
- 如果是数组就遍历卸载节点,不是数组就是没有子节点或者一个文本节点都不需要操作,直接将新vnode的字符串类型的children设置给容器,因为setElementText会直接覆盖文本
function patchChildren(oldN, newN, container) {
// 判断新子节点的类型是否是文本节点
if (typeof newN.children === "string") {
// 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
if (Array.isArray(oldN.children)) {
oldN.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, newN.children)
}
}
结果:
新子节点是一组子节点
解决思路:
- 将旧子节点是不是数组,先把旧子节点遍历卸载,再把新子节点挂载
- 排除数组就是没有节点、一个文本节点的情况,不管无论哪种情况直接容器清空就不需要判断,再把新子节点遍历挂载
function patchChildren(oldN, newN, container) {
// 判断新子节点的类型是否是文本节点
if (typeof newN.children === "string") {
// 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
if (Array.isArray(oldN.children)) {
oldN.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, newN.children)
+ } else if (Array.isArray(newN.children)) { // 说明新子节点是一组子节点
+ // 判断旧子节点是否也是一组子节点
+ if (Array.isArray(oldN.children)) {
+ // 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的Diff 算法
+ // 将旧的一组子节点全部卸载
+ oldN.children.forEach(c => unmount(c))
+ // 再将新的一组子节点全部挂载到容器中
+ newN.children.forEach(c => patch(null, c, container))
+ } else {
+ // 此时:
+ // 旧子节点要么是文本子节点,要么不存在
+ // 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
+ setElementText(container, '')
+ newN.children.forEach(c => patch(null, c, container))
+ }
+ }
}
结果:
新子节点没有子节点
解决思路:
- 判断旧子节点是不是数组,是数组就遍历旧子节点卸载
- 判断旧子节点是不是文本子节点,是文本子节点就清空内容
function patchChildren(oldN, newN, container) {
// 判断新子节点的类型是否是文本节点
if (typeof newN.children === "string") {
// 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
// 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
if (Array.isArray(oldN.children)) {
oldN.children.forEach((c) => unmount(c))
}
// 最后将新的文本节点内容设置给容器元素
setElementText(container, newN.children)
} else if (Array.isArray(newN.children)) { // 说明新子节点是一组子节点
// 判断旧子节点是否也是一组子节点
if (Array.isArray(oldN.children)) {
// 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的Diff 算法
// 将旧的一组子节点全部卸载
oldN.children.forEach(c => unmount(c))
// 再将新的一组子节点全部挂载到容器中
newN.children.forEach(c => patch(null, c, container))
} else {
// 此时:
// 旧子节点要么是文本子节点,要么不存在
// 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
setElementText(container, '')
newN.children.forEach(c => patch(null, c, container))
}
+ } else { // 代码运行到这里,说明新子节点不存在
+ if (Array.isArray(oldN.children)) {
+ oldN.children.forEach(c => unmount(c))
+ } else if (typeof oldN.children === "string") {
+ // 旧子节点是文本子节点,清空内容即可
+ setElementText(container, "")
+ // 如果也没有旧子节点,那么什么都不需要做
+ }
+ }
}
结果:
文本节点和注释节点
注释节点与文本节点不同于普通标签节点,它们不具有标签名称,所以需要人为创造一些唯一的标识
// 文本节点的 type 标识
const Text = Symbol()
const newVNode = {
// 描述文本节点
type: Text,
children: '我是文本内容'
}
文本节点
解决思路:
- 在patch增加判断type是不是等于Text(自定义的type)
- 判断旧vnode是否存在,不存在就根据新vnode创建文本节点再插入容器
- 如果存在就直接替换文本内容就行,因为一开始就判断标签内容不一样就会把旧vnode卸载
function patch(oldN, newN, container) {
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
// 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
const { type } = newN
// 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
if (typeof type === "string") {
// 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
if (!oldN) {
mountElement(newN, container)
} else {
patchElement(oldN, newN)
}
+ } else if (type === Text) { // 如果新 vnode 的类型是 Text,则说明该 vnode 描述的是文本节点
+ if (!oldN) {
+ // 使用 createTextNode 创建文本节点
+ const el = createText(newN.children)
+ // 将文本节点插入到容器中
+ insert(el, container)
+ } else {
+ // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
+ const el = newN.el = oldN.el
+ if (oldN.children !== newN.children) {
+ setText(el, newN.children)
+ }
+ }
+ }
else if (typeof type === 'object') {
// 如果 newN.type 的值的类型是对象,则它描述的是组件
} else if (type === 'xxx') {
// 处理其他类型的 vnode
}
}
结果:
注释节点
注释节点的处理方式与文本节点的处理方式类似。不同的是,我 们需要使用 document.createComment 函数创建注释节点元素
// 注释节点的 type 标识
const Comment = Symbol()
const newVNode = {
// 描述注释节点
type: Comment,
children: '我是注释内容'
}
解决思路:
function patch(oldN, newN, container) {
// 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
// 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
const { type } = newN
// 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
if (typeof type === "string") {
// 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
if (!oldN) {
mountElement(newN, container)
} else {
patchElement(oldN, newN)
}
} else if (type === Text) { // 如果新 vnode 的类型是 Text,则说明该 vnode 描述的是文本节点
if (!oldN) {
// 使用 createTextNode 创建文本节点
const el = createText(newN.children)
// 将文本节点插入到容器中
insert(el, container)
} else {
// 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
const el = newN.el = oldN.el
if (oldN.children !== newN.children) {
setText(el, newN.children)
}
}
+ } else if (type === Comment) {
+ if (!oldN) {
+ // 使用 createTextNode 创建文本节点
+ const el = createComment(newN.children)
+ console.log(el,container)
+ // 将文本节点插入到容器中
+ insert(el, container)
+ } else {
+ // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
+ const el = newN.el = oldN.el
+ if (oldN.children !== newN.children) {
+ setText(el, newN.children)
+ }
+ }
}
else if (typeof type === 'object') {
// 如果 newN.type 的值的类型是对象,则它描述的是组件
} else if (type === 'xxx') {
// 处理其他类型的 vnode
}
}
结果:
Fragment
与文本节点和注释节点类似,片段也没有所谓的标签名称,因此也需要为片段创建唯一标识,Fragment 本身并不会渲染任何内容,所以渲染器只会渲染 Fragment 的子节点
const Fragment = Symbol()
const vnode = {
type: Fragment,
children: [
{ type: 'li', children: 'text 1' },
{ type: 'li', children: 'text 2' },
{ type: 'li', children: 'text 3' }
]
}
解决思路:
- 在patch增加判断type是否是Fragment
- 如果oldN不存在就遍历newN的children递归patch挂载
- 存在oldN就替换oldN的children
- 如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
function patch(oldN, newN, container) {
if (oldN && oldN.type !== newN.type) {
unmount(oldN)
oldN = null
}
const { type } = newN
if (typeof type === 'string') {
// 省略部分代码
} else if (type === Text) {
// 省略部分代码
+ } else if (type === Fragment) { // 处理 Fragment 类型的 vnode
+ if (!oldN) {
+ // 如果旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
+ newN.children.forEach(c => patch(null, c, container))
+ } else {
+ // 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可
+ patchChildren(oldN, newN, container)
+ }
+ }
}
function unmount(vnode) {
+ // 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
+ if (vnode.type === Fragment) {
+ vnode.children.forEach(c => unmount(c))
+ return
+ }
// 获取 el 的父元素
const parent = vnode.el.parentNode
// 调用 removeChild 移除元素
if (parent) parent.removeChild(vnode.el)
}
结果:
转载自:https://juejin.cn/post/7268658252567609402