ReactHooks 原理
useState
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| import React,{useState} from "react"; import ReactDOM from "react-dom"; function Counter(a) { let [state, setState] = useState(0); let [number, setNumber] = useState(10); return ( <> <p>{state}</p> <p>{number}</p> <button onClick={() => setState(state + 1)}>+</button> <button onClick={() => setNumber(number + 1)}>+</button> </> ); } function render() { ReactDOM.render(<Counter />, document.getElementById("root")); } render();
|
可以看到
- useState 可以传多个
- useState 能独立执行
- 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
| import React from "react"; import ReactDOM from "react-dom"; let lastStates = []; let index = 0; function useState(initialState) { const currentIndex = index; lastStates[index] = lastStates[index] || initialState; function setState(newState) { lastStates[currentIndex] = newState; render(); } return [lastStates[index++], setState]; } function Counter(a) { let [state, setState] = useState(0); let [number, setNumber] = useState(10); return ( <> <p>{state}</p> <p>{number}</p> <button onClick={() => setState(state + 1)}>+</button> <button onClick={() => setNumber(number + 1)}>+</button> </> ); } function render() { ReactDOM.render(<Counter />, document.getElementById("root")); index = 0; } render();
|
- React 中是通过类似单链表的形式来代替数组 lastStates 的
- 每个组建都会生成一个 lastStates
- 第一次渲染时候,是根据 useState 顺序,逐个声明 state 并且将其放入全局 lastStates 中,如果有判断可能导致下标发生变化
useMemo
- useMemo 可以用来优化组件的更新.
- useMemo 一般用于密集型计算大的一些缓存.
举个例子
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
| import React, { useState, memo, useMemo } from "react"; import ReactDOM from "react-dom"; function Counter() { let [name, setName] = useState("张三"); let [age, setAge] = useState(18); return ( <> <button onClick={() => setName(name + "的父亲")}>改变名字</button> <button onClick={() => setAge(age + 18)}>改变性别</button> <ChildrenName name={name}></ChildrenName> <ChildrenAge age={age}></ChildrenAge> </> ); } function ChildrenName({ name }) { console.log("子组件ChildrenName"); return ( <> <div>{name}</div> </> ); } function ChildrenAge({ age }) { console.log("子组件ChildrenAge"); return ( <> <div>{age}</div> </> ); } function render() { ReactDOM.render(<Counter />, document.getElementById("root")); } render();
|
上面组件当中,Counter 有两个自组件 ChildrenName,ChildrenAge。点击按钮改变名字或者性别都能导致两个组件的更新。
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
| import React, { useState, memo } from "react"; import ReactDOM from "react-dom"; function Counter() { let [name, setName] = useState("张三"); let [age, setAge] = useState(18); return ( <> <button onClick={() => setName(name + "的父亲")}>改变名字</button> <button onClick={() => setAge(age + 18)}>改变性别</button> <ChildrenName name={name}></ChildrenName> <ChildrenAge age={age}></ChildrenAge> </> ); } function ChildrenName({ name }) { console.log("子组件ChildrenName"); return ( <> <div>{name}</div> </> ); } ChildrenAge = memo(ChildrenAge); function ChildrenAge({ age }) { console.log("子组件ChildrenAge"); return ( <> <div>{age}</div> </> ); } function render() { ReactDOM.render(<Counter />, document.getElementById("root")); } render();
|
引入 memo 后可以看到再改变 name 不会触发 ChildrenAge 组件更新,但是这时候如果我们传入的 age 是对象的话还是会触发更新。
这时候引入 useMemo
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 55 56 57 58 59
| import React, { useState, memo } from "react"; import ReactDOM from "react-dom"; let lastMemo; let lastMemoDependencies; function useMemo(callback, dependencies) { if (lastMemoDependencies) { let isChanged; isChanged = !dependencies.every((item, index) => { return lastMemoDependencies[index] === item; }); if (isChanged) { lastMemo = callback(); lastMemoDependencies = dependencies; } } else { lastMemo = callback(); lastMemoDependencies = dependencies; } return lastMemo; } function Counter() { let [name, setName] = useState("张三"); let [age, setAge] = useState(18); let memoAge = useMemo( () => ({ age, }), [age] ); return ( <> <button onClick={() => setName(name + "的父亲")}>改变名字</button> <button onClick={() => setAge(age + 18)}>改变性别</button> <ChildrenName name={name}></ChildrenName> <ChildrenAge age={{memoAge}}></ChildrenAge> </> ); } function ChildrenName({ name }) { console.log("子组件ChildrenName"); return ( <> <div>{name}</div> </> ); } ChildrenAge = memo(ChildrenAge); function ChildrenAge({ age }) { console.log("子组件ChildrenAge"); return ( <> <div>{age.age}</div> </> ); } function render() { ReactDOM.render(<Counter />, document.getElementById("root")); } render();
|
将对象包裹起来可以再传进去可以看到再改变名字不会触发 ChildrenAge 组件更新。
我们实现一个 useMemo,接收两个参数 callback,dependencies,并且只对数据有效,callback 是内部执行了
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let lastMemo; let lastMemoDependencies; function useMemo(callback, dependencies) { if (lastMemoDependencies) { let isChanged; isChanged = !dependencies.every((item, index) => { return lastMemoDependencies[index] === item; }); if (isChanged) { lastMemo = callback(); lastMemoDependencies = dependencies; } } else { lastMemo = callback(); lastMemoDependencies = dependencies; } return lastMemo; }
|
简单的 useMemo 就实现了,如果有多个 useMemo,类似 useState 将 lastMemo,lastMemoDependencies 换成数组,加入下标即可。
useCallback
useCallback 和 useMemo 极其相似,只不过一个用来缓存数据,一个用来缓存函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| let lastCallback; let lastCallbackDependencies; function useCallback(callback,dependencies){ if(lastCallbackDependencies){ let changed = !dependencies.every((item,index)=>{ return item == lastCallbackDependencies[index]; }); if(changed){ lastCallback = callback; lastCallbackDependencies = dependencies; } }else{ lastCallback=callback; lastCallbackDependencies=dependencies; } return lastCallback; }
|
举个例子
1 2 3 4 5 6 7 8 9 10 11 12
| function List({bar, baz}) { React.useEffect(() => { axios('url',data) }, [ids, obj]) return <div>...</div> }
function NewsList() { const obj = React.useCallback(() => {}, []) const ids = React.useMemo(() => [2,3,4], []) return <List id={id} name={name} /> }
|
比如上述例子,当 useEffect 的依赖是非原始数据类型的时候,useCallback,useMemo 就派上用场了
useReducer
useReducer 实现
1 2 3 4 5 6 7 8 9
| let lastState; function useReducer(reducer,initialState){ lastState= lastState||initialState; function dispatch(action){ lastState= reducer(lastState,action); render(); } return [lastState,dispatch]; }
|
应用
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 ,{ useReducer } from 'react'; import ReactDOM from 'react-dom'; function reducer(state,action){ if(action.type === 'add'){ return state+1; }else{ return state; } } function Counter(){ let [state,dispatch] = useReducer(reducer,0); return ( <div> <p>{state}</p> <button onClick={()=>dispatch({type:'add'})}>+</button> </div> )
} function render(){ ReactDOM.render( <Counter/>, document.getElementById('root') ); } render();
|
useContext,useEffect
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
| import React, { useState, useContext, useRef} from "react"; import ReactDOM from "react-dom"; let AppContext = React.createContext(); function Counter() { const { state, setState } = useContext(AppContext); return ( <div> <p>{state.number}</p> <button onClick={() => setState({ number: state.number + 1 })}>+</button> </div> ); } function App() { let [state, setState] = useState({ number: 0 }); const ref = useRef(0); return ( <div> <div ref={ref}> <AppContext.Provider value={{ state, setState }}> <Counter /> </AppContext.Provider> </div> </div> ); } function render() { ReactDOM.render(<App />, document.getElementById("root")); } render();
|
实现
1 2 3 4 5 6 7 8 9 10
| function useContext(Context){ return Context._currentValue } let lastRef ; function useRef(initialRef){ lastRef=lastRef||initialRef; return { current:lastRef } }
|
useEffect,useLayoutEffect
useEffect 和 useLayoutEffect 都是副作用,区别在于 useEffect 不会阻塞渲染会在 render 之后执行,useLayoutEffect 则会阻塞渲染在渲染之前执行
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
| import React, { useRef, useLayoutEffect, useEffect } from "react"; import ReactDOM from "react-dom"; function Animation() { const ref = useRef(); useLayoutEffect(() => { console.log("useLayoutEffect"); ref.current.style.transform = `translate(500px)`; ref.current.style.transition = "all 800ms"; }); let style = { width: "100px", height: "100px", backgroundColor: "red", }; console.log("Animation "); return ( <div style={style} ref={ref}> 内容 </div> ); } function render() { ReactDOM.render(<Animation />, document.getElementById("root")); } render();
|
上述例子中,可以明显的看到 useEffect 执行会有动画,useLayoutEffect 不会。那么怎么能达到这种效果呢,
先来看张图
其中 useEffect 在浏览器绘制后执行,而 useLayoutEffect 在绘制前执行,那么怎么做呢?每个组件可以理解成一个事件环,首先执行任务回调中同步代码,执行完毕后,再执行当前回调中的所有微任务,执行宏任务=>下一个任务。其中渲染页面在微任务之前执行,因此我们只需要把 useEffect 中的 callback 编程微任务,而 useLayoutEffect 变成宏任务即可。
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
| et lastDependencies; function useEffect(callback,dependencies){ if(lastDependencies){ let changed = !dependencies.every((item,index)=>{ return item == lastDependencies[index]; }); if(changed){ setTimeout(callback); lastDependencies = dependencies; } }else{ setTimeout(callback); lastDependencies = dependencies; } } let lastLayoutDependencies; function useLayoutEffect(callback,dependencies){ if(lastLayoutDependencies){ let changed = !dependencies.every((item,index)=>{ return item == lastLayoutDependencies[index]; }); if(changed){ Promise.resolve().then(callback); lastLayoutDependencies = dependencies; } }else{ Promise.resolve().then(callback); lastLayoutDependencies = dependencies; } }
|