渲染器的核心功能:挂载与更新
1.挂载子节点和元素的属性
1.2挂载子节点 (vnode.children)
vnode.children可以是字符串类型的,也可以是数组类型的,如下:
javascript
const vnode ={
type: 'div',
children: [
{
type: 'p',
children: 'hello'
}
]
}
可以看到,vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM 树。
为了完成子节点的渲染,我们需要修改 mountElement 函数
,如下面的代码所示:
javascript
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 字段
,如下面的代码所示:
javascript
const vnode ={
type: 'div',
// 使用 props 描述一个元素的属性
props: {
id: 'foo'
},
children: [
{
type: 'p',
children: 'hello'
}
]
}
vnode.props 是一个对象,它的键代表元素的属性名称,它的值 代表对应属性的值。这样,我们就可以通过遍历 props 对象的方式, 把这些属性渲染到对应的元素上,如下面的代码所示:
javascript
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
如何区分:
javascript
key in el 返回值为true则是:DOM Properties,返回false则是HTML Attributes
如何争取的设置到元素上:
- DOM Properties
javascript
el[key] = value
- HTML Attributes
javascript
el.setAttribute(key, value)
3.正确的设置元素的属性
思路:
-
1.我们知道元素的属性分为2种,而且这2种的设置方式不一样。因此,我们要特殊处理。
-
2.处理特殊情况:例如button按钮,它的vnode节点如下:
javascript
const button = {
type: 'button',
props: {
disabled: ''
}
}
但是在解析的时候,会出现问题,用户的本意是"不禁用"按钮,但如果渲染器仍然使用 setAttribute 函数设置属性值,则会产生意外的效果,即按钮被禁 用了.
那么应该怎么办呢?一个很自然的思路是,我们可以优先设置 DOM Properties,例如:
javascript
el.disabled = false
- 3.处理特殊情况2: form表单的一些只读属性:
javascript
<form id="form1"></form>
<input form="form1" />
在这段代码中,我们为 标签设置了 form 属性 (HTML Attributes)。它对应的 DOM Properties 是 el.form,但 el.form 是只读的,因此我们只能够通过 setAttribute 函数来设 置它。
javascript
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 Attributes
el.setAttribute(key, vnode.props[key])
}
}
}
}
4.class的处理
class有三种不同的vnode表示方式,
- 方式1:
javascript
const vnode = {
type: 'p',
props: {
class: { foor: true, bar: false }
}
}
- 方式2
javascript
const vnode = {
type: 'p',
props: {
class: 'foo bar'
}
}
- 方式3
javascript
const vnode = {
type: 'p',
props: {
class: [ 'foo bar', { bar: true }]
}
}
因此我们需要一个normalizeClass函数来将不同类型的class值正常化为字符串。
javascript
const vnode = {
type: 'p',
props: {
class: normalizeClass([ 'foo bar', { baz: true }])
}
}
处理之后:
javascript
const vnode = {
type: 'p',
props: {
class: 'foo bar baz'
}
}
处理之后,设置class的方式也有三种1.className, 2.setAttribute, 3.classList 但是这三种设置的方式不同,性能也是不一样,经过调查发现className的性能是最优的,因此我们使用className设置元素的class。
javascript
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 Attributes
el.setAttribute(key, vnode.props[key])
}
}
}
}
简化上面的操作,我们可以把处理元素属性的逻辑放在一个函数(patchProps)里面:
javascript
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 Attributes
el.setAttribute(key, vnode.props[key])
}
}
在mountElement函数中调用
javascript
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 函数中,以便后续代码可以复用它,如下面的代码所示:
javascript
// 卸载
unmount(vnode) {
// 获取 el 的父元素
const parent = vnode.el.parentNode
// 调用 removeChild 移除元素
parent && parent.removeChild(vnode.el)
}
简化render函数
javascript
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 函数的代码以满足需求:
javascript
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.了解事件的处理
javascript
const vnode = {
type: 'p',
props: {
// 使用 onXxx 描述事件
onClick: () => {
alert('clicked')
}
},
children: 'text'
}
javascript
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 的值即可:
javascript
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._vei
const 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.value
invoker.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 事件:
javascript
const vnode = {
type: 'p',
props: {
onClick: () => {},
onContextmenu:() => {}
},
children: 'text'
}
当渲染器尝试渲染这上面代码中给出的 vnode 时,会先绑定 click 事件,然后再绑定 contextmenu 事件。后绑定的 contextmenu 事件的处理函数将覆盖先绑定的 click 事件的处理函 数。为了解决事件覆盖的问题,我们需要重新设计 el._vei 的数据结 构。我们应该将 el._vei 设计为一个对象,它的键是事件名称,它的 值则是对应的事件处理函数,这样就不会发生事件覆盖的现象了
:
javascript
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.value
invoker.value = nextValue
// 绑定 invoker 作为事件处理函数
el.addEventListener(name, invoker)
} else {
// 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
invoker.value = nextValue
}
} else if (invoker) {
// 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
el.removeEventListener(name, invoker)
}
}
}
另外,一个元素不仅可以绑定多种类型的事件,对于同一类型的 事件而言,还可以绑定多个事件处理函数
。我们知道,在原生 DOM 编 程中,当多次调用 addEventListener 函数为元素绑定同一类型的 事件时,多个事件处理函数可以共存,例如:
javascript
el.addEventListener('click', fn1)
el.addEventListener('click', fn2)
当点击元素时,事件处理函数 fn1 和 fn2 都会执行。因此,为了 描述同一个事件的多个事件处理函数,我们需要调整 vnode.props 对象中事件的数据结构:
javascript
const vnode = {
type: 'p',
props: {
onClick: [() => {}, () => {}]
},
children: 'text'
}
我们使用一个数组来描述事件,数组中的每 个元素都是一个独立的事件处理函数,并且这些事件处理函数都能够 正确地绑定到对应元素上。为了实现此功能,我们需要修改 patchProps 函数中事件处理相关的代码:
javascript
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 = {})
//根据事件名称获取 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 是数组,则遍历它并逐个调用事件处理函数
+ if (Array.isArray(invoker.value)) {
+ invoker.value.forEach((fn) => fn(e))
+ } else {
// 否则直接作为函数调用
+ invoker.value(e)
}
}
// 将真正的事件处理函数赋值给 invoker.value
invoker.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.其他情况,无论是单个元素子节点,还是多个子节点(可能是文 本和元素的混合),都可以用数组来表示。
如下面的代码所示:
javascript
// 没有子节点
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 函数的代码所
示:
javascript
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:
javascript
const Text = Symbol()
const newVnode = {
type: Text,
children: '我是文本内容'
}
const Comment = Symbol()
const newVnode = {
type: Comment,
children: '我是注释内容'
}
由于文本节点和注释节点只关心文本内 容,所以我们用 vnode.children 来存储它们对应的文本内容。
有了用于描述文本节点和注释节点的 vnode 对象后,我们就可以 使用渲染器来渲染它们了:
javascript
function patch(n1, n2, container) {
if(!n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (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.type
if(n2.children !== n1.children) {
setText(el, n2.children)
}
}
} else if (typeof type === Fragment) {
} else {
}
}
javascript
const redner = createRenderer({
createElement() {},
setElementText() {},
insert() {},
patchProps() {},
// 创建文本节点
createText(text) {
return document.createTextNode(text)
},
// 设置文本节点的内容
setText(el, text) {
el.nodeValue = text
}
})
注释节点的处理方式与文本节点的处理方式类似。不同的是,我 们需要使用 document.createComment 函数创建注释节点元素。
11.Fragment
Fragment节点类型:
javascript
const vnode = {
type: Fragment,
children: [
{ type: 'l1', children: '1' },
]
}
当渲染器渲染 Fragment 类型的虚拟节点时,由于 Fragment 本 身并不会渲染任何内容,所以渲染器只会渲染 Fragment 的子节点,
如下面的代码所示:
javascript
function patch(n1, n2, container) {
if(!n1 && n1.type !== n2.type) {
unmount(n1)
n1 = null
}
const { type } = n2
if (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函数:
javascript
function unmount(vnode) {
+ if(vnode.type === Fragment) {
+ vnode.children.forEach((c) => unmount(c) )
+ }
const parent = vnode.el.parentNode
parent && parent.removeChild(vnode.el)
}
代码:
javascript
// 创建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.props
patchProps(el, key, prevValue, nextValue) {
// 匹配以 on 开头的属性,视其为事件
if (/^on/.test(key)) {
console.log('key', key)
// 根据属性名称得到对应的事件名称,例如 onClick ---> click
const 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.el
if (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)
// 处理children
if (typeof vnode.children === 'string') {
setElementText(el, vnode.children)
} else if (Array.isArray(vnode.children)) {
vnode.children.forEach(child => {
patch(null, child, el)
});
}
// 处理pprops
if (vnode.props) {
for (const key in vnode.props) {
// 调用 patchProps 函数即可
patchProps(el, key, null, vnode.props[key])
}
}
insert(el, container)
}
// 更新子节点步骤:
// 第一步:更新 props
// 第二步:更新 children
function patchElement(n1, n2) {
const el = n2.el = n1.el
const oldProps = n1.props
const newProps = n2.props
// 第一步:更新 props
for (const key in newProps) {
if (newProps[key] !== oldProps[key]) {
patchProps(el, key, oldProps[ke], newProps[key])
}
}
// 第二步:更新 children
patchChildren(n1, n2, el)
}
// 更新 children
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, '')
}
}
}
// 卸载
function unmount(vnode) {
console.log(vnode)
const parent = vnode.el.parentNode
parent && 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'))