新蜂商城开源仓库:https://github.com/newbee-ltd(内涵 Vue 2.x 和 Vue 3.x 的 H5 商城开源代码)
Vue 3.x + Vant 3.x 高仿微信记账本开源地址:https://github.com/Nick930826/daily-cost
写在前面
这篇文章我构思了很久,想用比较白话的形式阐述关于 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 App from './App';
import React from 'react'
import './index.css'
// 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
的事件机制,也是自身单独实现了一份,不是上述描述的这么简单。
虚拟 DOM 存在的意义
这个从我个人角度理解的话有以下几点。
DOM 操作更加方便吗
直接操作 DOM
,和间接操作 JS
来控制虚拟 DOM
,在我看来各有千秋。
🤔 思考一下,虽然频繁的直接操作 DOM
,浏览器重绘页面,会带来一定的性能开销。但是,当项目到了一定的复杂程度之后, 虚拟 DOM
的 diff
算法也同样会造成浏览器的性能开销,这里我没有数据作为依据,纯靠个人瞎掰。(逃)
开发爽了
但是从开发体验的角度出发,你不得不佩服这种全新的开发模式。你只要写一个函数方法,就能将其渲染成页面,这对于将项目模块化、组件化,起到了至关重要的帮助。
跨平台
我认为这个才是将虚拟 DOM
发挥到极致的地方,大家应该都有耳闻,uni-app、taro 等第三方解决方案,都可以一套代码多端运行,究其远离,还是虚拟 DOM
的功劳。上述代码中,我简单的通过手写 MyRender
函数,将虚拟 DOM
转化为真实 DOM
,不是简单的想让大家了解这一个能力,而是通过这个方法映射出,其实还可以通过其他的复杂操作,将虚拟 DOM
转化为小程序(各大平台)、App等代码形式。
总结
这篇文章,再以 Vue
角度出发,也是同样走得通的,所以我觉得技术上的东西,没有什么框架之分。框架为我们做的事情,其实在本质上其实都差不多。我再说一句 大言不惭 的话,前端知识也就这样,关键是你能不能利用这些知识去创造更多有价值的东西,就比如尤雨溪、Dan这些大佬。