React|React Hooks 深入系列 —— 设计模式

React|React Hooks 深入系列 —— 设计模式
文章图片
image React 的 logo 是一个原子图案, 原子组成了物质的表现。类似的, React 就像原子般构成了页面的表现; 而 Hooks 就如夸克, 其更接近 React 本质的样子, 但是直到 4 年后的今天才被真正设计出来。 —— Dan in React Conf(2018)
why Hooks?
一: 多个组件间逻辑复用: 在 Class 中使用 React 不能将带有 state 的逻辑给单独抽离成 function, 其只能通过嵌套组件的方式来解决多个组件间逻辑复用的问题, 基于嵌套组件的思想存在 HOC 与 render props 两种设计模式。但是这两种设计模式是否存在缺陷呢?

  • 嵌套地狱, 当嵌套层级过多后, 数据源的追溯会变得十分困难, 导致定位 bug 不容易; (hoc、render props)
  • 性能, 需要额外的组件实例存在额外的开销; (hoc、render props)
  • 命名重复性, 在一个组件中同时使用多个 hoc, 不排除这些 hoc 里的方法存在命名冲突的问题; (hoc)
二: 单个组件中的逻辑复用: Class 中的生命周期 componentDidMountcomponentDidUpdate 甚至 componentWillUnMount 中的大多数逻辑基本是类似的, 必须拆散在不同生命周期中维护相同的逻辑对使用者是不友好的, 这样也造成了组件的代码量增加。
三: Class 的其它一些问题: 在 React 使用 Class 需要书写大量样板, 用户通常会对 Class 中 Constructor 的 bind 以及 this 的使用感到困惑; 当结合 class 与 TypeScript 一起使用时, 需要对 defaultValue 做额外声明处理; 此外 React Team 表示 Class 在机器编译优化方面也不是很理想。
useState 返回的值为什么是数组而非对象?
原因是数组的解构比对象更加方便, 可以观察以下两种数据结构解构的差异。
返回数组时, 可以直接解构成任意名字。
[name, setName] = useState('路飞') [age, setAge] = useState(12)

返回对象时, 却需要多一层的命名。
{value: name, setValue: setName} = useState('路飞') {value: name, setValue: setName} = useState(12)

Hooks 传递的设计
Hooks 是否可以设计成在组件中通过函数传参来使用? 比如进行如下调用?
const SomeContext = require('./SomeContext)function Example({ someProp }, hooks) { const contextValue = https://www.it610.com/article/hooks.useContext(SomeContext) return {someProp}{contextValue} }

使用传递的劣势是会出现冗余的传递。(可以联想 context 解决了什么)
Hooks 与 Class 中调用 setState 有不同的表现差异么?
Hooks 中的 setState 与 Class 中最大区别在于 Hooks 不会对多次 setState 进行合并操作。如果要执行合并操作, 可执行如下操作:
setState(prevState => { return { ...prevState, ...updateValues } })

此外可以对 class 与 Hooks 之间 setState 是异步还是同步的表现进行对比, 可以先对以下 4 种情形 render 输出的个数进行观察分析:
是否能使用 React Hooks 替代 Redux
在 React 16.8 版本之后, 针对不是特别复杂的业务场景, 可以使用 React 提供的 useContextuseReducer 实现自定义简化版的 redux, 可见 todoList 中的运用。核心代码如下:
import React, { createContext, useContext, useReducer } from "react"// 创建 StoreContext const StoreContext = createContext()// 构建 Provider 容器层 export const StoreProvider = ({reducer, initialState, children}) => { return ({children}) }// 在子组件中调用 useStoreContext, 从而取得 Provider 中的 value export const useStoreContext = () => useContext(StoreContext)

但是针对特别复杂的场景目前不建议使用此模式, 因为 context 的机制会有性能问题。具体原因可见 react-redux v7 回退到订阅的原因
Hooks 中如何获取先前的 props 以及 state
React 官方在未来很可能会提供一个 usePrevious 的 hooks 来获取之前的 props 以及 state。
usePrevious 的核心思想是用 ref 来存储先前的值。
function usePrevous(value) { const ref = useRef() useEffect(() => { ref.current = value }) return ref.current }

Hooks 中如何调用实例上的方法
在 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

Is there something like instance variables
Hooks 中 getDerivedStateFromProps 的替代方案
在 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}` }

Hooks 中 forceUpdate 的替代方案
可以使用 useReducer 来 hack forceUpdate, 但是尽量避免 forceUpdate 的使用。
const [ignored, forceUpdate] = useReduce(x => x + 1, 0)function handleClick() { forceUpdate() }

Hooks 中 shouldComponentUpdate 的替代方案
在 Hooks 中可以使用 useMemo 来作为 shouldComponentUpdate 的替代方案, 但 useMemo 只对 props 进行浅比较。
React.useMemo((props) => { // your component })

useMemo 与 useCallback 的区别
useMemo(() => ) 等价于 useCallback()

  • useCallback: 一般用于缓存函数
  • useMemo: 一般用于缓存组件
依赖列表中移除函数是否是安全的? 通常来说依赖列表中移除函数是不安全的。观察如下 demo
const { useState, useEffect } = Reactfunction Example({ someProp }) { function doSomething() { console.log(someProp) // 这里只输出 1, 点击按钮的 2 并没有输出。 }useEffect( () => { doSomething() }, [] //这是不安全的, 因为在 doSomething 函数中使用了 someProps 属性 )return example }export default function() { const [value, setValue] = useState(1) return ( <> ) }

在该 demo 中, 点击 button 按钮, 并没有打印出 2。解决上述问题有两种方法。
方法一: 将函数放入 useEffect 中, 同时将相关属性放入依赖项中。因为在依赖中改变的相关属性一目了然, 所以这也是首推的做法。
function Example({ someProp }) { useEffect( () => { function doSomething() { console.log(someProp) } doSomething() }, [someProps] // 相关属性改变一目了然 )return example }

方法二: 把函数加入依赖列表中
function Example({ someProp }) { function doSomething() { console.log(someProp) }useEffect( () => { doSomething() }, [doSomething] )return example }

方案二基本上不会单独使用, 它一般结合 useCallback 一起使用来处理某些函数计算量较大的函数。
function Example({ someProp }) { const doSomething = useCallback(() => { console.log(someProp) }, [someProp])useEffect( doSomething(), [doSomething] )return example }

如何避免重复创建昂贵的对象
  • 方法一: 使用 useState 的懒初始化, 用法如下
const [value, setValue] = useState(() => createExpensiveObj)

【React|React Hooks 深入系列 —— 设计模式】见 lazy-initial-state;
  • 方法二: 使用自定义 useRef 函数
function Image(props) { const ref = useRef(null)function getExpensiveObj() { if (ref.current === null) { ref.current = ExpensiveObj }return ref.current }// if need ExpensiveObj, call getExpensiveObj() }

相关资料
  • Hooks RFCS
  • Hooks FAQ
  • Making Sense of React Hooks
  • Vue Function-based API RFC

    推荐阅读