React-咱们村刚通网之虚拟 DOM(一)

这是我参与更文挑战的第6天,活动详情查看: 更文挑战javascript

标题灵感来源评论区:html

image.png

1、什么是虚拟 DOM

什么是虚拟 DOM?简单来讲,虚拟 DOM 就是一个模拟真实 DOM 的树形结构,这个树结构包含了整个 DOM 结构的信息。java

正常咱们看到的真实 DOM 是这样的:node

image.png

而虚拟 DOM 则是这样的,包含了标签名称、标签属性、子节点等真实 DOM 信息:react

image.png

2、为何使用虚拟 DOM

虚拟 DOM 既然是模拟真实 DOM 的树形结构,那么为何要用虚拟 DOM 呢?直接操做 DOM 有什么缺点吗?git

直接操做 DOM 没有缺点,可是频繁的操做 DOM 就缺点很大,由于操做 DOM 会引发重排,频繁操做 DOM 时,浏览器会频繁重排,致使页面卡顿。github

浏览器渲染的大体流程以下:web

  1. 解析 HTML 文档,构建 DOM 树;
  2. 解析 CSS 属性,构建 CSSOM 树;
  3. 结合 DOM 树和 CSSOM 树,构建 render 树;
  4. 在 render 树的基础上进行布局, 计算每一个节点的几何结构(重排);
  5. 把每一个节点绘制在屏幕上(重绘);

image.png

重排(也叫回流、reflow)就是当涉及到 DOM 节点的布局属性发生变化时,就会从新计算该属性,浏览器会从新描绘相应的元素(上述第 4 步)。算法

DOM Tree 里的每一个节点都会有 reflow 方法,一个节点的 reflow 颇有可能致使子节点,甚至父点以及同级节点的 reflow。编程

所以,为了提高性能,咱们应该尽可能减小 DOM 操做。

1. 减小 DOM 操做

当有一个表格须要作排序功能时,有出生年月、性别等排序方式可选,当选择某排序方式时,表格将按该方式从新排序。

  • 真实 DOM:排序操做须要将表格内的全部 DOM 树删除后新建;
  • 虚拟 DOM:使用 diff 算法获得须要修改的部分,仅更新须要发生修改的 DOM 节点;

从上可知,虚拟 DOM 经过 diff 算法,帮助咱们大量的减小 DOM 操做。

2. 函数式的 UI 编程方式

从另外一个角度看,虚拟 DOM 为咱们提供了函数式的编程方式,使代码可读性和可维护性更高。

image.png

3、虚拟 DOM 的实现原理

注:该章节的虚拟 DOM 实现原理并非参比 React 源码,而是参比 simple-virtual-dom,可经过该章节简单了解虚拟 DOM 实现原理,React 中的虚拟 DOM 实现可查看 React 官网 Virtual DOM 及内核

虚拟 DOM 经过如下步骤实现:

  1. 构建虚拟 DOM 树;
  2. 比较新旧虚拟 DOM 树差别;
  3. 更新真实 DOM;

1. 构建虚拟 DOM

模拟真实 DOM 树,构建虚拟 DOM 树结构,包含标签名 tagName、属性对象 props、子节点 children、子节点数 count 等属性。

function Element (tagName, props = {}, children = []) {
  // 标签名
  this.tagName = tagName
  // 属性对象
  this.props = props
  // 子节点
  this.children = children
  // key标志
  const { key = void 666 } = this.props
  this.key = key

  // 子节点数量
  let count = 0
  this.children.forEach((child, index) => {
    if (child instanceof Element) {
      count += child.count
    }
    count++
  })
  this.count = count
}
复制代码

建立虚拟 DOM 对象:

console.log(el('div', {'id': 'container'}, [
  el('h1', {style: 'color: red'}, ['simple virtal dom'])
  ]))
复制代码

生成的虚拟 DOM 对象如图:

image.png

将虚拟 DOM 转换为真实 DOM:

Element.prototype.render = function () {
  const el = document.createElement(this.tagName)
  const props = this.props

  for (const propName in props) {
    const propValue = props[propName]
    _.setAttr(el, propName, propValue)
  }

  this.children.forEach((child) => {
    let childEl

    if (child instanceof Element) {
      childEl = child.render()
    } else {
      childEl = document.createTextNode(child)
    }
    el.appendChild(childEl)
  })

  return el
}
复制代码

填充进页面:

document.body.appendChild(el('div', {'id': 'container'}, [
  el('h1', {style: 'color: red'}, ['simple virtal dom'])
  ]).render())
复制代码

效果如图:

image.png

2. 比较两棵虚拟 DOM 树的差别

当数据更新时,须要对新旧虚拟 DOM 树进行对比。

  1. 当新旧节点都是字符串类型时,直接替换;
if (_.isString(oldNode) && _.isString(newNode)) {
    if (newNode !== oldNode) {
      currentPatch.push({ type: patch.TEXT, content: newNode })
    }
  // Nodes are the same, diff old node's props and children
  }
复制代码
  1. 当新旧节点的标签名、key 值相等时,对比属性 Props 以及子节点 children;
if (
  oldNode.tagName === newNode.tagName &&
  oldNode.key === newNode.key
) {
    // Diff props
    var propsPatches = diffProps(oldNode, newNode)
    if (propsPatches) {
      currentPatch.push({ type: patch.PROPS, props: propsPatches })
    }
    // Diff children. If the node has a `ignore` property, do not diff children
    if (!isIgnoreChildren(newNode)) {
      diffChildren(
        oldNode.children,
        newNode.children,
        index,
        patches,
        currentPatch
      )
    }
}
复制代码
  1. 若是新节点存在,且和旧节点标签名不一样,或者 key 不一样,则直接将新节点替换为旧节点。
currentPatch.push({
    type: PATCH_KEY.REPLACE, 
    node: newNode
})
复制代码

总结一下,虚拟 DOM 只在同层级间 Diff,若是标签不一样则直接替换该节点及其子节点。

尝试对比虚拟 DOM 以下:

function renderTree () {
  return el('div', {'id': 'container'}, [
          el('h1', {style: 'color: red'}, ['simple virtal dom']),
          el('p', ['the count is :' + Math.random()])
        ])
}

let tree = renderTree()

setTimeout(() => {
    const newTree = renderTree()
    const patches = diff(tree, newTree)
    console.log(patches)
}, 2000)
复制代码

对比差别为 p 标签的文本节点发生改变,输出结果如图:

image.png

3. 对真实 DOM 进行最小化修改

最后一步是根据 diff 结果,对真实 DOM 进行修改。

遍历真实 DOM 树,若是该 DOM 节点有 diff,则根据 diff 类型,处理 DOM 节点,若是该 DOM 节点无 diff,则遍历其子节点,直至遍历完成。

注:React 实现更优,具体请见 React fiber

function patch (node, patches) {
  var walker = {index: 0}
  dfsWalk(node, walker, patches)
}

function dfsWalk (node, walker, patches) {
  var currentPatches = patches[walker.index]

  var len = node.childNodes
    ? node.childNodes.length
    : 0
  for (var i = 0; i < len; i++) {
    var child = node.childNodes[i]
    walker.index++
    dfsWalk(child, walker, patches)
  }

  if (currentPatches) {
    applyPatches(node, currentPatches)
  }
}
复制代码

尝试更新真实 DOM,代码以下:

function renderTree () {
  return el('div', {'id': 'container'}, [
          el('h1', {style: 'color: red'}, ['simple virtal dom']),
          el('p', ['the count is :' + Math.random()])
        ])
}

let tree = renderTree()
const root = tree.render()
document.body.appendChild(root)

setTimeout(() => {
  const newTree = renderTree()
  const patches = diff(tree, newTree)
  patch(root, patches)
  tree = newTree
}, 2000)
复制代码

效果如图:

1.gif

上图可见,成功更新真实 DOM。

4、总结

本文从什么是虚拟 DOM、为何使用虚拟 DOM、虚拟 DOM 的实现原理等 3 个角度对虚拟 DOM 进行讲述。

虚拟 DOM 经过模拟真实 DOM 的树结构,收集大量 DOM 操做,经过 diff 算法对真实 DOM 进行最小化修改,减小浏览器重排,提高加载速度,达到优化网站性能的做用。

虚拟 DOM 采用函数式编程,让咱们码得更好看更快乐。

可经过 github源码 进行实操练习。

但愿能对你有所帮助,感谢阅读~

别忘了点个赞鼓励一下我哦,笔芯❤️

参考资料

相关文章
相关标签/搜索