Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

简单解析虚拟DOM #14

Open
zhouzhili opened this issue Aug 26, 2019 · 0 comments
Open

简单解析虚拟DOM #14

zhouzhili opened this issue Aug 26, 2019 · 0 comments

Comments

@zhouzhili
Copy link
Owner

目前前端开发框架 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)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant