你可能并不了解JSX
您好,我是沧沧凉凉,是一名前端开发者,目前在掘金、知乎以及个人博客上同步发表一些学习前端时遇到的趣事和知识,欢迎关注。
这一系列的React原理部分,我总结起来是非常困难的,因为会去综合各种资料,然后阅读,归纳,总结,最后再把自己的理解总结成一篇文章,因为对于React原理部分目前我也处于学习阶段,往往学着学着发现有一个东西没听过,卡住了,然后会把这篇文章暂放,又去学习新的东西,不过好在随着我逐渐深入了解,这些知识慢慢的串成了一条线,逐渐清晰起来。
React技术的更新迭代是非常迅速的,由于中文这边文档比较欠缺,目前很多介绍React的一些文章可能有部分内容已经是过时信息,而本篇文章着重就是梳理一下我在最近准备面试的过程中,对React进行深入学习,我发现我之前在写项目的过程中,虽然天天写JSX,但却从来没有深入理解过,为什么JSX能够被正确的渲染
JSX基础
其实React官方在深入 JSX这篇文章中,对于JSX的讲解非常详细,我这里会挑选一些重点重新梳理一遍,如果大家有时间,我还是推荐读一下官方文档。
JSX仅仅只是一个语法糖,其实它会被转换成一个方法:React.createElement(component, props, ...children)
。
也正是因为JSX最终都会被编译为React.createElement(component, props, ...children)
这种形式的代码,所以React(React 17之前)必须要在自定义组件中进行引入,比如下面的代码:
// 需要引入 React
import React from 'react';
function Test() {
return <div />;
}
点语法
使用点语法来引用一个React组件其实也是十分常见的,比如Antd中的<Form.Item />
组件,还有React文档中提供的下面的例子:
import React from 'react';
const MyComponents = {
DatePicker: function DatePicker(props) {
return <div>Imagine a {props.color} datepicker here.</div>;
},
};
function BlueDatePicker() {
return <MyComponents.DatePicker color="blue" />;
}
自定义组件
以小写字母开头的元素代表一个 HTML 内置组件,比如div,span,因为在转换的过程中,小写开头的元素会被转换成字符串传入React.createElement,比如”div”,”span”。
所以用户自定义的组件必须以大写开头!如 <Foo />
会编译为 React.createElement(Foo)
。而不会将它转换为字符串。
如果你非要以小写开头,那么得将它赋值给一个大写字母开头的变量。
import React from "react";
// 组件应该以大写字母开头,如果非要以小写开头,那么在使用之前需要赋值给一个大写开头的变量
function hello(props) {
return <div>Hello {props.toWhat}</div>;
}
export default function HelloWorld() {
// 赋值给一个大写开头的变量
const Hello = hello;
return <Hello toWhat="World"/>;
}
动态组件
如果要根据某个变量来决定组件的动态渲染,如下面的情况,那么你得先使用一个大写字母开头的变量去接收:
import React, { useState } from 'react';
const components = {
photo: () => <div>显示照片</div>,
video: () => <div>显示视频</div>,
};
export default function Story() {
const [storyType, setStoryType] = useState<'photo' | 'video'>('photo');
const SpecificStory: () => JSX.Element = components[storyType];
return (
<div>
<SpecificStory/>
<button
onClick={() => {
setStoryType('video');
}}
>
切换
</button>
</div>
);
}
效果:
条件渲染
React的文档中对于这一部分也介绍的非常详细,具体可以参考条件渲染,这里我只介绍我经常使用到的这一部分。
这里谈及几个常用的,官方的条件渲染这一章讲的更加的详细:
与运算符&&
这个形式的条件渲染我在写项目的时候运用的特别多,这也是我比较推荐的写法,
import React, { useState } from 'react';
export default function TestComponent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
{isVisible && <div>isVisible为true的时候才渲染</div>}
<button
onClick={() => {
setIsVisible(!isVisible);
}}
>
切换
</button>
</div>
);
}
最终的渲染结果:
三元表达式
import React, { useState } from "react";
export default function TestComponent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
{isVisible ? <div>isVisible为true的时候才渲染</div> : <div>isVisible为false的时候才渲染</div>}
<button
onClick={() => {
setIsVisible(!isVisible);
}}
>
切换
</button>
</div>
);
}
具体的渲染如下:
某些文档或者某些项目上面会以三元表达式来替代与运算符 &&,两者都可以达到同样的效果,但是我个人觉得使用三元表达式来替代&&会增加代码阅读时的复杂性。
import React, { useState } from 'react';
export default function TestComponent() {
const [isVisible, setIsVisible] = useState(true);
return (
<div>
{
// 这里用三元表达式可以实现&&同样的效果
isVisible ? <div>isVisible为true的时候才渲染</div> : null
}
<button
onClick={() => {
setIsVisible(!isVisible);
}}
>
切换
</button>
</div>
);
}
if语句
if语句在做一些比较复杂的交互时特别有用,比如一个界面某个变量为特定值的时候,会渲染不同的元素。
import React, { useState } from 'react';
export default function TestComponent() {
const [count, setCount] = useState(0);
return (
<div>
<IfTestComponent count={count} />
<div>当前count值:{count}</div>
<button
onClick={() => {
setCount(count + 1);
}}
>
切换
</button>
</div>
);
}
function IfTestComponent({ count }: { count: number }) {
if (count === 1) {
return <div>这是count等于1时的渲染</div>;
}
if (count === 2) {
return <div>这是count等于2时的渲染</div>;
}
if (count === 3) {
return <div>这是count等于3时的渲染</div>;
}
// 其它情况下就不再进行渲染
return null;
}
最终的渲染结果如下:
循环
关于循环的内容有点多,可以参考一下官方的列表 & Key这篇文章,我在之前做项目中,手动进行列表遍历渲染的情况并不多,一般这种数据都是直接放入Antd的Table组件中,以表格的形式展示出来。
我这里主要讲解一下循环的用法:
import React from "react";
const data = [
{ id: 1, name: '张三', age: 18, sex: '男' },
{ id: 2, name: '李四', age: 19, sex: '男' },
{ id: 3, name: '王五', age: 17, sex: '女' },
];
export default function TestComponent() {
return (
<div>
{data.map((item) => (
<div>
姓名:{item.name},年龄:{item.age},性别:{item.sex}
</div>
))}
</div>
);
}
其实你打开浏览器的控制台,你会发现一个警告:
根据我的经验来说,只要你使用了map,那么你需要在map返回的元素中绑定key值:
<div>
{data.map((item) => (
// 绑定key值
<div key={item.id}>
姓名:{item.name},年龄:{item.age},性别:{item.sex}
</div>
))}
</div>
这个时候警告就没了,可能你有时候不想在外面嵌套一层div,那么你可以使用空标签<Fragment />
:
export default function TestComponent() {
return (
<div>
{data.map((item) => (
// 绑定key值
<React.Fragment key={item.id}>
姓名:{item.name},年龄:{item.age},性别:{item.sex}
</React.Fragment>
))}
</div>
);
}
Props
在JSX中,Props的处理是非常灵活的,也正是因为灵活的Props,让React在创建一些复杂组件的时候就会显得异常的方便。
字符串字面量
当如果你要传入的prop是一个字符串时,你就可以省略括号,如下所示:
<MyComponent message="hello world" />
<MyComponent message={'hello world'} />
React官方说这种行为是不重要的,随便用哪一种都可以,但是我个人习惯在prop为字符串的时候省略掉括号。
Props的默认值
当如果不给prop赋值,那么它的默认值为true,也就是说下面的两种写法是等价的。
<MyTextBox autocomplete />
<MyTextBox autocomplete={true} />
官方表示不建议不传递 value 给 prop…然而我非常喜欢当为true时不传值给prop。
属性展开
我特别喜欢React的可以使用展开运算符 ...
来在 JSX 中传递整个 props 对象,更妙的是,使用ES6的语法可以只将部分属性传递下去。
import React from 'react';
export interface ButtonProps {
kind: string;
onClick?: () => void;
children?: React.ReactNode;
}
const Button = (props: ButtonProps) => {
// 这里可以使用对象的解构,将kind属性保留下来,剩余的其它属性传入button中
const { kind, ...other } = props;
const className = kind === 'primary' ? 'PrimaryButton' : 'SecondaryButton';
return <button className={className} {...other} />;
};
export default function TestComponent() {
return (
<div>
<Button kind="primary" onClick={() => console.log('clicked!')}>
Hello World!
</Button>
</div>
);
}
该特性在对一些UI组件进行二次封装上特别的好用,并且配合TypeScript,可以获得非常完美的组件使用提示。
children
布尔类型、Null 以及 Undefined 将会忽略,但是0依然会被渲染
import React from 'react';
export default function TestComponent() {
return (
<div>
{0}
{undefined}
{null}
{false}
{true}
</div>
);
}
最终渲染结果:
所以警惕下面这种代码,可能会引起不必要的BUG
import React from "react";
const list = [];
export default function TestComponent() {
return (
<div>
{list.length && <div>这里不会渲染</div>}
</div>
);
}
渲染结果和上面的图一样,为0
。
子元素
相信React子元素的用法我们在平时写项目中已经非常熟悉了,但是还有一种形式我们用的比较少,比如下面这种返回一个数组的形式:
数组
需要注意的是,如果返回的是存储在数组中的一组元素,那么还的给这些元素设置key值,不然会抛出异常。
import React from 'react';
export default function TestComponent() {
// 不需要用额外的元素包裹列表元素!
return [
// 不要忘记设置 key :)
<li key="A">First item</li>,
<li key="B">Second item</li>,
<li key="C">Third item</li>,
];
}
渲染结果:
函数
下面这种用法其实我在项目中没有使用过,但是官方上有提到,所以这里直接放出官方的代码:
import React from 'react';
// 调用子元素回调 numTimes 次,来重复生成组件
function Repeat(props: {
numTimes: number;
children: (index: number) => React.ReactNode;
}) {
let items = [];
for (let i = 0; i < props.numTimes; i++) {
items.push(props.children(i));
}
return <div>{items}</div>;
}
export default function TestComponent() {
return (
<Repeat numTimes={10}>
{(index) => <div key={index}>This is item {index} in the list</div>}
</Repeat>
);
}
渲染结果:
React.createElement
当然,你也可以直接使用React.createElement进行创建子元素,我们随便找两个例子来改一改,因为我觉得实际编写项目的过程中,应该不会有人直接使用React.createElement这个方法。
import React from 'react';
export default function TestComponent() {
return React.createElement(
'h1',
null,
'这是使用React.createElement创建的元素',
);
}
渲染结果:
到目前为止,上面的内容可能是大多数文章中都会提到的,JSX仅仅只是一个语法糖,其实它会被转换成一个方法:React.createElement,但我想说,下面的内容才是你在面试中可以和面试官battle的东西。
$$typeof
不管是写的JSX,还是使用的React.createElement,我们打印一下它的属性,会发现有下面的几个值:
前面的几个属性type、props、key、ref可能就算不知道它的具体意思,但是大概意思应该还是会懂,但是这个$$typeof完全猜不到它的含义,为什么它的值是Symbol.for('react.element')
。
你多多少少的可能听过XSS攻击,其实这个$$typeof值就是为了防止网站受到XSS攻击。
其实$$typeof: REACT_ELEMENT_TYPE标记了该对象是个
React Element。
因为JSON中是不支持 Symbol
类型,React 会检测 element.$$typeof
,如果元素丢失或者无效,会拒绝处理该元素。
这里就得提到React提供的另一个全局API React.isValidElement
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
可以发现,React在验证一个元素是不是合法元素时,会去验证$$typeof这个值,如果
具体详情可以看一下这篇文章:为什么React元素有一个$$typeof属性?
React 17
React17的发布并没有包含什么新特性,但是它提供了一个全新的JSX转换,意思就是JSX 不会再被转换为 React.createElement
- 使用全新的转换,你可以单独使用 JSX 而无需引入 React。
- 根据你的配置,JSX 的编译输出可能会略微改善 bundle 的大小。
- 它将减少你需要学习 React 概念的数量,以备未来之需。
至于这个第三点…在面试的时候可能就是需要问这些概念上的问题,所以即使你知道从React17开始,JSX就不再被编译成React.createElement了,但你还是得清楚曾经JSX曾经就是React.createElement的语法糖。
那么新的JSX转换器又会将JSX语句编译成什么呢?
如下所示:
function App() {
return <h1>Hello World</h1>;
}
编译结果:
// 由编译器引入(禁止自己引入!)
import {jsx as _jsx} from 'react/jsx-runtime';
function App() {
return _jsx('h1', { children: 'Hello world' });
}
根据文档中所说:jsx这个函数的属性即参数如下:
function jsx(type, props, key) {
return {
$$typeof: ReactElementSymbol,
type,
key,
props,
};
}
与React.createElement最大的不同是,现在children需要放在第二个参数,也就是props中。
至于这部分内容,React官方在介绍全新的 JSX 转换中有详细的讲解。
深入JSX
到上面为止都属于JSX的基础部分,而JSX更加深入的东西在Fiber架构下的diff算法部分,这里就先提一下,关于diff算法的部分,我也会逐渐开始总结。
JSX节点,最终会通过一系列的处理,变成一个Fiber节点。
关于React如何通过JSX创建出Fiber节点的,可以看一下createElement源码。
注意:源码中if (__DEV__) {}
的那部分都是在开发环境才会进入的语句,所以关于这部分我们可以不去理会它。
export function createElement(type, config, children) {
let propName;
const props = {};
let key = null;
let ref = null;
let self = null;
let source = null;
if (config != null) {
// 验证config中的ref是否合法
// return config.ref !== undefined;
if (hasValidRef(config)) {
ref = config.ref;
}
// 验证config中的key是否合法
// return config.key !== undefined;
if (hasValidKey(config)) {
key = '' + config.key;
}
self = config.__self === undefined ? null : config.__self;
source = config.__source === undefined ? null : config.__source;
// 将剩余的属性添加到一个新的 props 对象中
for (propName in config) {
// 验证是否有保留属性字段
if (
hasOwnProperty.call(config, propName) &&
!RESERVED_PROPS.hasOwnProperty(propName)
) {
props[propName] = config[propName];
}
}
}
/** children的数量 */
const childrenLength = arguments.length - 2;
// 数量为 1 则children为对象 否则为数组
if (childrenLength === 1) {
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 = childArray;
}
// 处理class组件中的defaultProps属性
if (type && type.defaultProps) {
const defaultProps = type.defaultProps;
for (propName in defaultProps) {
if (props[propName] === undefined) {
props[propName] = defaultProps[propName];
}
}
}
// 传入到 ReactElement 中进行处理 返回最终处理完毕的对象
return ReactElement(
type,
key,
ref,
self,
source,
ReactCurrentOwner.current,
props,
);
}
在createElement中,最终调用了ReactElement这个方法,那么我们看一下该方法的源码:
const ReactElement = function(type, key, ref, self, source, owner, props) {
const element = {
// React通过这个属性来判断是否是一个React元素
// const REACT_ELEMENT_TYPE = Symbol.for('react.element');
$$typeof: REACT_ELEMENT_TYPE,
// 元素内置的属性
type: type,
key: key,
ref: ref,
props: props,
// 记录了谁创建了该组件
_owner: owner,
};
return element;
};
我们再来看一个方法isValidElement,该方法用来验证对象是否是一个合法的React元素:
export function isValidElement(object) {
return (
typeof object === 'object' &&
object !== null &&
object.$$typeof === REACT_ELEMENT_TYPE
);
}
跟上面说的一致,$$typeof就是React用来判断该对象是否是一个合法的React元素。
JSX和Fiber
最后
JSX的使用时非常灵活的,我曾经在另一篇文章中写了我在项目中会用到的JSX骚操作,如果有兴趣的话也可以去看一下:迷人的JSX,你可以进行各种骚操作。
其实本篇文章讲的JSX也属于一些基础,在Fiber节点中还有大量和JSX相关的内容,我争取这几天一步一步的将它们进行吃透。
本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!