翼度科技»论坛 编程开发 JavaScript 查看内容

Vue渲染器如何对节点进行挂载和更新

9

主题

9

帖子

27

积分

新手上路

Rank: 1

积分
27
一、子节点和元素的属性

先前我们讨论了一个简单的渲染器是如何实现的 一文详解Vue中渲染器的简单实现_vue.js_脚本之家 (jb51.net) 但是实际上还有一些问题需要完善:

  • 子节点不一定只是一个文本节点,实际上其可能会是多个不同的节点。
  • 我们并没有对被挂载的元素的属性进行处理。

1.如何处理子节点为多个节点的情况:

处理vnode的数据结构以正确描述多个节点的情况
我们可以将
  1. vnode
复制代码
  1. children
复制代码
定义为一个数组,数组的每一项也是一个
  1. vnode
复制代码
,这样就可以去正确的描述其结构。
  1. const vnode = {
  2.   type: 'div',
  3.   children: [
  4.     {
  5.       type: 'p',
  6.       children: 'hello'
  7.     }
  8.   ]
  9. }
复制代码
如上代码所示其描述的子节点为一个
  1. <p>hello</p>
复制代码
,在数组中也可继续去添加别的不同类型的
  1. vnode
复制代码
,这样便形成了一种树形结构
  1. 虚拟DOM树
复制代码
,可以更好的去描述真实DOM的情况。


调整mountElement去正确的挂载修改后的vnode
  1.   function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     } else if (Array.isArray(vnode.children)) {
  6.       vnode.children.forEach(child => {
  7.         patch(null, child, el)
  8.       })
  9.     }
  10.    
  11.     insert(el, container)
  12.   }
复制代码
我们给
  1. vnode.children
复制代码
的类型做了一个判断,当其为数组类型时表示子节点有多个,通过遍历调用
  1. patch
复制代码
来进行挂载

2.如何处理被挂载元素的属性:

####如何修改vnode去描述元素属性:
  1. vnode
复制代码
添加
  1. props
复制代码
字段,其类型是一个对象,对象的键是属性名,值是属性值,
  1. const vnode = {
  2.   type: 'div',
  3.   props: {
  4.     id: 'foo'
  5.   },
  6.   children: [
  7.     {
  8.       type: 'p',
  9.       children: 'hello'
  10.     }
  11.   ]
  12. }
复制代码
调整mountElement去正确的挂载修改后的vnode
  1.   function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     } else if (Array.isArray(vnode.children)) {
  6.       vnode.children.forEach(child => {
  7.         patch(null, child, el)
  8.       })
  9.     }

  10.     if (vnode.props) {
  11.       for (const key in vnode.props) {
  12.         el.setAttribute(key, vnode.props[key])
  13.       }
  14.     }

  15.     insert(el, container)
  16.   }
复制代码
增加一个对
  1. props
复制代码
的判断,对其进行遍历,获取到
  1. props
复制代码
对象的键和值,并使用
  1. setAttribute
复制代码
函数将属性应用到
  1. el
复制代码
上。 除此之外还可以直接在DOM对象上直接进行元素属性的设置:
  1. if (vnode.props) {
  2.     for (const key in vnode.props) {
  3.         // 直接设置
  4.         el[key] = vnode.props[key]
  5.     }
  6. }
复制代码
以上两种设置方法都有一定的局限性,所以我们需要在不同情况下灵活进行使用,接下来我们将讨论其区别,从而明确其使用时机。

二、HTML Attributes 与 DOM Properties 的区别

假设有一个如下元素:
  1. <input id="my-input" type="text" value="foo" />
复制代码
对于此元素而言:

  • HTML Attributes是:
    1. id="my-input"
    复制代码
    1. type="text"
    复制代码
    1. value="foo"
    复制代码

  • DOM Properties是:浏览器解析元素的HTML后生成的一个DOM对象,假设以上元素对应的DOM对象为
    1. el
    复制代码
    ,则对应DOM Properties分别是
    1. el.id
    复制代码
    1. el.type
    复制代码
    ,
    1. el.value
    复制代码
    .

区别

二者名称不一定相同,比如
  1. <div class="foo"></div>
复制代码
对于上面的元素:
  1. class="foo"
复制代码
对应的 DOM Properties 是 el.className
二者也不是一一对应的, 有些HTML Attributes没有对应的DOM Properties反之亦然 关键的一点在于:
HTML Attributes 的作用是设置与之对应的 DOM Pr operties 的初始值 对于input标签的value属性而言,如果没有修改input值得情况下,
  1. el.value
复制代码
读取得到值是foo,但是当文本框被输入之后,此时再使用
  1. el.value
复制代码
去获取值时得到得值就是新输入得值,但是使用
  1. el.getAttribute('value')
复制代码
得到得值仍是
  1. foo
复制代码
,即HTML Attributes存储的是元素的初始值

三、完善元素属性的设置

当元素在正常的HTML文件中时,浏览器会自动分析 HTML Attributes 并设置对应的 DOM Properties,但是在Vue中,模板有时并不会被解析并设置对应关系。

1.对于属性值为布尔类型节点的处理

有如下元素:
  1. <button disabled>Button</button>
复制代码
在HTML中其会被解析的结果是button有一个
  1. disabled
复制代码
的HTML Attributes,对应的DOM Properties
  1. (el.disabled)
复制代码
的值设为true,按钮为禁止状态。 在Vue中该HTML对应如下vnode节点:
  1. const button = {
  2.     type: 'button',
  3.     props: {
  4.     disabled: ''
  5.     }
  6. }
复制代码
在渲染器中调用setAttribute设置disabled HTML Attributes时会起作用,按钮会被禁用
  1. el.setAttribute('disabled', '')
复制代码
但在vue的模板中会存在属性是变量的情况,如下
  1. <button :disabled="false">Button</button>
复制代码
此时渲染器渲染时使用的vnode是
  1. const button = {
  2.     type: 'button',
  3.     props: {
  4.         disabled: false
  5.     }
  6. }
复制代码
此时调用setAttribute设置disabled
  1. el.setAttribute('disabled', false)
复制代码
由于通过setAttribute设置的属性会字符串化即变成如下情况
  1. el.setAttribute('disabled', 'false')
复制代码
由于
  1. el.disable
复制代码
为布尔类型的值,当设置为
  1. 'false'
复制代码
时,其实就是
  1. true
复制代码
,即禁用按钮,这显然不符合期望。 我们可以通过DOM Properties设置即
  1. el.disabled = false
复制代码
。 通过DOM Properties设置可以解决当前的问题,但是如果属性值对于一开始的情况
  1. <button disabled>Button</button>
复制代码
又会存在问题,对于vnode
  1. const button = {
  2.     type: 'button',
  3.     props: {
  4.     disabled: ''
  5.     }
  6. }
复制代码
使用DOM Properties设置
  1. el.disabled = ''
复制代码
由于
  1. el.disable
复制代码
为布尔类型的值,当设置为
  1. ''
复制代码
时,其实就是
  1. false
复制代码
,即不禁用按钮,这也不符合期望。
很显然我们在对元素属性进行设置时需要对特殊的情况进行处理,而不是单一的使用setAttribute设置HTML Attributes或者设置DOM Properties,从而正确设置属性: 具体的解决方法是: 优先设置元素的 DOM Properties,但当值为空字符串时,要手动将值矫正为 true 因此对mountElement函数做优化
  1.   function mountElement(vnode, container) {
  2.     const el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     } else if (Array.isArray(vnode.children)) {
  6.       vnode.children.forEach(child => {
  7.         patch(null, child, el)
  8.       })
  9.     }

  10.     if (vnode.props) {
  11.       for (const key in vnode.props) {
  12.        if (key in el) {
  13.        // 获取该 DOM Properties 的类型
  14.        const type = typeof el[key]
  15.        const value = vnode.props[key]
  16.        // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
  17.        if (type === 'boolean' && value === '') {
  18.                el[key] = true
  19.            } else {
  20.                el[key] = value
  21.            }
  22.            } else {
  23.            // 如果要设置的属性没有对应的 DOM Properties,则使用 setAttribute 函数设置属性
  24.                el.setAttribute(key, vnode.props[key])
  25.            }
  26.       }
  27.     }

  28.     insert(el, container)
  29.   }
复制代码
在设置
  1. vnode
复制代码
的props时,首先确认是否存在DOM Properties,存在则优先使用,而当遇到属性值为空字符串时,将值变为true,若DOM Properties不存在使用setAttribute设置。

2.只读DOM Properties处理

有一些元素的DOM 是只读的,比如
  1. <form id="form1"></form>
  2. <input form="form1" />
复制代码
  1. input
复制代码
  1. el.form
复制代码
属性是只读的,此时我们只能使用setAttribute去设置它,需要对
  1. mountElement
复制代码
再次完善,增加一个
  1. shouldSetAsProps
复制代码
函数用于判断属性是否可以使用DOM Properties来设置否则使用setAttribute
  1.   function shouldSetAsProps(el, key, value) {
  2.     if (key === 'form' && el.tagName === 'INPUT') return false
  3.     return key in el
  4.   }
  5.   
  6.    function mountElement(vnode, container) {
  7.     const el = createElement(vnode.type)
  8.     if (typeof vnode.children === 'string') {
  9.       setElementText(el, vnode.children)
  10.     } else if (Array.isArray(vnode.children)) {
  11.       vnode.children.forEach(child => {
  12.         patch(null, child, el)
  13.       })
  14.     }

  15.     if (vnode.props) {
  16.       for (const key in vnode.props) {
  17.         const value = vnode.props[key]
  18.         if (shouldSetAsProps(el, key, value)) {
  19.           const type = typeof el[key]
  20.           if (type === 'boolean' && value === '') {
  21.             el[key] = true
  22.           } else {
  23.             el[key] = value
  24.           }
  25.         } else {
  26.           el.setAttribute(key, vnode.props[key])
  27.         }
  28.       }
  29.     }

  30.     insert(el, container)
  31.   }
复制代码
实际上类似
  1. form
复制代码
属性的情况很多,在类似的情况下也需要使用和处理form属性相似的逻辑进行优化

3.将渲染器处理为与平台无关

同样的为了不把渲染器限定在浏览器平台,需要将设置属性的逻辑也作为配置项处理
  1. const renderer = createRenderer({
  2.   createElement(tag) {
  3.     return document.createElement(tag)
  4.   },
  5.   setElementText(el, text) {
  6.     el.textContent = text
  7.   },
  8.   insert(el, parent, anchor = null) {
  9.     parent.insertBefore(el, anchor)
  10.   },
  11.   patchProps(el, key, preValue, nextValue) {
  12.     if (shouldSetAsProps(el, key, nextValue)) {
  13.       const type = typeof el[key]
  14.       if (type === 'boolean' && nextValue === '') {
  15.         el[key] = true
  16.       } else {
  17.         el[key] = nextValue
  18.       }
  19.     } else {
  20.       el.setAttribute(key, nextValue)
  21.     }
  22.   }
  23. })

  24. ...

  25. function mountElement(vnode, container) {
  26.     const el = createElement(vnode.type)
  27.     if (typeof vnode.children === 'string') {
  28.       setElementText(el, vnode.children)
  29.     } else if (Array.isArray(vnode.children)) {
  30.       vnode.children.forEach(child => {
  31.         patch(null, child, el)
  32.       })
  33.     }

  34.     if (vnode.props) {
  35.       for (const key in vnode.props) {
  36.         patchProps(el, key, null, vnode.props[key])
  37.       }
  38.     }

  39.     insert(el, container)
  40.   }

  41.   function patch(n1, n2, container) {
  42.     if (!n1) {
  43.       mountElement(n2, container)
  44.     } else {
  45.       //
  46.     }
  47.   }
复制代码
我们将
  1. patchProps
复制代码
函数作为配置项传入,并在
  1. mountElement
复制代码
中处理
  1. vnode.props
复制代码
时使用,这样就可以将逻辑抽离出去。

四、处理class

Vue中对class做了处理,有多种方式可以设置class

1.字符串
  1. <p class="foo bar"></p>
复制代码
2.对象
  1. <p :class="{ foo: true, bar: false }"></p>
复制代码
3.数组:可以组合以上两种类型
  1. <p :class="[ 'foo bar', {  baz: true  }  ]"></p>
复制代码
  1. class
复制代码
为字符串时,直接使用
  1. el.className
复制代码
进行设置即可,但是其余两种情况需要处理,在Vue中其使用
  1. normalizeClass
复制代码
去处理,主要的逻辑就是遍历数组和对象,然后使用+=逐步将数组中的class项和对象中值为true的项的键累加,变为字符串并返回。
  1. function normalizeClass(value) {
  2.   let res = ''
  3.   if (isString(value)) {
  4.     res = value
  5.   } else if (isArray(value)) {
  6.     for (let i = 0; i < value.length; i++) {
  7.       const normalized = normalizeClass(value[i])
  8.       if (normalized) {
  9.         res += normalized + ' '
  10.       }
  11.     }
  12.   } else if (isObject(value)) {
  13.     for (const name in value) {
  14.       if (value[name]) {
  15.         res += name + ' '
  16.       }
  17.     }
  18.   }
  19.   return res.trim()
  20. }
复制代码
五、节点的卸载:

在之前实现的渲染器中,卸载是直接使用innerHTML将容器的内容清空,这可以达到效果,但是却不太完善,因为在实际情况下:

  • 如果容器的内容由组件渲染的,则当其被卸载时需要触发组件的beforeUnmount等钩子函数。
  • 如果元素存在自定义指令,自定义指令中同时存在卸载时需要触发的钩子函数。
  • 直接使用innerHTML将容器的内容清空,元素上的事件不会被清空
    为了解决以上问题,我们使用如下方式去卸载节点
根据 vnode 对象获取与其相关联的真实 DOM 元素,然后使用原生 DOM 操作方法将该DOM 元素移除。
  1.   function mountElement(vnode, container) {
  2.     const el = vnode.el = createElement(vnode.type)
  3.     if (typeof vnode.children === 'string') {
  4.       setElementText(el, vnode.children)
  5.     } else if (Array.isArray(vnode.children)) {
  6.       vnode.children.forEach(child => {
  7.         patch(null, child, el)
  8.       })
  9.     }

  10.     if (vnode.props) {
  11.       for (const key in vnode.props) {
  12.         patchProps(el, key, null, vnode.props[key])
  13.       }
  14.     }

  15.     insert(el, container)
  16.   }
复制代码
调整
  1. mountElement
复制代码
,在创建真实DOM元素的时候,将创建的元素赋值给
  1. vnode.el
复制代码
,这样就能通过
  1. vnode.el
复制代码
取得并操作真实DOM。当需要卸载时首先使用
  1. vnode.el.parentNode
复制代码
拿到vnode对应的真实DOM,然后再使用
  1. removeChild
复制代码
移除(
  1. vnode.el
复制代码
):
  1.   function render(vnode, container) {
  2.     if (vnode) {
  3.       patch(container._vnode, vnode, container)
  4.     } else {
  5.       if (container._vnode) {
  6.         const parent = vnode.el.parentNode
  7.         if (parent) {
  8.           parent.removeChild(vnode.el)
  9.         }
  10.       }
  11.     }
  12.     container._vnode = vnode
  13.   }
复制代码
为方便复用以及后续对组件的生命周期钩子和自定义指令钩子的调用,我们将卸载的逻辑封装在
  1. unmount
复制代码
函数中。
  1.      function unmount(vnode) {
  2.         const parent = vnode.el.parentNode
  3.         if (parent) {
  4.           parent.removeChild(vnode.el)
  5.         }
  6.       }
  7.   
  8.     function render(vnode, container) {
  9.     if (vnode) {
  10.       patch(container._vnode, vnode, container)
  11.     } else {
  12.       if (container._vnode) {
  13.         unmount(container._vnode)
  14.       }
  15.       
  16.     }
  17.     container._vnode = vnode
  18.   }
  19.   
复制代码
六、对于patch函数的优化


1.新旧节点不一样时是否一定要使用patch打补丁呢?

在之前实现的渲染器中,我们使用patch对于节点处理逻辑如下:
  1. function patch(n1, n2, container) {
  2.     if (!n1) {
  3.         mountElement(n2, container)
  4.     } else {
  5.         // 更新
  6.     }
  7. }
复制代码
如果新旧节点均存在则意味着需要打补丁去更新其中的内容。但是考虑一种情况,当新旧节点的类型不同时,打补丁是没有意义的,因为类型的变化会导致节点属性的不同,比如vnode的类型(type)从
  1. 'p'
复制代码
变为
  1. 'input'
复制代码
,在这种情况下我们应该做的是卸载旧的vnode,然后挂载新的vnode。
  1.   function patch(n1, n2, container) {
  2.     if (n1 && n1.type !== n2.type) {
  3.       unmount(n1)
  4.       n1 = null
  5.     }

  6.     if (!n1) {
  7.       mountElement(n2, container)
  8.     } else {
  9.       patchElement(n1, n2)
  10.     }
  11.   }
复制代码
通过如上处理在patch中我们先去若旧节点存在并且新旧节点类型不同则调用
  1. unmount
复制代码
卸载旧节点,并将其值置为
  1. null
复制代码
,以便后续去判断是要执行挂载还是打补丁操作。若新旧节点类型相同则则使用
  1. patch
复制代码
去通过打补丁的方式更新。

2.vnode如果描述的是一个组件的话如何去处理挂载和打补丁呢?

在节点是一个组件的情况下,vnode的type会是一个对象,我们通过判断vnode的type是否为对象从而执行特定的操作:
  1.   function patch(n1, n2, container) {
  2.     if (n1 && n1.type !== n2.type) {
  3.       unmount(n1)
  4.       n1 = null
  5.     }

  6.     const { type } = n2

  7.     if (typeof type === 'string') {
  8.       if (!n1) {
  9.         mountElement(n2, container)
  10.       } else {
  11.         patchElement(n1, n2)
  12.       }
  13.     } else if (typeof type === 'object') {
  14.       // 组件
  15.     }
  16.   }
复制代码
七、如何给节点挂载事件:


1.在vnode节点中如何描述事件:

在vnode的props对象中,凡是以on开头的属性都被定义为事件:
  1. const vnode = {
  2.   type: 'p',
  3.   props: {
  4.     onClick: () => {
  5.         alert('clicked 1')
  6.       }
  7.   },
  8.   children: 'text'
  9. }
复制代码
如上所示我们给一个类型为'p'的vnode描述了一个
  1. onCLick
复制代码
事件

2.如何将描述有事件的vnode节点挂载

我们先前使用
  1. patchProps
复制代码
去挂载vnode的props,为了能够支持事件的挂载需要对其进行一定的修改
  1.   patchProps(el, key, preValue, nextValue) {
  2.     if (/^on/.test(key)) {
  3.       const name = key.slice(2).toLowerCase()
  4.       // 移除上一次绑定的事件处理函数prevValue
  5.       prevValue && el.removeEventListener(name, prevValue)
  6.       // 绑定新的事件处理函数
  7.       el.addEventListener(name, nextValue)
  8.     } else if (key === 'class') {
  9.       el.className = nextValue || ''
  10.     } else if (shouldSetAsProps(el, key, nextValue)) {
  11.       const type = typeof el[key]
  12.       if (type === 'boolean' && nextValue === '') {
  13.         el[key] = true
  14.       } else {
  15.         el[key] = nextValue
  16.       }
  17.     } else {
  18.       el.setAttribute(key, nextValue)
  19.     }
  20.   }
复制代码
如上使用正则去匹配on开头的key,首先判断是否已经挂载了一个同名的事件处理函数,有的话就先移除,然后再使用addEventListener挂载新的事件处理函数。

3.事件处理函数频繁更新时如何优化性能?

优化思路
我们可以将事件处理函数固定并命名为
  1. invoker
复制代码
,并将实际的事件处理函数赋值给
  1. invoker.value
复制代码
。这样在挂载的时候我们挂载的是
  1. invoker
复制代码
,并
  1. invoker
复制代码
内部执行真正的事件处理函数
  1. invoker.value
复制代码
,这样当需要更新事件处理函数时我们直接替换
  1. invoker.value
复制代码
的值即可,而不用使用
  1. removeEventListener
复制代码
去移除。为了能够在事件处理函数更新时判断有没有设置invoker我们将invoker缓存在
  1. el._vei
复制代码
上.
  1.   patchProps(el, key, prevValue, nextValue) {
  2.     if (/^on/.test(key)) {
  3.       let invoker = el._vei
  4.       const name = key.slice(2).toLowerCase()
  5.       if (nextValue) {
  6.         if (!invoker) {
  7.           invoker = el._vei = (e) => {
  8.               invoker.value(e)
  9.           }
  10.           invoker.value = nextValue
  11.           el.addEventListener(name, invoker)
  12.         } else {
  13.           invoker.value = nextValue
  14.         }
  15.       } else if (invoker) {
  16.         el.removeEventListener(name, invoker)
  17.       }
  18.     } else if (key === 'class') {
  19.       el.className = nextValue || ''
  20.     } else if (shouldSetAsProps(el, key, nextValue)) {
  21.       const type = typeof el[key]
  22.       if (type === 'boolean' && nextValue === '') {
  23.         el[key] = true
  24.       } else {
  25.         el[key] = nextValue
  26.       }
  27.     } else {
  28.       el.setAttribute(key, nextValue)
  29.     }
  30.   }
复制代码
一个vnode上同时存在多个事件应该如何处理 在之前的实现中我们直接将
  1. el._vei
复制代码
赋值给
  1. invoker
复制代码
,这样无法去处理vnode上的多个事件,如果像下面这样定义了多个事件,会导致后面的事件覆盖之前的事件
  1. const newVnode = {
  2.   type: 'p',
  3.   props: {
  4.     onClick: () => {
  5.         alert('click')
  6.     },
  7.     onContextmenu: () => {
  8.       alert('contextmenu')
  9.     }
  10.   },
  11.   children: 'text'
  12. }
复制代码
解决方式是:将
  1. patchProps
复制代码
中的
  1. el._vei
复制代码
定义为一个对象,将事件名称作为其键,值则是该事件对应的事件处理函数
  1.   patchProps(el, key, prevValue, nextValue) {
  2.     if (/^on/.test(key)) {
  3.       const invokers = el._vei || (el._vei = {})
  4.       //根据事件名称获取 invoker
  5.       let invoker = invokers[key]
  6.       const name = key.slice(2).toLowerCase()
  7.       if (nextValue) {
  8.         if (!invoker) {
  9.         // 将事件处理函数缓存到 el._vei[key] 下,避免覆盖
  10.           invoker = el._vei[key] = (e) => {
  11.               invoker.value(e)
  12.           }
  13.           invoker.value = nextValue
  14.           el.addEventListener(name, invoker)
  15.         } else {
  16.           invoker.value = nextValue
  17.         }
  18.       } else if (invoker) {
  19.         el.removeEventListener(name, invoker)
  20.       }
  21.     } else if (key === 'class') {
  22.       el.className = nextValue || ''
  23.     } else if (shouldSetAsProps(el, key, nextValue)) {
  24.       const type = typeof el[key]
  25.       if (type === 'boolean' && nextValue === '') {
  26.         el[key] = true
  27.       } else {
  28.         el[key] = nextValue
  29.       }
  30.     } else {
  31.       el.setAttribute(key, nextValue)
  32.     }
  33.   }
复制代码
一个事件需要多个事件处理函数执行应该如何处理 当同一个事件存在多个事件处理函数,比如同时存在两个click的事件处理函数
  1. const vnode = {
  2.   type: 'p',
  3.   props: {
  4.     onClick: [
  5.       () => {
  6.         alert('clicked 1')
  7.       },
  8.       () => {
  9.         alert('clicked 2')
  10.       }
  11.     ]
  12.   },
  13.   children: 'text'
  14. }
复制代码
此时我们需要对
  1. el._vei[key]
复制代码
增加一层判断,时数组的情况下,需要遍历去调用其中的事件处理函数
  1.   patchProps(el, key, prevValue, nextValue) {
  2.     if (/^on/.test(key)) {
  3.       const invokers = el._vei || (el._vei = {})
  4.       let invoker = invokers[key]
  5.       const name = key.slice(2).toLowerCase()
  6.       if (nextValue) {
  7.         if (!invoker) {
  8.           invoker = el._vei[key] = (e) => {
  9.           //如果是数组,遍历调用事件处理函数
  10.             if (Array.isArray(invoker.value)) {
  11.               invoker.value.forEach(fn => fn(e))
  12.             } else {
  13.               invoker.value(e)
  14.             }
  15.           }
  16.           invoker.value = nextValue
  17.           el.addEventListener(name, invoker)
  18.         } else {
  19.           invoker.value = nextValue
  20.         }
  21.       } else if (invoker) {
  22.         el.removeEventListener(name, invoker)
  23.       }
  24.     } else if (key === 'class') {
  25.       el.className = nextValue || ''
  26.     } else if (shouldSetAsProps(el, key, nextValue)) {
  27.       const type = typeof el[key]
  28.       if (type === 'boolean' && nextValue === '') {
  29.         el[key] = true
  30.       } else {
  31.         el[key] = nextValue
  32.       }
  33.     } else {
  34.       el.setAttribute(key, nextValue)
  35.     }
  36.   }
复制代码
八、事件冒泡处理

当vnode的父子节点的事件之间有关联时,会因为事件冒泡出现一定问题,如下情况
  1. const { effect, ref } = VueReactivity

  2. const bol = ref(false)

  3. effect(() => {
  4.   const vnode = {
  5.     type: 'div',
  6.     props: bol.value ? {
  7.       onClick: () => {
  8.         alert('父元素 clicked')
  9.       }
  10.     } : {},
  11.     children: [
  12.       {
  13.         type: 'p',
  14.         props: {
  15.           onClick: () => {
  16.             bol.value = true
  17.           }
  18.         },
  19.         children: 'text'
  20.       }
  21.     ]
  22.   }
  23.   renderer.render(vnode, document.querySelector('#app'))
  24. })
复制代码
看一下以上代码:

  • 定义了一个响应式数据
    1. bol
    复制代码
    ,初始值为
    1. false
    复制代码
  • 在副作用函数
    1. effect
    复制代码
    中使用了
    1. bol
    复制代码
    ,并且调用了渲染器将vnode渲染到了id为
    1. app
    复制代码
    的节点上
  • vnode中父节点的事件
    1. onClick
    复制代码
    的存在与否取决于bol的值,若为
    1. true
    复制代码
    则父元素的
    1. onClick
    复制代码
    事件才会挂载。 首次渲染时由于
    1. bol
    复制代码
    1. false
    复制代码
    ,所以vnode中的父节点并不会被绑定一个
    1. onClick
    复制代码
    事件
当点击了渲染处理的p元素,即vnode的子节点时,会出现父元素的click事件也会被选择的情况,其过程如下:

  • 点击了
    1. p
    复制代码
    元素,
    1. bol
    复制代码
    被修改,副作用函数重新执行
  • 父元素div的props中
    1. onClick
    复制代码
    事件挂载
  • 对p的点击事件冒泡到了父元素
    1. div
    复制代码
    上,导致触发了其上的
    1. onClick
    复制代码
    事件
其流程如下:

为了解决这个问题: 对patchProps进行处理:屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行
  1. patchProps(el, key, prevValue, nextValue) {
  2.     if (/^on/.test(key)) {
  3.       const invokers = el._vei || (el._vei = {})
  4.       let invoker = invokers[key]
  5.       const name = key.slice(2).toLowerCase()
  6.       if (nextValue) {
  7.         if (!invoker) {
  8.           invoker = el._vei[key] = (e) => {
  9.             if (e.timeStamp < invoker.attached) return
  10.             if (Array.isArray(invoker.value)) {
  11.               invoker.value.forEach(fn => fn(e))
  12.             } else {
  13.               invoker.value(e)
  14.             }
  15.           }
  16.           invoker.value = nextValue
  17.           invoker.attached = performance.now()
  18.           el.addEventListener(name, invoker)
  19.         } else {
  20.           invoker.value = nextValue
  21.         }
  22.       } else if (invoker) {
  23.         el.removeEventListener(name, invoker)
  24.       }
  25.     } else if (key === 'class') {
  26.       el.className = nextValue || ''
  27.     } else if (shouldSetAsProps(el, key, nextValue)) {
  28.       const type = typeof el[key]
  29.       if (type === 'boolean' && nextValue === '') {
  30.         el[key] = true
  31.       } else {
  32.         el[key] = nextValue
  33.       }
  34.     } else {
  35.       el.setAttribute(key, nextValue)
  36.     }
  37.   }
复制代码
修改后的代码如上: 我们在invoker上添加一个属性
  1. attached
复制代码
用于记录事件处理函数被挂载的时间,在事件处理函数
  1. invoke.value
复制代码
被执行前进行判断,如果事件处理函数被绑定的时间
  1. invoke.attached
复制代码
晚于事件触发的事件
  1. e.timeStamp
复制代码
时,则取消副作用函数的执行。

九、子节点的更新

在处理完了节点的事件挂载之后,我们需要处理子节点的更新 在文章开始我们讨论了子节点
  1. vnode.children
复制代码
的类型主要有以下三种:
  1. null
复制代码
  1. string
复制代码
(文本节点)、
  1. Array
复制代码
(一个或者多个节点) 通过分析可知: 在子节点的更新过程中,新旧节点都有三种类型,这样总共会有九种情况,但是并不是每一种情况都要特殊处理,只需要考虑如下情况:

1.当新节点的类型是一个文本节点的情况下


  • 旧子节点为null或者文本节点时,直接将新节点的文本内容更新上去即可;
  • 旧子节点是一组节点时,需要遍历这一组节点并使用unmount函数卸载;

2.当新节点的类型是一组节点的情况下


  • 旧子节点为null或者文本节点时,直接将旧节点内容清空并逐一挂载新节点即可;
  • 旧子节点是一组节点时,需要遍历旧节点并使用unmount函数逐一卸载,并逐一挂载新的节点;(在实际处理过程中性能不佳,所以Vue使用了diff算法去处理这种情况下的更新

3.当新子节点不存在:


  • 旧子节点也不存在,则无需处理;
  • 旧子节点是一组子节点,则需要逐个卸载;
  • 旧子节点是文本子节点,则清空文本内容;
根据以上三种情况,我们将
  1. patchChildren
复制代码
函数进行更新
  1.   function patchChildren(n1, n2, container) {
  2.       //新子节点是文本节点
  3.     if (typeof n2.children === 'string') {
  4.       if (Array.isArray(n1.children)) {
  5.         n1.children.forEach((c) => unmount(c))
  6.       }
  7.       setElementText(container, n2.children)
  8.       //新子节点是一组节点
  9.     } else if (Array.isArray(n2.children)) {
  10.       if (Array.isArray(n1.children)) {
  11.         n1.children.forEach(c => unmount(c))
  12.         n2.children.forEach(c => patch(null, c, container))
  13.       } else {
  14.         setElementText(container, '')
  15.         n2.children.forEach(c => patch(null, c, container))
  16.       }
  17.       //新子节点不存在
  18.     } else {
  19.       if (Array.isArray(n1.children)) {
  20.         n1.children.forEach(c => unmount(c))
  21.       } else if (typeof n1.children === 'string') {
  22.         setElementText(container, '')
  23.       }
  24.     }
  25.   }
复制代码
十、如何描述没有标签的节点:文本和注释节点

在先前的实现中vnode的节点类型type是一个字符串,根据其类型我们可以判断标签名称,但是没有标签名称的节点需要如何处理呢,比如下面的节点?
  1. <div>
  2.     <!-- 注释节点 -->
  3.     我是文本节点
  4. </div>
复制代码
1.如何使用vnode描述

为了表示没有标签名称的节点,我们需要使用Symbol数据类型去作为vnode.type的值,这样就可以确保其唯一性
这样我们用于描述文本节点的vnode如下:
  1. const Text = Symbol()
  2. const newVnode = {
  3.   type: Text,
  4.   children: '文本节点内容'
  5. }
复制代码
用于描述注释节点的vnode如下:
  1. const Comment = Symbol()
  2. const newVnode = {
  3.   type: Comment,
  4.   children: '注释节点内容'
  5. }
复制代码
2.如何渲染

假设我们需要调整
  1. patch
复制代码
函数去适应如上vnode文本节点的渲染:
  1.   function patch(n1, n2, container) {
  2.     if (n1 && n1.type !== n2.type) {
  3.       unmount(n1)
  4.       n1 = null
  5.     }

  6.     const { type } = n2

  7.     if (typeof type === 'string') {
  8.       if (!n1) {
  9.         mountElement(n2, container)
  10.       } else {
  11.         patchElement(n1, n2)
  12.       }
  13.     } else if (type === Text) {
  14.       if (!n1) {
  15.         // 使用 createTextNode 创建文本节点
  16.         const el = n2.el = document.createTextNode(n2.children)
  17.         // 将文本节点插入到容器中
  18.         insert(el, container)
  19.       } else {
  20.         // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
  21.         const el = n2.el = n1.el
  22.         if (n2.children !== n1.children) {
  23.             el.nodeValue = n2.children
  24.         }
  25.       }
  26.     }
复制代码
增加了一个对type类型的判断,如果类型是Text证明是文本节点,则判断旧节点上是否存在,如果旧节点存在只需要更新文本内容即可,否则需要先创建文本节点,再将其插入到容器中。

3.优化渲染器的通用性

以上实现的代码中仍旧依赖了浏览器的API:createTextNodeel.nodeValue,为了保证渲染器的通用性,需要将这部分功能提取成为独立的函数,并且作为用于创建渲染器的函数createRenderer的参数传入:
  1.   function patch(n1, n2, container) {
  2.     if (n1 && n1.type !== n2.type) {
  3.       unmount(n1)
  4.       n1 = null
  5.     }

  6.     const { type } = n2

  7.     if (typeof type === 'string') {
  8.       if (!n1) {
  9.         mountElement(n2, container)
  10.       } else {
  11.         patchElement(n1, n2)
  12.       }
  13.     } else if (type === Text) {
  14.       if (!n1) {
  15.       // 使用 createTextNode 创建文本节点
  16.       const el = n2.el = createText(n2.children)
  17.       // 将文本节点插入到容器中
  18.       insert(el, container)
  19.       } else {
  20.         const el = n2.el = n1.el
  21.         if (n2.children !== n1.children) {
  22.         // 调用 setText 函数更新文本节点的内容
  23.             setText(el, n2.children)
  24.         }
  25.       }
  26.     }
复制代码
我们将依赖到浏览器API的createTextNodeel.nodeValue分别放到了createTextsetText两个函数内,并在创建渲染器的函数createRenderer中作为参数传入并使用:
  1. function createRenderer(options) {

  2.   const {
  3.     createElement,
  4.     insert,
  5.     setElementText,
  6.     patchProps,
  7.     createText,
  8.     setText
  9.   } = options
  10.   
  11.   省略内容
  12. }

  13. const renderer = createRenderer({
  14.   createElement(tag) {
  15.     ...
  16.   },
  17.   setElementText(el, text) {
  18.     ...
  19.   },
  20.   insert(el, parent, anchor = null) {
  21.     ...
  22.   },
  23.   createText(text) {
  24.     return document.createTextNode(text)
  25.   },
  26.   setText(el, text) {
  27.     el.nodeValue = text
  28.   },
  29.   patchProps(el, key, prevValue, nextValue) {
  30.     ...
  31.   }
  32. })
复制代码
这样对于文本节点的操作,不再仅依赖于浏览器的API我们可以通过改变createRendereroptions参数对象里面的createTextsetText方法灵活选择。
以上就是Vue渲染器如何对节点进行挂载和更新的详细内容,更多关于Vue节点挂载和更新的资料请关注脚本之家其它相关文章!

来源:https://www.jb51.net/javascript/321013gpv.htm
免责声明:由于采集信息均来自互联网,如果侵犯了您的权益,请联系我们【E-Mail:cb@itdo.tech】 我们会及时删除侵权内容,谢谢合作!

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x

举报 回复 使用道具