高级指引
代码分割
-
打包 打包:是一个将文件引入并合并到一个单独文件的过程,最终形成一个“bundle”,然后在页面上引入该bundle,整个应用即可一次性加载。 打包工具:Webpack、Rollup、Browserify。 -
代码分割 随着代码的增多,形成的bundle也会增大,因此对bundle进行分割,使用Webpack等打包器,来创建多个包并在运行时动态加载,可以实现“懒加载”,提高应用的性能。 -
import() 代码分割后,需要通过动态的import()语法来引入代码。 代码分割和babel同时使用时,需要确保babel能解析动态import语法而不是将其转换,需要安装 @babel/plugin-syntax-dynamic-import 插件。 -
React.lazy:React.lazy不适用于服务端渲染。 -
异常捕获边界 -
在哪个地方进行代码分割:路由。 -
命名导出:React.lazy目前只支持默认导出(default exports)
Context
通常的做法
在一个应用中,一个变量需要被多个不同层级的子元素都使用时,通常的做法是在每层的组件上都使用props来获取和向下传递这个值。
class App extends React.Component {
render() {
return <Toolbar theme="dark" />;
}
}
function Toolbar(props) {
return (
<div>
<ThemedButton theme={props.theme} />
</div>
);
}
class ThemedButton extends React.Component {
render() {
return <button>{this.props.theme}</button>;
}
}
Context的用法
React提供一个context对象,可以在不使用props传递的情况下,让所有需要的子元素都能获取到这个值。
const ThemeContext = React.createContext("light");
class AppContext extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<ToolBar />
</ThemeContext.Provider>
);
}
}
function ToolBar() {
return (
<div>
<ThemeButton />
</div>
);
}
class ThemeButton extends React.Component {
static contextType = ThemeContext;
render() {
return <button>{this.context}</button>;
}
}
Context的主要应用场景是在很多不同层级的组件需要访问同样的数据(获取它的值和改变),但是中间的组件并不需要这些内容的时候。 Context的缺点是会让组件的复用性变得很差。
Context的替代方案
另一种不使用Context的方法,是低层级需要使用数据的组件作为一个值来进行传递,中间组件就不需要去知道这些数据了。
这种方法虽然减少了要传递的props的数量,但是它是将父组件与和它密切相关的子组件分开了,组件的层级、逻辑变得更复杂了。
使用context的通用场景:管理当前的locale、theme、一些缓存数据。 这些场景使用context会比替代方案要简单很多。
API
- React.creatContext
const MyContext = React.createContext(defaultValue);
上面的语句创建了一个Context对象,并传入了一个defaultValue 作为默认值。
当子组件使用Context对象的时候,React会找到离子组件最近的Context.Provider中的value值,如果没有找到则会使用defaultValue 。
- Context.Provider
<MyContext.Provider value="" />
每个Context对象都会返回一个Provider React组件,它允许消费组件(使用数据的组件)订阅context的变化。
Provider接收一个value值,并传递给它的消费组件,当value值发生改变时,它内部的所有消费组件都会重新渲染,且不受其他组件是否更新的影响。
这里的更新是通过新旧值的检测来实现的,使用了与Object.is相同的算法。
- Class.contextType
MyClass.contextType = MyContext;
组件上的contextType属性被赋值了一个context对象后,就可以在组件中通过this.context 属性来获取context对象的value值了,并且可以在任意的生命周期中访问到它。
也可以通过static来声明类属性(不过它现在还是一个实验性的语法)
static contextType = MyContext;
- Context.Consumer
<MyContext.Consumer>
{(value) => <button>{value}</button>}
</MyContext.Consumer>
Context.Consumer组件,可以让你在函数式组件中使用context对象。 因为函数式组件中不能添加属性,所以无法获取到context对象,所以使用.Consumer API,通过(value)=>{}的方式来使用context对象的value值。
- Context.displyName
MyContext.displayName = "myContext";
context对象接收一个displayName 属性,可以在React devTools工具中看到当前组件的value是从哪一个context对象获取的。
动态Context
- 创建一个context对象
const themes = {
light: {
foreground: "#000000",
background: "#eeeeee",
},
dark: {
foreground: "#ffffff",
background: "#222222",
},
};
const ThemeContext = React.createContext(themes.dark);
- 一个使用context对象的button
class ThemedButton extends React.Component {
render() {
let props = this.props;
let theme = this.context;
return (
<button {...props} style={{ backgroundColor: theme.background }}></button>
);
}
}
ThemedButton.contextType = ThemeContext;
- 一个中间件:给button添加方法
function ToolBar(props) {
return <ThemedButton onClick={props.changeTheme}>Change Theme</ThemedButton>;
}
- 一个父组件:提供context对象的数据和方法
class AppDinamic extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: themes.light,
};
this.toggleTheme = () => {
this.setState((state) => ({
theme: state.theme === themes.dark ? themes.light : themes.dark,
}));
};
}
render() {
return (
<ThemeContext.Provider value={this.state.theme}>
<ToolBar changeTheme={this.toggleTheme} />
</ThemeContext.Provider>
);
}
}
实现:点击button按钮,进行主题的切换。
多个context
当一个组件需要使用多个context时,可以将他们嵌套传递,然后在子组件中使用.Consumer 组件嵌套获取。
父组件:
<ThemeContext.Provider value="dark">
<UserContext.Provider value="Guest">
<ToolBar />
</UserContext.Provider>
</ThemeContext.Provider>
子组件:
<ThemeContext.Consumer>
{(theme) => (
<UserContext.Consumer>
{(user) => (
<div>
<button>{theme}</button>
<button>{user}</button>
</div>
)}
</UserContext.Consumer>
)}
</ThemeContext.Consumer>
context对象使用的注意事项
context对象的数据在Provider组件中通过value属性来传递,当value的值改变时,消费组件会重新渲染。 但是当Provider组件被重新,而value的值是一个引用数据类型的数据时,React会因为检测到value值的变化而去重新渲染消费组件。
解决这个问题的办法:将value属性的值,放在组件的state中,然后通过调用this.state来给value赋值
<MyContext.Provider value={this.state.value}>
<Toolbar />
</MyContext.Provider>
错误边界
错误边界是一种React组件,这种组件可以捕获发生在其子组件树任何位置的JavaScript错误,并打印这些错误,同时展示降级UI,而不展示那些发生崩溃的子组件树。
错误边界组件需要自己包装。
错误边界在渲染期间、生命周期方法和整个组件树的构造函数中捕获错误。
错误边界无法捕获错误的场景:事件处理、异步代码、服务端渲染、它自身(非它的子组件)抛出的错误。
refs转发
refs转发:将ref自动地通过组件传递到其子组件。
转发refs到DOM组件
- 使用``React.forwardRef`方法来创建组件,传入ref参数,在button元素中进行转发。
const FancyButton = React.forwardRef((props, ref) => (
<button ref={ref} className="FancyButton">
{props.children}
</button>
));
- 通过
React.createRef() 获取ref,在组件中应用ref,向button元素传入内容
const ref = React.createRef();
return <FancyButton ref={ref}>Click me!</FancyButton>;
Ref转发既可以在DOM组件上使用,也可以在class组件中使用。
Fragments
Fragments的作用类似的div(不需要但在React规范中又必须存在)。
一个简单的例子:
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
最终构成的代码将会是这样:
<table>
<tr>
<div>
<td>Hello<td>
<td>World</td>
</div>
<tr>
</table>
但是我们知道<tr> 标签中是不可以包裹<td> 和<th> 之外的其他标签的,<div> 的存在是一个错误。
然而子组件的最外层又必须要有一个容器标签来进行包裹。 这种情况下,我们就可以使用Fragments 。
render() {
return (
<React.Fragment>
<td>Hello</td>
<td>World</td>
</React.Fragment>
);
}
这样运行的代码就不会报错了。
另一种简洁的写法是使用<></> (短语法)来进行包裹,也可以达到同样的效果。
短语法的缺点是,不支持key和各种属性,因此无法在一个循环中使用,而Fragments则支持key和属性的存在。
高阶组件
与第三方库协同
深入JSX
JSX:React.createElement(component, props, …children)函数的语法糖,下面介绍JSX语法使用过程中需要注意的一些问题和规范。
- 要求React必须在作用域内
<myButton color="blue" shadowSize={2}>
Click Me
</myButton>
在上面的JSX语句中,我们首先指定了React元素。 它要求React必须在作用域内,因为JSX语句会被编译为React.createElement 的形式。
- JSX语法中可以通过点语法使用组件
创建MyComponents变量:
const MyComponents = {
DataPicker: function DataPicker(props) {
return <div>Imagine a {props.color} datapicker here.</div>;
},
};
通过点语法使用MyComponents变量下的DataPicker组件:
return <MyComponents.DataPicker color="blue"></MyComponents.DataPicker>;
- 用户定义的组件必须以大写字母开头
如果我们创建了一个不以大写字母开头的组件:
function hello(props) {
return <div>Hello {props.toWhat}</div>;
}
那么我们在使用它时,将会这么引入:
<hello toWhat="World"></hello>
因为标签以小写字母开头,它会被React识别为HTML元素(例如<div> ),而不能实现想要的效果。
- JSX类型不能是一个表达式
如果JSX组件需要通过一个表达式来获取,不可以直接将表达式作为JSX写在代码里,你需要先将表达式的值赋给一个大写字母开头的变量,然后再使用。
错误写法:
<components[props.storyType] story={props.story} />;
正确写法:
const SpecificStory = components[props.storyType];
return <SpecificStory story={props.story} />;
- JSX中传递的props可以是一个JavaScript表达式
<ComponentOne foo={1 + 2 + 3 + 4}></ComponentOne>
但是像if、for循环这样不属于表达式的内容不可以在prop的{}中使用。
- Props的默认值为
true
当你没有给props赋值时,它的默认值为true。
<MyComponent autocomplete /> <==> <MyComponent autocomplete={true} />
- props可以使用扩展运算符
可以通过{…props}来给所有prop赋值:
const props = { firstName: "Ben", lastName: "Hector" };
<Greeting {...props} />
也可以单独把某一个值提取出来:
const Button = (props) => {
const { kind, ...other } = props;
const className = kind === "primary" ? "PrimaryButton" : "SecondaryButton";
return <button {...other}>{className}</button>;
};
<Button kind="primary" onClick={() => console.log("clicked")}></Button>
- JSX中的子元素
在JSX语法中对于当前未知的子元素,可以使用props.children来进行占位。
这个子元素可以是:字符串字面量、组件、JS表达式、函数等等。 只要确保这个子元素在渲染之前能被转换成React可以理解的对象即可。
但是有一些内容作为子元素时,会被JSX忽略掉:布尔类型、null、undefined。 如果想要渲染这些值,需要先将他们转为字符串。
以下代码渲染出来的结果是空的:
<p>{undefined}</p>
以下代码会有内容被渲染:
<p>undefined</p>
<p>{String(undefined)}</p>
性能优化
Portals
Portals一般用法
ReactDOM.createPortal(this.props.children,this.el);
使用一个state来控制子元素(this.props.children )的显示与出现。
由于子元素是一个组件,所以可以通过生命周期来控制子元素是否被挂载到this.el 上,这个官方案例会是一个很好的例子。
适用场景:对话框、悬浮卡、提示框等。
通过Portals进行事件冒泡
Refs 与DOM
Refs:通过它我们能访问DOM节点,或在render方法中创建React元素。
适合使refs的情况:
- 管理焦点、文本选择或媒体播放
- 触发强制动画
- 集成第三方DOM库
【注】避免使用refs来做任何可以通过声明式方式来实现的事情。
- 创建ref
this.myRef = React.createRef();
- 使用ref
<input type="text" ref={this.myRef}></input>
- 访问ref
const ele = this.myRef.current;
通过this.myRef.current 可以访问到使用了该ref的元素,即第2步中的input元素。
React会在组件挂载时给current 属性传入DOM元素,并在组件卸载时传入null值。
【注】这里是在class组件中使用ref的方法,并不适用与函数组件,函数组件中ref的使用在React新增的Hook内容中进行介绍。
回调Refs
与上面在创建ref后再使用不同,回调ref通过函数传入的参数来访问到ref元素,然后使用:
先创建ref元素并设为null,然后创建获取ref元素的函数和使用ref元素的函数:
constructor(props) {
super(props);
this.textInput = null;
this.setTextInputRef = (element) => {
this.textInput = element;
};
this.focusTextInput = () => {
if (this.textInput) this.textInput.focus();
};
}
然后通过ref={this.setTextInputRef} 和onClick={this.focusTextInput} 来使用ref。
React会在组件挂载时,调用ref回调函数并传入DOM元素,当卸载时调用它并传入null。
Render Props
render props:在React组件之间使用一个值为函数的prop共享代码的简单技术。
<MyComponent render={data => (
<h1>Hello {data.target}</h1>
)} />
TypeScript
TypeScript是一个JavaScript的类型超集,包含独立的编译器,可以在构建时发现bug和错误。
严格模式
使用PropTypes进行类型检查
创建一个子组件:
class Greeting extends React.Component {
render() {
return <h1>Hello, {this.props.name}</h1>;
}
}
然后在父组件中使用它:
render() {
return <Greeting name={true} />;
}
在不使用类型检查的情况下,正常输出,结果为:Hello, 。
给子组件加上类型检查:
import PropTypes from "prop-types";
Greeting.propTypes = {
name: PropTypes.string,
};
这时再运行就会产生警告。 类型检查要求name的类型是字符串,但是我们传入的却是布尔类型的。
将父组件修改为:
render() {
return <Greeting name={"true"} />;
}
传入name的值是字符串类型,则不会产生警告。
PropTypes的类型
PropTypes的类型可以是:array、bool、func、number、object、string、symbol、node、element、elementType、any。
- node:任何可被渲染的元素
- element:一个React元素
- elementType:一个React元素类型
在PropTypes.类型 后加上.isRequired ,表示这个值是必须的,如果没有被提供,则会产生警告。
还有其他更复杂的使用:
PropTypes.oneOf(['News','Photos'])
PropTypes.oneOfType([
PropTypes.string,
PropTypes.number,
PropTypes.instanceOf(Message)
])
PropTypes.arrayOf(PropTypes.number)
PropTypes.objectOf(PropTypes.number)
PropTypes.shape({
color: PropTypes.string,
fontSize: PropTypes.number
})
默认Props值
可以使用defaultProps 属性来个组件的prop设置默认值。
Greeting.defaultProps = {
name: 'Stranger'
}
PropTypes在函数组件中的使用
function HelloWorldComponent({ name }) {
return <p>Hello, {name}</p>;
}
HelloWorldComponent.propTypes = {
name: PropTypes.string,
};
|