30分钟深入理解react hooks
react一直提倡使用函数组件,但函数组件没有实例,没有生命周期函数。hooks就是加了声明周期和状态管理。
1. 什么是 Hooks
react一直提倡使用函数组件,但函数组件没有实例,没有生命周期函数。
hooks就是加了声明周期和状态管理
2. hooks解决了哪些问题?
2.1. 类组件的不足
状态逻辑难复用
逻辑负责难以维护
this 指向问题
2.2 Hooks 优势
能优化类组件的三大问题
能在无需修改组件结构的情况下复用状态逻辑(自定义 Hooks )
能将组件中相互关联的部分拆分成更小的函数(比如设置订阅或请求数据)
副作用的关注点分离
1 2 3 4
| 副作用指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、 本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。以往这些副作用都 是写在类组件生命周期函数中的。而 useEffect 在全部渲染完毕后才会执行,useLayoutEffect 会在浏览器 layout 之后,painting 之前执行。
|
3. 注册事项
- 只能在函数内部的最外层调用 Hook,不要在循环、条件判断或者子函数中调用
- 只能在 React 的函数组件中调用 Hook,不要在其他 JavaScript 函数中调用
4. useState && useMemo && useCallback
1 2
| const [state, setState] = useState(initialState);
|
每次setState都会重新渲染dom,怎么优化?
4.1 未使用useCallback
1
| git reset --hard 4cd230ddda7808a12da45185a1da3de7282b032f
|
4.2 使用useCallback
1
| git reset --hard 3b6163d14324040bb2a6e31884b0c8b414447be6
|
4.3 未使用useMemo
1
| git reset --hard 51d53ff7e6d26b47baedd992af6f8803f48d2f8e
|
4.4 使用useMemo
1
| git reset --hard 7b7683e6821518a2397471fde9f963dd1253aa41
|
5. 函数式更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React, { useState } from "react"; function Counter2(){ let [number,setNumber] = useState(0); function lazy(){ setTimeout(()=>{ setNumber(number=>number+1); },3000); } return ( <> <p>{number}</p> <button onClick={()=>setNumber(number+1)}>+</button> <button onClick={lazy}>lazy</button> </> ) } export default Counter2;
|
6.惰性初始化 state
useState的初始值可以通过传入一个函数计算返回初始值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React, { useState } from "react";
function Counter5(props){ console.log('Counter5 render'); function getInitState(){ console.log("getInitState__"); return {number:props.number || 1}; } let [counter,setCounter] = useState(getInitState); return ( <> <p>{counter.number}</p> <button onClick={()=>setCounter({number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>setCounter</button> </> ) } export default Counter5;
|
7. 性能优化
7.1 Object.is(浅比较)
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React, { useState } from "react"; function Counter(){ const [counter,setCounter] = useState({name:'计数器',number:0}); console.log('render Counter') return ( <> <p>{counter.name}:{counter.number}</p> <button onClick={()=>setCounter({...counter,number:counter.number+1})}>+</button> <button onClick={()=>setCounter(counter)}>++</button> </> ) } export default Counter;
|
7.2 减少渲染次数
7.3 useState 源码中的链表实现???
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| import React from 'react'; import ReactDOM from 'react-dom';
let firstWorkInProgressHook = {memoizedState: null, next: null}; let workInProgressHook; function useState(initState) { let currentHook = workInProgressHook.next ? workInProgressHook.next : {memoizedState: initState, next: null}; function setState(newState) { currentHook.memoizedState = newState; render(); } if (workInProgressHook.next) { workInProgressHook = workInProgressHook.next; } else { workInProgressHook.next = currentHook; workInProgressHook = currentHook; } return [currentHook.memoizedState, setState]; }
function Counter() { const [name, setName] = useState('计数器'); const [number, setNumber] = useState(0); return ( <> <p>{name}:{number}</p> <button onClick={() => setName('新计数器' + Date.now())}>新计数器</button> <button onClick={() => setNumber(number + 1)}>+</button> </> ) } function render() { workInProgressHook = firstWorkInProgressHook; ReactDOM.render(<Counter/>, document.getElementById('root')); } render();
|
8. useReducer
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| import React, { useReducer } from "react";
const initialState = 0; function reducer(state, action) { switch (action.type) { case 'increment': return {number: state.number + 1}; case 'decrement': return {number: state.number - 1}; default: throw new Error(); } } function init(initialState){ return {number:initialState}; } function Counter(){ const [state, dispatch] = useReducer(reducer, initialState, init); return ( <> Count: {state.number} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ) } export default Counter;
|
9. useContext
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54
| import React, { useReducer, createContext, useContext } from 'react'; const initialState = {number : 0}; function reducer(state = initialState, action) { switch (action.type) { case 'ADD': return { number: state.number + 1 }; default: break; } } const CounterContext = createContext(undefined);
function SubCounter_one() { return ( <CounterContext.Consumer> { (value: any) => { return ( <> <p>{value.state.number}</p> <button onClick={() => value.dispatch({ type: 'ADD' })}>+</button> </> ) } }
</CounterContext.Consumer> ) }
function SubCounter() { const { state, dispatch} = useContext(CounterContext); console.log("state___", state) return ( <> <p>{state.number}</p> <button onClick={() => dispatch({ type: 'ADD' })}>+</button> </> ) }
function Counter() { const [state, dispatch] = useReducer(reducer, initialState, () => (initialState)); return ( <CounterContext.Provider value={state, dispatch}> <SubCounter /> {/* <SubCounter_one /> */} </CounterContext.Provider> ) } export default Counter;
|
10. useEffect
- componentDidMount、componentDidUpdate 和 componentWillUnmount
- effect(副作用):指那些没有发生在数据向视图转换过程中的逻辑,如 ajax 请求、访问原生dom 元素、本地持久化缓存、绑定/解绑事件、添加订阅、设置定时器、记录日志等。
- 副作用操作可以分两类:需要清除的和不需要清除的。
10.1 修改标题
10.1.1 使用class组件
- componentDidMount和componentDidUpdate需要执行两次
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React from "react"; class Counter extends React.Component{ state = {number:0}; add = ()=>{ this.setState({number:this.state.number+1}); }; componentDidMount(){ this.changeTitle(); } componentDidUpdate(){ this.changeTitle(); } changeTitle = ()=>{ document.title = `你已经点击了${this.state.number}次`; }; render(){ return ( <> <p>{this.state.number}</p> <button onClick={this.add}>+</button> </> ) } } export default Counter;
|
10.1.2 使用useEffect
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import React, { Component, useState, useEffect } from 'react'; function Counter() { const [number, setNumber] = useState(0); useEffect(() => { document.title = `你点击了${number}次`; }); return ( <> <p>{number}</p> <button onClick={() => setNumber(number + 1)}>+</button> </> ) } export default Counter;
|
10.1.3 清除副作用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25
| import React, { Component, useState, useEffect } from 'react'; function Counter(){ let [number,setNumber] = useState(0); let [text,setText] = useState(''); useEffect(()=>{ console.log('开启一个新的定时器') let $timer = setInterval(()=>{ setNumber(number=>number+1); },1000); return ()=>{ console.log('destroy effect'); clearInterval($timer); } }); return ( <> <input value={text} onChange={(event)=>setText(event.target.value)}/> <p>{number}</p> <button>+</button> </> ) } export default Counter;
|
- 推荐启用 eslint-plugin-react-hooks[1] 中的 exhaustive-deps 规则
10.1.4 使用多个 Effect 实现关注点分离
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { Component, useState, useEffect } from 'react'; function Counter(){ let [number,setNumber] = useState(0); let [text,setText] = useState(''); useEffect(()=> { document.title = "我是新标题"; console.log("标题变化了") }, []); useEffect(()=> { console.log("number变化了:", number); }, [number]); return ( <> <input value={text} onChange={(event)=>setText(event.target.value)}/> <p>{number}</p> <button onClick={(e)=>setNumber(number +1)}>+</button> </> ) } export default Counter;
|
11. useLayoutEffect
- useLayoutEffect 会在 浏览器 layout 之后,painting 之前执行,可能会阻塞视图更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| import React, { Component, useState, useEffect } from 'react'; function Counter(){ let [number,setNumber] = useState(0); let [text,setText] = useState(''); useEffect(()=> { document.title = "我是新标题"; console.log("标题变化了") }, []); useEffect(()=> { console.log("number变化了:", number); }, [number]); return ( <> <input value={text} onChange={(event)=>setText(event.target.value)}/> <p>{number}</p> <button onClick={(e)=>setNumber(number +1)}>+</button> </> ) } export default Counter;
|
12. useRef & useImperativeHandle
获取焦点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| import React, { useState, useEffect, useRef } from 'react';
function Parent() { let [number, setNumber] = useState(0); return ( <> <Child /> <button onClick={() => setNumber(number + 1)}>+</button> </> ) } let input; function Child() { const inputRef: any = useRef(); console.log('input===inputRef', input === inputRef); input = inputRef; function getFocus() { inputRef?.current?.focus(); } return ( <> <input type="text" ref={inputRef} /> <button onClick={getFocus}>获得焦点</button> </> ) } export default Parent;
|
获取子组件的焦点
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| import React, { useState, useEffect, useRef } from 'react';
function Child(props, ref) { return ( <input type="text" ref={ref} /> ) } const Child2 = React.forwardRef(Child);
function Parent() { let [number, setNumber] = useState(0); const inputRef: any = useRef(); function getFocus() { inputRef.current.value = 'focus'; inputRef.current.focus(); } return ( <> <Child2 ref={ inputRef } /> <button onClick={() => setNumber(number + 1 )}>+</button> <button onClick={getFocus}>获得焦点</button> </> ) } export default Parent;
|
自定义暴露给父组件的实例值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import React, { useState, useEffect, createRef, useRef, forwardRef, useImperativeHandle } from 'react';
function Child(props, parentRef) { let focusRef: any = useRef(); let inputRef: any = useRef(); useImperativeHandle(parentRef, () => { return { focusRef, inputRef, name: '计数器', focus() { focusRef.current.focus(); }, changeText(text) { inputRef.current.value = text; } } }); return ( <> <input ref={focusRef} /> <input ref={inputRef} /> </> ) } const ForwardChild = forwardRef(Child);
function Parent() { const parentRef: any = useRef(); function getFocus() { parentRef.current.focus(); parentRef.current.addNumber(666); parentRef.current.changeText('<script>\alert(123)</script>'); console.log(parentRef.current.name); } return ( <> <ForwardChild ref={parentRef} /> <button onClick={getFocus}>获得焦点</button> </> ) } export default Parent;
|