最近一整子有在阅读 【我不是外星人】的 React 进阶实践指南。很多地方看一遍两遍理解的还是不够透彻,所以用笔记的方式记录下来,方便后续理解。
开篇
学习 React 一定离不开 JSX ,当其他人问起 “你是怎么理解 JSX 的 ?”
大概率我们都会说:“通过 babel 转换,将 jsx 转化为 React.createElement …. ”
那我抛出几个问题 ?
- 什么是 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 的语法糖。
简单理解一下转化,后面我们会详细介绍部分内容。
<MyButton color="blue" shadowSize={2}>
Click Me
</MyButton>
会编译为:
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 方法。
React.createElement 简称 h 函数,在 Vue 中也是类似的叫法。h是指 hyperscript,一种可以通过 js 来创建 html 的库。设计思想以及作用都是和 hyperscript 是一样的,所以简称为 h 函数没啥问题。
想知道 React.createElement 做了什么,我们还是得来看看其内部的实现, 此处代码转于 【深入理解 jsx】
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// props 变量用于储存元素属性的键值对集合
const props = {};
// key、ref、self、source 均为 React 元素的属性,此处不必深究
let key = null;
let ref = null;
let self = null;
let source = null;
// config 对象中存储的是元素的属性
if (config != null) {
// 进来之后做的第一件事,是依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
}
// 此处将 key 值字符串化
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 接着就是要把 config 里面的属性都一个一个挪到 props 这个之前声明好的对象里面
for (propName in config) {
if (
// 筛选出可以提进 props 对象里的属性
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// childrenLength 指的是当前元素的子元素的个数,减去的 2 是 type 和 config 两个参数占用的长度
const childrenLength = arguments.length - 2;
// 如果抛去type和config,就只剩下一个参数,一般意味着文本节点出现了
if (childrenLength === 1) {
// 直接把这个参数的值赋给props.children
props.children = children;
// 处理嵌套多个子元素的情况
} else if (childrenLength > 1) {
// 声明一个子元素数组
const childArray = Array(childrenLength);
// 把子元素推进数组里
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 最后把这个数组赋值给props.children
props.children = childArray;
}
// 处理 defaultProps
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
通过 React.createElement 创建出来的节点其实浏览器还是不认识的,回想一下我们平常使用 React 的过程,还缺少了一个 render方法。
const element = <div> hello 邵小白 </div>
const container = document.getElementById('root')
ReactDOM.render(element, container)
ReactDom 指的是渲染库,因为我们已经通过React.createElment 创建出一颗树(fiber 树)来了,后面想让哪个平台认识,就做一些平台内部的处理就好了,比如 ReactDOM 就是想让浏览器认识我们 fiber 树的一个工具库。
我们简单实现一下 React.DOM.render 方法,帮助大家理解,其实本质上还是通过 dom 上的 createElement 以及 appendChild 去做的这样一件事情。
render(element,container){
// 判断元素类型
const dom = element.type === 'TEXT_ELEMENT' ?
document.createTextNode('') :
document.createElement(element.type)
// 将元素中 除了children 属性之外的其他 props 添加在需要创建的节点身上
Object.keys(element.props).
filter(key => key !== 'children')
.forEach(name => {
dom[name] = element.props[name]
})
// 递归调用
element.props.children.forEach(child => {
render(child, dom)
})
// 将最后生成的 dom-tree 添加到 容器中
container.appendChild(dom)
}
当然实际上 render 方法不会这么简单,还需要考虑线程阻塞的问题,这里就不过多介绍了。但是通过实现这个 render 方法相信你一定有了新的理解。