vuejs 设计与实现 - 渲染器 - 挂载与更新

渲染器的核心功能:挂载与更新

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.更新子节点

对于一个元素来说,它的子节点无非有以下三种情况:

    1. 没有子节点,此时 vnode.children 的值为 null。
    1. 具有文本子节点,此时 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'))
相关推荐
hong_zc21 分钟前
初始 html
前端·html
小小吱26 分钟前
HTML动画
前端·html
Bio Coder43 分钟前
学习用 Javascript、HTML、CSS 以及 Node.js 开发一个 uTools 插件,学习计划及其周期
javascript·学习·html·开发·utools
糊涂涂是个小盆友1 小时前
前端 - 使用uniapp+vue搭建前端项目(app端)
前端·vue.js·uni-app
浮华似水1 小时前
Javascirpt时区——脱坑指南
前端
王二端茶倒水1 小时前
大龄程序员兼职跑外卖第五周之亲身感悟
前端·后端·程序员
_oP_i1 小时前
Web 与 Unity 之间的交互
前端·unity·交互
钢铁小狗侠1 小时前
前端(1)——快速入门HTML
前端·html
凹凸曼打不赢小怪兽2 小时前
react 受控组件和非受控组件
前端·javascript·react.js
狂奔solar2 小时前
分享个好玩的,在k8s上部署web版macos
前端·macos·kubernetes