手寫React Hook核心原理

React Hook原理

基本準備工做

利用 creact-react-app 建立一個項目 在這裏插入圖片描述javascript

已經把項目放到 github:github.com/Sunny-lucki… 能夠卑微地要個star嗎css

手寫useState

useState的使用

useState能夠在函數組件中,添加state Hook。前端

調用useState會返回一個state變量,以及更新state變量的方法。useState的參數是state變量的初始值,初始值僅在初次渲染時有效vue

更新state變量的方法,並不會像this.setState同樣,合併state。而是替換state變量。 下面是一個簡單的例子, 會在頁面上渲染count的值,點擊setCount的按鈕會更新count的值。java

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方法react

當咱們使用這個方法時,若是是第一次使用,則取initState的值,不然就取上一次的值(laststate).git

在內部,咱們建立了一個setState方法,該方法用於更新state的值github

而後返回一個lastSate屬性和setState方法。api

看似完美,可是咱們其實忽略了一個問題:每次執行玩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[currentIndex ] = 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 最外層包裝組件,而且須要顯示的經過 <MyContext.Provider value={{xx:xx}}> 的方式傳入 value,指定 context 要對外暴露的信息。

子組件在匹配過程當中只會匹配最新的 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> <h1>{state.number}</h1> <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> <h1>{state.number}</h1> <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> <h1>{number}</h1> <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> <h1>{number}</h1> <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> <h1>{number}</h1> <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> <h1>{number}</h1> <button onClick={() => setNumber(number+1)}>+</button> </div>
    )
}
function render() {
    ReactDOM.render(
        <App />,
        document.getElementById('root')
    );
}
render()
複製代碼

恭喜你閱讀到這裏,又變強了有沒有 已經把項目放到 github:github.com/Sunny-lucki…

文章首發於公衆號《前端陽光》

相關文章
相關標籤/搜索