最近一整子有在阅读 【我不是外星人】的 React 进阶实践指南。很多地方看一遍两遍理解的还是不够透彻,所以用笔记的方式记录下来,方便后续理解。

开篇

学习 React 一定离不开 JSX ,当其他人问起 “你是怎么理解 JSX 的 ?”
大概率我们都会说:“通过 babel 转换,将 jsx 转化为 React.createElement …. ”
那我抛出几个问题 ?

  • 什么是 JSX ?
  • 为什么是它
  • 浏览器是如何 “认识” JSX 的

    什么是 JSX(JavaScriptXml)

    JSX 是一种 JavaScript 的语法扩展,首先在React中被进入,其格式比较像是模版语言,但事实上完全是在JavaScript内部实现的。元素是构成 React 应用的最小单位,JSX 就是用来声明 React 当中的元素。React 主要使用 JSX 来描述用户界面,但React并不强制要求使用JSX [1] ,而JSX也在React之外的框架得到了广泛的支持,包括 Vue.js ,Solid 等。
    简单理解:

  • JSX 就是 React 用来描述用户界面的一个模版,在这个模版里面既可以写 JS 又可以写 HTML 标签。

  • 又或者说 JSX 其实是 React.createElement 的语法糖。

简单理解一下转化,后面我们会详细介绍部分内容。

  1. <MyButton color="blue" shadowSize={2}>
  2. Click Me
  3. </MyButton>

会编译为:

  1. React.createElement( MyButton, {color: 'blue', shadowSize: 2}, 'Click Me' )

为什么是 JSX

这个问题其实可以换个角度思考,想想 React 需要什么 ?

  • 一个声明式的编程方式(声明式编程不用告诉计算机问题领域,从而避免随之而来的副作用)
  • 代码结构尽可能的简洁
  • 样式、结构和事件尽可能的可以实现高内聚,实际上 Vue3 options Api 的转化也是学习了 React 中的设计思想
  • 不想引入新的概念,在原生 JS 的基础上进行扩展即可。(Vue 中的单文件组件 就是一个新的概念,需要学习很多指令)

知道了上面 React 需要啥,那为什么选择 JSX 呢 ?这个问题就引刃而解了,很显然,我们谈到 React 设计需要的这些特点都是指向了 JSX

浏览器是如何认识 JSX 的 ?

我们知道浏览器是无法直接识别 JSX 的,那我们只能通过一些特殊的手段来将其转化,让其变为一个一个的 dom 节点,然后再在这个节点上添加一些样式,事件。
通过官网的 babejs 在线转换 ,我们可以看到一个一个标签都转化为了 React.createElement 方法。
image.png

React.createElement 简称 h 函数,在 Vue 中也是类似的叫法。h是指 hyperscript,一种可以通过 js 来创建 html 的库。设计思想以及作用都是和 hyperscript 是一样的,所以简称为 h 函数没啥问题。
想知道 React.createElement 做了什么,我们还是得来看看其内部的实现, 此处代码转于 【深入理解 jsx

  1. export function createElement(type, config, children) {
  2. // propName 变量用于储存后面需要用到的元素属性
  3. let propName;
  4. // props 变量用于储存元素属性的键值对集合
  5. const props = {};
  6. // key、ref、self、source 均为 React 元素的属性,此处不必深究
  7. let key = null;
  8. let ref = null;
  9. let self = null;
  10. let source = null;
  11. // config 对象中存储的是元素的属性
  12. if (config != null) {
  13. // 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
  14. if (hasValidRef(config)) {
  15. ref = config.ref;
  16. }
  17. // 此处将 key 值字符串化
  18. if (hasValidKey(config)) {
  19. key = '' + config.key;
  20. }
  21. self = config.__self === undefined ? null : config.__self;
  22. source = config.__source === undefined ? null : config.__source;
  23. // 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
  24. for (propName in config) {
  25. if (
  26. // 筛选出可以提进 props 对象里的属性
  27. hasOwnProperty.call(config, propName) &&
  28. !RESERVED_PROPS.hasOwnProperty(propName)
  29. ) {
  30. props[propName] = config[propName];
  31. }
  32. }
  33. }
  34. // childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
  35. const childrenLength = arguments.length - 2;
  36. // 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
  37. if (childrenLength === 1) {
  38. // 直接把这个参数的值赋给props.children
  39. props.children = children;
  40. // 处理嵌套多个子元素的情况
  41. } else if (childrenLength > 1) {
  42. // 声明一个子元素数组
  43. const childArray = Array(childrenLength);
  44. // 把子元素推进数组里
  45. for (let i = 0; i < childrenLength; i++) {
  46. childArray[i] = arguments[i + 2];
  47. }
  48. // 最后把这个数组赋值给props.children
  49. props.children = childArray;
  50. }
  51. // 处理 defaultProps
  52. if (type && type.defaultProps) {
  53. const defaultProps = type.defaultProps;
  54. for (propName in defaultProps) {
  55. if (props[propName] === undefined) {
  56. props[propName] = defaultProps[propName];
  57. }
  58. }
  59. }
  60. return ReactElement(
  61. type,
  62. key,
  63. ref,
  64. self,
  65. source,
  66. ReactCurrentOwner.current,
  67. props,
  68. );
  69. }

通过 React.createElement 创建出来的节点其实浏览器还是不认识的,回想一下我们平常使用 React 的过程,还缺少了一个 render方法。

  1. const element = <div> hello 邵小白 </div>
  2. const container = document.getElementById('root')
  3. ReactDOM.render(element, container)

ReactDom 指的是渲染库,因为我们已经通过React.createElment 创建出一颗树(fiber 树)来了,后面想让哪个平台认识,就做一些平台内部的处理就好了,比如 ReactDOM 就是想让浏览器认识我们 fiber 树的一个工具库。
我们简单实现一下 React.DOM.render 方法,帮助大家理解,其实本质上还是通过 dom 上的 createElement 以及 appendChild 去做的这样一件事情。

  1. render(element,container){
  2. // 判断元素类型
  3. const dom = element.type === 'TEXT_ELEMENT' ?
  4. document.createTextNode('') :
  5. document.createElement(element.type)
  6. // 将元素中 除了children 属性之外的其他 props 添加在需要创建的节点身上
  7. Object.keys(element.props).
  8. filter(key => key !== 'children')
  9. .forEach(name => {
  10. dom[name] = element.props[name]
  11. })
  12. // 递归调用
  13. element.props.children.forEach(child => {
  14. render(child, dom)
  15. })
  16. // 将最后生成的 dom-tree 添加到 容器中
  17. container.appendChild(dom)
  18. }

当然实际上 render 方法不会这么简单,还需要考虑线程阻塞的问题,这里就不过多介绍了。但是通过实现这个 render 方法相信你一定有了新的理解。