开篇
学习 React 一定离不开 JSX ,当其他人问起 “你是怎么理解 JSX 的 ?”
大概率我们都会说:“通过 babel 转换,将 jsx 转化为 React.createElement .... ”
那我先抛出几个问题,希望能帮助你更好的理解 JSX
- 什么是 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 的语法糖。
简单理解一下转化,后面我们会详细介绍部分内容。
<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 方法相信你一定有了新的理解。