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.rhkb.cn/news/89807.html

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

相关文章

【前端|Javascript第4篇】详解Javascript的事件模型:小白也能轻松搞懂!

前言 在当今数字时代&#xff0c;前端技术正日益成为塑造用户体验的关键。而其中一个不可或缺的核心概念就是JavaScript的事件模型。或许你是刚踏入前端领域的小白&#xff0c;或者是希望深入了解事件模型的开发者&#xff0c;不论你的经验如何&#xff0c;本篇博客都将带你揭开…

聚类与回归

聚类 聚类属于非监督式学习&#xff08;无监督学习&#xff09;&#xff0c;往往不知道因变量。 通过观察学习&#xff0c;将数据分割成多个簇。 回归 回归属于监督式学习&#xff08;有监督学习&#xff09;&#xff0c;知道因变量。 通过有标签样本的学习分类器 聚类和…

SpringCloud中 Sentinel 限流的使用

引入依赖 <dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-sentinel</artifactId> </dependency>手动编写限流规则&#xff0c;缺点是不够灵活&#xff0c;如果需要改变限流规则需要修改源码…

打破传统直播,最新数字化升级3DVR全景直播

导语&#xff1a; 近年来&#xff0c;随着科技的不断创新和发展&#xff0c;传媒领域也正经历着一场前所未有的变革。在这个数字化时代&#xff0c;直播已经不再仅仅是在屏幕上看到一些人的视频&#xff0c;而是将观众带入一个真实世界的全新体验。其中&#xff0c;3DVR全景直…

数据结构:力扣OJ题(每日一练)

题一&#xff1a;有效的括号 给定一个只包括 (&#xff0c;)&#xff0c;{&#xff0c;}&#xff0c;[&#xff0c;] 的字符串 s &#xff0c;判断字符串是否有效。 有效字符串需满足&#xff1a; 左括号必须用相同类型的右括号闭合。左括号必须以正确的顺序闭合。每个右括号…

SpringBoot复习:(31)Controller中返回的对象是如何转换成json字符串给调用者的?

首先&#xff0c;SpringBoot自动装配了HttpMessageConvertersAutoConfiguration这个自动配置类 而这个自动配置类又通过Import注解导入了JacksonHttpMessageConvertersConfiguration类&#xff0c; 在这个类中配置了一个类型为MappingJackson2HttpMessageConverter类型的bean…

技术广度必备——高并发设计之分布式锁的实现方式

文章目录 问题背景前言实现基于MySQL实现唯一索引乐观锁悲观锁 基于Redis基于Zookeeper原理使用Curator框架实现ZK分布式锁缺点 问题背景 研究有哪几种方案可以实现分布式锁&#xff0c;技术选型的场景下能用到。 前言 本文参考过的文章有分布式锁的几种实现方式方式大致分为3种…

IDEA 设置字体大小无效

设置字体大小&#xff0c;一般都是从file>settings>editor>font>Size里设置&#xff0c;一般都有效。 但是&#xff0c;如果是更换了主体&#xff0c;则需要从主体颜色菜单那里这是&#xff0c;你看这个页面&#xff0c;上面黄色三角也提示你了&#xff0c;要去颜色…

风丘科技将亮相 EVM ASIA 2023

风丘科技将首次亮相 EVM ASIA 2023 WINDHILL will debut EVM ASIA 2023 ——可持续移动的未来 —The Future of SUSTAINABLE Mobility EVM ASIA 2023是亚太地区电气化的国际性展会&#xff0c;专注于新能源汽车、充电技术及汽车零件制造等。展会致力于促进包括充电站、交通…

【dnf5文档】新一代RedHat自动化包管理器

前言 HI,CSDN的码友们&#xff0c;距离上一次我发文章已经过去了半年的时间&#xff0c;现在我又来介绍自己新发现和探究的开源技术了。计算机的发展总是飞速的&#xff0c;当我在写这篇文章的时候&#xff0c;Fedora rawhide已经进入了40版本、默认采用的自动化包管理器为dnf…

论文阅读——Adversarial Eigen Attack on Black-Box Models

Adversarial Eigen Attack on Black-Box Models 作者&#xff1a;Linjun Zhou&#xff0c; Linjun Zhou 攻击类别&#xff1a;黑盒&#xff08;基于梯度信息&#xff09;&#xff0c;白盒模型的预训练模型可获得&#xff0c;但训练数据和微调预训练模型的数据不可得&#xff…

SpringBoot Thymeleaf模板引擎

Thymeleaf 模板引擎 前端交给我们的页面&#xff0c;是html页面。如果是我们以前开发&#xff0c;我们需要把他们转成jsp页面&#xff0c;jsp好处就是当我们查出一些数据转发到JSP页面以后&#xff0c;我们可以用jsp轻松实现数据的显示&#xff0c;及交互等。 jsp支持非常强大…

【Linux】高级IO

目录 IO的基本概念 钓鱼五人组 五种IO模型 高级IO重要概念 同步通信 VS 异步通信 阻塞 VS 非阻塞 其他高级IO 阻塞IO 非阻塞IO IO的基本概念 什么是IO&#xff1f; I/O&#xff08;input/output&#xff09;也就是输入和输出&#xff0c;在著名的冯诺依曼体系结构当中…

k8s常用资源管理 控制

目录 Pod&#xff08;容器组&#xff09;&#xff1a;Pod是Kubernetes中最小的部署单元&#xff0c;可以包含一个或多个容器。Pod提供了一种逻辑上的封装&#xff0c;使得容器可以一起共享网络和存储资源 1、创建一个pod 2、pod管理 pod操作 目录 创建Pod会很慢 Pod&…

MySQL表的增删查改

目录 一&#xff0c;新增 二&#xff0c;查询 2.1 全列查询 2.2 指定列查询 2.3 查询字段为表达式 2.4 别名 - as 2.5 去重 - distinct 2.6 排序 - order by 2.7 条件查询 - where 2.8 分页查询 - limit 三&#xff0c;修改 - update 四&#xff0c;删除 - delete 一…

考公-判断推理-定义判断

第九节课 例题 例题 例题 例题 例题 例题 脚一滑&#xff0c;就是工伤&#xff0c;这难道不是操作不当吗 例题 不要较真&#xff0c;公务员&#xff0c;把没有全局观念的人排除在公务员队伍之外 例题 例题 下次看到不字&#xff0c;先给我画上 例题 例题 例题 例题…

管理类联考——逻辑——论证逻辑——汇总篇——因果推理

因果推理的逻辑方法&#xff08;穆勒五法) 确定现象之间因果关系的方法有五种&#xff1a; 求同法、求异法、求同求异并用法、共变法、剩余法。这五种方法统称为穆勒五法。用穆勒五法确定的因果关系具有或然性。 PS&#xff1a;求同球童&#xff1b;求异球衣&#xff0c;求同…

图解结构体大小和位域例子

struct A {short a; char b; int c : 1; char d : 4; short e : 7; }; 备注&#xff1a;蓝色&#xff1a;表示占一个符号位空间红色&#xff1a;表示补齐其他颜色&#xff1a;实际最大值所占空间 &#xff08;1&#xff09;图解例1 st…

opencv实战项目 手势识别-手势音量控制(opencv)

本项目是使用了谷歌开源的框架mediapipe&#xff0c;里面有非常多的模型提供给我们使用&#xff0c;例如面部检测&#xff0c;身体检测&#xff0c;手部检测等。 手势识别系列文章 1.opencv实现手部追踪&#xff08;定位手部关键点&#xff09; 2.opencv实战项目 实现手势跟踪…

答疑:Arduino IDE配置其他开发板下载速度慢

基于案例&#xff1a;Linux环境Arduino IDE中配置ATOM S3 通常&#xff0c;网络问题较多&#xff0c;可以使用一些技巧。 https://m5stack.oss-cn-shenzhen.aliyuncs.com/resource/arduino/package_m5stack_index.json 没有配置&#xff0c;不支持M5Stack&#xff08;ESP32&…