zhouzhili / blog

个人博客,记录工作学习中遇到的一些知识点,内容以前端为主。
10 stars 1 forks source link

简单解析虚拟DOM #14

Open zhouzhili opened 5 years ago

zhouzhili commented 5 years ago

目前前端开发框架 2 巨头ReactVue都使用到了虚拟 DOM(virtual dom)技术,以及现在面试基本都会问到的问题:你了解虚拟 DOM 吗?那么,现在我就来简单的聊一聊虚拟 DOM

为什么要用虚拟 DOM

在 web 开发中,操作 DOM 是非常损耗性能的,由于浏览器 JS 是单进程,如果在页面渲染方面占用了太长时间,那么在功能影响方法就会堵塞,十分影响用户体验。同时,现代 web 应用越来越庞大,功能也更加复杂,以及 AJAX 的使用,页面也能响应更多的用户操作,页面的改变也越来越大,如果使用以前的 JQuery,那么 web 开发工作将会越发复杂,需要不停的响应用户的操作,更新数据,更新页面,从开发维护到应用性能上都很受影响。

如何提高开发效率,减少维护成本以及提高 web 应用性能呢?React给我们带来了virtual dom

什么是虚拟 DOM

虚拟 DOM 就是使用 JS 模拟出来的 DOM 结果,比如一段简单的 HTML:

<div id="title">this is title</div>

使用 JS 我们可以这样表示:

{
  "tagName": "div",
  "props": {
    "id": "title"
  },
  "children": ["this is title"]
}

对于更复杂的 HTML 标签,我们在 JS 里都可以用以上的结构来模拟:tagNamepropschildren。这样,我们就可以用 JS 来描述页面 UI 了。

如何解析虚拟 DOM

有了虚拟 DOM 之后,我们页面要呈现出来还是需要转换为 HTML 标签才行,那么现在就是如何解析虚拟 DOM 了,现在,我们有这样一段 VNode:

const vNode = {
  tagName: 'div',
  props: null,
  children: [
    {
      tagName: 'p',
      props: {
        class: 'list p',
        onClick: e => {
          console.log(e)
        }
      },
      children: ['apple']
    },
    {
      tagName: 'p',
      props: { class: 'list p' },
      children: ['orange']
    }
  ]
}

通过分析可以发现,这是一个 DIV 标签,同时含有 2 个 P 标签,我们首先通过tagName创建DOMElement,如果没有tagName我们创建TextNode,然后为 Element 添加属性以及子元素,我们可以设计一个 render 函数来解析它,render 函数接收 vNode 以及解析后挂载的位置:

function render(vNode, dom) {
  // 文本标签
  if (isString(vNode)) {
    const text = document.createTextNode(vNode)
    dom.appendChild(text)
    // 数组
  } else if (isArray(vNode)) {
    vNode.forEach(child => render(child, dom))
    // 对象
  } else if (isObject(vNode)) {
    const { tagName, props, children } = vNode
    const root = document.createElement(tagName)
    dom.appendChild(root)
    if (props) {
      addAttribute(root, props)
    }
    if (isArray(children)) {
      children.forEach(child => render(child, root))
    }
  }
}

这里还有一些判断 JS 类型的工具函数:

const isArray = arr => Object.prototype.toString.call(arr) === '[object Array]'
const isObject = obj => Object.prototype.toString.call(obj) === '[object Object]'
const isString = str => Object.prototype.toString.call(str) === '[object String]'

在属性解析里面,我们还可能会有事件需要设置,我们编写一个简单的属性设置函数:

function addAttribute(el, props) {
  Object.keys(props).forEach(key => {
    const val = props[key]
    // 添加事件
    if (key.startsWith('on')) {
      const eventName = key.toLowerCase().slice(2)
      if (eventName) {
        el.addEventListener(eventName, val, false)
      }
    } else {
      el.setAttribute(key, val)
    }
  })
}

使用虚拟 DOM

这样一个简单的虚拟 DOM 解析函数就写好了,它可以根据虚拟 DOM 渲染成真实的 UI 界面,如何测试并使用它呢?我们可以使用 Jason Miller 开发的 htm,它可以无需编译即可在浏览器中使用,十分简单,使用如下:

import htm from '../src/index.mjs'

function h(tagName, props, ...children) {
  return { tagName, props, children }
}

const html = htm.bind(h)

let fruits = ['apple', 'orange']

const handleClick = (e, index) => {
  console.log(e, index)
}

const vNode = html`
  <div>
    ${fruits.map(
      (f, i) =>
        html`
          <p class="list p" onClick=${e => handleClick(e, i)}>${f}</p>
        `
    )}
  </div>
`

render(vNode, document.body)

这样就可以轻松把我们的虚拟 DOM 渲染到页面上了。但是仅仅这样使用的话还不如直接插入innerHTML,在效率上并没有很大的提高,其实虚拟 DOM 的优势还需要结合diff算法才能体现,现在我们只是实现了虚拟 DOM 的解析工作。

以上只是一个简单的虚拟 DOM 示例,实际中,我们可能还会包括类组件,函数组件等,我们需要对他们进行一一的解析,后面将会逐步展开。

其实,虚拟 DOM 真正的意义在于使用函数式来开发 UI,使得UI=h(vNode),其中的 h 函数就是我们解析 VNOde 的函数,并且其他与 UI 相关的绘制工作我们都可以使用函数式来完成,唯一需要修改的就是我们的解析函数了,无论怎么样UI=h(vNode)