新蜂商城开源仓库: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 官网 查看编译后的样子,如下所示:
我们来验证一下,直接写成编译后的 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'))
复制代码
浏览器展示如下:
我们不妨在 index.js
中打印一下 App
和 App()
,看看有什么不同,如下所示:
console.log('App:', App)
console.log('App():', App())
复制代码
打印结果如下:
这里你可以看到,在不执行
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
:
嚯喔!~~(羞涩)。 甚至你还可以在给“十三哥”来点“绿”,点击“尼克陈”来点方法,代码如下:
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
}
}
...
复制代码
浏览器展示如下:
这里申明,
ReactDOM.render
的内容并没有我上述写的那么简单,涉及到的源码也相当庞大,这里只是我简单的将虚拟DOM
转化成真实DOM
的一个小用例。包括React
的事件机制,也是自身单独实现了一份,不是上述描述的这么简单。