目录
context
在一个典型的 React 应用中,数据是通过 props 属性自上而下(由父及子)进行传递的,但这种做法对于某些类型的属性而言是极其繁琐的(例如:地区偏好,UI 主题),这些属性是应用程序中许多组件都需要的。Context 提供了一种在组件之间共享此类值的方式,而不必显式地通过组件树的逐层传递 props。
import React ,{ Component } from 'react';
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0,
}
}
add = () => {
this.setState({count: this.state.count + 1})
}
render(){
let { count } = this.state;
return (
<div>
<Middle count={count} addBtn={this.add}/>
</div>
)
}
}
function Middle(props){
return (
<div>
<Counter count={props.count} addBtn={props.addBtn}/>
</div>
)
}
function Counter(props){
return (
<div>
<p onClick={props.addBtn}>You clicked {props.count} times</p>
</div>
)
}
export default App;
以上例子是向 Counter 组件传递一个 state 和修改 state 的函数,即便 Middle 组件不需要,但props只能逐层传递,这种传递方式略显繁琐。
使用 context,可以避免使用通过中间元素传递 props。
import React ,{ Component, createContext } from 'react';
//创建一个context
const BatteryContext = createContext();
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0,
}
}
add = () => {
this.setState({count: this.state.count + 1})
}
render(){
let { count } = this.state;
return (
<BatteryContext.Provider value={{ count, addBtn: this.add}}>
<Middle/>
</BatteryContext.Provider>
)
}
}
function Middle(props){
return (
<Counter/>
)
}
function Counter(props){
return (
<BatteryContext.Consumer>
{
obj => <h1 onClick={obj.addBtn}>Count : {obj.count}</h1>
}
</BatteryContext.Consumer>
)
}
export default App;
只能使用 createContext 创建 context,Provider 接收一个 value
属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。当 Provider 的 value
值发生变化时,它内部的所有消费组件都会重新渲染。
Consumer 函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value
值等同于往上组件树离这个 context 最近的 Provider 提供的 value
值。如果没有对应的 Provider,value
参数等同于传递给 createContext()
的 defaultValue
。
import React ,{ Component, createContext } from 'react';
//创建一个context
const BatteryContext = createContext(10);
class App extends React.Component {
constructor(props){
super(props);
}
render(){
return (
<Middle/>
)
}
}
function Middle(props){
return (
<Counter/>
)
}
function Counter(props){
return (
<BatteryContext.Consumer>
{
count => <h1>Count : {count}</h1>
}
</BatteryContext.Consumer>
)
}
export default App;
当 Consumer 向上找不到 Provider 时,程序不会报错,defaultValue会生效
。
只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue
参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。
import React ,{ Component, createContext } from 'react';
const BatteryContext = createContext(10);
const OnlineContext = createContext();
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 60,
online: false
}
}
render(){
const { count, online } = this.state;
return (
<BatteryContext.Provider value={count}>
<OnlineContext.Provider value={online}>
<button onClick={() => this.setState({ count: count -1})}>减减</button>
<button onClick={() => this.setState({ online: !online})}>切换</button>
<Middle/>
</OnlineContext.Provider>
</BatteryContext.Provider>
)
}
}
function Middle(props){
return (
<Counter/>
)
}
function Counter(props){
return (
<BatteryContext.Consumer>
{
count => (
<OnlineContext.Consumer>
{
online => <h1>Count : {count} , Online : {online.toString()}</h1>
}
</OnlineContext.Consumer>
)
}
</BatteryContext.Consumer>
)
}
export default App;
Provider 和 Consumer 都可以互相嵌套,嵌套顺序不重要。
虽然 context 对于跨级传递信息很简洁,但是要注意不要滥用 context,不然会影响组件的独立性。context 相当于是全局变量,通常组件都是只使用一个 context。
contextType
import React ,{ Component, createContext } from 'react';
const BatteryContext = createContext();
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 30
}
}
render(){
const { count } = this.state;
return (
<BatteryContext.Provider value={count}>
<Middle/>
</BatteryContext.Provider>
)
}
}
function Middle(props){
return (
<Counter/>
)
}
class Counter extends React.Component{
// static contextType = BatteryContext;
render(){
const count = this.context;
return (
<h1>Count:{count}</h1>
)
}
}
Counter.contextType = BatteryContext;
export default App;
挂载在 class 上的 contextType
属性会被重赋值为一个由 React.createContext() 创建的 Context 对象。这能让你使用 this.context
来消费最近 Context 上的那个值。你可以在任何生命周期中访问到它,包括 render 函数中。
contextType 只能用在类组件,
如果一个组件中只使用一个Context的话,就可以使用contextType代替Consumer。也可使用static 这个类属性来初始化 contextType。
React.Lazy,
Suspense
现在前端项目基本都采用打包技术,比如 Webpack,JS逻辑代码打包后会产生一个 bundle.js 文件,而随着我们引用的第三方库越来越多或业务逻辑代码越来越复杂,相应打包好的 bundle.js 文件体积就会越来越大,因为需要先请求加载资源之后,才会渲染页面,这就会严重影响到页面的首屏加载。
React16.6.0发布了React.lazy来实现组件的懒加载,首屏加载可以按需加载组件,一些暂时没用到的组件先不加载,等到渲染的时候再加载,这很好的加快了网页加载速度,提高用户体验。
// about.jsx
import React ,{ Component } from "react";
export default class About extends React.Component {
render(){
return (
<div>About</div>
)
}
}
// app.jsx
import React ,{ Component, lazy, Suspense } from 'react';
let About = lazy(() => import(/*webpackChunkName:"about"*/'./about'));
class App extends React.Component {
render(){
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<About />
</Suspense>
</div>
)
}
}
export default App;
React.lazy 接收一个函数,返回一个组件。/*webpackChunkName*/自定义打包后的文件名。
Suspense 是 React内置组件,lazy 一定要和 Suspense 结合使用,不然程序会抛错,Suspense 包住懒加载引入的组件,fallback 函数是必要的,fallback 里面是返回一段 jsx。
我们还可以基于路由实现代码分割,大多数网络用户习惯于页面之间能有个加载切换过程。你也可以选择重新渲染整个页面,这样您的用户就不必在渲染的同时再和页面上的其他元素进行交互。
import React, { Suspense, lazy } from 'react';
import { BrowserRouter as Router, Route, Switch } from 'react-router-dom';
const Home = lazy(() => import('./routes/Home'));
const About = lazy(() => import('./routes/About'));
const App = () => (
<Router>
<Suspense fallback={<div>Loading...</div>}>
<Switch>
<Route exact path="/" component={Home}/>
<Route path="/about" component={About}/>
</Switch>
</Suspense>
</Router>
);
错误边界(Error Boundaries)
部分 UI 的 JavaScript 错误不应该导致整个应用崩溃,为了解决这个问题,React 16 引入了一个新的概念 —— 错误边界
如果一个 class 组件中定义了 static getDerivedStateFromError() 或 componentDidCatch() 这两个生命周期方法中的任意一个(或两个)时,那么它就变成一个错误边界。
import React ,{ Component, lazy, Suspense } from 'react';
let About = lazy(() => import('./about'));
class App extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
// static getDerivedStateFromError(error) {
// // 更新 state 使下一次渲染能够显示降级后的 UI
// return { hasError: true };
// }
componentDidCatch(error, errorInfo) {
this.setState({ hasError: true })
}
render(){
const { hasError } = this.state;
if(hasError) {
return (
<div>error</div>
)
}
return (
<div>
<Suspense fallback={<div>loading...</div>}>
<About />
</Suspense>
</div>
)
}
}
export default App;
当出现网络加载错误或者其他问题导致组件不能正常加载渲染出来,那么错误边界就能捕获到错误,展示错误信息,使页面不至于展示空白。
PureComponent
React 是一种视图层的前端框架,数据层更新数据,视图层重新渲染页面,但有时候页面是不需要重渲染的而进行了重渲染就会降低我们应用的性能。
import React ,{ Component } from 'react';
class Foo extends React.Component {
render(){
console.log('foo');
return (
<div>{this.props.name}</div>
)
}
}
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
}
}
render(){
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => this.setState({count: this.state.count + 1})}>click me</button>
<Foo name="Mike"/>
</div>
)
}
}
export default App;
App组件更新 state,Foo 组件每次都重新渲染,Foo组件并没有依赖App组件的 state 值,所以Foo组件视图无需更新,那么我们来优化一下。
class Foo extends React.Component {
shouldComponentUpdate(nextProps,nextState){
if(this.props.name !== nextProps.name){
return true;
}
return false;
}
render(){
console.log('foo');
return (
<div>{this.props.name}</div>
)
}
}
shouldComponentUpdate 这个生命周期函数默认都是返回 true,如果是返回 false 组件将不会重渲染,我们通过比较上一次的props的值和更新后的props的值比较,如果值相等则组件Foo不会重渲染。
React16 可用 React.PureComponent
来代替手写 shouldComponentUpdate。
import React ,{ Component, PureComponent } from 'react';
class Foo extends PureComponent {
render(){
console.log('foo');
return (
<div>{this.props.name}</div>
)
}
}
这个与上面 shouldComponentUpdate 实现效果完全一致。但 PureComponent 它只进行浅比较,所以当 props 或者 state 某种程度是可变的话,浅比较会有遗漏,那你就不能使用它了。当数据结构很复杂时,情况会变得麻烦。
class Foo extends PureComponent {
render(){
console.log('foo');
return (
<div>{this.props.person.age}</div>
)
}
}
class App extends React.Component {
constructor(props){
super(props);
this.state = {
person: {
age: 18
}
}
}
render(){
const person = this.state.person;
return (
<div>
<button onClick={() => {
person.age++;
this.setState({ person })
}}>click me</button>
<Foo person={this.state.person}/>
</div>
)
}
}
以上demo就会存在数据改变而试图没有更新的缺陷,对于Foo来说,person的数据和结构都没有改变。
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0,
person: {
age: 18
}
}
}
render(){
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => {
this.setState({ count: this.state.count + 1})
}}>click me</button>
<Foo person={this.state.person} cb={() => {}}/>
</div>
)
}
}
以上修改count的值,但是Foo组件每次都会重渲染,那是因为每次都会生成一个新的匿名函数。可进行如下修改:
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0,
person: {
age: 18
}
}
}
add = () => {
}
render(){
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => {
this.setState({ count: this.state.count + 1})
}}>click me</button>
<Foo person={this.state.person} cb={this.add}/>
</div>
)
}
}
React.memo
我们学习了 通过继承 PureComponent 来优化组件重渲染,但是函数组件就不适用了,因为函数组件无法去继承一个类,React 16为我们提供了 memo 来给函数组件优化重渲染的问题。
import React ,{ Component, memo } from 'react';
const Foo = memo((props) => {
console.log('foo');
return (
<div>{props.name}</div>
)
}
)
class App extends React.Component {
constructor(props){
super(props);
this.state = {
count: 0
}
}
render(){
return (
<div>
<p>{this.state.count}</p>
<button onClick={() => {
this.setState({ count: this.state.count + 1})
}}>click me</button>
<Foo name='Mike' />
</div>
)
}
}
React.memo()可接受2个参数,第一个参数为纯函数的组件,第二个参数用于对比props控制是否刷新。React.memo()返回一个组件
const Foo = memo((props) => {
console.log('foo');
return (
<div>{props.name}</div>
)
},(prevProps, nextProps) => {
//return true不会渲染,return false 才会渲染
if(prevProps.name == nextProps.name){
return true
}
return false
}
)