这是我参与11月更文挑战的第20天,活动详情查看:2021最后一次更文挑战
在这篇我们主要来讲解JSX,从以下三个问题入手
- 什么
JSX?他和js有什么关系? JSX底层原理是什么?- 为什么
React要选择JSX?
什么是JSX?
借用官网的一句话:它被称为 JSX,是一个 JavaScript 的语法扩展。JSX作为描述组件内容的数据结构,为JavaScript赋予了更多的视觉表现力。
JSX底层原理是什么?
我们在Babel官网上进行尝试,在左侧输入我们的jsx内容,右侧会对其编译成如下内容:
由图可知,JSX在编译时会被Babel编译为React.createElement方法。也就说明了我们在组件中即使没有使用React模块,为什么还要引入他,因为我们的JSX被编译后会使用它,不然会报错,而在React17.0后我们不需要引入React模块也能使用JSX(如果要使用React其他功能就需要引入),具体解释可以见官网:介绍全新的 JSX 转换。
createElement
从上面得知JSX底层就是React.createElement方法,那我们来看看这个方法是怎么实现的
* Create and return a new ReactElement of the given type.
* See https://reactjs.org/docs/react-api.html#createelement
*/
/**
*
* @param {*} type 类型:html标签类型(div)、组件(函数组件、类组件),告诉我们他是个什么类型的组件
* @param {*} config 传入的配置:props、classname等
* @param {*} children :子节点(文本内容,子组件)
* @returns
*/
export function createElement(type, config, children) {
// propName 变量用于储存后面需要用到的元素属性
let propName;
// Reserved names are extracted
// props 变量用于储存元素属性的键值对集合
const props = {};
// React 元素属性
let key = null;
let ref = null;
let self = null;
let source = null;
// 传入了 config ==> props classname style等
if (config != null) {
// 依次对 ref、key、self 和 source 属性赋值
if (hasValidRef(config)) {
ref = config.ref;
if (__DEV__) {
warnIfStringRefCannotBeAutoConverted(config);
}
}
if (hasValidKey(config)) {
if (__DEV__) {
checkKeyStringCoercion(config.key);
}
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// RESERVED_PROPS 是 key、ref、self、source
// 除了上面四个属性,将剩下的其他属性依次添加到我们声明的 props 对象中,
for (propName in config) {
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
// 获取子节点,除了type、config后的参数都将作为子节点
const childrenLength = arguments.length - 2;
// 只有一个一个子节点,直接赋值给 props.children 属性
if (childrenLength === 1) {
props.children = children;
} else if (childrenLength > 1) {
// 不止一个子节点时,便利存到一个数组中,在后面赋值给 props.children 属性
const childArray = Array(childrenLength);
for (let i = 0; i < childrenLength; i++) {
childArray[i] = arguments[i + 2];
}
// 开发环境时不允许修改 childArray 数组
// 见[Object.freeze](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze)
if (__DEV__) {
if (Object.freeze) {
Object.freeze(childArray);
}
}
// 将我们得到的 children 子节点(数组) 赋值给 props.children 属性
props.children = childArray;
}
// 处理 defaultProps(设置的默认值),处理组件时,有自己默认的 defaultProps,将它与我们定义的 props 进行合并
/**
const FunctionComponent = (props) => <div className='function-title'>FunctionComponent, {props.name} </div>
* FunctionComponent.defaultProps = { name: 'xiong' }
*/
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
// props 没传,就以 defaultProps 为准;传了就以 props 为准
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 省略... 对开发环境的处理
// ReactElement 函数就是对我们的参数合并,然后添加 React节点的标识符 $$typeof: REACT_ELEMENT_TYPE;_owner 等属性
// 返回一个 ReactElement 元素
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
ReactElement
const ReactElement = function(type, key, ref, self, source, owner, props) {
// 将我们传入的参数组合成 element,
const element = {
// This tag allows us to uniquely identify this as a React Element
$$typeof: REACT_ELEMENT_TYPE, // React节点的标识符
// Built-in properties that belong on the element
type: type, // 对应的html的标签
key: key,
ref: ref,
props: props,
// Record the component responsible for creating this element.
_owner: owner,
};
// ....
// 开发环境对我们的参数进行冻结,不许修改 Object.freeze
// 返回我们生成的包含组件数据的对象 element
return element;
};
看完以上两个函数,我们是否可以大胆猜测,我们在写了JSX后React对应帮我们做了哪些事,JSX被转换成了什么?
JSX会被解析成React.createElement方法嵌套调用,该方法接受多个参数,第一个参数代表类型(标签名、组件名),第二个是传入的props、classname等,第三个及以后是他的子节点React.createElement方法会做什么操作呢?首先对ref、key、self 和 source属性赋值,然后从config中取出其他属性放入到props中,接着去解析他的子节点(可能有多个),作为props.children,并且将props和组件的defaultProps合并,最后调用ReactElement函数,将我们组合好的数据传入ReactElement函数对我们的参数进行合并,重点是加入React节点的标识符
$$typeof: REACT_ELEMENT_TYPE,返回组合好的对象
const jsx = <div className='jsx-title'>jsx</div>
// ===>
{
$$typeof: Symbol(react.element)
key: null
props: {className: 'jsx-title', children: 'jsx'}
ref: null
type: "div" // 标签名、类名、函数、匿名函数(() => {})
_owner: null
_store: {validated: true}
_self: undefined
_source: {fileName: '/Users/xiongling/Desktop/react-analysis/react-demo/src/App.js'
}
至此,我们的JSX分析完毕,最终就会被解析成一个React Element对象。
为什么React要选择JSX?
其实更准确的说是我们为什么要使用JSX?首先如果我们不适用JSX,那我们就要在代码里面写上百个React.crearteElement,各种循环嵌套,可读性也不高,一不小心就写错了,很有可能写不明白(遭到开发者的弃用)。所以当我们使用JSX语法糖后,可以让开发者使用熟悉的HTML标签来创建虚拟DOM,降低学习成本,提升开发效率与开发体验。
其他知识
如何判断React.createElement返回的对象就是React Element?
在React中提供了一个全局API React.isValidElement,一个非null对象并且有React节点的标识符$$typeof: REACT_ELEMENT_TYPE,那么他就是合法的React Element。
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
如何区分类组件和函数组件?
类组件和函数组件都会变成React Element对象,其中有一个type属性标识他的类型,类组件就是他的类名,函数组件就是函数名或者匿名函数
AppClass instanceof Function === true;
AppFunc instanceof Function === true;
有上面可知无法通过引用类型区分ClassComponent和FunctionComponent。那么React是如何区分的呢?React通过ClassComponent实例原型上的isReactComponent变量判断是否是ClassComponent。
ClassComponent.prototype.isReactComponent = {};
JSX与Fiber节点是一样的吗?
不是一样的,JSX是一种描述当前组件内容的数据结构,他不包含组件schedule、reconcile、render所需的相关信息。
比如如下信息就不包括在JSX中:
- 组件在更新中的
优先级 - 组件的
state - 组件被打上的用于Renderer的
标记
参考文章: