React 使用
1. JSX 基本使用
- 变量、表达式
- class style
- 子元素和组件
在 render 函数中举例子
-
获取变量,插值语法
const pElem = <p>{this.state.name}</p> return pElem
-
表达式
大括号里都是 js 表达式
const exprElem = <p>{this.state.flag ? 'yes' : 'no'}</p> return exprElem
-
子元素
const imgElem = <div> <p>我的头像</p> <img src="xxxx.png"/> <img src={this.state.imgUrl}/> </div> return imgElem
-
class
// class const classElem = <p className="title">设置 css class</p> return classElem
-
style
// style const styleData = { fontSize: '30px', color: 'blue' } const styleElem = <p style={styleData}>设置 style</p> // 内联写法,注意 {{ 和 }} const styleElem = <p style={{ fontSize: '30px', color: 'blue' }}>设置 style</p> return styleElem
-
原生 HTML
// 原生 html const rawHtml = '<span>富文本内容<i>斜体</i><b>加粗</b></span>' const rawHtmlData = { __html: rawHtml // 注意,必须是这种格式 } const rawHtmlElem = <div> <p dangerouslySetInnerHTML={rawHtmlData}></p> <p>{rawHtml}</p> </div> return rawHtmlElem
-
加载组件
// 加载组件 const componentElem = <div> <p>JSX 中加载一个组件</p> <hr/> <List/> </div> return componentElem
2. 条件判断
-
if … else …
render() { const blackBtn = <button className="btn-black">black btn</button> const whiteBtn = <button className="btn-white">white btn</button> // if else if (this.state.theme === 'black') { return blackBtn } else { return whiteBtn }
-
三元表达式
// 三元运算符 return <div> { this.state.theme === 'black' ? blackBtn : whiteBtn } </div>
-
|| 和 &&
// && return <div> { this.state.theme === 'black' && blackBtn } </div>
3. 渲染列表
- map
- key
class ListDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [
{
id: "id-1",
title: "标题1",
},
{
id: "id-2",
title: "标题2",
},
{
id: "id-3",
title: "标题3",
},
],
};
}
render() {
return (
<ul>
{
/* vue v-for */
this.state.list.map((item, index) => {
// 这里的 key 和 Vue 的 key 类似,必填,不能是 index 或 random
return (
<li key={item.id}>
index {index}; id {item.id}; title {item.title}
</li>
);
})
}
</ul>
);
}
}
4. this 指向
bind this 用来修改方法中的 this 指向。
举例子:
标签触发方法
render() {
return <p onClick={this.clickHandler1}>
{this.state.name}
</p>
}
clickHandler1() {
// console.log('this....', this) // this 默认是 undefined
this.setState({
name: 'lisi'
})
}
这时候点击 p 标签,会报错。
原因在于 this 指向的丢失,具体细节是,clickHandler1 函数赋值给 onClick,此时触发事件就是直接调用,而不是通过实例调用的了,按理来说,此时 this 应该是全局的 window,但结果是 undefined。
babel 下为严格模式,禁止自定义函数的 this 指向 window,所以此时的 this 为 undefined。但这里并不是 babel 严格模式的原因。
重点来了:class 中所有定义的方法,方法里开启了局部严格模式,所以禁止自定义函数的 this 指向 window,因此 this 为 undefined。
绑定 this 的方法
-
bind 函数
通过 bind 来绑定 this 指向,在 render 方法中使用 .bind(this) 是可行的,但是在构造器里进行 bind,只要在构造的时候 bind 一次,性能最优。
constructor(props) { ...... this.clickHandler1 = this.clickHandler1.bind(this) } render() { // this - 使用 bind return <p onClick={this.clickHandler1}> {this.state.name} </p>
bind 函数传参
// 传递参数 - 用 bind(this, a, b) render() { return <ul>{this.state.list.map((item, index) => { return <li key={item.id} onClick={this.clickHandler4.bind(this, item.id, item.title)}> index {index}; title {item.title} </li> })}</ul> ...... } // 传递参数 clickHandler4(id, title, event) { console.log(id, title) console.log('event', event) // 最后追加一个参数,即可接收 event }
-
使用箭头函数
箭头函数体内的
this
对象,就是定义该函数时所在的作用域指向的对象,而不是使用时所在的作用域指向的对象。所以箭头函数里的 this 恒定指向对象实例。简单来说,箭头函数不绑定 this,会不会其所在上下文的 this 作为自己的 this。这种方法最省事,推荐使用。
拓展:箭头函数不绑定 arguments,取而代之用 rest 参数解决。
clickHandler2 = () => { this.setState({ name: 'lisi' }) } render() { // this - 使用箭头函数 return <p onClick={this.clickHandler2}> {this.state.name} </p>
5. React 事件和 DOM 事件的区别
// 获取 event
clickHandler3 = (event) => {
event.preventDefault() // 阻止默认行为
event.stopPropagation() // 阻止冒泡
console.log('target', event.target) // 指向当前元素,即当前元素触发
console.log('current target', event.currentTarget) // 指向当前元素,假象!!!
// 注意,event 其实是 React 封装的。可以看 __proto__.constructor 是 SyntheticEvent 组合事件
console.log('event', event) // 不是原生的 Event ,原生的是 MouseEvent
console.log('event.__proto__.constructor', event.__proto__.constructor)
// 原生 event 如下。其 __proto__.constructor 是 MouseEvent
console.log('nativeEvent', event.nativeEvent)
console.log('nativeEvent target', event.nativeEvent.target) // 指向当前元素,即当前元素触发
console.log('nativeEvent current target', event.nativeEvent.currentTarget) // 指向 document !!!即,绑定事件的元素是 document。但是从 React17 版本开始,事件就不再绑定打 document 上了。
// 1. event 是 SyntheticEvent ,模拟出来 DOM 事件所有能力
// 2. event.nativeEvent 是原生事件对象
// 3. 所有的事件,都被挂载到 document 上
// 4. 和 DOM 事件不一样,和 Vue 事件也不一样
}
render() {
// event
return <a onClick={this.clickHandler3}>
click me
</a>
- React 16 绑定到 document
- React 17 事件绑定到 root 组件
- 有利于多个 React 版本并存,例如微前端。document 只能有一个,但是 root 可以有多个
6. React 表单
-
受控组件
render() { // 受控组件(非受控组件,后面再讲) return ( <div> <p>{this.state.name}</p> <label htmlFor="inputName">姓名:</label> {/* 用 htmlFor 代替 for */} <input id="inputName" value={this.state.name} onChange={this.onInputChange} /> </div> ); // textarea - 使用 value return ( <div> <textarea value={this.state.info} onChange={this.onTextareaChange} /> <p>{this.state.info}</p> </div> ); // select - 使用 value return ( <div> <select value={this.state.city} onChange={this.onSelectChange}> <option value="beijing">北京</option> <option value="shanghai">上海</option> <option value="shenzhen">深圳</option> </select> <p>{this.state.city}</p> </div> ); // checkbox return ( <div> <input type="checkbox" checked={this.state.flag} onChange={this.onCheckboxChange} /> <p>{this.state.flag.toString()}</p> </div> ); // radio return ( <div> male{" "} <input type="radio" name="gender" value="male" checked={this.state.gender === "male"} onChange={this.onRadioChange} /> female{" "} <input type="radio" name="gender" value="female" checked={this.state.gender === "female"} onChange={this.onRadioChange} /> <p>{this.state.gender}</p> </div> ); // 非受控组件 - 后面再讲 } onInputChange = (e) => { this.setState({ name: e.target.value, }); }; onTextareaChange = (e) => { this.setState({ info: e.target.value, }); }; onSelectChange = (e) => { this.setState({ city: e.target.value, }); }; onCheckboxChange = () => { this.setState({ flag: !this.state.flag, }); }; onRadioChange = (e) => { this.setState({ gender: e.target.value, }); }; }
受控组件指的是,表单内容和 state 关联在一起,因此通过 state 就可以控制组件内容。
非受控组件就是不受 state 控制的组件
-
input textarea select 用 value
-
checkbox radio 用 checked
7. React 组件使用
- props 传递数据
- props 传递函数
- props 类型检查
以 TodoList 为例,里边有详细的流程:
父组件 TodoListDemo:
class TodoListDemo extends React.Component {
constructor(props) {
super(props);
// 状态(数据)提升,父组件管理数据
this.state = {
list: [
{
id: "id-1",
title: "标题1",
},
{
id: "id-2",
title: "标题2",
},
{
id: "id-3",
title: "标题3",
},
],
footerInfo: "底部文字",
};
}
render() {
return (
<div>
<Input submitTitle={this.onSubmitTitle} />
<List list={this.state.list} />
<Footer text={this.state.footerInfo} length={this.state.list.length} />
</div>
);
}
onSubmitTitle = (title) => {
this.setState({
list: this.state.list.concat({
id: `id-${Date.now()}`,
title,
}),
});
};
}
输入框组件 Input:
class Input extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
};
}
render() {
return (
<div>
<input value={this.state.title} onChange={this.onTitleChange} />
<button onClick={this.onSubmit}>提交</button>
</div>
);
}
onTitleChange = (e) => {
this.setState({
title: e.target.value,
});
};
onSubmit = () => {
const { submitTitle } = this.props;
submitTitle(this.state.title); // 'abc'
this.setState({
title: "",
});
};
}
// props 类型检查
Input.propTypes = {
submitTitle: PropTypes.func.isRequired,
};
列表组件 List:
class List extends React.Component {
constructor(props) {
super(props);
}
render() {
const { list } = this.props;
return (
<ul>
{list.map((item, index) => {
return (
<li key={item.id}>
<span>{item.title}</span>
</li>
);
})}
</ul>
);
}
}
// props 类型检查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
};
Footer 组件,底部文字和列表内项目数量展示:
class Footer extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<p>
{this.props.text}
{this.props.length}
</p>
);
}
componentDidUpdate() {
console.log("footer did update");
}
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.text !== this.props.text ||
nextProps.length !== this.props.length
) {
return true; // 可以渲染
}
return false; // 不重复渲染
}
// React 默认:父组件有更新,子组件则无条件也更新!!!
// 性能优化对于 React 更加重要!
// SCU 一定要每次都用吗?—— 需要的时候才优化
}
8. setState
- 不可变值
- 可能是异步更新
- 可能会被合并
8.1 setState 不可变值
state 要在构造函数中定义
class StateDemo extends React.Component {
constructor(props) {
super(props);
// 第一,state 要在构造函数中定义
this.state = {
count: 0,
};
}
不能直接修改 state
不能直接修改的原因在于,eact的底层实现是diff,所以必须依赖数据的 immutability,破坏了immutability 会发生各种难以注意到的问题。在性能提升上,为了 SCU,比较新 state 和旧 state 是否相同时,能够分清楚哪些属性是真正引起视图更新的。
increase = () => {
// 第二,不要直接修改 state ,使用不可变值 ----------------------------
// this.state.count++ // 错误
this.setState({
count: this.state.count + 1 // SCU
})
// 操作数组、对象的的常用形式
对 state 里的数组进行修改
// 不可变值(函数式编程,纯函数) - 数组
const list5Copy = this.state.list5.slice()
list5Copy.splice(2, 0, 'a') // 中间插入/删除
this.setState({
list1: this.state.list1.concat(100), // 追加
list2: [...this.state.list2, 100], // 追加
list3: this.state.list3.slice(0, 3), // 截取
list4: this.state.list4.filter(item => item > 100), // 筛选
list5: list5Copy // 其他操作
})
// 注意,不能直接对 this.state.list 进行 push pop splice 等,这样违反不可变值
对 state 里的对象进行修改
// 不可变值 - 对象
this.setState({
// 旧的对象传进去再拼接上新内容
obj1: Object.assign({}, this.state.obj1, {a: 100}),
obj2: {...this.state.obj2, a: 100}
})
// 注意,不能直接对 this.state.obj 进行属性设置,这样违反不可变值
8.2 setState 是同步还是异步
一般情况下是异步的
this.setState(
{
count: this.state.count + 1,
},
() => {
// 联想 Vue $nextTick - DOM
console.log("count by callback", this.state.count); // 回调函数中可以拿到最新的 state
}
);
console.log("count", this.state.count); // 因为是异步的,所以拿不到最新值
setTimeout 中 setState 是同步的
// setTimeout 中 setState 是同步的
setTimeout(() => {
this.setState({
count: this.state.count + 1,
});
console.log("count in setTimeout", this.state.count);
}, 0);
自己定义的 DOM 事件,setState 是同步的
bodyClickHandler = () => {
this.setState({
count: this.state.count + 1,
});
console.log("count in body event", this.state.count);
};
componentDidMount() {
// 自己定义的 DOM 事件,setState 是同步的
document.body.addEventListener("click", this.bodyClickHandler);
}
componentWillUnmount() {
// 及时销毁自定义 DOM 事件
document.body.removeEventListener("click", this.bodyClickHandler);
// clearTimeout
}
8.3 setState 何时会合并 state
传入对象,会被合并。原因在于异步更新,setState 之前的初始值都是一样的。
// 传入对象,会被合并(类似 Object.assign )。执行结果只一次 +1
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
this.setState({
count: this.state.count + 1,
});
传入函数就不会被合并,一个一个去执行
// 传入函数,不会被合并。执行结果是 +3
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
this.setState((prevState, props) => {
return {
count: prevState.count + 1,
};
});
9. 组件生命周期
最常用的生命周期
基本的声明周期
父子组件生命周期函数执行顺序
-
进入页面:parent-constructor -> parent-getDerivedStateFromProps -> parent-render -> child-constructor -> child-getDerivedStateFromProps -> child-render -> child-componentDidMount -> parent-componentDidMount
-
更新页面:parent-getDerivedStateFromProps -> parent-shouldComponentUpdate -> parent-render -> child-getDerivedStateFromProps -> child-shouldComponentUpdate -> child-render -> child-componentDidUpdate -> parent-componentDidUpdate
-
销毁页面:parent-componentWillUnmount -> child-componentWillUnmount
10. React 高级特性
- 函数组件
- 非受控组件
- Portals
- context
- 异步组件
- 性能优化
- 高阶组件 HOC
- Render Props
10. React 函数组件和 class 组件有何区别
函数组件
- 纯函数,输入 props,输出 JSX
- 没有实例,没有生命周期,没有 state
- 不能扩展其他方法
函数组件
- 有组件实例
- 有生命周期
- 有 state 和 setState
11. 非受控组件
- ref
- defaultValue defaultChecked
- 手动操作 DOM 元素
非受控组件的特点,该组件不受 state 操控。
输入框组件作为非受控组件
通过 React.createRef()
创建 ref 创建一个属性,如下面代码的 nameInputRef,然后在非受控组件里使用新建的 ref。
.current
获取当前的节点,便可以获得里边的值
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
name: "双越",
flag: true,
};
this.nameInputRef = React.createRef(); // 创建 ref
}
render() {
// input defaultValue
return (
<div>
{/* 使用 defaultValue 而不是 value ,使用 ref */}
<input defaultValue={this.state.name} ref={this.nameInputRef} />
{/* state 并不会随着改变 */}
<span>state.name: {this.state.name}</span>
<br />
<button onClick={this.alertName}>alert name</button>
</div>
);
alertName = () => {
const elem = this.nameInputRef.current; // 通过 ref 获取 DOM 节点
alert(elem.value); // 不是 this.state.name
};
}
checkbox 作为非受控组件
默认初始值使用 defaultChecked,但是并不会同步到 state 中。
// checkbox defaultChecked
return (
<div>
<input type="checkbox" defaultChecked={this.state.flag} />
</div>
);
例子:通过 ref 获取文件名
class App extends React.Component {
constructor(props) {
super(props);
this.fileInputRef = React.createRef();
}
render() {
// file
return (
<div>
<input type="file" ref={this.fileInputRef} />
<button onClick={this.alertFile}>alert file</button>
</div>
);
}
alertFile = () => {
const elem = this.fileInputRef.current; // 通过 ref 获取 DOM 节点
alert(elem.files[0].name);
};
}
总结:非受控组件 - 使用场景
- 必须手动操作 DOM 元素,setState 实现不了
- 文件上传
<input type="file" />
- 某些富文本编辑器,需要传入 DOM 元素
受控组件 vs 非受控组件
- 优先使用受控组件,符合 React 设计原则
- 必须操作 DOM 时,再使用非受控组件
12. React Portals
- 组件默认会按照既定层次嵌套渲染
- 如何让组件渲染到父组件以外?
例子,将样式渲染到 body 上
为了浏览器的兼容,想要把 model 内容挂在 body 上,因为有更好的浏览器兼容性。
.modal {
position: fixed;
width: 300px;
height: 100px;
top: 100px;
left: 50%;
margin-left: -150px;
background-color: #000;
/* opacity: .2; */
color: #fff;
text-align: center;
}
正常渲染
父组件传东西进来,可以通过 this.props.children
获得,就很像 vue 里的 slot。
父组件:
class AdvancedUse extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<PortalsDemo>Modal 内容</PortalsDemo>
</div>
);
}
}
子组件:
class PortalsDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
// 正常渲染
return (
<div className="modal">
{this.props.children} {/* vue slot */}
</div>
);
......
渲染结果:
使用 React Portals
子组件:
portal 有传送门的意思,使用 createPortal
建立一个传送门,重点是第二个参数,第二个参数指传送到哪个 DOM 节点下。
class PortalsDemo extends React.Component {
constructor(props) {
super(props)
this.state = {
}
}
render() {
// 使用 Portals 渲染到 body 上。
// fixed 元素要放在 body 上,有更好的浏览器兼容性。
return ReactDOM.createPortal(
<div className="modal">{this.props.children}</div>,
document.body // DOM 节点
);
......
渲染结果:
可以看到,父组件里没有 class 为 model 的组件了,而是挂载到了 body 的第一层
总结:Portals 使用场景
- 父组件 overflow: hidden,是 BFC,限制子组件展示,可以让子组件逃离父组件
- 父组件 z-index 值太小,导致子组件在里边展示的话会被覆盖
- fixed 需要放在 body 第一层级
13. React Context
- 公共信息(语言、主题)如何传递给每个组件?
- 用 props 太繁琐
- 用 redux 有时小题大做
举例:切换主题
例如,theme 属性要通过两层传递到子组件。
import React from "react";
// 创建 Context 填入默认值(任何一个 js 变量)
const ThemeContext = React.createContext("light");
// 底层组件 - 函数是组件
function ThemeLink(props) {
// const theme = this.context // 会报错。函数式组件没有实例,即没有 this
// 函数式组件可以使用 Consumer
return (
<ThemeContext.Consumer>
{(value) => <p>link's theme is {value}</p>}
</ThemeContext.Consumer>
);
}
// 底层组件 - class 组件
class ThemedButton extends React.Component {
// 指定 contextType 读取当前的 theme context。
// static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext
render() {
const theme = this.context; // React 会往上找到最近的 theme Provider,然后使用它的值。
return (
<div>
<p>button's theme is {theme}</p>
</div>
);
}
}
ThemedButton.contextType = ThemeContext; // 指定 contextType 读取当前的 theme context。
// 中间的组件再也不必指明往下传递 theme 了。
function Toolbar(props) {
return (
<div>
<ThemedButton />
<ThemeLink />
</div>
);
}
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
theme: "light",
};
}
render() {
return (
<ThemeContext.Provider value={this.state.theme}>
<Toolbar />
<hr />
<button onClick={this.changeTheme}>change theme</button>
</ThemeContext.Provider>
);
}
changeTheme = () => {
this.setState({
theme: this.state.theme === "light" ? "dark" : "light",
});
};
}
export default App;
-
先使用
createContext
创建一个 context 对象并赋予一个默认值const ThemeContext = React.createContext("light");
-
用生成的对象,作为一个生产方(Provider),值为 value 属性,里边的值可以自己控制
render() { return ( <ThemeContext.Provider value={this.state.theme}> <Toolbar /> <hr /> <button onClick={this.changeTheme}>change theme</button> </ThemeContext.Provider> ); }
-
消费者消费数据,class 组件和函数组件的写法不同
-
class 组件,先定义一个静态的属性,contextType,然后赋值为 context 对象,然后用
this.context
获取值即可// 底层组件 - class 组件 class ThemedButton extends React.Component { // 指定 contextType 读取当前的 theme context。 // static contextType = ThemeContext // 也可以用 ThemedButton.contextType = ThemeContext render() { const theme = this.context; // React 会往上找到最近的 theme Provider,然后使用它的值。 return ( <div> <p>button's theme is {theme}</p> </div> ); } } ThemedButton.contextType = ThemeContext;
-
函数组件,通过 Consumer 包裹一个函数,函数的参数就是目标值
function ThemeLink(props) { // const theme = this.context // 会报错。函数式组件没有实例,即没有 this // 函数式组件可以使用 Consumer return ( <ThemeContext.Consumer> {(value) => <p>link's theme is {value}</p>} </ThemeContext.Consumer> ); }
-
关键知识点:使用场景和使用的相关语法
14. 异步组件
-
webpack 支持 import() 异步加载 js 模块
if (isRequire) { const result = await import('./b') // 返回一个 promise }
异步组件在打包的时候都会把当前的组件单独打成一个 js 包去异步加载。
但是这个也会产生问题:因为是动态加载模块,所以 Tree-Shaking 不起作用,所以可以创建一个新模块来按需引入需要异步加载的模块,然后动态加载新模块即可。
-
React .lazy 和 React.Suspense
- React.lazy 动态加载异步组件
- React.Suspense 组件用来,当异步组件未加载出来的时候提供加载信息
const ContextDemo = React.lazy(() => import('./ContextDemo')) class App extends React.Component { constructor(props) { super(props); } render() { return ( <div> <p>引入一个动态组件</p> <hr /> <React.Suspense fallback={<div>Loading...</div>}> <ContextDemo /> </React.Suspense> </div> ); // 1. 强制刷新,可看到 loading (看不到就限制一下 chrome 网速) // 2. 看 network 的 js 加载
15. React 性能优化
- 性能优化,永远都是面试的重点
- 性能优化对于 React 更加重要
- 回顾讲 setState 时重点强调的不可变值
15.1 shouldComponentUpdate (SCU)
SCU 基本用法
shouldComponentUpdate(nextProps, nextState) {
if (nextState.count !== this.state.count) {
return true; // 可以渲染
}
return false; // 不重复渲染
}
重点在于:为什么 React 的 SCU 默认返回值为 true,且提供了定制是否更新的权力,而不是将 SCU 的比较写在框架里?
SCU 默认返回什么
如果 React 没有做优化,只要父组件更新,子组件就会无条件更新。
SCU 默认返回值为 true,即重新触发了组件渲染,就默认需要更新。
因此性能优化对于 React 更加重要。
TodoList 里 Footer 组件正确写法:todo items 数量或者 footer 展示文字不变,就不进行更新。
class Footer extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<p>
{this.props.text}
{this.props.length}
</p>
);
}
componentDidUpdate() {
console.log("footer did update");
}
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.text !== this.props.text ||
nextProps.length !== this.props.length
) {
return true; // 可以渲染
}
return false; // 不重复渲染
}
// React 默认:父组件有更新,子组件则无条件也更新!!!
// 性能优化对于 React 更加重要!
// SCU 一定要每次都用吗?—— 需要的时候才优化
}
SCU 一定要配合不可变值
在 List 组件里,进行深度比较,如果相等,则不重复渲染,不相等则渲染。
如果在提交输入框内容的时候,用下面的写法,将会产生一个很严重的 bug,即 List 组件不更新。
onSubmitTitle = (title) => {
// 正确的用法
this.setState({
list: this.state.list.concat({
id: `id-${Date.now()}`,
title,
}),
});
// 为了演示 SCU ,故意写的错误用法
this.state.list.push({
id: `id-${Date.now()}`,
title
})
this.setState({
list: this.state.list
})
};
原因在于,直接修改了 state 内容,违反了不可变规则。这样搞的话,nextProps 便是修改后的内容,比较的时候,this.props.list 和 nextProps.list 两者内容相同,因此理所当然不能更新了。
但是 React 有很多的开发者,其中不乏不规范的开发者或者新手,并不了解这种规则,会瞎写。React 为了避免这种行为造成的 bug,索性 SCU 返回值为 true。
引申:SCU 使用了深度比较,非常耗费性能,可能还不如直接返回 true 效果好。使用浅层比较效果会比较好。
因此,SCU 一定要配合不可变值!
import React from "react";
import PropTypes from "prop-types";
import _ from "lodash";
class Input extends React.Component {
constructor(props) {
super(props);
this.state = {
title: "",
};
}
render() {
return (
<div>
<input value={this.state.title} onChange={this.onTitleChange} />
<button onClick={this.onSubmit}>提交</button>
</div>
);
}
onTitleChange = (e) => {
this.setState({
title: e.target.value,
});
};
onSubmit = () => {
const { submitTitle } = this.props;
submitTitle(this.state.title);
this.setState({
title: "",
});
};
}
// props 类型检查
Input.propTypes = {
submitTitle: PropTypes.func.isRequired,
};
class List extends React.Component {
constructor(props) {
super(props);
}
render() {
const { list } = this.props;
return (
<ul>
{list.map((item, index) => {
return (
<li key={item.id}>
<span>{item.title}</span>
</li>
);
})}
</ul>
);
}
// 增加 shouldComponentUpdate
shouldComponentUpdate(nextProps, nextState) {
// _.isEqual 做对象或者数组的深度比较(一次性递归到底)
if (_.isEqual(nextProps.list, this.props.list)) {
// 相等,则不重复渲染
return false;
}
return true; // 不相等,则渲染
}
}
// props 类型检查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired,
};
class TodoListDemo extends React.Component {
constructor(props) {
super(props);
this.state = {
list: [
{
id: "id-1",
title: "标题1",
},
{
id: "id-2",
title: "标题2",
},
{
id: "id-3",
title: "标题3",
},
],
};
}
render() {
return (
<div>
<Input submitTitle={this.onSubmitTitle} />
<List list={this.state.list} />
</div>
);
}
onSubmitTitle = (title) => {
// 正确的用法
this.setState({
list: this.state.list.concat({
id: `id-${Date.now()}`,
title,
}),
});
// // 为了演示 SCU ,故意写的错误用法
// this.state.list.push({
// id: `id-${Date.now()}`,
// title
// })
// this.setState({
// list: this.state.list
// })
};
}
export default TodoListDemo;
SCU 使用总结
- SCU 默认返回 true,即 React 默认重新渲染所有子组件
- 必须配合”不可变值“一起使用
- 可先不用 SCU,有性能问题时再考虑使用
15.2 PureComponent 和 memo
- PureComponent,SCU 中实现了浅比较
- memo,函数组件中的 PureComponent
- 浅比较已使用大部分情况(尽量不要做深度比较)
PureComponent
class List extends React.PureComponent {
constructor(props) {
super(props)
}
render() {
const { list } = this.props
return <ul>{list.map((item, index) => {
return <li key={item.id}>
<span>{item.title}</span>
</li>
})}</ul>
}
shouldComponentUpdate() {/*浅比较*/}
}
// props 类型检查
List.propTypes = {
list: PropTypes.arrayOf(PropTypes.object).isRequired
}
memo
15.3 immutable.js
- 彻底拥抱”不可变值“
- 基于共享数据(不是深拷贝),速度好
- 有一定学习和迁移成本,按需使用
性能优化 - 小结
- 面试重点,且涉及 React 设计理念
- SCU PureComponent memo immutable.js
- 按需使用 & state 层级
16. React 高阶组件
关于组件公共逻辑的抽离
- mixin,已被 React 弃用
- 高阶组件 HOC
- Render Props
高阶组件基本用法
Higher-Order Components就是一个函数,传给它一个组件,它返回一个新的组件。就相当于手机壳,通过包装组件,增强组件功能,抽离公共逻辑。
示例:抽离公共功能 - 鼠标划过显示 position 的值
import React from "react";
// 高阶组件
const withMouse = (Component) => {
class withMouseComponent extends React.Component {
constructor(props) {
super(props);
this.state = { x: 0, y: 0 };
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY,
});
};
render() {
return (
<div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
{/* 1. 透传所有 props 2. 增加 mouse 属性 */}
<Component {...this.props} mouse={this.state} />
</div>
);
}
}
return withMouseComponent;
};
const App = (props) => {
const a = props.a;
const { x, y } = props.mouse; // 接收 mouse 属性
return (
<div style={{ height: "500px" }}>
<h1>
The mouse position is ({x}, {y})
</h1>
<p>{a}</p>
</div>
);
};
export default withMouse(App); // 返回高阶函数
redux connect 是高阶组件
有两层括号,connect 先返回一个工厂函数,第二个括号放组件,返回一个高阶组件。
17. React Render Props
通过一个函数将 class 组件的 state 作为 props 传递给纯函数组件。
在父组件中向子组件添加了一个函数,子组件通过函数的调用传入一个值或者模板,来调用父元素方法,或者将模板插入组件。
举例:通过 Render Props 实现鼠标 position 的实现
import React from "react";
import PropTypes from "prop-types";
class Mouse extends React.Component {
constructor(props) {
super(props);
this.state = { x: 0, y: 0 };
}
handleMouseMove = (event) => {
this.setState({
x: event.clientX,
y: event.clientY,
});
};
render() {
return (
<div style={{ height: "500px" }} onMouseMove={this.handleMouseMove}>
{/* 将当前 state 作为 props ,传递给 render (render 是一个函数组件) */}
{this.props.render(this.state)}
</div>
);
}
}
Mouse.propTypes = {
render: PropTypes.func.isRequired, // 必须接收一个 render 属性,而且是函数
};
const App = (props) => (
<div style={{ height: "500px" }}>
<p>{props.a}</p>
<Mouse
render={
/* render 是一个函数组件 */
({ x, y }) => (
<h1>
The mouse position is ({x}, {y})
</h1>
)
}
/>
</div>
);
/**
* 即,定义了 Mouse 组件,只有获取 x y 的能力。
* 至于 Mouse 组件如何渲染,App 说了算,通过 render prop 的方式告诉 Mouse 。
*/
export default App;
HOC vs Render Props
- HOC:模式简单,但会增加组件层级
- Render Props:代码简洁,学习成本较高
- 按需使用
18. React 高级特性 - 知识点总结
- 函数组件
- 非受控组件
- Portals
- context
- 异步组件
- 性能优化(重要)
- 高阶组件 HOC
- Render Props
19. Redux
- 和 Vuex 作用相同,但比 Vuex 学习成本高
- 不可变值,纯函数
- 面试常考
19.1 Redux 使用
- 基本概念
- 单向数据流
- react-redux
- 异步 action
- 中间件
基本概念
- store state
- action
- reducer
单向数据流概述
https://www.redux.org.cn/docs/basics/DataFlow.html
- dispatch(action)
- reducer -> newState
- subscribe 触发通知
react-redux
- <Provider> connect
- connect
- mapStateToProps mapDispatchToProp
使用流程:
- 引入 provider
import React from "react";
import { Provider } from "react-redux";
import { createStore } from "redux";
import todoApp from "./reducers";
import App from "./components/App";
let store = createStore(todoApp);
export default function () {
return (
<Provider store={store}>
<App />
</Provider>
);
}
- 想要消费数据的话,需要使用 connect
-
函数组件中的普通写法,把 dispatch 函数作为参数传进去。
// 函数组件,接收 props 参数 let AddTodo = ({ dispatch }) => { // dispatch 即 props.dispatch let input return ( <div> <form onSubmit={e => { e.preventDefault() if (!input.value.trim()) { return } // 创建一个 todo dispatch(addTodo(input.value)) input.value = '' }} > <input ref={node => { input = node }} /> <button type="submit"> Add Todo </button> </form> </div> ) } // connect 高阶组件 ,将 dispatch 作为 props 注入到 AddTodo 组件中 AddTodo = connect()(AddTodo) export default AddTodo
-
如果需要携带更多信息,如 mapStateToProps,mapDispatchToProps 等,就需要自己自定义。定义后,就可以通过 props 的形式获取 store 里面的数据,或者 dispatch action。
// 函数组件 { todos, onTodoClick } = props const TodoList = ({ todos, onTodoClick }) => ( <ul> {todos.map(todo => ( <Todo key={todo.id} {...todo} onClick={() => onTodoClick(todo.id)} /> // 点击切换完成状态 ))} </ul> ) // 不同类型的 todo 列表 const getVisibleTodos = (todos, filter) => { switch (filter) { case 'SHOW_ALL': return todos case 'SHOW_COMPLETED': return todos.filter(t => t.completed) case 'SHOW_ACTIVE': return todos.filter(t => !t.completed) } } const mapStateToProps = state => { // state 即 vuex 的总状态,在 reducer/index.js 中定义 return { // 根据完成状态,筛选数据 todos: getVisibleTodos(state.todos, state.visibilityFilter) } } const mapDispatchToProps = dispatch => { return { // 切换完成状态 onTodoClick: id => { dispatch(toggleTodo(id)) } } } // connect 高阶组件,将 state 和 dispatch 注入到组件 props 中 const VisibleTodoList = connect( mapStateToProps, mapDispatchToProps )(TodoList) export default VisibleTodoList
异步 action
同步直接返回 action 对象,异步返回一个函数,里边再做一层 dispatch
但是前提是,需要引入中间件 redux-thunk
其他的异步 action 中间件:
- redux-thunk
- redux-promise
- redux-saga
19.2 Redux 中间件
在 dispatch 部分添加一些逻辑。
使用中间件
redux 中间件 - logger 实现
在 dispatch 的时候会输出相应的信息。
redux 数据流图
Redux 知识点总结
- 基本概念
- 单向数据流
- react-redux
- 异步 action
- 中间件
20. React-router
- 面试考点并不多(前提是熟悉 React)
- 路由模式(hash,H5 history),同 vue-router
- 路由配置(动态路由、懒加载),同 vue-router
路由模式
动态路由和跳转路由
Link 跳转路由,还可以用 JS history 跳转:
懒加载
总结
- 路由模式(hash、H5 history)
- 路由配置(动态路由、懒加载)
- 掌握基本使用
21. React 使用总结
- 基本使用 —— 常用,必须会
- 高级特性 —— 不常用,但体现深度
- Redux 和 React-router 使用