ReactHook详解: 改变的React Component写法思路
Author:zhoulujun Date:
在hooks出来之前,常见的代码重用方式是 HOC 和render props,这两种方式带来的问题是:
复杂组件变得难以理解:
我们在最初编写一个class组件时,往往逻辑比较简单,并不会非常复杂, 但是随着业务的增多,我们的class组件会变得越来越复杂
比如componentDidMount中,可能就会包含大量的逻辑代码:包括网络请求、一些事件的监听(还需要在 componentWillUnmount中移除)
而对于这样的class实际上非常难以拆分:因为它们的逻辑往往混在一起,强行拆分反而会造成过度设计,增加代码的复杂度
难以理解的class:
很多人发现学习ES6的class是学习React的一个障碍。
比如在class中,我们必须搞清楚this的指向到底是谁,所以需要花很多的精力去学习this
虽然前端开发人员必须掌握this,但是依然处理起来非常麻烦
组件复用状态很难:
在前面为了一些状态的复用我们需要通过高阶组件或render props
像我们之前学习的redux中connect或者react-router中的withRouter,这些高阶组件设计的目的就是为了状态的复用
或者类似于Provider、Consumer来共享一些状态,但是多次使用Consumer时,我们的代码就会存在很多嵌套
这些代码让我们不管是编写和设计上来说,都变得非常困难
你需要解构自己的组件,同时会带来很深的组件嵌套
React 的制作团队在 2019 年推出了 React Hooks, 改变了原始组件用 class 的开发方式, 全部采用函数的形式来编写组件, 大大的方便了开发者, 受到全世界前端的喜爱, Hooks 的诞生成为了前端工程师的一把好梭、
首先还是建议通读 https://zh-hans.reactjs.org/docs/hooks-intro.html
Hooks
「Hooks优势(优化类组件的三大问题):
函数组件无this问题(都在函数内部,没有实例化的概念)
自定义Hook方便复用状态逻辑
副作用的关注点分离(不是发生在数据向视图转化之中,都是在之外的。例如:发起网络请求、访问原型上的DOM元素、写本地持久化缓存、绑定解绑事件都是数据渲染视图之外的。这些一般都是放在生命周期中的。useEffect都是在每次渲染完成之后调用)
Hooks API 參考: https://zh-hant.reactjs.org/docs/hooks-reference.html
Hook | 特性 | 类比 Class |
---|---|---|
useState | State | this.state this.setState(newState) |
useEffect | 生命周期 | componentDidMount componentDidUpdate |
useContext | Context | this.context |
useReducer | State | Redux Reducer 式的 State 管理 |
useCallback | Function Props | this.myMethod.bind(this) |
useMemo | 性能优化 | 避免重复计算 |
useRef | Ref | createRef |
useImperativeHandle | 组件实例属性/方法 | forwardRef |
useLayoutEffect | 生命周期 | 同步componentDidMount 同步 componentDidUpdate |
useDebugValue | 调试 | Hooks 状态可视化(类似于从 React DevTools 看this.state ) |
useMemo
主要用来优化性能
Memoize是贯穿hook用法的一个非常重要的概念,理解它是正确使用hook的基石。
Memoize基本上就是把一些程序中一些不需要反复计算的值和上下文(context)保存在内存中,起到类似缓存的作用,下次运行计算时发现已经有计算并保存过这个值就直接从内存中读取而不再重新计算。
Javascript中比较常见的做法可参考lodash.memoize源代码,它通过给function设置一个Map的属性,将function的传参作为key,运行结果存为这个key的value值。下次调用这个function时,它就先去查看key是否存在,存在的话就直接将对应的值返回,跳过运行方法里的代码。
这在functional programming中非常的实用。不过也就是说,只有所谓纯粹的function才能适用这种方式,输入和输出是一一对应的关系。
React hooks都是被Memoize的,绑定在使用的component中,只有指定的值发生了变化,这个hook中的代码和代码上下文才会被更新和触发。
在函数组件内只能用 useEffect 来替代部分生命周期函数, 比如就没有 shouldCompnentUpdate 这个生命周期, 也就代表我们无法知道组件更新还是初步挂载
所以每一次调用都会执行一次内部逻辑, 非常影响性能
useState-状态钩子
用来声明状态变量, 它可以声明, 读取, 修改状态。
在hook之前,用function写的Component是无法拥有自己的状态值的。想要拥有自己的状态,只能痛苦地将function改成Class Component——Class Component中使用this.setState来设置一个部件的状态
class MyComponent extends React.Component{ constructor(props) { super(props); this.state= { count: 1 }; } toggleState = ()=>{ this.setState(prevState => { return { count: 1 - prevState.count }; }); } }
使用React hook之后这就可以在function component中实现,并且更为简洁:
function MyComponent (props) { const [count, setCount] = useState(0)//「数组解构」的语法让我们在调用 useState 时可以给 state 变量取不同的名字 const toggleState = () => setMyState(1 - count); return <button onClick={toggleState}>Toggle State</button> }
useState接收一个值作为一个state的初始值,返回一个数组。这个数组由两个成员组成:
这个状态的当前值——注意这个初始值只在初始渲染中才被赋值给对应变量,也就是只有在component第一次挂载时,渲染之前才做了一次初始化。后来更新引发的重新渲染都不会让初始值对状态产生影响。
改变这个状态值的方法
useState 支持传入函数,来延迟初始化:
const [count, setCount] = useState(() => { return props.defaultCount || 0 })
useState 就是一个 Hook。通过在函数组件里调用它来给组件添加一些内部 state,React 会在重复渲染时保留这个 state。
因为 JavaScript 是单线程的。在 useState 被调用时,它只能在唯一一个组件的上下文中。
如果一个组件内有多个 usreState,那就按照第一次运行的次序来顺序来返回的。
useEffect-副作用钩子
作用: Effect Hook可以告诉React需要在渲染后执行某些操作
参数: useEffect要求我们传入一个回调函数,在React执行完更新DOM操作之后,就会回调这个函数
执行时机: 首次渲染之后,或者每次更新状态之后,都会执行这个回调函数
绑定事件、发送请求、访问DOM元素)。数据获取,设置订阅以及手动更改 React 组件中的 DOM 都属于副作用。不管你知不知道这些操作,或是"副作用"这个名字,应该都在组件中使用过它们
副作用的时机
Mount 之后 对应 componentDidMount
Update 之后 对应 componentDidUpdate
Unmount 之前 对应 componentWillUnmount
在使用 useEffect 就可以覆盖上述的情况。
你可以把 useEffect Hook 看做componentDidMount,componentDidUpdate 和 componentWillUnmount 这三个函数的组合。
在一个组件的声明周期函数里可以做很多事情, React 在 Hooks 出现之前会用
componentWillMount 组件即将挂载
componentDidMount 组件完成挂载
componentDidUpdate 组件完成更新
componentWillUnmount 组件即将销毁
……
在 Hooks 出现之后, 就可以用 useEffect 函数来代替一些生命周期函数
拿官方案例来说:https://reactjs.org/docs/hooks-effect.html(建议通读一遍)
class MyComponent extends React.Component { componentDidMount() { this.loadData(); } componentDidUpdate(prevProps) { if (prevProps.id !== this.props.id) { this.loadData(); } } shouldComponentUpdate(nextProps) { return nextProps.id !== this.props.id || nextProps.data !== this.props.data; } loadData() { this.props.requestAPI(this.props.id); } render() { return <div>{this.props.data}</div>; } }
useEffect就合并并且大大地简化了这一过程:
function MyComponent(props) { React.useEffect(() => { props.requestAPI(props.id); }, [props.id, props.requestAPI); return <div>{props.data}</div>; } export default React.memo(MyComponent);
默认情况下,React 会在每次渲染后调用副作用函数 —— 包括第一次渲染的时候。副作用函数还可以通过返回一个函数来指定如何“清除”副作用。
跟 useState 一样,你可以在组件中多次使用 useEffect :
useEffect 实现生命周期的三种方式:
作为 componentDidMount 使用, [] 作为第二个参数
作为 componentDidUpdate 使用, 可指定依赖
作为 componentWillUnmount 使用, 通过 return
useEffect并不能等同或替代原有的component生命周期函数,他们设计的思路完全不同!
它完全是通过对数据和状态变化的检测,来“反馈”更新。
在前端程序里,我们习惯了一种我称为“事件思维”的方式,就是说发生某件事,就调用某段代码。
运用钩子,就是把某些数据的变化作为事情发生的标志。
useEffect传入的函数,则会在:
component 挂载后触发一次
渲染完成后才触发
第二个参数里传入的需要检测比较的数据有变化时才触发
如果没有第二个参数,则每次渲染都会触发
useEffect 标准上是在组件每次渲染之后调用,并且会根据自定义状态来决定是否调用还是不调用。
这边我们容易出错的地方就是在组件结束之后要「记住销毁事件的注册」,不然会导致资源的泄漏。现在我们把 App 组件的副作用用 useEffect 实现。
第一次调用就相当于componentDidMount,后面的调用相当于 componentDidUpdate。useEffect 还可以返回另一个回调函数,这个函数的执行时机很重要。作用是清除上一次副作用遗留下来的状态。
import React, { useState, useEffect } from 'react' function UseEffectHook (props) { const [count, setCount] = useState(0) const [size, setSize] = useState({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) const onResize = () => { setSize({ width: document.documentElement.clientWidth, height: document.documentElement.clientHeight }) } // 第一个useEffect代表生命周期中的componentDidUpdate, useEffect(() => { document.title = count }) // 不用第二个参数的情况执行几次呢((不传数组意味着每一次执行都会)) // (只调用一次) 第二个useEffect代表生命周期中的componentDidMount组件更新 和 componentWillMount组件卸载 useEffect(() => { window.addEventListener('resize', onResize, false) return () => { window.removeEventListener('resize', onResize, false) } }, []) // 第二个参数才是useEffect精髓,并且能优化性能,只有数组的每一项都不变的情况下,useEffect才不会执行 useEffect(() => { console.log('count:', count) }, [count]) // 第二个参数我们传入 [count], 表示只有 count 的变化时,我才打印 count 值,resize 变化不会打印。 return ( <button type="button" onClick={() => { setCount(count + 1) }} > Click({count}) size: {size.width}x{size.height} </button> ) } export default UseEffectHook
相比类组件,Hooks 不在关心是 mount 还是 update。用 useEffect统一在渲染后调用,就完整追踪了 count 的值。
通过返回一个回调函数来注销事件的注册。回调函数在视图被销毁之前触发,销毁的原因有两种:「重新渲染和组件卸载」。
第二个参数是一个可选的数组参数,只有数组的每一项都不变的情况下,useEffect 才不会执行。第一次渲染之后,useEffect 肯定会执行。由于我们传入的空数组,空数组与空数组是相同的,因此 useEffect 只会在第一次执行一次。
useContext 和 useReducer 实现 Redux
useContext 可以实现全局的状态管理
useReducer 可以根据 action 来改变状态
useContext-共享状态钩子
useContext 可以提供全局数据在被他包裹的所有子组件中
React Context API的用法(生产者消费者模式)。
// context初始值 const INITIAL_VALUE = ‘’; const MyContext = React.createContext(INITIAL_VALUE); // 划定context使用的范围,管理它的值: function App() { return ( <MyContext.Provider value=“context value”> <ConsumeComponent /> </MyContext.Provider> ); } // 获取context的值: function ConsumeComponent() { return ( <MyContext.Consumer> {contextValue => ( <div> <MyComponent value={contextValue} /> <div>{contextValue}</div> </div> )} </MyContext.Consumer> ); }
用Hook之前,消费这个context API是使用render props的方式。那如果这个context是一个原始数据,并不是用来直接显示的时候,就需要繁琐的特殊处理(比如传入到下一层component处理)
有了useContext hook之后,这就不再是个问题了。我们只需要修改消费者那一步:
function ConsumeComponent() { const contextValue = useContext(MyContext); return ( <div> <MyComponent value={contextValue} /> <div>{contextValue}</div> </div> ); }
context的值可以直接赋值给变量,然后想处理或者渲染都可以。
useReducer-action 钩子
useReducer 和 Redux 的 Reducer 类似, 参数是一个回调函数, 分别接收 state(数据), 和 action(对数据进行某种操作的描述)——
借鉴了redux的设计模式的状态操作工具而已。
回到useReducer,它的存在是为一步操作更新多个状态设计。
function loadStateReducer(state, action) { switch (action.type) { case ‘loading’: return { isLoading: true, error: undefined }; case ‘success’; return { isLoading: false, error: undefined }; case ‘error’: return { isLoading: false, error: action.error }; } return state; } function MyComponent({ loadDataAction, data }) { const [state, dispatch] = useReducer(loadStateReducer, { isLoading: false, error: undefined }); const { isLoading, error } = state; const loadData = useCallback(async () => { dispatch({ type: ‘loading’ }); try { await loadDataAction(); dispatch({ type: ‘success’ }); } catch(err) { dispatch({ type: ‘error’, error: err }); } }); return ( <div> <button onClick={loadData} disabled={isLoading}>Load Data</button> {error ? ( <p>{error}</p> ) : data ? ( <h3>Loaded data below</h3> <div>{data}</div> ) : null </div> ); }
讲真的,用useState设置一个object state是等同的操作。所以用哪个看个人喜好吧。
useRef
用来获取 DOM 元素和 保存变量
useRef返回一个ref对象,返回的ref对象在组件的整个生命周期保持不变
和 React16 中提供的 createRef 方法一样,用于获取 React 组件的 ref。
function TextInputWithFocusButton() { const inputEl = useRef(null); const onButtonClick = () => { //inputEl 的 current 属性指向 input 组件的 dom 节点 inputEl.current.focus(); }; return ( <div> <input ref={inputEl} type="text" /> <button onClick={onButtonClick}>Focus the input</button> </div> ); }
除了用于获取组件 ref 以外,useRef 还可以用于实现类似于 class 组件的实例属性(this.XXX),直接通过对 useRef 方法返回对象的 current 属性进行读写即可。
最常用的ref是两种场景:
引入DOM(或者组件, 需要是class组件) 元素
保存一个数据, 这个对象在整个生命周期可以保存不变
useCallback
当依赖的属性没有改变时, 不希望更新 render 时, 重新定义事件函数
试想一下: 当你更新 name 属性时, 重新调用 render 之后所有的事件处理函数重新全部定义, 非常浪费性能
useCallBack会返回一个函数 memoized(记忆的) 值
在依赖不变的情况下, 多定义的时候, 返回值是相同的
场景: 在将一个组件中的函数, 传递给子元素回调函数使用时, 使用useCallback对函数进行处理
import React, { useState, useCallback, memo } from 'react' const JMButton = memo(props => { console.log('HYButton重新渲染: ', props.title) return <button onClick={props.increment}>JMButton+1</button> }) export default function CallBackHomeDemo2() { // useCallback: 希望更新父组件的state时,子组件不被render渲染 // 1.使用memo包裹子组件进行性能优化,子组件没有依赖的props或state没有修改,不会进行render // 2.一个疑问: 为什么 btn1 还是被渲染了? // (1)因为子组件依赖的 increment1 函数,在父组件没有进行缓存(在函数重新render时,increment1被重新定义了) // (2)而 increment2 函数在父组件中被缓存了,所以memo函数进行性浅层比较时依赖的increment2是一样的所以没有被重新render渲染 // 3.useCallback在什么时候使用? // 场景: 在将一个组件中的函数, 传递给子元素进行回调使用时, 使用useCallback对函数进行处理. console.log('CallBackHomeDemo2重新渲染') const [count, setCount] = useState(0) const [show, setShow] = useState(true) const increment1 = () => { console.log('increment1被调用了') setCount(count + 1) } const increment2 = useCallback(() => { console.log('increment2被调用了') setCount(count + 1) }, [count]) return ( <div> <h2>CallBackHomeDemo: {count}</h2> <JMButton increment={increment1} title="btn1" /> <JMButton increment={increment2} title="btn2" /> <button onClick={e => setShow(!show)}>show切换</button> </div> ) }
Portal
在使用 React16 时,如果我们在渲染组件时需要渲染一个脱离于当前组件树之外的组件(如对话框、tooltip等),可以通过 ReactDOM.createPortal(Child, mountDom)* 函数创建一个 Portal,将 React 组件 Child 挂载到真实 DOM 元素 mountDom 上。示例代码:
React.Fragment 组件
React16 中可以通过 React.Fragment 组件来组合一列组件,而不需要为了返回一列组件专门引入一个 DIV 组件。其中 <></> 是 <React.Fragment></React.Fragment>的简写。
参考文章:
React With Reudx Hooks详解 https://segmentfault.com/a/1190000040075169
React Hooks基本使用详解 https://jishuin.proginn.com/p/763bfbd663c8
Hook 改变的 React Component 写法思路(1) - useState和useEffect https://blog.csdn.net/jennieji/article/details/89641894
函数式组件的崛起 http://www.ayqy.net/blog/the-rise-of-function-component/
https://www.teqng.com/2021/05/25/react-进阶必备:从函数式组件看-hooks-设计/
React Hooks 入门教程 https://www.ruanyifeng.com/blog/2019/09/react-hooks.html
React Hooks 详解 【近 1W 字】+ 项目实战 https://juejin.cn/post/6844903985338400782
转载本站文章《ReactHook详解: 改变的React Component写法思路》,
请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/jsBase/2020_0924_8733.html