diff算法-patch函数(一)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第21天,点击查看活动详情

前言

diff算法主要是三步:生成虚拟节点、将新旧虚拟节点对比(核心)、新节点替换旧节点。本篇文章主要讲如何实现patch函数中的其中一部分,因为patch内容较多,可以由浅入深的实现。

介绍

上篇文章实现了h函数,h函数功能是生成虚拟节点,下一步我们就来写patch函数,patch函数的功能就是,拿新的虚拟节点和旧的虚拟节点进行对比,然后通过一系列比较,从而把新的虚拟节点生成为真实的DOM节点,放到页面中,这是patch函数的功能,也是diff算法的核心。

前面我们也介绍了,旧的虚拟节点和新的虚拟节点对比规则有很多,例如:不相同的标签,暴力删除;相同节点不同层级,暴力删除;相同节点,存在key会进行替换等等。我们接下来就由简到深的实现patch函数。

我们先来观察一下snabbdom中的patch是如何使用的

image-20220619151648518

<div id="container">原始数据</div>
复制代码

在snabbdom中,patch接收的是两个参数,第一个为旧的真实DOM节点,第二个为新的虚拟DOM节点,但是我们介绍时,是拿新的虚拟节点和旧的虚拟节点进行对比,所以我们的第一步,是将真实DOM转为虚拟DOM,然后再将新的虚拟节点和旧的虚拟节点对比。

实现

  • 创建patch,接收两个参数:新节点和旧节点

    本篇文章主要讨论不是同一个节点的情况,直接暴力删除。

    patch传入新旧节点,并输出image-20220619165929208

    • 判断旧节点是否为虚拟DOM

      如果是真实DOM,就需要转为虚拟DOM。

      如何比较?需要知道真实DOM和虚拟DOM有何区别,真实DOM中有很多属性,但是肯定没有sel属性,而虚拟DOM有sel属性,根据这个属性来判断是否为虚拟节点

      • 不是虚拟节点,就将真实节点变为虚拟节点

        通过前面写的方法vnode,vnode需要传入五个属性:sel、data、children、text、elm

        所以需要根据真实节点,获取到上述五个属性:

        最后可以看看转变为的虚拟节点:

        此时,就完成了第一个参数转为虚拟DOM了

    • 判断是否为同一个节点

      通过两个节点的sel属性,来判断。

      如果是同一个节点,那么就要分析很多种情况,这里暂时不分析。

      主要分析不是同一个节点的情况。

      • 是同一个节点:暂不分析

      • 不是同一个节点:暴力替换

        • 把新的虚拟节点创建为DOM节点

          这里封装一个 createElement() ,传入的参数为新节点

          这里根据虚拟DOM创建DOM节点有两种情况:第三个参数为字符串或者第三个参数为数组。

          如果是字符串的话,直接插入到标签中即可

          如果是数组的话,需要递归

          通过判断节点是否有children属性,vnode.children

          • 第三个参数为字符串

            如果没有children属性,就是字符串,直接通过innerText插入字符串就行

          • 第三个参数为数组

            如果children是数组,就是有子节点,通过递归创建节点

          • 补充elm属性

        • 删除页面旧节点

          拿到旧虚拟节点,节点中 elm属性就是真实节点,通过oldVnode.elm就能拿到真实节点,再通过oldVnodeElm.parentNode.removeChild(oldVnodeElm)删除

        • 将新的DOM节点放入页面中

          判断新节点是否存在,如果存在,将新节点在旧节点的父节点内创建真实节点。

代码

index.js

import h from './handle/h'
import patch from './handle/patch'

const container = document.getElementById('container')


// 第三个参数为字符串的情况
let vnode1 = h('div', {}, 'helloWorld')

// 第三个参数为数组的情况
let vnode2 = h('ul', {}, [
  h('li', {}, 'a'),
  h('li', {}, 'b'),
  h('ul', {}, [
    h('li', {}, 'c'),
    h('li', {}, 'd')
  ])
])

patch(container, vnode1)
// patch(container, vnode2)
复制代码

patch.js

import vnode from './vnode'
import createElement from './createElement'
export default function (oldVnode, newVnode) {
  // 判断旧节点是否为真实节点。通过判断旧节点是否有el属性
  if (oldVnode.el === undefined) {
    // 将旧真实节点转为旧虚拟节点,通过vnode()
    oldVnode = vnode(
      oldVnode.tagName.toLowerCase(), // 获取标签名 el
      {}, // data
      [], // children
      undefined, // text
      oldVnode // 真实节点 elm
    )
  }
  // 判断是否为同一节点,根据新旧节点的el属性panduan 
  if (oldVnode === newVnode) {
    // 暂不分析
  } else { // 不是同一节点,暴力删除
    // 创建 createElement() ,将新虚拟节点转为 真实DOM
    let newVnodeElm = createElement(newVnode)
    // 获取旧虚拟节点,通过 elm 得到旧真实节点
    let oldVnodeElm = oldVnode.elm
    // 创建新节点
    if (newVnode) {
      // 根据旧节点的父级节点(body)来插入
      oldVnodeElm.parentNode.insertBefore(newVnodeElm, oldVnodeElm)
    }
    // 删除旧节点
    oldVnodeElm.parentNode.removeChild(oldVnodeElm)
  }
}
复制代码

vnode.js

export default function (sel, data, children, text, elm) {
  return {
    sel,
    data,
    children,
    text,
    elm
  }
}
复制代码

createElement.js

// 方法接收一个虚拟节点,返回真实DOM
export default function createElement(vnode) {
  // 根据sel,创建DOM节点
  let domNode = document.createElement(vnode.sel)
  // 判断有无子节点,即第三个参数是否为数组
  if (vnode.children === undefined) {
    // 无子节点,就是字符串,直接插入
    domNode.innerText = vnode.text
  } else if (Array.prototype === vnode.children.__proto__) { // 有children,且为数组
    // 有子节点,是数组,需要递归
    for (let child of vnode.children) {
      // 递归子节点
      let childDom = createElement(child)
      domNode.appendChild(childDom)
    }
  }
  // 传入elm属性
  vnode.elm = domNode
  // 返回处理好的真实节点
  return domNode
}
复制代码

效果

原始数据:

image-20220619173211430

情况一:字符串替换

image-20220619173227397

情况二:数组替换

image-20220619173244822

总结

当我们创建了新的虚拟节点,需要通过patch函数,将新的虚拟节点替换旧节点,并展示到页面中。主要思路:将旧的真实节点转为旧的虚拟节点,再将新的虚拟节点和旧的虚拟节点进行比较(核心),本篇文章主要对,不同节点进行比较,不同节点时,通过暴力替换,将虚拟节点转为真实节点,此时有两种情况:字符串和数组,字符串直接插入到元素中即可,数组需要递归。最后删除旧节点,放入新节点。

猜你喜欢

转载自juejin.im/post/7110881926541475870