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

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

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

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

    1. 没有子节点,此时 vnode.children 的值为 null。
    1. 具有文本子节点,此时 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'))

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/34132.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

预测知识 | 神经网络、机器学习、深度学习

预测知识 | 预测技术流程及模型评价 目录 预测知识 | 预测技术流程及模型评价神经网络机器学习深度学习参考资料 神经网络 神经网络&#xff08;neural network&#xff09;是机器学习的一个重要分支&#xff0c;也是深度学习的核心算法。神经网络的名字和结构&#xff0c;源自…

vue-拦截器

第一步 起步 | Axios 中文文档 | Axios 中文网 安装 npm install axios ​ ​​​​​​ ​ ​ 第二步 ​ ​ 所有的请求都叫http协议 ​ ​ ​ ​ ​ 第三步 ​ 导入后即可使用里面的方法 ​ 任何一个东西都可以导出 ​ ​ 只有一个的时候只需要用defau…

PHP证券交易员学习网站mysql数据库web结构apache计算机软件工程网页wamp

一、源码特点 PHP证券交易员学习网站 是一套完善的web设计系统&#xff0c;对理解php编程开发语言有帮助&#xff0c;系统具有完整的源代码和数据库&#xff0c;系统主要采用B/S模式开发。 下载地址https://download.csdn.net/download/qq_41221322/88205549 PHP证券交易员…

神码ai伪原创【php源码】

这篇文章主要介绍了python下载后在哪打开&#xff0c;具有一定借鉴价值&#xff0c;需要的朋友可以参考下。希望大家阅读完这篇文章后大有收获&#xff0c;下面让小编带着大家一起了解一下。 火车头采集ai伪原创插件截图&#xff1a; https://www.python.org/ftp/python/3.9.5…

Hadoop理论及实践-HDFS四大组件关系(参考Hadoop官网)

NameNode&#xff08;名称节点&#xff0c;Master主节点&#xff09; NameNode主要功能 1、NameNode负责管理HDFS文件系统的元数据&#xff0c;包括文件&#xff0c;目录&#xff0c;块信息等。它将元数据Fsimage与Edit_log持久化到硬盘上。一个是Fsimage(镜像文件&#xff09…

Web 服务器 -【Tomcat】的简单学习

Tomcat1 简介1.1 什么是Web服务器 2 基本使用2.1 下载2.2 安装2.3 卸载2.4 启动2.5 关闭2.6 配置2.7 部署 3 Maven创建Web项目3.1 Web项目结构3.2 创建Maven Web项目 4 IDEA使用Tomcat4.1 集成本地Tomcat4.2 Tomcat Maven插件 Tomcat 1 简介 1.1 什么是Web服务器 Web服务器是…

父进程等待子进程退出 / 僵尸进程孤儿进程

Q&#xff1a;父进程为什么要等待子进程退出&#xff1f; A&#xff1a;回顾创建子进程的目的&#xff0c;就是让子进程去处理一些事情&#xff0c;那么“事情干完了没有”这件事&#xff0c;父进程需要知道并收集子进程的退出状态。子进程的退出状态如果不被收集&#xff0c;…

30、Flink SQL之SQL 客户端(通过kafka和filesystem的例子介绍了配置文件使用-表、视图等)

Flink 系列文章 1、Flink 部署、概念介绍、source、transformation、sink使用示例、四大基石介绍和示例等系列综合文章链接 13、Flink 的table api与sql的基本概念、通用api介绍及入门示例 14、Flink 的table api与sql之数据类型: 内置数据类型以及它们的属性 15、Flink 的ta…

iphone拷贝照片中间带E自动去重软件,以及java程序如何打包成jar和exe

文章目录 一、前提二、问题描述三、原始处理方式四、程序处理4.1 java程序如何打包exe4.1.1 首先打包jar4.1.2 开始生成exe4.1.3 软件使用方式 4.2 更换图标4.2.1 更换swing的打包jar图标4.2.2 更换exe图标 4.3 如何使生成的exe在没有java环境的电脑上运行4.3.1 Inno Setup打包…

el-select 动态添加多个下拉框

实现的效果如下: 主要的代码如下: 这是formdata 的结构 主要的逻辑 在这个 methods

Linux网络协议和管理

Linux网络协议和管理 一.网络设备基本知识 图1-网络设备基本知识 二.TCP/IP协议栈简介 1.概述 网络协议通常工作在不同的层中&#xff0c;每一层分别负责不同的通信功能。一个协议族&#xff0c; 比如T C P / I P&#xff0c;是一组不同层次上的多个协议的组合。T C P / I P通…

UVA-1601 万圣节后的早晨 题解答案代码 算法竞赛入门经典第二版

GitHub - jzplp/aoapc-UVA-Answer: 算法竞赛入门经典 例题和习题答案 刘汝佳 第二版 以三个点的当前位置作为状态&#xff0c;广度优先遍历&#xff0c;找到终点即为最短次数。 注意&#xff1a; 一次可以移动多个点&#xff0c;但是每个点只能移动一步。在同一次中&#xf…

工单管理系统有什么优点?工单系统是如何提高企业服务质量和运营效率的?

工单管理系统是一款基于云平台打造的高效报修工单管理系统&#xff0c;为企业报修管理、维保流程优化和后勤决策分析提供全面支持。通过应用工单管理系统&#xff0c;企业能够轻松提升报修效率&#xff0c;降低人工成本&#xff0c;同时提高后勤管理的质量和效益。系统利用先进…

快速上手React:从概述到组件与事件处理

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

Java进阶(1)——JVM的内存分配 反射Class类的类对象 创建对象的几种方式 类加载(何时进入内存JVM) 注解 反射+注解的案例

目录 引出java内存分配java内存分布概略图堆方法区常量池 创建对象内存分配 反射class文件的底层类加载顺序1.检查2.开辟静态资源空间3.常量池4.其他...5.创建一个唯一的类的对象获取Class对象的几种方式 创建对象几种方式new 看到new : new Book()反射 Class.forName(“包名.类…

逆向破解学习-割绳子

试玩 支付失败&#xff0c;请检查网络设置 Hook成功 Hook代码 import android.app.Application; import android.content.Context;import de.robv.android.xposed.XC_MethodHook; import de.robv.android.xposed.XposedHelpers; import de.robv.android.xposed.callbacks.XC_…

vue2 封装 webSocket 开箱即用

第一步&#xff1a; 下载 webSocket npm install vue-native-websocket --save 第二步&#xff1a; 需要在 main.js 中 引入 import websocket from vue-native-websocket; Vue.use(websocket, , {connectManually: true, // 手动连接format: json, // json格式reconnection:…

SpringMVC的架构有什么优势?——表单和数据校验(四)

前言 「作者主页」&#xff1a;雪碧有白泡泡 「个人网站」&#xff1a;雪碧的个人网站 「推荐专栏」&#xff1a; ★java一站式服务 ★ ★ React从入门到精通★ ★前端炫酷代码分享 ★ ★ 从0到英雄&#xff0c;vue成神之路★ ★ uniapp-从构建到提升★ ★ 从0到英雄&#xff…

深度学习(37)—— 图神经网络GNN(2)

深度学习&#xff08;37&#xff09;—— 图神经网络GNN&#xff08;2&#xff09; 这一期主要是一些简单示例&#xff0c;针对不同的情况&#xff0c;使用的数据都是torch_geometric的内置数据集 文章目录 深度学习&#xff08;37&#xff09;—— 图神经网络GNN&#xff08…

list模拟实现【引入反向迭代器】

文章目录 1.适配器1.1传统意义上的适配器1.2语言里的适配器1.3理解 2.list模拟实现【注意看反向迭代器】2.1 list_frame.h2.2riterator.h2.3list.h2.4 vector.h2.5test.cpp 3.反向迭代器的应用1.使用要求2.迭代器的分类 1.适配器 1.1传统意义上的适配器 1.2语言里的适配器 容…