一 、前言
我们在使用react的过程中 ,经常会遇到需要跨层级传递数据的情况 。props传递数据应用在这种场景下会极度繁琐 ,且不利于维护 ,于是context应运而生
官方解释: Context 提供了一种在组件之间共享此类值的方式 ,而不必显式地通过组件树的逐层传递 props
二 、用法
在正文之前 ,先简单介绍一下context的三种消费方法:
1.通过Consumer来消费上下文
const globalContext = React.createContext();
class TestUseContextSon1 extends React.Component {
render() {
return (
<globalContext.Consumer>
{(value) => {
return <div>{value.num}</div>;
}}
</globalContext.Consumer>
);
}
}
export default class TestUseContext extends React.Component {
constructor(props) {
super(props);
this.state = {
num: 2,
};
}
render() {
return (
<globalContext.Provider value={{ num: this.state.num }}>
<TestUseContextSon1 />
</globalContext.Provider>
);
}
}
2.通过静态变量contextType来消费上下文
const globalContext = React.createContext();
class TestUseContextSon2 extends React.Component {
static contextType = globalContext;
render() {
return <div>{this.context.num}</div>;
}
}
export default class TestUseContext extends React.Component {
...省略...
render() {
return (
<globalContext.Provider value={{ num: this.state.num }}>
<TestUseContextSon2 />
</globalContext.Provider>
);
}
}
3.通过hooks useContext来消费上下文
const globalContext = React.createContext();
const TestUseContextSon3 = (props) => {
const con = useContext(globalContext);
return <div>{con.num}</div>;
};
export default class TestUseContext extends React.Component {
...省略...
render() {
return (
<globalContext.Provider value={{ num: this.state.num }}>
<TestUseContextSon3 />
</globalContext.Provider>
);
}
}
比较:
Consumer既可以在类组件中使用 ,也可以在函数组件中使用
contextType只能在类组件中使用
useContext只能在函数组件中使用
三 、缺点
这里有一个例子:
import React, { useState } from "react";
const globalContext = React.createContext();
const Son1 = () => {
return <div>Son1</div>;
};
const Son2 = () => {
const value = useContext(globalContext);
return <div>Son2---{value.num}</div>;
};
export const Demo = () => {
const [value, setValue] = useState({ num: 1 });
return (
<globalContext.Provider value={value}>
<Son1 />
<Son2 />
</globalContext.Provider>
);
};
当我们改变value值时 ,会导致Son1 、Son2都发生重渲染 ,但这与我们的初衷相悖 ,造成了额外的开销 ,我们期望做到的是Son1不执行 ,Son2重新渲染 。在较长的一段时间内 ,我都认为是使用了context导致Provider下面的子组件发生了重渲染 。网上也有很多解释没有说清楚,容易误导人 。
实际情况是value的变化导致了Son1 、Son2发生重渲染 。如下示例: 即使我们不使用·context ,当value发生变化时 ,Son1 、Son2也会重渲染 。
const Son1 = () => {
return <div>Son1</div>;
};
const Son2 = () => {
return <div>Son2</div>;
};
export const Demo = () => {
const [value, setValue] = useState({ num: 1 });
return (
<Son1 />
<Son2 />
);
};
那么问题来了,我们使用context的时候 ,必然要向<globalContext.Provider value={value}>Provider的value中传入一个状态 ,但是当状态改变时又不可避免的造成Provider下的所有子组件重新渲染 ,我们期望只有消费了上下文的子组件重新渲染 ,那么有什么方法能够避免这种额外的开销吗?
四 、context优化
我们知道 ,所有消费了context的地方 ,只要Provider的value值发生变化 ,都会发生重渲染.只要我们有什么办法能够避开父组件状态发生变化 ,引起的子组件状态发生变化 ,那就可以减少很多不必要的开销 。
一重奏--使用PureComponent
const globalContext = React.createContext();
class TestUseContextSon2 extends React.PureComponent {
constructor(props) {
super(props);
this.state = {};
}
render() {
console.log("TestUseContextSon2----render");
return (
<globalContext.Consumer>
{(value) => {
console.log("Consumer----handle");
return <div>{value.num}</div>;
}}
</globalContext.Consumer>
);
}
}
const TestUseContext = () => {
const [value, setValue] = useState({ num: 1 });
return (
<globalContext.Provider value={value}>
<button onClick={() => setValue({ num: value.num + 1 })}>
点击
</button>
<TestUseContextSon2 />
</globalContext.Provider>
);
}
初始化的时候 ,两个console各执行一遍
点击按钮之后 ,TestUseContextSon2----render没有打印 ,Consumer----handle打印,达到预期结果 。
二重奏--使用shouldComponentUpdate
此处由于作者比较任性 ,省略100字 ,基本效果其实和PureComponent一致,不做过多描述 。
三重奏--使用React.memo
React.memo既可以用于函数组件 ,也可以用于类组件
const globalContext = React.createContext();
const TestUseContextSon3 = React.memo(function (props) {
console.log("TestUseContextSon3----render");
return (
<globalContext.Consumer>
{(value) => {
console.log("Consumer----handle");
return <div>{value.num}</div>;
}}
</globalContext.Consumer>
);
});
const TestUseContext = () => {
const [value, setValue] = useState({ num: 1 });
return (
<globalContext.Provider value={value}>
<button onClick={() => setValue({ num: value.num + 1 })}>
点击
</button>
<TestUseContextSon3 />
</globalContext.Provider>
);
}
点击按钮之后 ,TestUseContextSon2----render没有打印 ,Consumer----handle打印 ,达到预期结果 。 那如果我们使用useContext来消费上下文呢?
const TestUseContextSon4 = React.memo(function (props) {
const con = useContext(globalContext);
console.log("TestUseContextSon4----render");
return <div>{con.num}</div>;
});
点击按钮之后 ,TestUseContextSon4----render打印,也就是说当我们使用useContext来消费上下文的时候 ,整个函数组件会重新执行 。而Consumer仅仅只是局部执行 ,这意味更少的性能消耗 。
四重奏--Provider再封装+props.children
上面所述的三种方法都存在一个弊端 ,Provider的直接下级组件都需要用memo 、PureComponent 、shouldComponentUpdate处理 ,才能屏蔽掉父级状态变化带来的影响 ,那么有没有一种更方便的方式呢?
代码如下:
/** 主题 */
const ThemeContext = React.createContext({ theme: "red" });
const ThemeProvider = (props) => {
const [theme, setTheme] = useState({ theme: "red" });
console.log("ThemeProvider-----", theme.theme);
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{props.children}
</ThemeContext.Provider>
);
};
const Son1 = function (props) {
const { setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme({ theme: "blue" })}>改变主题</button>;
};
const Son2 = function (props) {
const { theme } = useContext(ThemeContext);
console.log("Son2----", theme.theme);
return <div>主题----{theme.theme}</div>;
};
const Son4 = function (props) {
console.log("Son4---没有使用上下文");
return <div>没有使用上下文</div>;
};
export default class ContextChildren extends React.Component {
render() {
return (
<ThemeProvider>
<Son1 />
<Son2 />
<Son4 />
</ThemeProvider>
);
}
}
在上面这段代码中 ,<Son1 /> 、<Son2 /> 、<Son3 />并没有直接放到ThemeContext.Provider组件下面 ,而是将该组件再次封装成ThemeProvider组件,并将状态管理也放在ThemeProvider组件中,然后通过props.children来引入组件子节点。
效果如下:
当我们点击按钮时 ,打印如下:
点击按钮,setTheme执行 ,状态由{ theme: "red" }变为{ theme: "blue" },引起ThemeProvider组件重新执行,打印ThemeProvider----- blue,组件Son2由于消费了上下文 ,重新执行 ,打印Son2---- blue
那么问题来了 ,为什么没有打印Son4呢?我们没有使用memo 、PureComponent等处理Son4组件,但是它确实不会重新执行 。
出现这种现象 ,其实是props.children引起的 ,props.children指向一个对象,这个对象中存放着<Son1 />、<Son2 /> 、<Son3 />执行的结果 ,ThemeProvider执行的时候 ,props.children指向的对象没有发生变化 ,只有当ContextChildren组件重新渲染的时候 ,<Son1 /> 、<Son2 />、<Son3 />才会重新执行 ,由于我们将状态放置于ThemeProvider组件中 ,所以ContextChildren组件不会重新渲染 ,<Son1 /> 、<Son2 /> 、<Son3 />也就不会重新执行,所以Son4---没有使用上下文没有打印 。
那如果将ThemeProvider组件改成这样呢?
const ThemeProvider = (props) => {
const [theme, setTheme] = useState({ theme: "red" });
console.log("ThemeProvider-----", theme.theme);
const content = React.Children.map(props.children, (child) => {
return child;
});
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{content}
</ThemeContext.Provider>
);
};
Son4依然没有执行
再改一下:
const ThemeProvider = (props) => {
const [theme, setTheme] = useState({ theme: "red" });
console.log("ThemeProvider-----", theme.theme);
const content = React.Children.map(props.children, (child) => {
return React.cloneElement(child);
});
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
{content}
</ThemeContext.Provider>
);
};
我们使用React.cloneElementapi克隆一下child
Son4执行了 ,我想这是因为克隆之后指向发生变化 ,导致组件重新执行
总结
本文简单介绍了一下context的几种用法,以及如何来屏蔽父级状态变化(provider的value一般是和父级组件状态挂钩的)导致未消费上下文的子组件重新渲染导致的额外开销。
以上就是react context优化四重奏教程示例的详细内容 ,更多关于react context 优化教程的资料请关注本站其它相关文章!
声明:本站所有文章 ,如无特殊说明或标注 ,均为本站原创发布 。任何个人或组织 ,在未征得本站同意时 ,禁止复制 、盗用 、采集 、发布本站内容到任何网站 、书籍等各类媒体平台 。如若本站内容侵犯了原著者的合法权益 ,可联系我们进行处理 。