聊聊 JSX 和虚拟 DOM

4,359 阅读6分钟

新蜂商城开源仓库:github.com/newbee-ltd(内涵 Vue 2.x 和 Vue 3.x 的 H5 商城开源代码)

Vue 3.x + Vant 3.x 高仿微信记账本开源地址:github.com/Nick930826/…

写在前面

这篇文章我构思了很久,想用比较白话的形式阐述关于 JSXVDOM 的知识点。翻阅了不少相关内容,多数文章都是以源码为基础,讲的内容不能说不好,但是至少我觉得对于刚入门的前端同学,内容篇硬。本篇文章以 React 作为切入点,分析理解 JSX 和虚拟 DOM ,当然 Vue 技术栈的同学也可以看,毕竟这两个框架都是互相学习互相借鉴的,知识都是互通的。

还是那句话,这篇文章篇理解,对新手较友好,大佬够自信的话,就此作罢。看完的同学觉得有帮助的话,可以点个赞,让我有继续写下去的动力。前几篇文章评论区有几位同学想了解别的知识,我都记着,等我过年回老家再码吧。

我学习一个知识点,习惯带着问题去找答案,所以本篇文章也不例外,我们带着下面几个问题看文章:

  • JSX 是什么?
  • 用不用 JSX 对开发有什么影响?
  • 虚拟 DOM 长啥样,怎样渲染成真实 DOM ?
  • 虚拟 DOM 存在的意义是什么?

把问题整明白了才是真的实力,别整天想着吊打面试官,面试官做错了什么。(逃)

JSX 是什么

它是 JS 的一个语法扩展。官方是这么定义它的:

JSX 是一个 JavaScript 的语法扩展,但它具有 JavaScript 的全部功能。

React 项目中我们是这样去书写 JSX ,如下:

const App = <div>
  test
</div>

不是说 React 是通过虚拟 DOM 来渲染页面的吗?此时,好像看不出虚拟 DOM 的样子。 别急,首先 babel 会为我们将 JSX 语法变异成 React.createElement() 的形式,具体可以通过 babel 官网 查看编译后的样子,如下所示:

image.png

我们来验证一下,直接写成编译后的 React.createElement 函数,页面会不会正常渲染,我们通过 create-react-app 构建一个 React 基础项目,修改 index.js 如下:

import React from 'react'
import ReactDOM from 'react-dom'

const App = () => {
  return React.createElement(
    "div",
    {
      className: "app"
    },
    "father",
    React.createElement(
      "div",
      null,
      "child"
    )
  )
}

ReactDOM.render(<App />, document.getElementById('root'))

浏览器展示如下:

image.png

我们不妨在 index.js 中打印一下 App 和 App() ,看看有什么不同,如下所示:

console.log('App:', App)
console.log('App():', App())

打印结果如下:

image.png

这里你可以看到,在不执行 App 的时候,它就是一个普通的函数,所以我们应该称它为函数组件 — Componnet ,而执行完后的返回结果,正是我们想要的虚拟 DOM ,这里我们可以称它为 React 元素 — ReactElement

这个 ReactElement 对象实例,本质上是以 JavaScript 对象形式存在的对 DOM 的描述,也就是虚拟 DOM。

上图中的虚拟 DOM 我们可以反推出真实 DOM 是长这样的:

<!--最外层的div-->
<div>
  <!--第一个子节点-->
	father
  <!--第二个子节点是被 div 包裹的,内容是child-->
  <div>child</div>
</div>

所以这时我们就能很自信的说,不用 JSX 开发项目,也是可以的。只要你已经无敌,全都用 React.createElement 去写标签以及标签内的方法、样式、自定义属性等等等等。 反正我肯定没有这么无敌,大傻子才这么"淦"吧。

虚拟 DOM 咋渲染成真实 DOM

我们继续沿用上面通过 create-react-app 构建好的 demo 项目,修改 index.js 如下:

import React from 'react'

// JSX 编写 React 组件
const App = () => <div>
  <div>十三哥:你是什么星座的?</div>
  <div>尼克陈:我是为你量身定座。</div>
</div>

// 自定义虚拟 DOM 转真实 DOM 函数 MyRender。
// vnode:虚拟DOM节点;root:插入的父节点(注意,这里不一定就是 index.html 里的 app 节点)。
const MyRender = (vnode, root) => {
  // 如果没有没有传入 root 节点,则不执行。
  if (!root) {
    return
  }
  let element // 声明一个空变量,用于下面存放节点信息。
  if (vnode.constructor !== Object) {
    // 如果 vnode 的类型为非 Object,则是没有标签包裹的普通字符,直接赋值 element。
    element = document.createTextNode(vnode);
  } else {
    // 否则,则是有标签包裹的类型,通过 createElement 事件创建新的标签,标签名就是 type 属性值。
    element = document.createElement(vnode.type);
  }
  // 塞进父节点 root。
  root.appendChild(element)
  
  // 如果 vnode 有 children 属性,则要进行递归操作。
  if (vnode.props && vnode.props.children) {
    const childrenVNode = vnode.props.children
    // 判断是不是数组,如果是,则进入 forEach 循环执行 MyRender
    if (Array.isArray(childrenVNode)) {
      childrenVNode.forEach((child) => {
        MyRender(child, element)
      })
    } else {
      // 否则直接执行 MyRender
      MyRender(childrenVNode, element)
    }
  }
}

// 初始化执行 MyRender 函数,注意第一个参数需要传入 ReactElement,也就是虚拟 DOM。
MyRender(App(), document.getElementById('root'))

代码解析已经都写在上述代码的注视中,每一行都有解释,认真看完,并不难理解。一顿操作,其实就是想方设法将虚拟 DOM ,通过 JS 方法,渲染成真实 DOM ,然后插入到根节点。 我们通过 npm run start 运行项目,看看浏览器是否能渲染出真实 DOM :

image.png

嚯喔!~~(羞涩)。 甚至你还可以在给“十三哥”来点“绿”,点击“尼克陈”来点方法,代码如下:

const App = () => <div>
  <div className='shisan' style={{ color: 'green' }}>十三哥:你是什么星座的?</div>
  <div onClick={() => console.log('别闹啊')}>尼克陈:我是为你量身定座。</div>
</div>

...
let element
if (vnode.constructor !== Object) {
  element = document.createTextNode(vnode)
} else {
  element = document.createElement(vnode.type)
  // 添加点击事件
  if (vnode.props.onClick) {
    element.addEventListener('click', () => {
      vnode.props.onClick()
    })
  }
  // 添加样式
  if (vnode.props.style) {
    Object.keys(vnode.props.style).forEach(key => {
      element.style[key] = vnode.props.style[key]
    })
  }
  // 添加类名
  if (vnode.props.className) {
    element.className = vnode.props.className
  }
}
...

浏览器展示如下:

Kapture 2021-01-31 at 12.36.56.gif

这里申明, ReactDOM.render 的内容并没有我上述写的那么简单,涉及到的源码也相当庞大,这里只是我简单的将虚拟 DOM 转化成真实 DOM 的一个小用例。包括 React 的事件机制,也是自身单独实现了一份,不是上述描述的这么简单。

虚拟 DOM 存在的意义

这个从我个人角度理解的话有以下几点。

DOM 操作更加方便吗

直接操作 DOM ,和间接操作 JS 来控制虚拟 DOM ,在我看来各有千秋。

🤔 思考一下,虽然频繁的直接操作 DOM ,浏览器重绘页面,会带来一定的性能开销。但是,当项目到了一定的复杂程度之后, 虚拟 DOM 的 diff 算法也同样会造成浏览器的性能开销,这里我没有数据作为依据,纯靠个人瞎掰。(逃)

开发爽了

但是从开发体验的角度出发,你不得不佩服这种全新的开发模式。你只要写一个函数方法,就能将其渲染成页面,这对于将项目模块化、组件化,起到了至关重要的帮助。

跨平台

我认为这个才是将虚拟 DOM 发挥到极致的地方,大家应该都有耳闻,uni-apptaro 等第三方解决方案,都可以一套代码多端运行,究其远离,还是虚拟 DOM 的功劳。上述代码中,我简单的通过手写 MyRender 函数,将虚拟 DOM 转化为真实 DOM ,不是简单的想让大家了解这一个能力,而是通过这个方法映射出,其实还可以通过其他的复杂操作,将虚拟 DOM 转化为小程序(各大平台)、App等代码形式。

总结

这篇文章,再以 Vue 角度出发,也是同样走得通的,所以我觉得技术上的东西,没有什么框架之分。框架为我们做的事情,其实在本质上其实都差不多。我再说一句 大言不惭 的话,前端知识也就这样,关键是你能不能利用这些知识去创造更多有价值的东西,就比如尤雨溪、Dan这些大佬。