手写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…

文章首发于公众号《前端阳光》

相关文章
相关标签/搜索