# React 实践揭秘之旅,中高级前端必备(上) > 感谢大家又被我的标题党骗进来了😂。这是我最近几个月亲身探寻过的一趟旅途,感受颇深。途中也遇到许多困难,但坚持到底,相信最终一定会让各位小伙伴受益匪浅,不虚此行! > > 同时也感恩大家对之前一个系列文章的喜欢和修正😘,提了不少的建议和问题,我也会用心写我的每一系列文章,与大家共同成长!! > > 跪求点赞、关注、Star![更多文章猛戳 ->](https://github.com/xd-tayde/blog) > > 下篇已经新鲜出炉: **[React 实践揭秘之旅,中高级前端必备(下) ->](https://github.com/xd-tayde/blog/blob/master/ReactGL-2.md)** ## 引言 之前面试三部曲简明地梳理了前端知识结构体系,浅尝辄止。这个系列则要进一步研究和领会 **内在的奥妙**。今天打算以一个比较新颖的角度切入,深入地梳理下 `React` 的内部实现。 - **1. 有利于大家在 React 日常业务使用中更加得心应手**; - **2. 也可将领会到的思想融会贯通,拓展到其它领域**; 那如何深入研究一个玩具呢?最好的方式便是: **拆解 - 重组装**。 众所周知,`React` 是一款非常神奇的 `Web UI` 框架,得益于强大的架构,**逻辑层** 与 **视图层** 的解耦,使其思想及开发模式可以很好地移植到其它平台,例如 `React-Native`。所以今天,我打算跳出常规的 `Web-DOM`,目标是 **以类 React 的模式来进行 Web 游戏开发**。其实就是我们需要实现一套 `React` 上层,并把底层对接 `WebGL API`。期待这样不同的视角,能给大家带来新的启发与帮助。😄 > Tips: > > `WebGL` 不是本文关注的重点,使用到的 API 也非常有限,并不需要你具备相关的知识储备。 ## 第一站: 穿越之门 - JSX 作为一名前端,我们需要实现一个个展示给用户的页面。因此视图层便是我们的工作之始。前端领域不断地快速发展,最终都是为了解答 **如何更高效地开发更完美的页面**。而 `React` 就是答案之一,其中 `JSX` 便是重要的第一站。 那什么是 `JSX` 呢? **`JSX` 就是在 `JS` 环境中约定的一种类 `HTML` 或 `XML` 的 动态模板语法,有着极高的 可读性 与 可拓展性,目的是 为了能更便捷地使用 JS 搭建视图结构与布局。** ```js // 这就是 JSX const jsx = Hello World ``` 这是一种新全新的 `JS` 语法,不属于标准,成功地把类 `HTML` 的标签型模板语法引入 JS 中,创造了一种全新高效的开发模式。但即使最新版的 V8 引擎也无法支持,那怎么执行呢?钥匙就是 **预编译**!得益于 `Babel` 的强大,我们可以通过 **预编译,将代码编译成浏览器看得懂的 `JS`**。 ### babel-plugin-transform-jsx 这是 `Babel` 的一款插件,主要的功能就是编译 `JSX`,直接配置即可 (对编译感兴趣的童鞋,可以继续深入下了解下 `Babel` 的编译原理,这里就不作展开了)。 ```js // 打包工具中的 babel loader 配置 use: { loader: 'babel-loader', options: { plugins: [["transform-jsx", { "function": "ReactWebGL.createElement", "useVariables": true }]], } } ``` 配置完毕,我们先来写段 `JSX` 试试。由于我们不使用 `DOM` 了,视图层被直接绘制于 `canvas` 上,就自然不能使用常规的 `HTML` 标签了。我们先来定个容器标签 (``)。 ```js const jsx = ( ReactWebGL Hello World Child ) ``` 咦。秒报错。先不管,我们来看下编译后的文件: ```js var jsx = ReactGL.createElement({ elementName: Container, attributes: { name: 'parent' }, children: [ "ReactWebGL Hello World", ReactWebGL.createElement({ elementName: Container, attributes: { name: "child" }, children: ["Child"] }), ] }); ``` 原来如此,其实 `Babel` 做的,就是把上面的 `JSX` 模板代码, 解析并提取出标签的信息后,转换成常规的函数形式。这个函数就是我们配置中指定的 `ReactWebGL.createElement`。 接下来我们来看下报错。很明显,是因为在上下文中有一些变量没有进行定义,那接下来我们先定义上: ```js // 由于编译是直接 变量传递 // 因此标签名作为一个变量需要先定义为 string; const Container = 'Container' // 匹配配置中的 `ReactWebGL.createElement` const ReactWebGL = { createElement(tag) { console.log(tag) } } ``` ## 第二站: 天空之城 - Virtual DOM 来到第二站: 什么是 **虚拟DDM** 呢? 顾名思义,**它并不是真正的视图元素,而是将真实的视图元素抽象为一个 Javascript 对象,包含完整描述了一个真实元素的所有信息,但并不具备渲染功能**。同时由于 `DOM` 本身便是 **树形结构**,因此使用 `Javascript` 对象便能很好对整个页面结构进行描述。 刚才 `JSX` 编译后,参数便是一个最简单的 **虚拟DOM** 对象(我们称为 `VNode`): ```js // 一个最简单的 VNode { // 类型 type: 'Container' // 属性 props: { name: 'parent' } // 子级列表 children: [...] } ``` 这其实只是一个普通的 `Javascript` 对象,那为什么要设计它呢?可能很多人都会有这种观念: **虚拟DOM 快啊,diff 算法非常厉害,直接操作真实的 DOM 消耗很大。** 掐指一算,事情并没有那么简单,听我细细道来🧐。想象下,当出现以下场景时: - **初次渲染**: - 解析 `JSX`,生成 **虚拟DOM 树**,然后经过各种计算,最终调用 `DOM` 绘制一个个视图元素; - 很明显,我们通过 `HTML` 或 `innerHTML` 直接创建元素会更快,且白屏时间更短,多了上层的 **计算消耗与内存消耗**,反而是一种 **性能损耗**; - **极小更新**: - 需要修改一个标题文案,调用 `setState` 触发更新。此时,`React` 并不知道新的 `state` 会引起多大的变动,需要经过全局逐一的 `diff` 确定变动的元素再触发更新。 - 相较而言,获取对应标题元素修改,省去 `diff`,会更直接高效。那这么说来,**虚拟DOM** 反而变慢了,那为什么还要用它呢? 如果你的场景是 大量且分散 地更新页面中的元素,那 **虚拟DOM** 能大大减少其业务逻辑的复杂度,达到一个比较高的消耗性价比。直接操作 `DOM` 元素,一来逻辑复杂,代码健壮性弱;二来错误的操作反而可能导致更差的性能。 所以快慢并不是衡量 **虚拟DOM** 价值的因素,其价值更多在于: - **虚拟DOM** 其实是一种 **牺牲最小性能与空间,换取 架构优化 的方式**,能较大提升项目的可拓展性与可维护性; - 对 `DOM` 的操作进行 **集中化管理**,更加安全稳定且高效; - **渲染** 与 **逻辑** 的解耦,高度的 **组件化** 与 **模块化**,提升了代码复用性及开发效率; - **虚拟DOM** 是一种抽象化的对象,可以对接不同的渲染层,完成 **跨平台渲染**。例如 `RN / SSR` 等方案; `JSX` 仅仅是一种 **独立的动态模板语法**,通过编译转化为 **虚拟DOM**,跟 `React` 并无耦合。因此可以将其 **接入到任意环境或者框架**,例如我们也可以在 `Vue` 中使用 `JSX`。 聊完 **虚拟DOM**,我们回归主线。需要先来设计一个最简单的 `VNode` 结构,以刚才 `Babel` 编译出的结构为基础,加上额外的值,方便渲染。 ```js // VNode 定义 interface VNode { // 标签类型 type: any, // 标签属性 props: { [key: string]: any }, // 子级列表 children: VNode[], // 唯一标识 key: string // 获取视图元素 ref: any // 视图元素 elm: any // 文本内容 text: string | number | undefined } // VNode 生产函数 function createVNode(type, props, ref, key, children, elm, text) { return { type, props, children, ref, key, elm, text, } } ``` 现在定义了 `VNode` 后,我们就可以开始编写对应的生成函数(`createElement`)了。 ## 第三站: 转换之桥 - createElement 这个函数就是传说中的 `h` 函数,用于 **模板 到 虚拟DOM 之间的桥梁**。在现在的大多数主流 **虚拟DOM** 库中,都拥有该函数。在 `React` 中,它是通过 `Babel` 将 `JSX` 编译成 `h` 函数,即 `React.createElement`。而在 `Vue` 中,则是通过 `vue-loader` 将 `