渲染器的核心功能:挂载与更新
1.挂载子节点和元素的属性
1.2挂载子节点 (vnode.children)
vnode.children可以是字符串类型的,也可以是数组类型的,如下:
const vnode ={type: 'div',children: [{type: 'p',children: 'hello'}]
}
可以看到,vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM 树。
为了完成子节点的渲染,我们需要修改 mountElement 函数
,如下面的代码所示:
function mountElement(vnode, container) {// 创建dom元素const el = createElement(vnode.type)console.log(vnode.children)+ // 处理子元素if (typeof vnode.children === 'string') {setElementText(el, vnode.children)} else if (Array.isArray(vnode.children)) {// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它vnode.children.forEach(child => {patch(null, child, el)});}insert(el, container)
}
在上面这段代码中,我们增加了新的判断分支。使用 Array.isArray 函数判断vnode.children 是否是数组,如果是 数组,则循环遍历它,并调 patch 函数挂载数组中的虚拟节点。在挂 载子节点时,需要注意以下两点:
- 传递给 patch 函数的第一个参数是 null。因为是挂载阶段,没 有旧 vnode,所以只需要传递 null 即可。这样,当 patch 函数 执行时,就会递归地调用 mountElement 函数完成挂载。
- 传递给 patch 函数的第三个参数是挂载点。由于我们正在挂载的 子元素是 div 标签的子节点,所以需要把刚刚创建的 div 元素作 为挂载点,这样才能保证这些子节点挂载到正确位置。
1.2元素的属性(vnode.props)
我们知道,HTML 标签有很多属 性,其中有些属性是通用的,例如 id、class 等,而有些属性是特定 元素才有的,例如 form 元素的 action 属性
。实际上,渲染一个元 素的属性比想象中要复杂,不过我们仍然秉承一切从简的原则,先来 看看最基本的属性处理。
为了描述元素的属性,我们需要为虚拟 DOM 定义新的 vnode.props 字段
,如下面的代码所示:
const vnode ={type: 'div',// 使用 props 描述一个元素的属性props: {id: 'foo'},children: [{type: 'p',children: 'hello'}]
}
vnode.props 是一个对象,它的键代表元素的属性名称,它的值 代表对应属性的值。这样,我们就可以通过遍历 props 对象的方式, 把这些属性渲染到对应的元素上,如下面的代码所示:
function mountElement(vnode, container) {// 创建dom元素const el = createElement(vnode.type)console.log(vnode.children)// 处理子元素if (typeof vnode.children === 'string') {setElementText(el, vnode.children)} else if (Array.isArray(vnode.children)) {// 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它vnode.children.forEach(child => {patch(null, child, el)});}
+ // 处理元素属性insert(el, container)
}
2.HTML Attributes DOM Properties
元素的属性分为2种:
- 1.HTML Attributes
- 2.DOM Properties
如何区分:
key in el 返回值为true则是:DOM Properties,返回false则是HTML Attributes
如何争取的设置到元素上:
- DOM Properties
el[key] = value
- HTML Attributes
el.setAttribute(key, value)
3.正确的设置元素的属性
思路:
-
1.我们知道元素的属性分为2种,而且这2种的设置方式不一样。因此,我们要特殊处理。
-
2.处理特殊情况:例如button按钮,它的vnode节点如下:
const button = {type: 'button',props: {disabled: ''}
}
但是在解析的时候,会出现问题,用户的本意是“不禁用”按钮,但如果渲染器仍然使用 setAttribute 函数设置属性值,则会产生意外的效果,即按钮被禁 用了.
那么应该怎么办呢?一个很自然的思路是,我们可以优先设置 DOM Properties,例如:
el.disabled = false
- 3.处理特殊情况2: form表单的一些只读属性:
<form id="form1"></form>
<input form="form1" />
在这段代码中,我们为 标签设置了 form 属性 (HTML Attributes)。它对应的 DOM Properties 是 el.form,但 el.form 是只读的,因此我们只能够通过 setAttribute 函数来设 置它。
function shouldSetAsProps(el, key, value) {// 特殊处理if(key === 'form' && el.tagName === 'INPUT') return false//兜底return key in el
}function mountElement(vnode, container) {const el = createElement(vnode.type)// 省略 children 的处理// 处理元素的属性
+ if (vnode.props) {for (const key in vnode.props) {const value = vnode.props[key]// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置if (shouldSetAsProps(el, key, vaue)) {if (type === 'boolean' && value === '') {el[key] = true} else {el[key] = false}} else {// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性// HTML Attributesel.setAttribute(key, vnode.props[key])}}}
}
4.class的处理
class有三种不同的vnode表示方式,
- 方式1:
const vnode = {type: 'p',props: {class: { foor: true, bar: false }}
}
- 方式2
const vnode = {type: 'p',props: {class: 'foo bar'}
}
- 方式3
const vnode = {type: 'p',props: {class: [ 'foo bar', { bar: true }]}
}
因此我们需要一个normalizeClass函数来将不同类型的class值正常化为字符串。
const vnode = {type: 'p',props: {class: normalizeClass([ 'foo bar', { baz: true }])}
}
处理之后:
const vnode = {type: 'p',props: {class: 'foo bar baz'}
}
处理之后,设置class的方式也有三种1.className, 2.setAttribute, 3.classList 但是这三种设置的方式不同,性能也是不一样,经过调查发现className的性能是最优的,因此我们使用className设置元素的class。
function mountElement(vnode, container) {const el = createElement(vnode.type)// 省略 children 的处理// 处理元素的属性if (vnode.props) {for (const key in vnode.props) {const value = vnode.props[key]+ if (key === 'class') {+ el.className = value || ''+ } else if (shouldSetAsProps(el, key, vaue)) {// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置if (type === 'boolean' && value === '') {el[key] = true} else {el[key] = false}} else {// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性// HTML Attributesel.setAttribute(key, vnode.props[key])}}}
}
简化上面的操作,我们可以把处理元素属性的逻辑放在一个函数(patchProps)里面:
function patchProps(el, key, prevValue, nextValue){// 对class 特殊处理if (key === 'class') {el.className = value || ''} else if (shouldSetAsProps(el, key, vaue)) {// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置if (type === 'boolean' && value === '') {el[key] = true} else {el[key] = false}} else {// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性// HTML Attributesel.setAttribute(key, vnode.props[key])}
}
在mountElement函数中调用
function mountElement(vnode, container) {const el = createElement(vnode.type)// 省略 children 的处理// 处理元素的属性if (vnode.props) {for (const key in vnode.props) {const value = vnode.props[key]
+ patchProps(el, key, null, vnode.props[key])}}
}
5.卸载操作(unmount)
卸载操作的时机: 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作
我们不能简单地使用 innerHTML 来完成卸 载操作。正确的卸载方式是,根据 vnode 对象获取与其相关联的真实 DOM 元素,然后使用原生 DOM 操作方法将该 DOM 元素移除。
由于卸载操作是比较常见且基本的操作,所以我们应该将它封装 到 unmount 函数中,以便后续代码可以复用它,如下面的代码所示:
// 卸载
unmount(vnode) {// 获取 el 的父元素const parent = vnode.el.parentNode// 调用 removeChild 移除元素parent && parent.removeChild(vnode.el)}
简化render函数
function render(vnode, container) {if(vnode) {patch(container._vnode, vnode, container)} else {if(container._vnode) {// 调用 unmount 函数卸载 vnode
+ unmount(container._vnode)} }container._vnode = vnode
}
将卸载操作封装到 unmount 中,还能够带来两点额外的好处:
- 在 unmount 函数内,我们有机会调用绑定在 DOM 元素上的指令钩子函数,例如 beforeUnmount、unmounted 等。
- 当 unmount 函数执行时,我们有机会检测虚拟节点 vnode 的类型。如果该虚拟节点描述的是组件,则我们有机会调用组件相的生命周期函数。
6.区分vnode类型
一个 vnode 可以用来描述普通标签
,也 可以用来描述组件
,还可以用来描述Fragment
等。对于不同类型的 vnode,我们需要提供不同的挂载或打补丁的处理方式
。所以,我们 需要继续修改 patch 函数的代码以满足需求:
funcion patch(n1, n2, container) {if(n1 && n1.type !== n2.type) {unmount(n1)n1 = null}// 代码运行到这里,证明 n1 和 n2 所描述的内容相同const { type } = n2
+ if (typeof type === 'string') {if(!n1) {// 挂载子节点mountElement(n2, container)} else {// 更新子节点patchElement(n1, n2)}
+ } else if (typeof type === 'object') {// 如果 n2.type 的值的类型是对象,则它描述的是组件
+ } else if (type === 'Fragment') {// 处理其他类型的 vnode}
}
根据 vnode.type 进一步确认它们的类型是 什么,从而使用相应的处理函数进行处理。例如,如果 vnode.type的值是字符串类型,则它描述的是普通标签元素,这时我们会调用mountElement 或 patchElement 完成挂载和更新操作;如果vnode.type 的值的类型是对象,则它描述的是组件,这时我们会调用与组件相关的挂载和更新方法。
7.了解事件的处理
const vnode = {type: 'p',props: {// 使用 onXxx 描述事件onClick: () => {alert('clicked')}},children: 'text'
}
patchProps(el, key, prevValue, nextValue) {
+ if (/^on/.test(key)) {
+ const name = key.slice(2).toLowerCase()// 移除上一次绑定的事件处理函数
+ prevValue && el.removeEventListener(name, prevValue)// 绑定新的事件处理函数
+ el.addEventListener(name, nextValue)} else if (key === 'class') {// 省略部分代码} else if (shouldSetAsProps(el, key, nextValue)) {// 省略部分代码} else {}
}
这么做代码能够按照预期工作,但其实还有一种性能更优的方式 来完成事件更新。在绑定事件时,我们可以绑定一个伪造的事件处理 函数 invoker,然后把真正的事件处理函数设置为 invoker.value 属性的值。这样当更新事件的时候,我们将不再需要调用 removeEventListener 函数来移除上一次绑定的事件,只需要更新 invoker.value 的值即可:
patchProps(el, key, prevValue, nextValue) {if (/^on/.test(key)) {// const name = key.slice(2).toLowerCase()// prevValue && el.removeEventListener(name, prevValue)// el.addEventListener(name, nextValue)let invoker = el._veiconst name = key.slice(2).toLowerCase()if (nextValue) {if (!invoker) {// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中// vei 是 vue event invoker 的首字母缩写invoker = el._vei = (e) => {// 当伪造的事件处理函数执行时,会执行真正的事件处理函数invoker.value(e)}// 将真正的事件处理函数赋值给 invoker.valueinvoker.value = nextValue// 绑定 invoker 作为事件处理函数el.addEventListener(name, invoker)} else {// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可invoker.value = nextValue}} else if (invoker) {// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定el.removeEventListener(name, invoker)}}
}
观察上面的代码,事件绑定主要分为两个步骤:
- 先从 el._vei 中读取对应的 invoker,如果 invoker 不存在, 则将伪造的 invoker 作为事件处理函数,并将它缓存到 el._vei 属性中。
- 把真正的事件处理函数赋值给 invoker.value 属性,然后把伪 造的 invoker 函数作为事件处理函数绑定到元素上。可以看到, 当事件触发时,实际上执行的是伪造的事件处理函数,在其内部 间接执行了真正的事件处理函数 invoker.value(e)。
当更新事件时,由于 el._vei 已经存在了,所以我们只需要将 invoker.value 的值修改为新的事件处理函数即可。这样,在更新 事件时可以避免一次 removeEventListener 函数的调用,从而提 升了性能。实际上,伪造的事件处理函数的作用不止于此,它还能解 决事件冒泡与事件更新之间相互影响的问题,下文会详细讲解。
但目前的实现仍然存在问题。现在我们将事件处理函数缓存在 el._vei 属性中,问题是,在同一时刻只能缓存一个事件处理函数。
这意味着,如果一个元素同时绑定了多种事件,将会出现事件覆盖的 现象。例如同时给元素绑定 click 和 contextmenu 事件:
const vnode = {type: 'p',props: {onClick: () => {},onContextmenu:() => {}},children: 'text'
}
当渲染器尝试渲染这上面代码中给出的 vnode 时,会先绑定 click 事件,然后再绑定 contextmenu 事件。后绑定的 contextmenu 事件的处理函数将覆盖先绑定的 click 事件的处理函 数。为了解决事件覆盖的问题,我们需要重新设计 el._vei 的数据结 构。我们应该将 el._vei 设计为一个对象,它的键是事件名称,它的 值则是对应的事件处理函数,这样就不会发生事件覆盖的现象了
:
if (/^on/.test(key)) {
+ const invokers = el._vei || (el._vei = {})//根据事件名称获取 invoker
+ let invoker = invokers[key]const name = key.slice(2).toLowerCase()if (nextValue) {if (!invoker) {// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中// vei 是 vue event invoker 的首字母缩写
+ invoker = el._vei[key] = (e) => {// 当伪造的事件处理函数执行时,会执行真正的事件处理函数invoker.value(e)}// 将真正的事件处理函数赋值给 invoker.valueinvoker.value = nextValue// 绑定 invoker 作为事件处理函数el.addEventListener(name, invoker)} else {// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可invoker.value = nextValue}} else if (invoker) {// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定el.removeEventListener(name, invoker)}}
}
另外,一个元素不仅可以绑定多种类型的事件,对于同一类型的 事件而言,还可以绑定多个事件处理函数
。我们知道,在原生 DOM 编 程中,当多次调用 addEventListener 函数为元素绑定同一类型的 事件时,多个事件处理函数可以共存,例如:
el.addEventListener('click', fn1)
el.addEventListener('click', fn2)
当点击元素时,事件处理函数 fn1 和 fn2 都会执行。因此,为了 描述同一个事件的多个事件处理函数,我们需要调整 vnode.props 对象中事件的数据结构:
const vnode = {type: 'p',props: {onClick: [() => {}, () => {}]},children: 'text'
}
我们使用一个数组来描述事件,数组中的每 个元素都是一个独立的事件处理函数,并且这些事件处理函数都能够 正确地绑定到对应元素上。为了实现此功能,我们需要修改 patchProps 函数中事件处理相关的代码:
if (/^on/.test(key)) {// const name = key.slice(2).toLowerCase()// prevValue && el.removeEventListener(name, prevValue)// el.addEventListener(name, nextValue)const invokers = el._vei || (el._vei = {})//根据事件名称获取 invokerlet invoker = invokers[key]const name = key.slice(2).toLowerCase()if (nextValue) {if (!invoker) {// 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中// vei 是 vue event invoker 的首字母缩写invoker = el._vei[key] = (e) => {// 如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数+ if (Array.isArray(invoker.value)) {+ invoker.value.forEach((fn) => fn(e))+ } else {// 否则直接作为函数调用+ invoker.value(e)}}// 将真正的事件处理函数赋值给 invoker.valueinvoker.value = nextValue// 绑定 invoker 作为事件处理函数el.addEventListener(name, invoker)} else {// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可invoker.value = nextValue}} else if (invoker) {// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定el.removeEventListener(name, invoker)}}
我们修改了 invoker 函数的实现。当 invoker 函数执行时,在调用真正的事件处理函数之前,要先检查 invoker.value 的数据结构是否是数组,如果是数组则遍历它,并 逐个调用定义在数组中的事件处理函数。
8.事件冒泡与更新时机问题
9.更新子节点
对于一个元素来说,它的子节点无非有以下三种情况:
-
- 没有子节点,此时 vnode.children 的值为 null。
-
- 具有文本子节点,此时 vnode.children 的值为字符串,代表 文本的内容。
- 3.其他情况,无论是单个元素子节点,还是多个子节点(可能是文 本和元素的混合),都可以用数组来表示。
如下面的代码所示:
// 没有子节点
var vnode = {type: 'div',children: null
}
// 文本子节点
var vnode = {type: 'div',children: 'Some Text'
}// 其他情况,子节点使用数组表示
var vnode = {type: 'div',children: [{ type: 'p' },'Some Text']
}
现在,我们已经规范化了 vnode.children 的类型。既然一个 vnode 的子节点可能有三种情况,那么当渲染器执行更新时,新旧子 节点都分别是三种情况之一。所以,我们可以总结出更新子节点时全 部九种可能,如图 8-5 所示:
但落实到代码,我们会发现其实并不需要完全覆盖这九种可能。接下来我们就开始着手实现,如下面 patchElement 函数的代码所
示:
function patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 1.旧子节点是一组子节点,先逐个卸载// 2.旧子节点是字符串,直接使用setElementText设置即可// 3.旧子节点无,直接使用setElementText设置即可if (Array.isArray(n1.children)) {n1.children.forEach((c) =>unmount(c) )}setElementText(container, n2.children)} else if (Array.isArray(n2.children)) {// 1.旧子节点是一组子节点,进行patch操作// 2.旧子节点是字符串,直接使用setElementText设置即可// 3.旧子节点无,直接使用setElementText设置即可if(Array.isArray(n1.children)) {} else {setElementText(container, '')n2.children.forEach((c) => patch(null, c, container))}} else {// 代码运行到这里,说明新子节点不存在// 1.旧子节点是一组子节点,只需逐个卸载即可// 2.旧子节点是文本节点,清空内容即可if(Array.isArray(n1.children)) {n2.children.forEach((c) =>unmount(c) )} else if (typeof n1.children === 'string') {setElementText(container, '')}}
}
10.文本节点和注释节点
文本节点和注释节点的vnode:
const Text = Symbol()
const newVnode = {type: Text,children: '我是文本内容'
}const Comment = Symbol()
const newVnode = {type: Comment,children: '我是注释内容'
}
由于文本节点和注释节点只关心文本内 容,所以我们用 vnode.children 来存储它们对应的文本内容。
有了用于描述文本节点和注释节点的 vnode 对象后,我们就可以 使用渲染器来渲染它们了:
function patch(n1, n2, container) {if(!n1 && n1.type !== n2.type) {unmount(n1)n1 = null}const { type } = n2if (typeof type === 'string') {
+ } else if (typeof type === Text) {// 如果没有旧节点,则进行挂载if (!n1) {// 使用 createTextNode 创建文本节点// e2.el = document.createTextNode(n2.children)const el = n2.el = createText(n2.children)// 将文本节点插入到容器中insert(el, container)} else {// 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即const el = n2.el = n1.el // el = n1.typeif(n2.children !== n1.children) {setText(el, n2.children)}}} else if (typeof type === Fragment) {} else {}
}
const redner = createRenderer({createElement() {},setElementText() {},insert() {},patchProps() {},// 创建文本节点createText(text) {return document.createTextNode(text)},// 设置文本节点的内容setText(el, text) {el.nodeValue = text}
})
注释节点的处理方式与文本节点的处理方式类似。不同的是,我 们需要使用 document.createComment 函数创建注释节点元素。
11.Fragment
Fragment节点类型:
const vnode = {type: Fragment,children: [{ type: 'l1', children: '1' },]
}
当渲染器渲染 Fragment 类型的虚拟节点时,由于 Fragment 本 身并不会渲染任何内容,所以渲染器只会渲染 Fragment 的子节点,
如下面的代码所示:
function patch(n1, n2, container) {if(!n1 && n1.type !== n2.type) {unmount(n1)n1 = null}const { type } = n2if (typeof type === 'string') {} else if (typeof type === Text) {} else if (typeof type === Fragment) {// 处理 Fragment 类型的 vnode// 如果旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可n2.children.forEach(c => patch(null, c, container))} else {// 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可patchChildren(n1, n2, container)}
}
Fragment 本身并不渲染任何内容,所以只需要处理它的子节点即可。
Fragment类型的虚拟节点卸载,如下面的unmount函数:
function unmount(vnode) {
+ if(vnode.type === Fragment) {
+ vnode.children.forEach((c) => unmount(c) )
+ }const parent = vnode.el.parentNodeparent && parent.removeChild(vnode.el)
}
代码:
// 创建render渲染器
const renderer = createRenderer({createElement(tag) {// console.log(`创建元素 ${tag}`)return document.createElement(tag)},setElementText(el, text) {// console.log(`设置${JSON.stringify(el)}的文本内容:${text}`)el.textContent = text},insert(el, parent, anchor = null) {// console.log(`将${JSON.stringify(el)}添加到:${JSON.stringify(parent)}`)parent.insertBefore(el, anchor)},// 处理vnode.propspatchProps(el, key, prevValue, nextValue) {// 匹配以 on 开头的属性,视其为事件if (/^on/.test(key)) {console.log('key', key)// 根据属性名称得到对应的事件名称,例如 onClick ---> clickconst name = key.slice(2).toLowerCase()// 移除上一次绑定的事件处理函数prevValue && el.removeEventListener(name, prevValue)el.addEventListener(name, nextValue)} else if (key === 'class') {el.className = nextValue || ''} else if (key in el) {const type = typeof el[key]if (type === 'boolean' && nextValue === '') {el[key] = true} else {el[key] = nextValue}} else {el.setAttribute(key, nextValue)}},createText(text) {return document.createTextNode(text)},setText(el, text) {el.nodeValue = text}
})function createRenderer(options) {const { createElement, setElementText, insert, patchProps, createText, setText } = options// 渲染元素function render(vnode, container) {if (vnode) {patch(container._vnode, vnode, container)} else {if (container._vnode) {unmount(container._vnode)// container.innerHTML = ''}}container._vnode = vnode}// 更新function patch(n1, n2, container) {if (n1 && n1.type !== n2.type) {unmount(n1)n1 = null}// 代码运行到这里,证明 n1 和 n2 所描述的内容相同const { type } = n2// 如果 n2.type 的值是字符串类型,则它描述的是普通标签元素if (typeof type === 'string') {if (!n1) {mountElement(n2, container)} else {// 更新子节点patchElement(n1, n2)}} else if (typeof type === 'object') {// 组件} else if (typeof type === Text) {if (!n1) {const el = n2.el = createText(n2.children)insert(el, container)} else {const el = n2.el = n1.elif (n2.children !== n1.children) {setText(el, n2.children)}}} else if (typeof type === Fragment) {// 如果旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载if (!n1) {n2.children.forEach(c => patch(null, c, container))} else {// 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可patchChildren(n1, n2, container)}}}// 挂载元素function mountElement(vnode, container) {// 让 vnode.el 引用真实 DOM 元素, 卸载的时候用const el = vnode.el = createElement(vnode.type)// 处理childrenif (typeof vnode.children === 'string') {setElementText(el, vnode.children)} else if (Array.isArray(vnode.children)) {vnode.children.forEach(child => {patch(null, child, el)});}// 处理ppropsif (vnode.props) {for (const key in vnode.props) {// 调用 patchProps 函数即可patchProps(el, key, null, vnode.props[key])}}insert(el, container)}// 更新子节点步骤:// 第一步:更新 props// 第二步:更新 childrenfunction patchElement(n1, n2) {const el = n2.el = n1.elconst oldProps = n1.propsconst newProps = n2.props// 第一步:更新 propsfor (const key in newProps) {if (newProps[key] !== oldProps[key]) {patchProps(el, key, oldProps[ke], newProps[key])}}// 第二步:更新 childrenpatchChildren(n1, n2, el)}// 更新 childrenfunction patchChildren(n1, n2, container) {if (typeof n2.children === 'string') {// 1.旧子节点是一组子节点,先逐个卸载// 2.旧子节点是字符串,直接使用setElementText设置即可// 3.旧子节点无,直接使用setElementText设置即可if (Array.isArray(n1.children)) {n1.children.forEach((c) => unmount(c))}setElementText(container, n2.children)} else if (Array.isArray(n2.children)) {// 1.旧子节点是一组子节点,进行patch操作// 2.旧子节点是字符串,直接使用setElementText设置即可// 3.旧子节点无,直接使用setElementText设置即可if (Array.isArray(n1.children)) {} else {setElementText(container, '')n2.children.forEach((c) => patch(null, c, container))}} else {// 代码运行到这里,说明新子节点不存在// 1.旧子节点是一组子节点,只需逐个卸载即可// 2.旧子节点是文本节点,清空内容即可if (Array.isArray(n1.children)) {n2.children.forEach((c) => unmount(c))} else if (typeof n1.children === 'string') {setElementText(container, '')}}}// 卸载function unmount(vnode) {console.log(vnode)const parent = vnode.el.parentNodeparent && parent.removeChild(vnode.el)}return {render}
}// 测试
const Text = Symbol()
const vnode = {type: 'div',// children: 'hello'props: {id: 'red',onClick: () => {alert('clicked')}},children: [{type: 'p',children: 'hello',props: {class: 'ddd'}},{type: 'Text',children: '我是文本内容'},{type: 'Fragment',children: [{ type: 'li', children: '1', text: '1' },{ type: 'li', children: '2', text: '2' }]}]
}
console.log(vnode)renderer.render(vnode, document.getElementById('app'))