JSX 和虚拟 DOM

 

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

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

写在前面

这篇文章我构思了很久,想用比较白话的形式阐述关于 JSX 和 VDOM 的知识点。翻阅了不少相关内容,多数文章都是以源码为基础,讲的内容不能说不好,但是至少我觉得对于刚入门的前端同学,内容篇硬。本篇文章以 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 官网 查看编译后的样子,如下所示:

JSX 和虚拟 DOM

我们来验证一下,直接写成编译后的 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'))
复制代码

浏览器展示如下:

JSX 和虚拟 DOM

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

console.log('App:', App)
console.log('App():', App())
复制代码

打印结果如下:

JSX 和虚拟 DOM

这里你可以看到,在不执行 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 :

JSX 和虚拟 DOM

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

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
  }
}
...
复制代码

浏览器展示如下:

JSX 和虚拟 DOM

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

上一篇:React 虚拟DOM及JSX总结


下一篇:[vue] 你有使用过JSX吗?说说你对JSX的理解