Skip to content

挂载与更新

挂载子节点和元素的属性

挂载子节点

js
const vnode = {
  type: 'div',
  children: [
    {
      type: 'p',
      children: 'hello'
    }
  ]
}
function mountElement(vnode, container) {
    // 创建DOM元素
    const el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) { 
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => { 
        patch(null, child, el) 
      }) 
    }
    // 将元素添加到容器中
    insert(el, container)
  }
  • 传递给patch 函数的第一个参数是null。因为是挂载阶段,没有旧vnode ,所以只需要传递null 即可。当patch函数执行时,就会递归地调用mountElement函数完成挂载。
  • 传递给patch函数的第三个参数是挂载点。由于正在挂载点子元素是div 标签的子节点,所以需要把刚刚创建的div 元素作为挂载点,这样才能保证这些子节点挂载到正确位置。

挂载元素属性

js
const vnode1 = {
  type: 'div',
  props: {
    id: foo
  },
  children:[
    {
      type: 'p',
      children: 'hello'
    }
  ]
}
function mountElement(vnode, container) {
    // 创建DOM元素
    const el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if(Array.isArray(vnode.children)) {
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if(vnode.props) {  
      for(const key in vnode.props) { 
        el.setAttribute(key, vnode.props[key]) 
      } 
    } 
    // 将元素添加到容器中
    insert(el, container)
  }

HTML Attributes 与 DOM Properties

HTML Attributes 指的就是定义在HTML 标签上的属性。当浏览器解析HTML代码后,会创建一个与之相符的DOM元素对象,这个DOM对象会包含很多属性,这些属性就是所谓的DOM Properties

很多HTML AttributesDOM对象上有与之同名的DOM Properties。但DOM ProtertiesHTML Attributes 的名字不总是一模一样的。

js
<input value = 'foo' />
// 这时将文本内容修改为 bar
console.log(el.value) // 'bar'
console.log(el.getAttribute('value')) // foo
console.log(el.value) // 'bar'
console.log(el.defaultValue) // foo

实际上,HTML Attributes 的作用是设置与之对应的DOM Properties 的初始值。一旦值改变,那么DOM Properties 始终存储着当前值,而通过getAttribute 函数得到的仍然是初始值。

说明一个HTML Attributes 可能关联多个DOM Properties

正确的设置元素属性

js
function mountElement(vnode, container) {
    // 创建DOM元素
    const el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if (vnode.props) {
      for (const key in vnode.props) {
        // 用in操作符判断key是否存在对应的DOM Properties
        if (key in el) { 
          // 获取该DOM Properties的类型
          const type = typeof el[key] 
          const value = vnode.props[key] 
          // 如果是布尔类型,并且value是空字符串,则将值矫正为true
          if (type === 'boolean' && value === '') { 
            el[key] = true
          } else { 
            el[key] = value 
          } 
        } else { 
          el.setAttribute(key, vnode.props[key]) 
        } 
        el.setAttribute(key, vnode.props[key]) 
      }
    }
    // 将元素添加到容器中
    insert(el, container)
  }

改进

有些DOM Properties 是只读的,因此只能够通过setAttribute 函数来设置它。

如:

html
<form id='form1'></form>
<input form='form1' />
js
function shouldSetAsProps(el, key, value) { 
  if(key === "form" && el.tagName === 'INPUT') return false
  return key in el 
} 

function mountElement(vnode, container) {
    // 创建DOM元素
    const el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if (vnode.props) {
      for (const key in vnode.props) {
        // 用in操作符判断key是否存在对应的DOM Properties
        if (key in el) { 
        if (shouldSetAsProps(el, key, value)) { 
          // 获取该DOM Properties的类型 
          const type = typeof el[key]
          const value = vnode.props[key]
          // 如果是布尔类型,并且value是空字符串,则将值矫正为true 
          if (type === 'boolean' && value === '') {
            el[key] = true
          } else {
            el[key] = value
          }
        } else {
          el.setAttribute(key, vnode.props[key])
        }
      }
    }
    // 将元素添加到容器中
    insert(el, container)
  }

抽离

js
const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },
  patchProps(el, key, preValue, nextValue) { 
    if (shouldSetAsProps(el, key, nextValue)) { 
      const type = typeof el[key] 
      if (type === 'boolean' && nextValue === '') { 
        el[key] = true
      } else { 
        el[key] = nextValue 
      } 
    } else { 
      el.setAttribute(key, nextValue) 
    } 
  } 
})

function mountElement(vnode, container) {
    // 创建DOM元素
    const el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if (vnode.props) {
      for (const key in vnode.props) {
        patchProps(el, key, null, vnode.props[key]) 
        // 用in操作符判断key是否存在对应的DOM Properties
        if (shouldSetAsProps(el, key, value)) { 
          // 获取该DOM Properties的类型
          const type = typeof el[key] 
          const value = vnode.props[key] 
          // 如果是布尔类型,并且value是空字符串,则将值矫正为true
          if (type === 'boolean' && value === '') { 
            el[key] = true
          } else { 
            el[key] = value 
          } 
        } else { 
          el.setAttribute(key, vnode.props[key]) 
        } 
      }
    }
    // 将元素添加到容器中
    insert(el, container)
  }

class 的处理

vue中设置类名的几种方式

  • 指定class为一个字符串

    html
    <p class='foo bar'></p>
  • 指定class为一个对象值

    html
    <p :class='cls'></p>
    js
    const cls = {foo: true, bar: false}
    js
    //对应的vnode
    const vnode = {
      type: 'p',
      props: {
        class: { foo: true, bar: false}
      }
    }
  • class是包含上述两种类型的数组

    html
    <p :class='arr'></p>
    js
    const arr = [
      'foo bar',
      {
        baz: true
      }
    ]
    js
    // 对应的vnode
    const vnode ={
      type: 'p',
      props: {
        class: [
          'foo bar',
          {baz: true}
        ]
      }
    }

class的值可以是多种类型,必须在设置元素的class之前将值归一化为统一的字符串形式,再把该字符串作为元素的class值去设置。

在浏览器中设置class的三种方式

  • setAttribute 性能最差
  • el.className 性能最优
  • el.classList 性能中等
js
// 创建一个渲染器
const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },
  patchProps(el, key, preValue, nextValue) { 
    if(key === 'class') { 
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) { 
      const type = typeof el[key] 
      if (type === 'boolean' && nextValue === '') { 
        el[key] = true 
      } else { 
        el[key] = nextValue 
      } 
    } else { 
      el.setAttribute(key, nextValue) 
    } 
  } 
})

卸载操作

卸载操作发生在更新阶段。在初次挂载完成后,后续渲染会触发更新。

js
// 首次挂载
renderer.render(vnode, document.querySelector('#app'))
// 再次挂载,触发更新
renderer.render(newVnode, document.querySelector('#app'))

更新的几种情况:

  • 首次挂载完成后,后续调用render函数渲染空内容
js
renderer.render(vnode, document.querySelector('#app'))
// 新vnode内容为null, 意味着卸载之前渲染的内容
renderer.render(null, document.querySelector('#app'))
js
 function render(vnode, container) {
   if (vnode) {
     patch(container._vnode, vnode, container)
   } else {
     if (container._vnode) {
       container.innerHTML = ''
     }
   }
   container._vnode = vnode
 }

当内容为空时,通过container.innerHTMl 清空容器。不严谨

  • 容器的内容可能是由某个或多个组件渲染的,当卸载操作发生时,应该正确地调用这些组件的 beforeUnmountunmounted等生命周期函数
  • 即使内容不是由组件渲染的,有的元素存在自定义指令,我们应该在卸载操作发生时正确的执行对应的指令钩子函数
  • 使用innerHTML清空容器元素内容的另一个缺陷,它不会移除绑定的DOM元素上的事件处理函数

正确的卸载方式是,根据vnode对象获取与其相关联的真实DOM元素,然后使用原生DOM操作方法将该DOM元素移除。

js
function mountElement(vnode, container) {
   // 创建DOM元素
   const el = createElement(vnode.type) 
   const el = vnode.el = createElement(vnode.type) 
   // 处理子节点,如果子节点是字符串,代表元素具有文本节点
   if (typeof vnode.children === 'string') {
     setElementText(el, vnode.children)
   } else if (Array.isArray(vnode.children)) {
     // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
     vnode.children.forEach(child => {
       patch(null, child, el)
     })
   }

   if (vnode.props) {
     for (const key in vnode.props) {
       patchProps(el, key, null, vnode.props[key])
     }
   }
   // 将元素添加到容器中
   insert(el, container)
 }

  function render(vnode, container) {
   if (vnode) {
     patch(container._vnode, vnode, container)
   } else {
     if (container._vnode) {
       container.innerHTML = ''
        // 根据vnode获取要卸载的真实DOM元素
       const el = container._vnode.el 
       // 获取el的父元素
       const parent = el.parentNode 
       // 调用removeChild移除元素
       if(parent) { 
         parent.removeChild(el) 
       } 
     }
   }
   container._vnode = vnode
 }

将移除操起提取到 unmount

js
function createRenderer(options) {
 const {
   createElement,
   insert,
   setElementText,
   patchProps
 } = options
 function patch(n1, n2, container) {
   // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
   // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
   // 这时只需要挂载
   if (!n1) {
     mountElement(n2, container)
   } else {
     // n1 存在,意味着打补丁,
   }
 }

 function unmount(vnode) { 
   const parent = vnode.el.parentNode 
   if(parent) { 
     parent.removeChild(vnode.el) 
   } 
 } 

 function render(vnode, container) {
   if (vnode) {
     patch(container._vnode, vnode, container)
   } else {
     if (container._vnode) {
       unmount(vnode) 
       // 根据vnode获取要卸载的真实DOM元素
       const el = container._vnode.el 
       // 获取el的父元素
       const parent = el.parentNode 
       // 调用removeChild移除元素
       if(parent) { 
         parent.removeChild(el) 
       } 
     }
   }
   container._vnode = vnode
 }

 function mountElement(vnode, container) {
   // 创建DOM元素
   const el = vnode.el = createElement(vnode.type)
   // 处理子节点,如果子节点是字符串,代表元素具有文本节点
   if (typeof vnode.children === 'string') {
     setElementText(el, vnode.children)
   } else if (Array.isArray(vnode.children)) {
     // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
     vnode.children.forEach(child => {
       patch(null, child, el)
     })
   }

   if (vnode.props) {
     for (const key in vnode.props) {
       patchProps(el, key, null, vnode.props[key])
     }
   }
   // 将元素添加到容器中
   insert(el, container)
 }

 return {
   render
 }
}

区分vnode的类型

js
function patch(n1, n2, container) {
   // 如果n1存在,则对比n1和n2的类型
   if(n1 && n1.type !== n2.type) { 
     // 如果新旧vnode的类型不同,则直接将旧的vnode卸载
     unmount(n1) 
     n1 = null
   } 
   // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
   // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
   // 这时只需要挂载
   if (!n1) { 
     mountElement(n2, container) 
   } else { 
     // n1 存在,意味着打补丁,
   } 
    const { type } = n2 
   if (typeof type === 'string') { 
     // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
     // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
     // 这时只需要挂载
     if (!n1) { 
       mountElement(n2, container) 
     } else { 
       // n1 存在,意味着打补丁,
       patch(n1, n2) 
     } 
   } else if (typeof type === 'object') { 
     // 组件
   } else if (type === 'xxx') { 
     // 其他类型的vnode
   } 
 }

事件的处理

js
 patchProps(el, key, preValue, nextValue) {
   // 匹配以on开头的属性,视其为事件
   if(/^on/.test(key)) {  
     const name = key.slice(2).toLowerCase() 
      // 移除上一次绑定 的事件处理函数
     preValue && el.removeEventListener(name, preValue) 
     // 绑定新的事件处理函数
     el.addEventListener(name, nextValue) 
   } else if (key === 'class') {
     el.className = nextValue || ''
   } else if (shouldSetAsProps(el, key, nextValue)) {
     const type = typeof el[key]
     if (type === 'boolean' && nextValue === '') {
       el[key] = true
     } else {
       el[key] = nextValue
     }
   } else {
     el.setAttribute(key, nextValue)
   }
 }

优化

伪造一个事件处理函数 invoker, 把真正的事件处理函数设置为invoker.value属性的值。这样,当更新事件的时候,将不在需要调用removeEventListener函数来移除上一次绑定的事件,只需更新invoker.value的值即可

js
patchProps(el, key, preValue, nextValue) {
   // 匹配以on开头的属性,视其为事件
   if(/^on/.test(key)) {
     // 获取为该元素伪造的事件处理函数 invoker
     let invoker = el._vei 
     const name = key.slice(2).toLowerCase()
     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) 
     } 
     // 移除上一次绑定 的事件处理函数
     preValue && el.removeEventListener(name, preValue) 
     // 绑定新的事件处理函数
     el.addEventListener(name, nextValue) 
   } else if (key === 'class') {
     el.className = nextValue || ''
   } else if (shouldSetAsProps(el, key, nextValue)) {
     const type = typeof el[key]
     if (type === 'boolean' && nextValue === '') {
       el[key] = true
     } else {
       el[key] = nextValue
     }
   } else {
     el.setAttribute(key, nextValue)
   }
 }


const vnode1 = {
 type: 'div',
 props: {
   id: foo,
   onClick: () => {
     alert('clicked')
   }
 },
 children: [
   {
     type: 'p',
     children: 'hello'
   }
 ]
}

改进2

当一个元素同时绑定多种事件时,将会出现事件覆盖。同一类型的事件,还可以绑定多个事件处理函数。

js
const vnode1 = {
 type: 'div',
 props: {
   id: foo,
   onClick: () => {
     alert('clicked')
   },
   onContextmenu: () => { 
     alert('contextmenu') 
   } 
 },
 children: [
   {
     type: 'p',
     children: 'hello'
   }
 ]
}

需要重新设计el._vei的数据结构。 将el._vei设计为一个对象,它的键是事件名称,值则是对应的事件处理函数。

js
patchProps(el, key, preValue, nextValue) {
    // 匹配以on开头的属性,视其为事件
    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 是数组,则遍历它并逐个调用事件处理函数
            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.value = nextValue
        }
      } else if(invoker) {
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  }

事件冒泡与更新时机的问题

绑定事件处理函数发生在事件冒泡之前

事件触发的时间要早于事件处理函数被绑定的时间。当一个事件触发时,目标元素上还没有绑定相关的事件处理函数,可以根据这个特点来解决问题:屏蔽所有绑定时间晚于事件触发时间的事件处理函数的执行

js
patchProps(el, key, preValue, nextValue) {
    // 匹配以on开头的属性,视其为事件
    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) => {
            if (e.timeStamp < invoker.attached) return
            // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
            // 如果invoker.value 是数组,则遍历它并逐个调用事件处理函数
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach(fn => fn(e))
            } else {
              invoker.value(e)
            }
          }
          // 将真正的事件处理函数赋值给invoker.value
          invoker.value = nextValue
          invoker.attached = performance.now() 
          // 绑定invoker作为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          invoker.value = nextValue
        }
      } else if (invoker) {
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      el.setAttribute(key, nextValue)
    }
  }

更新子节点

新旧节点分别有一下三种类型

  • 没有子节点
  • 文本子节点
  • 一组子节点
js
function patchElement(n1, n2) {
  const el = n2.el = n1.el
  const oldProps = n1.props
  const newProps = n2.props
  for (const key in newProps) {
    if (newProps[key] !== oldProps[key]) {
      patchProps(el, key, oldProps[key], newProps[key])
    }
  }
  for (const key in oldProps) {
    if (!(key in newProps)) {
      patchProps(el, key, oldProps[key], null)
    }
  }

  patchChildren(n1, n2, el)
}

function patchChildren(n1, n2, container) {
  // 判断新子节点的类型是否是文本节点
  if (typeof n2.children === 'string') {
    // 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
    // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
    if (Array.isArray(n1.children)) {
      n1.children.forEach((c) => unmount(c))
    }
    // 最后将新的文本节点内容设置给容器元素
    setElementText(container, n2.children)
  } else if (Array.isArray(n2.children)) {
    // 新子节点是一组子节点

    // 判断旧子节点是否也是一组子节点
    if (Array.isArray(n1.children)) {
      // 新旧节点都是一组子节点,diff算法
      // 将旧的一组子节点全部卸载
      n1.children.forEach(c => unmount(c))
      // 再将新的一组子节点全部挂载到容器中
      n2.children.forEach(c => patch(null, c, container))
    } else {
      // 旧子节点要么是文本子节点,要不不存在
      // 无论哪种情况,都需要将容器清空,然后将新的一组子节点逐个挂载
      setElementText(container, '')
      n2.children.forEach(c => patch(null, c, container))
    }
  } else {
    // 新子节点不存在
    // 旧子节点是一组子节点,只需逐个卸载即可
    if(Array.isArray(n1.children)) {
      n1.children.forEach(c => unmount(c))
    } else if(typeof n1.children === 'string') {
      // 旧子节点是文本子节点,清空内容即可
      setElementText(container, '')
    }
    // 若没有旧子节点,什么都不需要做
  }
}

文本节点和注释节点

如何用虚拟DOM描述更多类型的真实DOM。

html
<div><!-- 注释节点 --> 我是文本节点</div>

注释节点和文本节点不同于普通标签节点,它们不惧有标签名称,所以需要人为创造一些唯一的标识,并将其作为注释节点和文本节点的type属性值。

js
// 文本节点的type标识
const Text = Symbol()

const newVnode = {
  // 描述文本节点
  type: Text,
  children: '我是文本内容'
}

// 注释节点的type标识
const Comment = Symbol()

const newVnode = {
  // 描述注释节点
  type: Comment,
  children: '我是注释内容'
}
js
function createRenderer(options) {
  const {
    createElement,
    insert,
    setElementText,
    patchProps,
    createText, 
    setText, 
    createComment, 
    setComment
  } = options
  function patch(n1, n2, container) {
    // 如果n1存在,则对比n1和n2的类型
    if (n1 && n1.type !== n2.type) {
      // 如果新旧vnode的类型不同,则直接将旧的vnode卸载
      unmount(n1)
      n1 = null
    }
    const { type } = n2
    if (typeof type === 'string') {
      // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
      // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
      // 这时只需要挂载
      if (!n1) {
        mountElement(n2, container)
      } else {
        // n1 存在,意味着打补丁,
        patchElement(n1, n2)
      }
    } else if (type === Text) { 
      // 如果新的vnode的类型是Text,则说明该vnode描述的是文本节点

      // 如果没有旧节点,则进行挂载
      if (!n1) { 
        // 使用createTextNode 创建文本节点
        const el = n2.el = createText(n2.children) 
        // 将文本节点插入到容器中
        insert(el, container) 
      } else { 
        // 如果旧vnode存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = n2.el = n1.el 
        if (n2.children !== n1.children) { 
          setText(el, n2.children) 
        } 
      } 
    } else if (type === Comment) { 
      if (!n1) { 
        const el = n2.el = createComment(n2.children) 
        insert(el, container) 
      } else { 
        const el = n2.el = n1.el 
        if (n2.children !== n1.children) { 
          setComment(el, n2.children) 
        } 
      } 
    } 
    else if (typeof type === 'object') {
      // 组件
    } else if (type === 'xxx') {
      // 其他类型的vnode
    }

  }

  function unmount(vnode) {
    const parent = vnode.el.parentNode
    if (parent) {
      parent.removeChild(vnode.el)
    }
  }

  function render(vnode, container) {
    if (vnode) {
      patch(container._vnode, vnode, container)
    } else {
      if (container._vnode) {
        unmount(vnode)
        // 根据vnode获取要卸载的真实DOM元素
        const el = container._vnode.el
        // 获取el的父元素
        const parent = el.parentNode
        // 调用removeChild移除元素
        if (parent) {
          parent.removeChild(el)
        }
      }
    }
    container._vnode = vnode
  }

  function mountElement(vnode, container) {
    // 创建DOM元素
    const el = vnode.el = createElement(vnode.type)
    // 处理子节点,如果子节点是字符串,代表元素具有文本节点
    if (typeof vnode.children === 'string') {
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      // 如果childern是数组,则遍历每一个子节点,并调用patch函数挂载它们
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }

    if (vnode.props) {
      for (const key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    // 将元素添加到容器中
    insert(el, container)
  }

  return {
    render
  }
}

// 创建一个渲染器
const renderer = createRenderer({
  createElement(tag) {
    return document.createElement(tag)
  },
  setElementText(el, text) {
    el.textContent = text
  },
  insert(el, parent, anchor = null) {
    parent.insertBefore(el, anchor)
  },
  patchProps(el, key, preValue, nextValue) {
    // 匹配以on开头的属性,视其为事件
    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) => {
            if (e.timeStamp < invoker.attached) return
            // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
            // 如果invoker.value 是数组,则遍历它并逐个调用事件处理函数
            if (Array.isArray(invoker.value)) {
              invoker.value.forEach(fn => fn(e))
            } else {
              invoker.value(e)
            }
          }
          // 将真正的事件处理函数赋值给invoker.value
          invoker.value = nextValue
          invoker.attached = performance.now()
          // 绑定invoker作为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          invoker.value = nextValue
        }
      } else if (invoker) {
        el.removeEventListener(name, invoker)
      }
    } else if (key === 'class') {
      el.className = nextValue || ''
    } else if (shouldSetAsProps(el, key, nextValue)) {
      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 
  }, 
  createComment(text) { 
    return document.createComment(text) 
  }, 
  setComment(el, comment) { 
    el.nodeValue = comment 
  } 
})

Fragment (片断)

js
const Fragment = Symbol()

const vnode = {
  type: Fragment,
  children: [
    { type: 'li', children: 'text1' },
    { type: 'li', children: 'text2' },
    { type: 'li', children: 'text3' },
  ]
}

片段也没有所谓的标签名称,所以也需要为片段创建唯一的标识,即Fragment。对于Fragment类型的vnode来说,它的children存储的内容就是模版中所有跟节点。

js
function patch(n1, n2, container) {
    // 如果n1存在,则对比n1和n2的类型
    if (n1 && n1.type !== n2.type) {
      // 如果新旧vnode的类型不同,则直接将旧的vnode卸载
      unmount(n1)
      n1 = null
    }
    const { type } = n2
    if (typeof type === 'string') {
      // 如果 n1 不存在,意味着挂载,则调用mountElement 函数完成挂载
      // n1 代表旧的vnode, n2 代表新的vnode,当n1不存在时,意味着没有旧的vnode,
      // 这时只需要挂载
      if (!n1) {
        mountElement(n2, container)
      } else {
        // n1 存在,意味着打补丁,
        patchElement(n1, n2)
      }
    } else if (type === Text) {
      // 如果新的vnode的类型是Text,则说明该vnode描述的是文本节点

      // 如果没有旧节点,则进行挂载
      if (!n1) {
        // 使用createTextNode 创建文本节点
        const el = n2.el = createText(n2.children)
        // 将文本节点插入到容器中
        insert(el, container)
      } else {
        // 如果旧vnode存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = n2.el = n1.el
        if (n2.children !== n1.children) {
          setText(el, n2.children)
        }
      }
    } else if (type === Comment) {
      if (!n1) {
        const el = n2.el = createComment(n2.children)
        insert(el, container)
      } else {
        const el = n2.el = n1.el
        if (n2.children !== n1.children) {
          setComment(el, n2.children)
        }
      }
    } else if (type === Fragment) { 
      if (!n1) { 
        // 如果旧vnode不存在,则只需要将Fragment的children逐个挂载即可
        n2.children.forEach(c => patch(null, c, container)) 
      } else { 
        // 如果旧vnode存在,则只需要更新Fragment的children即可
        patchChildren(n1, n2, container) 
      }
    }
    else if (typeof type === 'object') {
      // 组件
    } else if (type === 'xxx') {
      // 其他类型的vnode
    }

  }

  function unmount(vnode) {
    if (vnode.type === Fragment) { 
      vnode.children.forEach(c => unmount(c)) 
      return
    } 
    const parent = vnode.el.parentNode
    if (parent) {
      parent.removeChild(vnode.el)
    }
  }