本文是 React Hooks 深刻系列的后续。此篇详细介绍了 Hooks 相对 class 的优点所在, 并介绍了相关 api 的设计思想, 同时对 Hooks 如何对齐 class 的生命周期钩子做了阐述。html
React 的 logo 是一个原子图案, 原子组成了物质的表现。相似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 其更接近 React 本质的样子, 可是直到 4 年后的今天才被真正设计出来。 —— Dan in React Conf(2018)react
一: 多个组件间逻辑复用
: 在 Class 中使用 React 不能将带有 state 的逻辑给单独抽离成 function, 其只能经过嵌套组件的方式来解决多个组件间逻辑复用的问题, 基于嵌套组件的思想存在 HOC 与 render props
两种设计模式。可是这两种设计模式是否存在缺陷呢?git
二: 单个组件中的逻辑复用
: Class 中的生命周期 componentDidMount
、componentDidUpdate
甚至 componentWillUnMount
中的大多数逻辑基本是相似的, 必须拆散在不一样生命周期中维护相同的逻辑对使用者是不友好的, 这样也形成了组件的代码量增长。github
三: Class 的其它一些问题: 在 React 使用 Class 须要书写大量样板, 用户一般会对 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 当结合 class 与 TypeScript 一块儿使用时, 须要对 defaultValue 作额外声明处理; 此外 React Team 表示 Class 在机器编译优化方面也不是很理想。redux
缘由是数组的解构比对象更加方便, 能够观察如下两种数据结构解构的差别。设计模式
返回数组时, 能够直接解构成任意名字。api
[name, setName] = useState('路飞')
[age, setAge] = useState(12)
复制代码
返回对象时, 却须要多一层的命名。数组
{value: name, setValue: setName} = useState('路飞')
{value: old, setValue: setOld} = useState(12)
复制代码
Hooks 是否能够设计成在组件中经过函数传参来使用? 好比进行以下调用?缓存
const SomeContext = require('./SomeContext)
function Example({ someProp }, hooks) {
const contextValue = hooks.useContext(SomeContext)
return <div>{someProp}{contextValue}</div>
}
复制代码
使用传递的劣势是会出现冗余的传递。(能够联想 context 解决了什么)安全
Hooks 中的 setState 与 Class 中最大区别在于 Hooks 不会对屡次 setState 进行合并操做。若是要执行合并操做, 可执行以下操做:
setState(prevState => {
return { ...prevState, ...updateValues }
})
复制代码
此外能够对 class 与 Hooks 之间 setState
是异步仍是同步的表现进行对比, 能够先对如下 4 种情形 render 输出的个数进行观察分析:
class 中的 setState:
export default class App extends React.Component {
state = {
name: '路飞',
old: 12,
gender: 'boy'
}
// 情形 ①: class 中异步调用 setState
componentDidMount() {
this.setState({
name: '娜美'
})
this.setState({
old: 13
})
this.setState({
gender: 'girl'
})
}
// 情形 ②: class 中同步调用 setState
componentDidMount() {
setTimeout(() => {
this.setState({
name: '娜美'
})
this.setState({
old: 13
})
this.setState({
gender: 'girl'
})
})
}
render() {
console.log('render')
const { name, old, gender } = this.state
return (
<>{name}{old}{gender}</> ) } } 复制代码
Hooks 中的 setState
export default function() {
const [name, setName] = useState('路飞')
const [old, setOld] = useState('12')
const [gender, setGender] = useState('boy')
// 情形③: Hooks 中异步调用 setState
useEffect(() => {
setName('娜美')
setOld('13')
setGender('girl')
}, [])
// 情形④: Hooks 中同步调用 setState
useEffect(() => {
setTimeout(() => {
setName('娜美')
setOld('13')
setGender('girl')
}, 0)
}, [])
console.log('render')
return (
<>{name}{old}{gender}</> ) } 复制代码
情形①、情形②、情形③、情形④ 中 render 输出的次数分别是 2, 4, 2, 4。能够看出在 React 中使用 class 写法和 hooks 写法是一一对应的。此外 setState 的执行是异步仍是同步取决于其执行环境
。
在 React 16.8 版本以后, 针对不是特别复杂
的业务场景, 可使用 React 提供的 useContext
、useReducer
实现自定义简化版的 redux, 可见 todoList 中的运用。核心代码以下:
import React, { createContext, useContext, useReducer } from "react"
// 建立 StoreContext
const StoreContext = createContext()
// 构建 Provider 容器层
export const StoreProvider = ({reducer, initialState, children}) => {
return (
<StoreContext.Provider value={useReducer(reducer, initialState)}> {children} </StoreContext.Provider> ) } // 在子组件中调用 useStoreContext, 从而取得 Provider 中的 value export const useStoreContext = () => useContext(StoreContext) 复制代码
可是针对特别复杂的场景目前不建议使用此模式, 由于 context 的机制会有性能问题。具体缘由可见 react-redux v7 回退到订阅的缘由
React 官方在将来极可能会提供一个 usePrevious
的 hooks 来获取以前的 props 以及 state。
usePrevious
的核心思想是用 ref 来存储先前的值。
function usePrevious(value) {
const ref = useRef()
useEffect(() => {
ref.current = value
})
return ref.current
}
复制代码
在 Hooks 中使用 useRef() 等价于在 Class 中使用 this.something。
/* in a function */
const X = useRef()
X.current // can read or write
/* in a Class */
this.X // can read or write
复制代码
在 React 暗器百解 中提到了 getDerivedStateFromProps
是一种反模式, 可是极少数状况仍是用获得该钩子, Hooks 没有该 api, 那其如何达到 getDerivedStateFromProps 的效果呢?
function ScrollView({row}) {
const [isScrollingDown, setISScrollingDown] = setState(false)
const [prevRow, setPrevRow] = setState(null)
// 核心是建立一个 prevRow state 与父组件传进来的 row 进行比较
if (row !== prevRow) {
setISScrollingDown(prevRow !== null && row > prevRow)
setPrevRow(row)
}
return `Scrolling down ${isScrollingDown}`
}
复制代码
可使用 useReducer
来 hack forceUpdate
, 可是尽可能避免 forceUpdate 的使用。
const [ignored, forceUpdate] = useReduce(x => x + 1, 0)
function handleClick() {
forceUpdate()
}
复制代码
在 Hooks 中可使用 useMemo
来做为 shouldComponentUpdate
的替代方案, 但 useMemo
只对 props 进行浅比较。
React.useMemo((props) => {
// your component
})
复制代码
useMemo(() => <component />) 等价于 useCallback(<component />) 复制代码
一般来讲依赖列表中移除函数是不安全的。观察以下 demo
const { useState, useEffect } = React
function Example({ someProp }) {
function doSomething() {
console.log(someProp) // 这里只输出 1, 点击按钮的 2 并无输出。
}
useEffect(
() => {
doSomething()
},
[] // 🔴 这是不安全的, 由于在 doSomething 函数中使用了 someProps 属性
)
return <div>example</div>
}
export default function() {
const [value, setValue] = useState(1)
return (
<>
<Example someProp={value} />
<Button onClick={() => setValue(2)}>button</Button>
</>
)
}
复制代码
在该 demo 中, 点击 button 按钮, 并无打印出 2。解决上述问题有两种方法。
方法一: 将函数放入 useEffect
中, 同时将相关属性放入依赖项中。由于在依赖中改变的相关属性一目了然, 因此这也是首推的作法。
function Example({ someProp }) {
useEffect(
() => {
function doSomething() {
console.log(someProp)
}
doSomething()
},
[someProps] // 相关属性改变一目了然
)
return <div>example</div>
}
复制代码
方法二: 把函数加入依赖列表中
function Example({ someProp }) {
function doSomething() {
console.log(someProp)
}
useEffect(
() => {
doSomething()
},
[doSomething]
)
return <div>example</div>
}
复制代码
方案二基本上不会单独使用, 它通常结合 useCallback
一块儿使用来处理某些函数计算量较大的函数。
function Example({ someProp }) {
const doSomething = useCallback(() => {
console.log(someProp)
}, [someProp])
useEffect(
doSomething(),
[doSomething]
)
return <div>example</div>
}
复制代码
useState
的懒初始化, 用法以下const [value, setValue] = useState(() => createExpensiveObj)
复制代码
function Image(props) {
const ref = useRef(null)
function getExpensiveObj() {
if (ref.current === null) {
ref.current = ExpensiveObj
}
return ref.current
}
// if need ExpensiveObj, call getExpensiveObj()
}
复制代码