likes
comments
collection
share

Vue设计与实现:挂载与更新

作者站长头像
站长
· 阅读数 43

挂载子节点和元素的属性

子节点除了是字符串还会有节点就说明是vnode,所以需要把vnode.children 定义为数组

const vnode = {
  type: 'div',
  children: [
    {
      type: 'p',
      children: 'hello'
    }
  ]
}

也需要在挂载mountElement时判断children类型,如果children是数组需要递归patch,由于在挂载阶段是没有旧的vnode,所以第一个参数传null

  function mountElement(vnode, container) {
    // 调用 createElement 函数创建元素
    const el = createElement(vnode.type)
    if (typeof vnode.children === 'string') {
      // 调用 setElementText 设置元素的文本节点
      setElementText(el, vnode.children)
+    } else if (Array.isArray(vnode.children)) {
+      vnode.children.forEach(child => {
+        patch(null, child, el)
+      })
+    }
+    // 如果 vnode.props 存在才处理它
+    if (vnode.props) {
+      // 遍历 vnode.props
+      for (const key in vnode.props) {
+       // 调用 setAttribute 将属性设置到元素上
+        el.setAttribute(key, vnode.props[key])
+      }
+    }
    // 调用 insert 函数将元素插入到容器内
    insert(el, container)
  }

HTML Attributes 与 DOM Properties

HTML Attributes 指的就是定义在 HTML 标签上的属性,这里指的 就是 id="my-input"、type="text" 和 value="foo"

<input id="my-input" type="text" value="foo" />

DOM Properties是指DOM 对象的属性 Vue设计与实现:挂载与更新 很多 HTML Attributes 在 DOM 对象上有与之同名的 DOM Properties,例如 id="my-input" 对 应 el.id,type="text" 对应 el.type,value="foo" 对应 el.value 等

Vue设计与实现:挂载与更新 但 DOM Properties 与 HTML Attributes 的名字不总是一 模一样的,class="foo" 对应的 DOM Properties 则是 el.className Vue设计与实现:挂载与更新 Vue设计与实现:挂载与更新 一个 HTML Attributes 可能关联多个 DOM Properties,value="foo" 与 el.value 和 el.defaultValue 都有 关联 Vue设计与实现:挂载与更新 如果你通过 HTML Attributes 提供的默认值不合法,那么 浏览器会使用内建的合法值作为对应 DOM Properties 的默认值 Vue设计与实现:挂载与更新

正确地设置元素属性

浏览器会将该按钮设置为禁用状态

默认传true

<button disabled>Button</button>

Vue设计与实现:挂载与更新 但在vue会编译成空字符串

const button = {
  type: 'button',
  props: {
    disabled: ""
  },
  children: "button"
}

在mountElement中调用 setAttribute 函数设置属性,浏览器会将按钮禁用

el.setAttribute('disabled', '')

Vue设计与实现:挂载与更新

传false

<button :disabled="false">Button</button>
const button = {
  type: 'button',
  props: {
    disabled: false
  },
  children: "button"
}

因为使用 setAttribute 函数设置的值总是会被字符串化,浏览器仍然将按钮禁用

el.setAttribute('disabled', false)
// 等价于
el.setAttribute('disabled', 'false')

Vue设计与实现:挂载与更新 可以优先设置 DOM Properties,当disabled是空字符串会失败

Vue设计与实现:挂载与更新 由于 el.disabled 是布尔类型的值,所以当我们尝试将它设置为空字符串时,浏览器会将它的值矫正为布尔类型的值,即 false

Vue设计与实现:挂载与更新

解决思路:

优先设置元素的 DOM Properties,但当值为空字符串时,要手动将值矫正为 true

  1. 如果有vnode.props才执行,遍历vnode.props拿到每个prop
  2. 判断prop是否在dom上,如果存在再判断prop对应的value是否是空字符串,如果是空字符串手动转换为true赋值给el[key],否则把value赋值el[key]
  3. 如果prop不在dom上,就用setAttribute设置prop
+function shouldSetAsProps(el, key, value) {
+    // 有一些 DOM Properties 是只读的需要return
+    if (key === 'form' && el.tagName === 'INPUT') return false
+    // 兜底
+    return key in el
+}

function mountElement(vnode, container) {
    // 调用 createElement 函数创建元素
    const el = createElement(vnode.type)
+    if (vnode.props) {
+      for (const key in vnode.props) {
+        patchProps(el, key, null, vnode.props[key])
+      }
+    }
    if (typeof vnode.children === 'string') {
      // 调用 setElementText 设置元素的文本节点
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    // 调用 insert 函数将元素插入到容器内
    insert(el, container)
  }
  
  const renderer = createRenderer({
+  patchProps(el, key, prevValue, nextValue) {
+    if (shouldSetAsProps(el, key, nextValue)) {
+      const type = typeof el[key]
+      // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
+      if (type === 'boolean' && nextValue === '') {
+        el[key] = true
+      } else {
+        el[key] = nextValue
+      }
+    } else {
+      // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
+      el.setAttribute(key, nextValue)
+    }
+  }
})

结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

class 的处理

class 为一个字符串

<p class="foo bar"></p>
const vnode = {
  type: 'p',
  props: {
    class: 'foo bar'
  }
}

class 为一个对象

<p :class="cls"></p>
const vnode = {
  type: 'p',
  props: {
    class: { foo: true, bar: false }
  }
}

class 为一个数组

<p :class="arr"></p>
const vnode = {
  type: 'p',
  props: {
    class: [
      'foo bar',
      { baz: true }
    ]
  }
}

解决思路:

在设置元素的 class 之前将值归一化为统一的字符串形式

  1. 检查 classNames 是否为假值(null、undefined 等),如果是,则返回空字符串
  2. 检查 classNames 是否为字符串类型,如果是,则去除首尾空格后返回
  3. 如果 classNames 是数组类型,表示传入了一组 class 名称,我们需要递归调用 normalizeClass 对其中的每个元素进行处理,并使用空格拼接起来返回
  4. 最后,如果 classNames 是对象类型,我们会过滤掉值为真的属性,并只返回属性名拼接而成的字符串
function normalizeClass(classNames) {
  if (!classNames) {
    return '';
  }

  if (typeof classNames === 'string') {
    return classNames.trim();
  }

  if (Array.isArray(classNames)) {
    return classNames.map(normalizeClass).filter(Boolean).join(' ');
  }

  if (typeof classNames === 'object') {
    return Object.entries(classNames)
      .filter(([key, value]) => value)
      .map(([key]) => key)
      .join(' ');
  }

  return '';
}

由于 class 属性对应的 DOM Properties 是 el.className,所以表达式 'class' in el 的值将会是 false,并且el.className 的性能比setAttribute好

patchProps(el, key, prevValue, nextValue) {
+    // 对 class 进行特殊处理
+    if (key === "class") {
+      el.className = nextValue || ""
+    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
      el.setAttribute(key, nextValue)
    }
}

Vue设计与实现:挂载与更新

结果:

Vue设计与实现:挂载与更新

卸载操作

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

// 初次挂载
renderer.render(vnode, document.querySelector('#app'))
// 再次挂载新 vnode,将触发更新
renderer.render(newVNode, document.querySelector('#app'))

null 作为新 vnode

 // 初次挂载
 renderer.render(vnode, document.querySelector('#app'))
 // 新 vnode 为 null,意味着卸载之前渲染的内容
 renderer.render(null, document.querySelector('#app'))

在render函数中,当vnode为null并且container._vnode属性存在时会通过innerHTML清空容器,这样有三个缺点

  1. 旧容器可能是多个子组件组成,需要触发组件的 beforeUnmount、unmounted等生命周期函数
  2. 有的元素可能会存在自定义指令,需要执行对应的指令钩子函数
  3. 需要移除绑定在 DOM 元素上的事件处理函数

解决思路:

  1. 在创建真实dom时保存到vnode.el,这样虚拟dom就跟真实dom关联起来
  2. 当卸载时通过container._vnode.el拿到对应的真实dom,通过真实dom的父元素移除元素
function mountElement(vnode, container) {
-    const el = createElement(vnode.type)
+    // 让 vnode.el 引用真实 DOM 元素
+    const el = vnode.el = createElement(vnode.type)
    if (vnode.props) {
      for (const key in vnode.props) {
        patchProps(el, key, null, vnode.props[key])
      }
    }
    if (typeof vnode.children === 'string') {
      // 调用 setElementText 设置元素的文本节点
      setElementText(el, vnode.children)
    } else if (Array.isArray(vnode.children)) {
      vnode.children.forEach(child => {
        patch(null, child, el)
      })
    }
    // 调用 insert 函数将元素插入到容器内
    insert(el, container)
  }
  
+ function unmount(vnode) {
+   // 获取 el 的父元素
+   const parent = vnode.el.parentNode
+   // 调用 removeChild 移除元素
+   if (parent) parent.removeChild(el)
+ }
  
  function render(vnode, container) {
    if (vnode) {
      patch(container._vnode, vnode, container)
    } else {
      if (container._vnode) {
-        container.innerHTML = ''
+        // 调用 unmount 函数卸载 vnode
+        unmount(container._vnode)
      }
    }
    container._vnode = vnode
  }

区分 vnode 的类型

一个vnode可以用来描述普通标签,也可以用来描述组件,还可以用来描述Fragment,对于不同类型的 vnode,需要提供不同的挂载或打补丁的处理方式

  function patch(oldN, newN, container) {
+    // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
+    if (oldN && oldN.type !== newN.type) {
+      unmount(oldN)
+      oldN = null
+    }
+    // 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
+    const { type } = newN
+    // 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
+    if (typeof type === "string") {
+      // 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
+      if (!oldN) {
+        mountElement(newN, container)
+      } else {
+        patchElement(oldN, newN)
+      }
+    }
  }

事件的处理

在 vnode.props 对象中,凡是以字符串 on 开头的属性都视作事件

绑定一种事件

const vnode = {
  type: 'p',
  props: {
    // 使用 onXxx 描述事件
    onClick: () => {
      alert('clicked')
    }
  },
  children: 'text'
}

解决思路:

  1. 从 el._vei 中读取对应的 invoker,如果 invoker 不存在, 则将伪造的 invoker 作为事件处理函数,并将它缓存到 el._vei 属性中
  2. 当更新事件时,由于 el._vei 已经存在了,所以只需要将 invoker.value 的值修改为新的事件处理函数
  3. 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
patchProps(el, key, prevValue, nextValue) {
+    // 匹配以 on 开头的属性,视其为事件
+    if (/^on/.test(key)) {
+      // 获取为该元素伪造的事件处理函数 invoker
+      const invoker = el._vei
+      // 根据属性名称得到对应的事件名称,例如 onClick ---> click
+      const name = key.slice(2).toLowerCase()
+      if (nextValue) {
+        if (!invoker) {
+          // 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
+          invoker = el._vei = (e) => {
+            // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
+            invoker.value(e)
+          }
+          // 将真正的事件处理函数赋值给 invoker.value
+          invoker.value = nextValue
+          // 绑定事件,nextValue 为事件处理函数
+          el.addEventListener(name, invoker)
+        } else {
+          // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
+          invoker.value = nextValue
+        }
+      }
+      else if (invoker) {
+        // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
+        el.removeEventListener(name, invoker)
+      }
    }
    else if (key === "class") {
      el.className = nextValue || ""
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
      el.setAttribute(key, nextValue)
    }
  }

结果:

Vue设计与实现:挂载与更新

绑定多种类型事件

一个元素同时绑定了多种事件,将会出现事件覆盖的现象

const vnode = {
  type: 'p',
  props: {
    onClick: () => {
      alert('clicked')
    },
    onContextmenu: () => {
      alert('contextmenu')
    }
  },
  children: 'text'
}
renderer.render(vnode, document.querySelector('#app'))

Vue设计与实现:挂载与更新

解决思路:

  1. 定义 el._vei 为一个对象,保存每个事件对应的invoker
  2. 一开始每个事件对应的invoker为undefined,所以只执行!invoker的逻辑,只绑定当前遍历到的事件对应的invoker
patchProps(el, key, prevValue, nextValue) {
    // 匹配以 on 开头的属性,视其为事件
    if (/^on/.test(key)) {
+     // 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
+     const invokers = el._vei || (el._vei = {})
+     //根据事件名称获取 invoker
+     let invoker = invokers[key]
      // 根据属性名称得到对应的事件名称,例如 onClick ---> click
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          // 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
-         invoker = el._vei = (e) => {
+         invoker = el._vei[key] = (e) => {
            // 当伪造的事件处理函数执行时,会执行真正的事件处理函数
            invoker.value(e)
          }
          // 将真正的事件处理函数赋值给 invoker.value
          invoker.value = nextValue
          // 绑定事件,nextValue 为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
          invoker.value = nextValue
        }
      }
      else if (invoker) {
        // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
        el.removeEventListener(name, invoker)
      }
    }
    else if (key === "class") {
      el.className = nextValue || ""
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
      el.setAttribute(key, nextValue)
    }
  }

结果:

Vue设计与实现:挂载与更新

绑定同一类型的多个事件

const vnode = {
  type: 'p',
  props: {
    onClick: [
      // 第一个事件处理函数
      () => {
        alert('clicked 1')
      },
      // 第二个事件处理函数
      () => {
        alert('clicked 2')
      }
    ]
  },
  children: 'text'
}
renderer.render(vnode, document.querySelector('#app'))

解决思路:

如果 invoker.value 是数组,则遍历它并逐个调用事件处理函数

patchProps(el, key, prevValue, nextValue) {
    // 匹配以 on 开头的属性,视其为事件
    if (/^on/.test(key)) {
      // 定义 el._vei 为一个对象,存在事件名称到事件处理函数的映射
      const invokers = el._vei || (el._vei = {})
      //根据事件名称获取 invoker
      let invoker = invokers[key]
      // 根据属性名称得到对应的事件名称,例如 onClick ---> click
      const name = key.slice(2).toLowerCase()
      if (nextValue) {
        if (!invoker) {
          // 如果没有 invoker,则将一个伪造的 invoker 缓存到 el._vei 中
         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
          // 绑定事件,nextValue 为事件处理函数
          el.addEventListener(name, invoker)
        } else {
          console.log(key)
          // 如果 invoker 存在,意味着更新,并且只需要更新 invoker.value的值即可
          invoker.value = nextValue
        }
      }
      else if (invoker) {
        // 新的事件绑定函数不存在,且之前绑定的 invoker 存在,则移除绑定
        el.removeEventListener(name, invoker)
      }
    }
    else if (key === "class") {
      el.className = nextValue || ""
    } else if (shouldSetAsProps(el, key, nextValue)) {
      const type = typeof el[key]
      // 如果是布尔类型,并且 value 是空字符串,则将值矫正为 true
      if (type === 'boolean' && nextValue === '') {
        el[key] = true
      } else {
        el[key] = nextValue
      }
    } else {
      // 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
      el.setAttribute(key, nextValue)
    }
  }

结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

更新子节点

每种新子节点都有三种旧子节点,三种新子节点就有九种旧子节点,patchElement函数处理新旧子节点

// 没有子节点
vnode = {
  type: 'div',
  children: null
}
// 文本子节点
vnode = {
  type: 'div',
  children: 'Some Text'
}
// 其他情况,子节点使用数组表示
vnode = {
  type: 'div',
  children: [
    { type: 'p' },
    'Some Text'
  ]
}

Vue设计与实现:挂载与更新

更新 props

解决思路:

  1. 拿到旧vnode的el也就是真实dom赋值给新子节点的el,下次更新新vnode的el就是旧vnode的el
  2. 拿到新旧vnode的props,遍历新props,判断newProps、oldProps的key对应的value是否一样,不一样就以新props为准更新
  3. 遍历旧props,如果key不存在新props说明已经把oldProps对应的key删了,所以新值为null
function patchElement(oldN, newN) {
    const el = newN.el = old.el
    const oldProps = oldN.props
    const newProps = newN.props
    for (const key in newProps) {
      // key都在newProps、oldProps,只不过值不一样
      if (newProps[key] !== oldProps[key]) {
        patchProps(el, key, oldProps[key], newProps[key])
      }
    }

    for (const key in oldProps) {
      // 新的dom上已经把oldProps对应的key删了,所以新值为null
      if (!(key in newProps)) {
        patchProps(el, key, oldProps[key], null)
      }
    }

    // 更新 children
    patchChildren(oldN, newN, el)
  }

结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

更新 children

新子节点是文本节点

解决思路:
  1. 先判断新vnode的子节点是不是文本节点,如果是再判断旧vnode的子节点是不是数组
  2. 如果是数组就遍历卸载节点,不是数组就是没有子节点或者一个文本节点都不需要操作,直接将新vnode的字符串类型的children设置给容器,因为setElementText会直接覆盖文本
function patchChildren(oldN, newN, container) {
    // 判断新子节点的类型是否是文本节点
    if (typeof newN.children === "string") {
      // 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
      // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
      if (Array.isArray(oldN.children)) {
        oldN.children.forEach((c) => unmount(c))
      }
      // 最后将新的文本节点内容设置给容器元素
      setElementText(container, newN.children)
    } 
  }
结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

新子节点是一组子节点

解决思路:
  1. 将旧子节点是不是数组,先把旧子节点遍历卸载,再把新子节点挂载
  2. 排除数组就是没有节点、一个文本节点的情况,不管无论哪种情况直接容器清空就不需要判断,再把新子节点遍历挂载
function patchChildren(oldN, newN, container) {
    // 判断新子节点的类型是否是文本节点
    if (typeof newN.children === "string") {
      // 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
      // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
      if (Array.isArray(oldN.children)) {
        oldN.children.forEach((c) => unmount(c))
      }
      // 最后将新的文本节点内容设置给容器元素
      setElementText(container, newN.children)
+    } else if (Array.isArray(newN.children)) { // 说明新子节点是一组子节点
+      // 判断旧子节点是否也是一组子节点
+      if (Array.isArray(oldN.children)) {
+        // 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的Diff 算法
+        // 将旧的一组子节点全部卸载
+        oldN.children.forEach(c => unmount(c))
+        // 再将新的一组子节点全部挂载到容器中
+        newN.children.forEach(c => patch(null, c, container))
+      } else {
+        // 此时:
+        // 旧子节点要么是文本子节点,要么不存在
+        // 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
+        setElementText(container, '')
+        newN.children.forEach(c => patch(null, c, container))
+      }
+    } 
  }
结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

新子节点没有子节点

解决思路:
  1. 判断旧子节点是不是数组,是数组就遍历旧子节点卸载
  2. 判断旧子节点是不是文本子节点,是文本子节点就清空内容
function patchChildren(oldN, newN, container) {
    // 判断新子节点的类型是否是文本节点
    if (typeof newN.children === "string") {
      // 旧子节点的类型有三种可能:没有子节点、文本子节点以及一组子节点
      // 只有当旧子节点为一组子节点时,才需要逐个卸载,其他情况下什么都不需要做
      if (Array.isArray(oldN.children)) {
        oldN.children.forEach((c) => unmount(c))
      }
      // 最后将新的文本节点内容设置给容器元素
      setElementText(container, newN.children)
    } else if (Array.isArray(newN.children)) { // 说明新子节点是一组子节点
      // 判断旧子节点是否也是一组子节点
      if (Array.isArray(oldN.children)) {
        // 代码运行到这里,则说明新旧子节点都是一组子节点,这里涉及核心的Diff 算法
        // 将旧的一组子节点全部卸载
        oldN.children.forEach(c => unmount(c))
        // 再将新的一组子节点全部挂载到容器中
        newN.children.forEach(c => patch(null, c, container))
      } else {
        // 此时:
        // 旧子节点要么是文本子节点,要么不存在
        // 但无论哪种情况,我们都只需要将容器清空,然后将新的一组子节点逐个挂载
        setElementText(container, '')
        newN.children.forEach(c => patch(null, c, container))
      }
+    } else { // 代码运行到这里,说明新子节点不存在
+      if (Array.isArray(oldN.children)) {
+        oldN.children.forEach(c => unmount(c))
+      } else if (typeof oldN.children === "string") {
+        // 旧子节点是文本子节点,清空内容即可
+        setElementText(container, "")
+        // 如果也没有旧子节点,那么什么都不需要做
+      }
+    }
  }
结果:

Vue设计与实现:挂载与更新

Vue设计与实现:挂载与更新

文本节点和注释节点

注释节点与文本节点不同于普通标签节点,它们不具有标签名称,所以需要人为创造一些唯一的标识

// 文本节点的 type 标识
const Text = Symbol()
const newVNode = {
  // 描述文本节点
  type: Text,
  children: '我是文本内容'
}

文本节点

解决思路:

  1. 在patch增加判断type是不是等于Text(自定义的type)
  2. 判断旧vnode是否存在,不存在就根据新vnode创建文本节点再插入容器
  3. 如果存在就直接替换文本内容就行,因为一开始就判断标签内容不一样就会把旧vnode卸载
function patch(oldN, newN, container) {
    // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
    if (oldN && oldN.type !== newN.type) {
      unmount(oldN)
      oldN = null
    }
    // 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
    const { type } = newN
    // 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
    if (typeof type === "string") {
      // 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
      if (!oldN) {
        mountElement(newN, container)
      } else {
        patchElement(oldN, newN)
      }
+    } else if (type === Text) { // 如果新 vnode 的类型是 Text,则说明该 vnode 描述的是文本节点
+      if (!oldN) {
+        // 使用 createTextNode 创建文本节点
+        const el = createText(newN.children)
+        // 将文本节点插入到容器中
+        insert(el, container)
+      } else {
+        // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
+        const el = newN.el = oldN.el
+        if (oldN.children !== newN.children) {
+          setText(el, newN.children)
+        }
+      }
+    }
    else if (typeof type === 'object') {
      // 如果 newN.type 的值的类型是对象,则它描述的是组件
    } else if (type === 'xxx') {
      // 处理其他类型的 vnode
    }
  }

结果:

Vue设计与实现:挂载与更新

注释节点

注释节点的处理方式与文本节点的处理方式类似。不同的是,我 们需要使用 document.createComment 函数创建注释节点元素

// 注释节点的 type 标识
const Comment = Symbol()
const newVNode = {
  // 描述注释节点
  type: Comment,
  children: '我是注释内容'
}

解决思路:

function patch(oldN, newN, container) {
    // 如果新旧 vnode 的类型不同,则直接将旧 vnode 卸载
    if (oldN && oldN.type !== newN.type) {
      unmount(oldN)
      oldN = null
    }
    // 代码运行到这里,证明 oldN 和 newN 所描述的内容相同
    const { type } = newN
    // 如果 newN.type 的值是字符串类型,则它描述的是普通标签元素
    if (typeof type === "string") {
      // 如果 oldN 不存在,意味着挂载,则调用 mountElement 函数完成挂载
      if (!oldN) {
        mountElement(newN, container)
      } else {
        patchElement(oldN, newN)
      }
    } else if (type === Text) { // 如果新 vnode 的类型是 Text,则说明该 vnode 描述的是文本节点
      if (!oldN) {
        // 使用 createTextNode 创建文本节点
        const el = createText(newN.children)
        // 将文本节点插入到容器中
        insert(el, container)
      } else {
        // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
        const el = newN.el = oldN.el
        if (oldN.children !== newN.children) {
          setText(el, newN.children)
        }
      }
+    } else if (type === Comment) {
+      if (!oldN) {
+        // 使用 createTextNode 创建文本节点
+        const el = createComment(newN.children)
+        console.log(el,container)
+        // 将文本节点插入到容器中
+        insert(el, container)
+      } else {
+        // 如果旧 vnode 存在,只需要使用新文本节点的文本内容更新旧文本节点即可
+        const el = newN.el = oldN.el
+        if (oldN.children !== newN.children) {
+          setText(el, newN.children)
+        }
+      }
    }
    else if (typeof type === 'object') {
      // 如果 newN.type 的值的类型是对象,则它描述的是组件
    } else if (type === 'xxx') {
      // 处理其他类型的 vnode
    }
  }

结果:

Vue设计与实现:挂载与更新

Fragment

与文本节点和注释节点类似,片段也没有所谓的标签名称,因此也需要为片段创建唯一标识,Fragment 本身并不会渲染任何内容,所以渲染器只会渲染 Fragment 的子节点

const Fragment = Symbol()
const vnode = {
  type: Fragment,
  children: [
    { type: 'li', children: 'text 1' },
    { type: 'li', children: 'text 2' },
    { type: 'li', children: 'text 3' }
  ]
}

解决思路:

  1. 在patch增加判断type是否是Fragment
  2. 如果oldN不存在就遍历newN的children递归patch挂载
  3. 存在oldN就替换oldN的children
  4. 如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
function patch(oldN, newN, container) {
  if (oldN && oldN.type !== newN.type) {
    unmount(oldN)
    oldN = null
  }

  const { type } = newN

  if (typeof type === 'string') {
    // 省略部分代码
  } else if (type === Text) {
    // 省略部分代码
+  } else if (type === Fragment) { // 处理 Fragment 类型的 vnode
+    if (!oldN) {
+      // 如果旧 vnode 不存在,则只需要将 Fragment 的 children 逐个挂载即可
+      newN.children.forEach(c => patch(null, c, container))
+    } else {
+      // 如果旧 vnode 存在,则只需要更新 Fragment 的 children 即可
+      patchChildren(oldN, newN, container)
+    }
+  }
}

 function unmount(vnode) {
+    // 在卸载时,如果卸载的 vnode 类型为 Fragment,则需要卸载其 children
+    if (vnode.type === Fragment) {
+      vnode.children.forEach(c => unmount(c))
+      return
+    }
    // 获取 el 的父元素
    const parent = vnode.el.parentNode
    // 调用 removeChild 移除元素
    if (parent) parent.removeChild(vnode.el)
  }

结果:

Vue设计与实现:挂载与更新 Vue设计与实现:挂载与更新

转载自:https://juejin.cn/post/7268658252567609402
评论
请登录