笔者在17年接触前端以来,一直都是用Vue。可以说歪打正着吧,加入过的每个团队都是Vue,完美的避开了React。但是大环境下,React在大厂的地位不可动摇,只有一门Vue是不够的。如何高效学习React并掌握面试要领,这里我将把自己的学习历程做成笔记,分享给大🔥
目录大全
直接看看本文涉及的React
知识点checklist👇
一、基本使用 | 二、高级特性 | 三、浅析原理 | 四、Hooks |
---|---|---|---|
1.jsx基本使用 | 1.非受控组件 | 1.vNode+diff | 1.useState |
2.条件渲染、列表渲染 | 2.React Portals | 2.jsx本质 | 2.useEffect |
3.事件event | 3.React Context | 3.React合成事件 | 3.useRef、useContext |
4.受控组件 | 4.异步组件 | 4.batchUpdate机制 | 4.useReducer |
5.父子组件通信 | 5.SCU | 5.组件更新和渲染(fiber) | 5.useMemo |
6.setState | 6.高阶组件HOC | 6.性能优化 | 6.useCallback |
7.生命周期 | 7.自定义Hook | ||
8.Hooks注意事项 |
先从class组件
入手,再了解原理
,最后再到React Hooks
。其中,小弟还会根据Vue
的相关知识点跟React
做对比,以加深对React
的理解和掌握。只看不做可能印象不够深刻,大🔥可以跟着文章走自己敲一敲~
👉另外,想学习Vue
源码的朋友,点击传送门进入小弟的专栏看看哈~
一、基本使用
1. jsx基本使用
案例代码:
class MyComponent extends React.Component{
constructor(props) {
super(props)
this.state = {
text: '模板插值',
imageUrl: 'xxx.png'
}
}
render() {
const myStyle = { color: 'blue' }
return (
<div>
<!-- 模板插值 -->
<p>{this.state.text}</p>
<!-- 属性 -->
<img src={this.state.imageUrl} alt=""/>
<!-- 类名 -->
<p className="text">{this.state.text}</p>
<!-- 样式 -->
<p style={myStyle}>{this.state.text}</p>
<!-- 原生html -->
<p dangerouslySetInnerHTML={{ __html: '<i>我是斜体字</i>' }} />
</div>
);
}
}
- 插值
{}
。对应Vue的{{}}
语法- 模板中使用。
<p>{this.state.text}</p>
- 属性中使用。
<img src={this.state.imageUrl} alt=""/>
- 模板中使用。
- className。Vue中直接写class,动态类名使用v-bind
- class写成className(由于class是js的保留字)。
<p className="text">{this.state.text}</p>
- class写成className(由于class是js的保留字)。
- style。对应Vue的v-bind
- 使用对象。
<p style={myStyle}>{this.state.text}</p>
- 直接写内联。
<p style={{ color: 'blue' }}>{this.state.text}</p>
- 使用对象。
- 原生html。对应Vue的v-html
<p dangerouslySetInnerHTML={{ __html: '<i>123</i>' }} />
2. 条件渲染、列表渲染
- 条件渲染。对应Vue的
v-if
// React render () { return this.state.flag ? <p>is flag</p> : <p>not flag</p> } // Vue <p v-if="flag">is flag</p> <p v-else>not flag</p>
- 列表渲染(
map函数
、key
)。对应Vue的v-for
// React state = { list: ['list1', 'list2'] } render () { // 在{函数体}中,通过map返回一个li的list return ( <ul> { this.state.list.map(item => <li key={item}>{item}</li>) } </ul> ) } // Vue <ul> <li v-for="item in list" :key="item">{{ item }}</li> </ul>
到这里可以看出其实二者真的很相似,只是:
- React提供了简单的jsx语法,具体实现需要js的能力。内部没有再提供指令,理解为简洁上手吧。
- Vue提供了很多指令,如v-if、v-for这种。用之前需要先了解指令的相关用法,日常使用会有一定便捷~
3. 事件——event
事件写法: onXxxx
,如onClick。Vue中是@click
、v-on:click
-
事件调用方法时的this
- bind(this)
- 箭头函数
class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { text: '模板插值' } // bind(this) this.handleClick = this.handleClick.bind(this) } handleClick = function () { this.setState({ text: '修改text' }) } // 箭头函数 handleArrowClick = () => { this.setState({ text: '修改text from handleStaticClick' }) } render() { return ( <div> <p>{this.state.text}</p> <!-- bind(this)也可以在这里写,但是写在constructor性能更佳 --> <button onClick={this.handleClick}>change</button> <!-- 这里用的静态方法。静态方法this永远指向该实例 --> <button onClick={this.handleArrowClick}>change</button> </div> ) } }
- 浅析理解:React的事件不是真实绑定在dom上。并且事件调用并不是发生在组件层,而是发生在最外层的事件监听器(事件委托)。而js的
this
关键字是运行时确定的,在函数被调用时才绑定执行环境(谁调用就指向谁)。所以事件中需要绑定this
;或者使用箭头函数写法(定义时就绑定this
指向当前组件)
-
event参数(Vue的
event
是原生事件)- event并不是原生事件,而是React封装的组合事件。如下代码和输出:
handleClick = function (event) { // react的合成事件 console.log('reactEvent', event) // 原生事件 console.log('nativeEvent', event.nativeEvent) // 指向当前元素 console.log('nativeEvent', event.nativeEvent.target) // 指向document console.log('nativeEvent', event.nativeEvent.currentTarget) }
- 模拟出了DOM的所有能力。如
event.preventDefault()
、event.stopPropagation()
- 所有事件都绑定到
document
上(React17之前) - 所有事件都绑定到
root组件
(React17)如下图:
-
传递自定义参数
- 正常使用的方法传递即可,需要注意的是
event
参数会追加到最后一位
handleClick = function (param, event) { console.log(param, event) }
- 正常使用的方法传递即可,需要注意的是
4. 受控组件
- 受控组件简单理解:表单的值,受到state的控制
- state驱动视图渲染(可对应高级特性中的非受控组件进行理解)。如表单中实现双向绑定。对应Vue的
v-model
class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { inputVal: 'input' } } render() { return ( <div> <p>{this.state.inputVal}</p> <input type="text" value={this.state.inputVal} onInput={this.handleInput} /> </div> ) } // 需要手动实现数据变更(比v-model多了一步) handleInput = e => { this.setState({ inputVal: e.target.value }) } }
5. 父子组件通信
- 传递数据和接收数据。对应Vue的是
v-bind
和接收props
- 传递数据:
<ChildComponent count={this.state.count} />
- 接收数据:
class ChildComponent extends React.Component { constructor(props) { super(props) } render() { return <div> {this.props.count} </div> } }
- 传递函数。对应Vue的
@xxx=xxx
、this.$emit('xxx')
// 父组件 class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { count: 99 } } render() { return ( <div> <ChildComponent count={this.state.count} changeCount={this.changeCount} /> </div> ) } // 接收子组件传递的count来修改自身的count changeCount = count => { this.setState({ count }) } } // 子组件 class ChildComponent extends React.Component { constructor(props) { super(props) } render() { return <div> {this.props.count} <button onClick={this.addCount}>addCount</button> </div> } addCount = () => { // 获取父组件传递的props const { changeCount, count } = this.props // 调用父组件的changeCount方法 changeCount(count + 1) }
6. setState
- 不可变值
- class组件使用
setState
修改值,要注意不可变值的概念。不可变值也是从Vue转React时需要记住的一个概念~// 修改基础类型数据 this.setState({ // 保证原有值不变,不能直接操作state的值(不能写成this.state.count++) // this.state.count + 1 是一个新的结果 ✅ // this.state.count++ 则是直接修改了this.state中的值 ❌ count: this.state.count + 1 }) // 修改引用类型——数组 this.setState({ // 避免使用push、pop等直接修改原数组的方法 list: [...this.state.list, 3], // 新增一个值 list2: this.state.list.slice(1) // 截取 }) // 修改引用类型——对象 this.setState({ // 避免直接在原对象上做修改 obj: {...this.state.obj, name: 'jingboran'} })
- 实际开发中可能会遇到深层对象的情况需要setState,可以使用
immer
、immutable
这种不可变值的库。这里推荐一篇相关的文章,讲得比较详细。
- setState是同步执行还是异步执行?
- 分两种情况(具体参考如代码演示):
- 正常用法时是异步执行;
- 在setTimeout和自定义DOM事件的cb中是同步执行
// 直接用setState是异步的,这里console中拿不到修改后的值 this.setState({ count: this.state.count + 1 }) // 同步console拿不到修改后的值(如果需要拿修改后的值,可以在setState中添加回调) console.log(this.state.count) // 在setTimeout中用setState是同步的 setTimeout(() => { this.setState({ count: this.state.count + 1 }) // 这里能拿到最新的结果 console.log(this.state.count) }) // 在自定义的DOM事件用setState也是同步 componentDidMount() { // 绑定body的点击事件 document.body.addEventListener('click', () => { this.setState({ count: this.state.count + 1 }) // 能拿到最新结果 console.log(this.state.count) }) }
- setState是否会被合并执行
- 两种情况:
- 合并执行——传入对象(注意:如果在
setTimeout
的回调中是同步执行,也就不存在合并执行的说法了)// 常规用法时,setState异步执行,结果只加1(被合并执行) this.setState({ count: this.state.count + 1 }) this.setState({ count: this.state.count + 1 })
- 不合并执行——传入函数
// 结果加2,分别都执行 this.setState((prevState) => { return { count: prevState.count + 1 } }) this.setState((prevState) => { return { count: prevState.count + 1 } })
- 合并执行——传入对象(注意:如果在
7. 生命周期(对比Vue)
- React
- constructor类似Vue的
init
阶段(initLifeCycle
、initState
......) - render类似Vue的
$mount
阶段(执行render
-> 得到VNode
->patch
到DOM上) componentDidMount
等价Vue的mounted
- 在组件更新阶段,render之前还有一个
shouldComponentUpdate
的阶段(能控制本次组件是否需要render),可以在该阶段做一些性能优化
- constructor类似Vue的
- Vue(直接贴上官网的生命周期图回顾一下~)
二、高级特性
1. 非受控组件
- state不控制视图渲染。理解:数据不受组件state的控制,直接跟DOM相关
- 使用场景:必须要操作DOM,setState不能满足。比如input的
type=file
时的一些交互class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { inputText: '' } // 获取DOM。对应Vue的ref this.textInput = React.createRef() } render() { return ( <div> <!-- 对应Vue中的ref,但是Vue传入的是个字符串 --> <input type="text" ref={this.textInput} defaultValue={this.state.inputText} /> </div> ) } handleClick = function () { // 直接获取DOM的值 console.log(this.textInput.current.value) } }
2. React Portals
- 让组件渲染到父组件以外。对应Vue3的
teleport
- 场景:比如日常开发中,父组件设置overflow:hidden的布局。导致组件内的一些弹窗、下拉菜单(绝对定位)等被切割的问题,这时候需要逃离父组件的嵌套
return ReactDOM.createPortal( <div> <p>子组件挂在到body层,逃离父组件</p> </div>, document.body )
3. React Context
- 两个核心:1.数据创建:
provider
;2.数据消费:consumer
- 具有数据透传的能力,子孙组件能拿到最外层组件的数据。有点类似Vue的
v-bind="$attrs"
// MyContext文件 import { createContext } from 'react' export default createContext() // 外层组件Provider import MyContext from './MyContext' class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { val: 'providerVal' } } render() { return ( // 设置value值 <MyContext.Provider value={this.state.val}> <div> <!-- Second子组件 --> <Second /> </div> </MyContext.Provider> ) } } // 子组件Consumer——Class Component import MyContext from './MyContext' import Third from './Third' export default class Second extends React.Component { // 通过静态属性contextType接收MyContext static contextType = MyContext // 通过this.context访问 render() { return ( <div> { this.context } <!-- Second组件的子组件 --> <Third/> </div> ) } } // 孙组件Consumer——function Component import MyContext from './MyContext' import {useContext} from 'react' export default function Third () { // 使用useContext获取 const context = useContext(MyContext) return ( <div>{ context }</div> ) }
4. 异步组件
-
两个核心
lazy
+Suspense
。跟Vue的异步组件很相似。可以一起学习理解// Vue中的异步组件——工厂函数 Vue.component('async-component', function (resolve) { require(['./async-component'], resolve) }) // Vue中的异步组件——Promise Vue.component( 'async-component', () => import('./async-component') ) // React的异步组件 // lazy接收一个Promise,并返回一个新的React组件 const Second = React.lazy(() => import('./Second')) class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { val: 'providerVal' } } render() { return ( <MyContext.Provider value={this.state.val}> <div> <!-- fallback为必传属性(未加载完时显示),接收一个组件 --> <React.Suspense fallback={<div>loading...</div>}> <Second /> </React.Suspense> </div> </MyContext.Provider> ) } }
5. shouldComponentUpdate
- React的性能优化的重点。
SCU
是组件update前的一个生命周期,可控制组件更新 - 默认返回
true
(组件每次都会执行render)。可通过自定义逻辑(如比对数据变化判断是否需要更新),返回false对组件进行性能优化 - 基本用法:
class MyComponent extends React.Component{ constructor(props) { super(props) } shouldComponentUpdate (nextProps, nextState, nextContext) { console.log(nextState.count, this.state.count) if (nextState.count !== this.state.count) { return true // 允许渲染,会往下执行render } return false // 不渲染,不执行render } render() { return ... } }
- 为什么需要SCU这个生命周期优化
- React的组件更新逻辑:
父组件更新,子组件无条件更新
。对比Vue,这点有很大的区别
。熟悉Vue响应式原理的都知道,Vue的数据是收集对应组件的渲染Watcher的,也就是形成了一个对应关系。只有当前组件依赖的值发生改变才会触发组件更新。 - 如下代码。父组件点击按钮会修改
count
值触发父组件更新。如果不设置SCU
,则父组件更新会导致子组件ShowText
更新。观察可知,子组件接收的props
并未被修改,其实是不需要重新render的。所以SCU的优化场景就出现了~// 父组件 class MyComponent extends React.Component{ constructor(props) { super(props) this.state = { count: 0, text: 'children Component' } } render() { return ( <div> <p>{this.state.count}</p> <button onClick={this.handleClick}>add count</button> <!-- 注意子组件接收的text并未发生改变 --> <ShowCount count={this.state.text} /> </div> ) } handleClick = function () { // 父组件点击修改Count this.setState({ count: this.state.count + 1 }) } } // 子组件 class ShowText extends React.Component{ constructor(props) { super(props) } componentDidUpdate(prevProps, prevState, snapshot) { console.log('is update') } shouldComponentUpdate (nextProps, nextState, nextContext) { // 判断props的text是否改变 if (nextProps.text !== this.props.text) { return true // 允许渲染,会往下执行render } return false // 不渲染,不执行render } render() { return ( <div> <p>{this.props.text}</p> </div> ); } }
- 思考:为什么React不在内部实现SCU,而是提供了个生命周期出来给开发者自行实现?
- 不是所有场景都有必要做性能优化。出现卡顿的时候再进行优化往往是比较合理的开发模式。如果所有情况都直接在SCU做深度比对state,这样本来就会造成性能损耗,往往是不必要的~
- SCU一定要配合不可变值来用。如果内部实现
SCU
深度比对state
再判断是否更新组件,且不遵循不可变值写法可能会造成组件更新失败问题。因为不排除有不遵循不可变值写法的开发者。比如this.state.list.push(3)
,再写this.setState({list: this.state.list})
这种情况
PureComponent
——实现了浅比较的SCU- 其实就是在SCU中对state和props做了一层浅比较
- 用上述案例来改造,能达到一样的效果。父组件改变count,子组件不会执行更新
// 用法:extends React.PureComponent export default class ShowText extends React.PureComponent{ constructor(props) { super(props) } componentDidUpdate(prevProps, prevState, snapshot) { console.log('is update') } render() { return ( <div> <p>{this.props.text}</p> </div> ); } }
6. 高阶组件HOC
- 组件的公共逻辑抽离。其实就是个高阶函数的概念。如果觉得不好理解,可以看看小弟另一篇文章,戳这里,有高阶函数的案例介绍
- 接收一个组件,返回一个新的组件。比如每个组件都需要拥有计数器的能力,则可这么写:
// 高阶组件函数 function HOCFactory (Component) { // 接收一个组件参数:Component class CountComponent extends React.Component { constructor(props) { super(props) // 公用逻辑计数器的count this.state = { count: 0 } } render() { return ( <div> <!-- 透传所有的props,并把count传递到组件以复用 --> <Component {...this.props} count={this.state.count}/> <button onClick={this.handleAdd}>add Count</button> </div> ) } handleAdd = () => { this.setState({ count: this.state.count + 1 }) } } // 返回一个新的组件 return CountComponent } // 普通组件 class NormalComponent extends React.PureComponent{ constructor(props) { super(props) } render() { return ( <div> <!-- 接收高阶组件传递的count并显示 --> <p>公用计数器:{this.props.count}</p> <p>组件自身的逻辑...</p> </div> ); } } export default HOCFactory(NormalComponent)
- 使用方式跟普通组件一样,但是公用逻辑已经抽离
import HOCComponent from './HOCComponent' class MyComponent extends React.Component{ constructor(props) { super(props) } render() { return ( <div> <HOCComponent /> </div> ) } }
- 效果如图所示
- Vue的大概HOC也来一个~其实原理都是一样的~
const HOCFactory = Component => {
const instance = new Vue({
// 实现公共组件逻辑
created () {},
mounted () {},
data () {},
// render中传入参数的组件Component
render (h) => h(Component, {
props: {
// 传入props
}
})
})
// 返回这个组件实例
return instance
}
三、浅析原理
1. vNode + diff算法
- Vue2、Vue3、React具体vNode和diff实现不同,大致理解其中一个的实现即可,原理都相似
- 理解组件化流程:render -> vNode -> patch -> DOM
- 理解vNode是如何提升性能的。单纯vNode并不能提升性能,而是需要配合diff
- diff算法的核心三步走(vue2为例):
- 首先,同层vNode对比(
tag
、key
、data
)。不同则整个vNode替换 - 其次,旧vNode有子节点、新vNode无子节点。删除子节点
- 其次,新vNode有子节点、旧vNode无子节点。新增子节点
- 最后,都有子节点,进行子节点比对(
首位指针法
)
- 首先,同层vNode对比(
2. jsx本质
- 理解render函数的,可以自己手写Render函数。可以了解下编译原理(React的render函数可以再
Babel
官网编译查看变异结果) - 对应Vue中的template的本质(.vue文件最终也是编译成render函数)
- render函数基本写法
- 第一个参数:
- 字符串(如html的保留标签)
- 组件对象(React规定组件首字母大写)
- 第二个参数:data属性(如原生的id、class;组件之间的props)
- 第三个参数:children。可以是个数组,也可以是个普通string(如果children是个标签或者组件,则嵌套的是另一个createElement函数)
// Vue简易render函数 createElement('tagName', { 属性... }, [children])
- 第一个参数:
3. React合成事件SyntheticEvent
- 事件怎么触发handle(以onClick为例),如图
- 思考:为什么需要合成事件?
- 更好的兼容跨平台、跨端。将事件机制抽离,减少对DOM的依赖,以快速兼容跨平台
- 事件委托的优势。减少内存消耗,避免频繁的绑定、解绑事件(列表click场景)
- 方便事件统一管理。如事务机制
- React17为什么把事件绑定从document移到root组件
- 微前端场景。有利于多个React版本并存,不会造成document中事件紊乱
- 微前端场景。有利于多个React版本并存,不会造成document中事件紊乱
4. setState和batchUpdate
- setState流程
- dirtyComponent(state已经被更新的component)
- dirtyComponent(state已经被更新的component)
- batchUpdate机制
- isBatchingUpdates状态。为true时异步执行、为false时同步执行。函数开始执行时为
true
,函数执行结束时置为false
/* 异步执行 */ xxxfn = () => { // 开始:处于batch update // isBatchingUpdates = true this.setState({ ... }) // isBatchingUpdates = false // 函数结束 } /* 同步执行 */ xxxfn = () => { // 开始:处于batch update // isBatchingUpdates = true setTimeout(() => { // 异步执行,所以此时isBatchingUpdates为false this.setState({ ... }) }) // isBatchingUpdates = false // 函数结束 }
- 怎么命中batchUpdate机制
- 生命周期和其中调用的函数
- React中注册的事件和其调用的函数
- isBatchingUpdates状态。为true时异步执行、为false时同步执行。函数开始执行时为
- transaction(事务)机制
- 通过
perform
去执行任何函数- 创建函数前,定义函数开始前执行的
initialize
和结束时的close
- 再执行函数(如下图,从左到右)
- 创建函数前,定义函数开始前执行的
- 通过
5. 组件更新和渲染(fiber)
- setState(
newState
) -> dirtyComponents(可能有子组件) render()
生成 newVNode- patch 到 DOM 拆分成两个阶段(
fiber
优化)- reconciliation阶段 -> 执行diff( js执行计算 )
- commit阶段(渲染) -> 将diff渲染DOM
fiber
性能优化- 将reconciliation阶段的任务拆分
- DOM渲染时暂停执行js计算,空闲时恢复js计算
- 使用的api:window.requestIdleCallback
6.性能优化
- 列表渲染使用key(非index、random)
- 组件销毁时销毁定时器、解绑事件
- 异步组件使用
- 减少函数的bind(this)次数(class组件里是在
render
还是在constrctor
bind?) - SCU、pureComponent(浅比较SCU)、memo
- 不可变值优化——
immer
、immutable
(state层级设计要浅)
四、React Hooks
大致了解class组件的问题:
- 不易拆分。大型class组件拆分困难
- 逻辑混乱。相同的业务逻辑分散到不同的class的方法中调用
- 逻辑复用复杂。Mixins、HOC、Render prop
一句话理解函数组件:接收props -> 返回一个jsx
- 没有组件实例
- 没有生命周期
- 没有state、setState
所以需要通过
Hooks
增强函数组件的能力
1. State Hook
useState
作用等同于class组件中的state、setState
核心用法:
- 传入默认值,返回数组。
const [count, setCount] = useState(0)
- 通过state,这里的count获取值
- 通过setState,这里setCount修改值
// 手敲一顿,加深印象
function ClickCount () {
const [count, setCount] = useState(0)
const handleClick = function () {
setCount(count + 1)
}
return (
<div>
<p>点击了{count}次</p>
<button onClick={handleClick}>点击</button>
</div>
)
}
2. Effect Hook
useEffect
让函数组件 模拟 生命周期
- 模拟class组件
DidMount
和DidUpdate
。传入一个回调函数useEffect(() => { console.log('DidMount & DidUpdate') })
- 模拟class组件
DidMount
。第二个参数传入空数组:useEffect(() => { console.log('DidMount') }, [])
- 模拟class组件
DidUpdate
。第二个数组参数中,传入需要收集的依赖(有点类似Vue的watch
)// 上面useState案例中的count,只要count修改了就会再次执行这个cb useEffect(() => { console.log('DidUpdate') }, [count])
- 模拟class组件
WillUnMount
。回调函数中return一个函数useEffect(() => { const timer = setInterval(() => { console.log('sb') }, 1000) // 注意回调会在每个下次执行useEffect前执行 return () => { console.log('WillUnMount') // 组件销毁时清除timer clearInterval(timer) } }, [count])
- 需要注意的是只是模拟
WillUnMount
,但是不全相等。只要第二个参数不是空数组
,每个下次执行useEffect前都会先执行return中的函数。总结:只要update就会执行一次销毁,并不是只有组件销毁时才执行
- 需要注意的是只是模拟
- 完全模拟
WillUnMount
(组件销毁才执行return fn)。第二个参数传入空[]
useEffect(() => { return () => {} }, [])
3. useRef和useContext
useRef
。主要可用来获取DOMconst btnRef = useRef() useEffect(() => { // mount时打印按钮的DOM console.log('btnRef', btnRef.current) }, []) return ( <div> <p>点击了{count}次</p> <button ref={btnRef}>点击</button> </div> )
useContext
。接收Context- 使用可参考:二、高级特性 —— 3. React Context
4. useReducer
理解useReducer
:复杂化的useState
- 比如修改state要执行多个逻辑,可以使用
useReducer
。参考以下代码案例const initReducer = { count: 1 } const reducer = (state, action) => { switch (action.type) { case 'add': // 如果需要再add的时候,处理很多逻辑,就可以使用useReducer return { count: state.count + 1 } default: return { count: state.count - 1 } } } function ClickCount () { // 用法跟useState很像 const [state, dispatch] = useReducer(reducer, initReducer) const handleAdd = function () { // 通过执行dispatch传入type完成setState dispatch({ type: 'add' }) } const handleReduce = function () { dispatch({}) } return ( <div> <p>count = {state.count}</p> <button onClick={handleAdd}>add</button> <button onClick={handleReduce}>reduce</button> </div> ) }
- useReducer 和 redux 的区别
- 单组件的状态管理。只是复杂的state处理调用简单化,组件通信需要props等
- redux属于全局的状态管理。多组件共享数据
5. useMemo
使用 useMemo
缓存数据 + memo
子组件浅比较(类似 PureComponent
)进行性能优化,可对比class组件
中的SCU优化。注意引用类型值一定要配合 useMemo
。可参考如下demo,分两种情况来写,加深大家的理解
- 如果props为常规类型数据,可以不使用
useMemo
// memo包裹,实现props的浅比较 const Child = memo((props) => { console.log('child render') return <div>{props.val}</div> }) function ClickCount () { const [count, setCount] = useState(0) // 如果传入的props是常量,则可以不使用useMemo。但是引用类型一定要用 const val = '测试子组件更新' const handleAdd = function () { setCount(count + 1) } return ( <div> <p>count = {count}</p> <Child val={val} /> <button onClick={handleAdd}>add</button> </div> ) }
- 如果props是引用类型数据,则必须使用
useMemo
- 第二个参数跟
useEffect
一样,传入依赖,当依赖改变时不再缓存之前的数据
const Child = memo((props) => { console.log('child render') return <div>{props.val.text}</div> }) function ClickCount () { const [count, setCount] = useState(0) const [text, setText] = useState('测试子组件更新') // 注意这里时引用类型,每次函数重新执行,都会生成新的引用类型。好比:{} !== {} const val = useMemo(() => ({ text }), [text]) const handleAdd = function () { setCount(count + 1) } return ( <div> <p>count = {count}</p> <Child val={val} /> <button onClick={handleAdd}>add</button> </div> ) }
- 第二个参数跟
6. useCallback
原理跟上述 useMemo
类似,useMemo缓存数据,useCallback缓存函数。主要是优化 props
传递函数时,子组件数据没变但是每次都更新的问题~
- React Hooks 的优化策略主要就是
useMemo
和useCallback
使用方法跟useMemo很类似,只是第二个参数是个空数组即可,不需要传依赖。这里直接给用法~
const fn = useCallback(() => { ... }, [])
7. 自定义Hook
可以很好的进行组件公共逻辑抽离。相比HOC优势:
- 减少组件嵌套
- 避免在组件间处理
props
透传
注意自定义Hook命名规范:useXxx
(use开头)
这里以一个Dialog为Demo,自己手写一个Hook~
- 实现打开弹窗
showDialog
- 实现关闭弹窗
hideDialog
- 实现弹窗状态切换
switchStatus
// 自定义hook 控制 Dialog
function useDialog () {
const [visible, setVisible] = useState(false)
const showDialog = () => setVisible(true)
const hideDialog = () => setVisible(false)
const switchStatus = () => setVisible(!visible)
return {
visible,
showDialog,
hideDialog,
switchStatus
}
}
// 组件中使用useDialog
function App() {
// 使用自定义hook,接收状态、操作函数
const {visible, showDialog, hideDialog, switchStatus} = useDialog()
return (
<div className="App">
{ visible && <div>dialog</div> }
<button onClick={showDialog}>打开弹窗</button>
<button onClick={hideDialog}>关闭弹窗</button>
<button onClick={switchStatus}>切换弹窗状态</button>
</div>
);
}
8. Hooks注意事项
-
只能用在
函数组件
或者自定义Hook
中使用 -
只能在顶层。 不能在循环、判断中使用
Hook
- 要保证Hooks每次执行顺序都是一样的(不能被打断)
if (flag) return useEffect(() => {}) ❌
- 为什么要保证执行顺序一样?
- 函数组件执行完即销毁。class组件执行完,组件实例不会被销毁
- 组件初始化、组件更新,都需要重新执行函数。从而获取最新的组件
- 函数组件没有实例,如何对应上初始化的时候的值?👇(顺序对应)
// 组件初始化时 name=jingboran;age=26 const [name, setName] = useState('jingboran') const [age, setAge] = useState(26) // 组件更新,重新执行函数,这时候name,age会恢复上次name、age的值 const [name, setName] = useState('jingboran') const [age, setAge] = useState(26) // 如果存在if、for等逻辑,以if为例 ❌ if (flag) const [name, setName] = useState('jingboran') // 如果这次执行flag为false,那age就会对应到name的值,就会导致错乱 const [age, setAge] = useState(26)
-
useState
的默认值只有在组件初始化时有效,组件更新无效(由上可知,组件更新时,state是恢复上次的state,所以不走默认值)function App() { const [name, setName] = useState('井柏然') const info = { name } return ( <div className="App"> <button onClick={() => setName('jingboran')}>change Name</button> <Children info={info} /> </div> ); } function Children ({ info }) { // name只有在首次render的时候才会取info.name作为默认值 const [name, setName] = useState(info.name) return ( <div> <!-- 组件更新随父组件改变 --> <p>接收的props {info.name} </p> <!-- 组件更新恢复上次的name值,如果要修改,只能内部用setName --> <p>组件内部state {name}</p> </div> ) }
- 效果图如下
-
useEffect
中 第二个参数是空数组 时直接使用setState的坑- 如下代码,useEffect只有在组件初始化的时候执行。在Effect中修改了count,触发组件重新渲染,不会重新执行Effect函数,所以每次的interval中,拿到的都是首次的count
- 解决方案:
- 传入依赖值count(组件更新时会重新执行Effect)
- 第二个参数不传。Effect函数在DidMount和DidUpdate中都会重新执行
const [count, setCount] = useState(0) useEffect(() => { const timer = setInterval(() => { console.log('count 一直都是1', count) // 这里的count一直都是初始时候的count,所以一直都是 0 + 1 setCount(count + 1) }, 1000) return () => { clearInterval(timer) } }, []) // 这里没有传任何依赖
-
useEffect
第二个参数注意接收数组、对象的依赖!操作不当可能会造成死循环
-
判断依赖是否改变而重新执行Effect的实现时Object.is()
// 避免直接在useEffect的依赖中写引用类型 useEffect(() => {}, [{}, []]) ❌
写在最后,从自己学React走过的弯路来说,其实最高效率的还是有一份全面知识体系的checklist。如果没有实战项目,又只对着文档敲,其实学不到什么,即使当时会了,一段时间就会忘记。没有形成知识体系,印象也不深刻。学习一门新框架最好还是循序渐进,按照一定的章法学习,从基础用法、高级用法、原理层面逐步深入,这样就能牢牢掌握了!希望我们最后都在大厂相遇,都走在自己希望走上的道路😝