您好,登錄后才能下訂單哦!
本篇內容主要講解“ReactHook核心原理是什么”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“ReactHook核心原理是什么”吧!
基本準備工作
手寫useState:useState的使用,原理實現。
React.memo介紹
手寫useCallback:useCallback的使用,原理實現。
手寫useMemo:使用,原理。
手寫useReducer:使用,原理。
手寫useContext:使用,原理。
手寫useEffect:使用,原理。
手寫useLayoutEffect:使用,原理。
基本準備工作
利用 creact-react-app 創建一個項目
已經把項目放到 github:https://github.com/Sunny-lucking/HowToBuildMyReactHook 可以卑微地要個star嗎
手寫useState
useState的使用
useState可以在函數組件中,添加state Hook。
調用useState會返回一個state變量,以及更新state變量的方法。useState的參數是state變量的初始值,初始值僅在初次渲染時有效。
更新state變量的方法,并不會像this.setState一樣,合并state。而是替換state變量。下面是一個簡單的例子, 會在頁面上渲染count的值,點擊setCount的按鈕會更新count的值。
function App(){ const [count, setCount] = useState(0); return ( <div> {count} <button onClick={() => { setCount(count + 1); }} > 增加 </button> </div> ); } ReactDOM.render( <App />, document.getElementById('root') );
原理實現
let lastState function useState(initState) { lastState = lastState || initState; function setState(newState) { lastState = newState } return [lastState,setState] } function App(){ //。。。 } ReactDOM.render( <App />, document.getElementById('root') );
如代碼所示,我們自己創建了一個useState方法
當我們使用這個方法時,如果是第一次使用,則取initState的值,否則就取上一次的值(laststate).
在內部,我們創建了一個setState方法,該方法用于更新state的值
然后返回一個lastSate屬性和setState方法。
看似完美,但是我們其實忽略了一個問題:每次執行玩setState都應該重新渲染當前組件的。
所以我們需要在setState里面執行刷新操作
let lastState function useState(initState) { lastState = lastState || initState; function setState(newState) { lastState = newState render() } return [lastState,setState] } function App(){ const [count, setCount] = useState(0); return ( <div> {count} <button onClick={() => { setCount(count + 1); }} > 增加 </button> </div> ); } // 新增方法 function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
如代碼所示,我們在setState里添加了個render方法。render方法則會執行
ReactDOM.render( <App />, document.getElementById('root') );
也就是重新渲染啦。
好了,現在是不是已經完整了呢?
不,還有個問題:就說我們這里只是用了一個useState,要是我們使用了很多個呢?難道要聲明很多個全局變量嗎?
這顯然是不行的,所以,我們可以設計一個全局數組來保存這些state
let lastState = [] let stateIndex = 0 function useState(initState) { lastState[stateIndex] = lastState[stateIndex] || initState; const currentIndex = stateIndex function setState(newState) { lastState[stateIndex] = newState render() } return [lastState[stateIndex++],setState] }
這里的currentIndex是利用了閉包的思想,將某個state相應的index記錄下來了。
好了,useState方法就到這里基本完成了。是不是so easy!!!
React.memo介紹
看下面的代碼!你發現什么問題?
import React ,{useState}from 'react'; import ReactDOM from 'react-dom'; import './index.css'; function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } function App(){ const [count, setCount] = useState(0); return ( <div> <Child data={123}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
沒錯,就是盡管我們傳個子組件的props是固定的值,當父組件的數據更改時,子組件也被重新渲染了,我們是希望當傳給子組件的props改變時,才重新渲染子組件。
所以引入了React.memo。
看看介紹
React.memo() 和 PureComponent 很相似,它幫助我們控制何時重新渲染組件。
組件僅在它的 props 發生改變的時候進行重新渲染。通常來說,在組件樹中 React 組件,只要有變化就會走一遍渲染流程。但是通過 PureComponent 和 React.memo(),我們可以僅僅讓某些組件進行渲染。
import React ,{useState,memo}from 'react'; import ReactDOM from 'react-dom'; import './index.css'; function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } Child = memo(Child) function App(){ const [count, setCount] = useState(0); return ( <div> <Child data={123}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
因此,當Child被memo包裝后,就只會當props改變時才會重新渲染了。
當然,由于React.memo并不是react-hook的內容,所以這里并不會取討論它是怎么實現的。
手寫useCallback
useCallback的使用
當我們試圖給一個子組件傳遞一個方法的時候,如下代碼所示
import React ,{useState,memo}from 'react'; import ReactDOM from 'react-dom'; function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } // eslint-disable-next-line Child = memo(Child) function App(){ const [count, setCount] = useState(0); const addClick = ()=>{console.log("addClick")} return ( <div> <Child data={123} onClick={addClick}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
發現我們傳了一個addClick方法 是固定的,但是卻每一次點擊按鈕子組件都會重新渲染。
這是因為你看似addClick方法沒改變,其實舊的和新的addClick是不一樣的,如圖所示
在這里插入圖片描述
這時,如果想要,傳入的都是同一個方法,就要用到useCallBack。
如代碼所示
import React ,{useState,memo,useCallback}from 'react'; import ReactDOM from 'react-dom'; function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } // eslint-disable-next-line Child = memo(Child) function App(){ const [count, setCount] = useState(0); // eslint-disable-next-line const addClick = useCallback(()=>{console.log("addClick")},[]) return ( <div> <Child data={123} onClick={addClick}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
useCallback鉤子的第一個參數是我們要傳遞給子組件的方法,第二個參數是一個數組,用于監聽數組里的元素變化的時候,才會返回一個新的方法。
原理實現
我們知道useCallback有兩個參數,所以可以先寫
function useCallback(callback,lastCallbackDependencies){ }
跟useState一樣,我們同樣需要用全局變量把callback和dependencies保存下來。
let lastCallback let lastCallbackDependencies function useCallback(callback,dependencies){ }
首先useCallback會判斷我們是否傳入了依賴項,如果沒有傳的話,說明要每一次執行useCallback都返回最新的callback
let lastCallback let lastCallbackDependencies function useCallback(callback,dependencies){ if(lastCallbackDependencies){ }else{ // 沒有傳入依賴項 } return lastCallback }
所以當我們沒有傳入依賴項的時候,實際上可以把它當作第一次執行,因此,要把lastCallback和lastCallbackDependencies重新賦值
let lastCallback let lastCallbackDependencies function useCallback(callback,dependencies){ if(lastCallbackDependencies){ }else{ // 沒有傳入依賴項 lastCallback = callback lastCallbackDependencies = dependencies } return lastCallback }
當有傳入依賴項的時候,需要看看新的依賴數組的每一項和來的依賴數組的每一項的值是否相等
let lastCallback let lastCallbackDependencies function useCallback(callback,dependencies){ if(lastCallbackDependencies){ let changed = !dependencies.every((item,index)=>{ return item === lastCallbackDependencies[index] }) }else{ // 沒有傳入依賴項 lastCallback = callback lastCallbackDependencies = dependencies } return lastCallback } function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) }
當依賴項有值改變的時候,我們需要對lastCallback和lastCallbackDependencies重新賦值
import React ,{useState,memo}from 'react'; import ReactDOM from 'react-dom'; let lastCallback // eslint-disable-next-line 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 } function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } // eslint-disable-next-line Child = memo(Child) function App(){ const [count, setCount] = useState(0); // eslint-disable-next-line const addClick = useCallback(()=>{console.log("addClick")},[]) return ( <div> <Child data={123} onClick={addClick}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
手寫useMemo
使用
useMemo和useCallback類似,不過useCallback用于緩存函數,而useMemo用于緩存函數返回值
let data = useMemo(()=> ({number}),[number])
如代碼所示,利用useMemo用于緩存函數的返回值number,并且當只有監聽元素為[number],也就是說,當number的值發生改變的時候,才會重新執行
()=> ({number})
然后返回新的number
原理
所以,useMemo的原理跟useCallback的差不多,仿寫即可。
import React ,{useState,memo,}from 'react'; import ReactDOM from 'react-dom'; let lastMemo // eslint-disable-next-line let lastMemoDependencies function useMemo(callback,dependencies){ if(lastMemoDependencies){ let changed = !dependencies.every((item,index)=>{ return item === lastMemoDependencies[index] }) if(changed){ lastMemo = callback() lastMemoDependencies = dependencies } }else{ // 沒有傳入依賴項 lastMemo = callback() lastMemoDependencies = dependencies } return lastMemo } function Child({data}) { console.log("天啊,我怎么被渲染啦,我并不希望啊") return ( <div>child</div> ) } // eslint-disable-next-line Child = memo(Child) function App(){ const [count, setCount] = useState(0); // eslint-disable-next-line const [number, setNumber] = useState(20) let data = useMemo(()=> ({number}),[number]) return ( <div> <Child data={data}></Child> <button onClick={() => { setCount(count + 1)}}> 增加 </button> </div> ); } function render(){ ReactDOM.render( <App />, document.getElementById('root') ); } render()
手寫useReducer
使用
先簡單介紹下useReducer。
const [state, dispatch] = useReducer(reducer, initState);
useReducer接收兩個參數:
第一個參數:reducer函數,第二個參數:初始化的state。
返回值為最新的state和dispatch函數(用來觸發reducer函數,計算對應的state)。
按照官方的說法:對于復雜的state操作邏輯,嵌套的state的對象,推薦使用useReducer。
聽起來比較抽象,我們先看一個簡單的例子:
// 官方 useReducer Demo // 第一個參數:應用的初始化 const initialState = {count: 0}; // 第二個參數:state的reducer處理函數 function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { // 返回值:最新的state和dispatch函數 const [state, dispatch] = useReducer(reducer, initialState); return ( <> // useReducer會根據dispatch的action,返回最終的state,并觸發rerender Count: {state.count} // dispatch 用來接收一個 action參數「reducer中的action」,用來觸發reducer函數,更新最新的狀態 <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); }
其實意思可以簡單的理解為,當state是基本數據類型的時候,可以用useState,當state是對象的時候,可以用reducer,當然這只是一種簡單的想法。大家不必引以為意。具體情況視具體場景分析。
原理
看原理你會發現十分簡單,簡單到不用我說什么,不到十行代碼,不信你直接看代碼
import React from 'react'; import ReactDOM from 'react-dom'; let lastState // useReducer原理 function useReducer(reducer,initialState){ lastState = lastState || initialState function dispatch(action){ lastState = reducer(lastState,action) render() } return [lastState,dispatch] } // 官方 useReducer Demo // 第一個參數:應用的初始化 const initialState = {count: 0}; // 第二個參數:state的reducer處理函數 function reducer(state, action) { switch (action.type) { case 'increment': return {count: state.count + 1}; case 'decrement': return {count: state.count - 1}; default: throw new Error(); } } function Counter() { // 返回值:最新的state和dispatch函數 const [state, dispatch] = useReducer(reducer, initialState); return ( <> {/* // useReducer會根據dispatch的action,返回最終的state,并觸發rerender */} Count: {state.count} {/* // dispatch 用來接收一個 action參數「reducer中的action」,用來觸發reducer函數,更新最新的狀態 */} <button onClick={() => dispatch({type: 'increment'})}>+</button> <button onClick={() => dispatch({type: 'decrement'})}>-</button> </> ); } function render(){ ReactDOM.render( <Counter />, document.getElementById('root') ); } render()
手寫useContext
使用
createContext 能夠創建一個 React 的 上下文(context),然后訂閱了這個上下文的組件中,可以拿到上下文中提供的數據或者其他信息。
基本的使用方法:
const MyContext = React.createContext()
如果要使用創建的上下文,需要通過 Context.Provider 最外層包裝組件,并且需要顯示的通過
子組件在匹配過程中只會匹配最新的 Provider,也就是說如果有下面三個組件:ContextA.Provider->A->ContexB.Provider->B->C
如果 ContextA 和 ContextB 提供了相同的方法,則 C 組件只會選擇 ContextB 提供的方法。
通過 React.createContext 創建出來的上下文,在子組件中可以通過 useContext 這個 Hook 獲取 Provider 提供的內容
const {funcName} = useContext(MyContext);
從上面代碼可以發現,useContext 需要將 MyContext 這個 Context 實例傳入,不是字符串,就是實例本身。
這種用法會存在一個比較尷尬的地方,父子組件不在一個目錄中,如何共享 MyContext 這個 Context 實例呢?
一般這種情況下,我會通過 Context Manager 統一管理上下文的實例,然后通過 export 將實例導出,在子組件中在將實例 import 進來。
下面我們看看代碼,使用起來非常簡單
import React, { useState, useContext } from 'react'; import ReactDOM from 'react-dom'; let AppContext = React.createContext() function Counter() { let { state, setState } = useContext(AppContext) return ( <> Count: {state.count} <button onClick={() => setState({ number: state.number + 1 })}>+</button> </> ); } function App() { let [state, setState] = useState({ number: 0 }) return ( <AppContext.Provider value={{ state, setState }}> <div> <h2>{state.number}</h2> <Counter></Counter> </div> </AppContext.Provider> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
要是用過vue的同學,會發現,這個機制有點類似vue 中提供的provide和inject
原理
原理非常簡單,由于createContext,Provider 不是ReactHook的內容, 所以這里值需要實現useContext,如代碼所示,只需要一行代碼
import React, { useState } from 'react'; import ReactDOM from 'react-dom'; let AppContext = React.createContext() function useContext(context){ return context._currentValue } function Counter() { let { state, setState } = useContext(AppContext) return ( <> <button onClick={() => setState({ number: state.number + 1 })}>+</button> </> ); } function App() { let [state, setState] = useState({ number: 0 }) return ( <AppContext.Provider value={{ state, setState }}> <div> <h2>{state.number}</h2> <Counter></Counter> </div> </AppContext.Provider> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
手寫useEffect
使用
它跟class組件中的componentDidMount,componentDidUpdate,componentWillUnmount具有相同的用途,只不過被合成了一個api。
import React, { useState, useEffect} from 'react'; import ReactDOM from 'react-dom'; function App() { let [number, setNumber] = useState(0) useEffect(()=>{ console.log(number); },[number]) return ( <div> <h2>{number}</h2> <button onClick={() => setNumber(number+1)}>+</button> </div> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
如代碼所示,支持兩個參數,第二個參數也是用于監聽的。當監聽數組中的元素有變化的時候再執行作為第一個參數的執行函數
原理
原理發現其實和useMemo,useCallback類似,只不過,前面前兩個有返回值,而useEffect沒有。(當然也有返回值,就是那個執行componentWillUnmount函功能的時候寫的返回值,但是這里返回值跟前兩個作用不一樣,因為你不會寫
let xxx = useEffect(()=>{ console.log(number); },[number])
來接收返回值。
所以,忽略返回值,你可以直接看代碼,真的很類似,簡直可以用一模一樣來形容
import React, { useState} from 'react'; import ReactDOM from 'react-dom'; let lastEffectDependencies function useEffect(callback,dependencies){ if(lastEffectDependencies){ let changed = !dependencies.every((item,index)=>{ return item === lastEffectDependencies[index] }) if(changed){ callback() lastEffectDependencies = dependencies } }else{ callback() lastEffectDependencies = dependencies } } function App() { let [number, setNumber] = useState(0) useEffect(()=>{ console.log(number); },[number]) return ( <div> <h2>{number}</h2> <button onClick={() => setNumber(number+1)}>+</button> </div> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
你以為這樣就結束了,其實還沒有,因為第一個參數的執行時機錯了,實際上作為第一個參數的函數因為是在瀏覽器渲染結束后執行的。而這里我們是同步執行的。
所以需要改成異步執行callback
import React, { useState} from 'react'; import ReactDOM from 'react-dom'; let lastEffectDependencies function useEffect(callback,dependencies){ if(lastEffectDependencies){ let changed = !dependencies.every((item,index)=>{ return item === lastEffectDependencies[index] }) if(changed){ setTimeout(callback()) lastEffectDependencies = dependencies } }else{ setTimeout(callback()) lastEffectDependencies = dependencies } } function App() { let [number, setNumber] = useState(0) useEffect(()=>{ console.log(number); },[number]) return ( <div> <h2>{number}</h2> <button onClick={() => setNumber(number+1)}>+</button> </div> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
手寫useLayoutEffect
使用
官方解釋,這兩個hook基本相同,調用時機不同,請全部使用useEffect,除非遇到bug或者不可解決的問題,再考慮使用useLayoutEffect。
原理
原理跟useEffect一樣,只是調用時機不同
上面說到useEffect的調用時機是瀏覽器渲染結束后執行的,而useLayoutEffect是在DOM構建完成,瀏覽器渲染前執行的。
所以這里需要把宏任務setTimeout改成微任務
import React, { useState} from 'react'; import ReactDOM from 'react-dom'; let lastEffectDependencies function useLayouyEffect(callback,dependencies){ if(lastEffectDependencies){ let changed = !dependencies.every((item,index)=>{ return item === lastEffectDependencies[index] }) if(changed){ Promise.resolve().then(callback()) lastEffectDependencies = dependencies } }else{ Promise.resolve().then(callback()) lastEffectDependencies = dependencies } } function App() { let [number, setNumber] = useState(0) useLayouyEffect(()=>{ console.log(number); },[number]) return ( <div> <h2>{number}</h2> <button onClick={() => setNumber(number+1)}>+</button> </div> ) } function render() { ReactDOM.render( <App />, document.getElementById('root') ); } render()
到此,相信大家對“ReactHook核心原理是什么”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。