React Hook 原理解析

498 阅读7分钟

一. useState

useState是React用来管理变量的一个函数,他的底层实现大概是这个样子的

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

let _state;
//声明一个全局变量_state

function myUseState(initialValue) {
  _state = _state || initialValue;
  function setState(newState) {
    _state = newState;
    render();
    //state改变后重新渲染
  }
  return [_state, setState];
}

// 教学需要,不用在意 render 的实现
const render = () => ReactDOM.render(<App />, rootElement);

function App() {
  const [n, setN] = myUseState(0);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  1. 声明一个myUseState来代替useState,定义一个全局变量_state
  2. 当我们第一次渲染App时,调用myUseState,将0赋值给_state,之后再次调用就将全局_state赋值给它
  3. 在muUseState中声明一个函数setState,他接受一个新的state,并将值赋值给_state,并触发重新渲染

这样我们就实现了一个变量的useState函数,但是如果有多个变量呢,_state的值由谁决定,所以我们对他进行改进

//version 2.0
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

let _state = [];
//将_state定义成一个数组
let index = 0;
//index用于标记变量
function myUseState(initialValue) {
  const currentIndex = index;
  index += 1;
  _state[currentIndex] = _state[currentIndex] || initialValue;
  const setState = newState => {
    _state[currentIndex] = newState;
    //这时候返回的函数currentIndex已经是确定的了
    render();
  };
  return [_state[currentIndex], setState];
}

const render = () => {
  index = 0;
  ReactDOM.render(<App />, rootElement);
};

function App() {
  const [n, setN] = myUseState(0);
  const [m, setM] = myUseState(0);
  console.log(_state);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
      </p>
      <p>{m}</p>
      <p>
        <button onClick={() => setM(m + 1)}>+1</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  1. 我们将_state定义成一个变量的数组,每次调用此函数就将index+1并存入下一个变量 2.当setN触发时,将 _state[currentIndex]即n所对应的项更新,并重新渲染
  2. 重新渲染时重置index,因为如果不重置此时currentIndex就会变成3,使得_state数组加长

由于_state是一个数组,所以每一个变量所对应的下标是不能更改的,每次渲染都要执行myUseState,因此React是不允许在useState外层添加条件判断语句的

至此我们就完成了useState的基本功能的实现,当我们setN的时候并不会直接去把n更改了,而是会先存到_state这样一个中间变量中,然后在再次渲染页面的时候从这个对象中取出n的值并显示在页面中,所以原来的n已经不存在了,这个我们可以用一个异步函数来验证

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const [n, setN] = React.useState(0);
  const log = () => setTimeout(() => console.log(`n: ${n}`), 3000);
  return (
    <div className="App">
      <p>{n}</p>
      <p>
        <button onClick={() => setN(n + 1)}>+1</button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

当我们点击log时,延迟打印出当前的n,如果我们先点击log,再点击+1会怎样

n:0

log会打印出0而不是1,所以我们知道n这个变量不是同一个,setN会创建一个新的n

这里再补充一点关于setN

1. setN如果改变的是一个对象的部分属性,那么你必须将原来的对象用...展开符拷贝过来再用新属性覆盖

const [user,setUser] = useState({name:'Frank', age: 18})
  const onClick = ()=>{
    setUser({
    ...user,
      name: 'Jack'
    })
  }

2.如果我们直接对user.name进行修改是不可行的,因为user的地址没发生改变尽管它的部分属性变了,React还是不会触发更新,所以我们要直接返回一个新的对象而不是对user直接进行修改

如果我们要定义一个唯一的全局n怎么办,用useRef,他能在一个组件定义一个全局变量

二. useRef

import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const nRef = React.useRef(0);
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  return (
    <div className="App">
      <p>{nRef.current} 这里并不能实时更新</p>
      <p>
        <button onClick={() => (nRef.current += 1)}>+1</button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  1. 使用nRef.current获取当前的n,此时log的n和+1的n指的是同一个对象n
  2. 但是这个nRef也不能自动重新render
import React from "react";
import ReactDOM from "react-dom";
const rootElement = document.getElementById("root");

function App() {
  const nRef = React.useRef(0);
  const update = React.useState()[1];
  const log = () => setTimeout(() => console.log(`n: ${nRef.current}`), 1000);
  return (
    <div className="App">
      <p>{nRef.current} 这里并不能实时更新</p>
      <p>
        <button onClick={() => ((nRef.current += 1), update(nRef.current))}>
          +1
        </button>
        <button onClick={log}>log</button>
      </p>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);

我们借用React.useState的自动渲染,让+1时触发update

实际上React.useRecf(0)会将变量变成一个对象{current:0}这样才能保证每次引用的对象都是同一个

二-二 forwardRef

import React, { useRef } from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const buttonRef = useRef(null);
  return (
    <div className="App">
      <Button3 ref={buttonRef}>按钮</Button3>
    </div>
  );
}

const Button3 = React.forwardRef((props, ref) => {
  return <button className="red" ref={ref} {...props} />;
});

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

由于props本身并不能传递ref属性,所以我们需要用React.forwardRef来对其进行一个封装,这样就可以接受一个ref参数

useRef可以引用一个普通对象,也可以引用一个DOM对象

useRef的二次传递

三. useContext

useContex不仅能贯穿始终还能贯穿组件之间

import React from "react";
import ReactDOM from "react-dom";
import "./styles.css";
const rootElement = document.getElementById("root");

const themeContext = React.createContext(null);
//初始化一个局部全局变量他的作用域就是以下标签部分

function App() {
  const [theme, setTheme] = React.useState("red");
  return (
    <themeContext.Provider value={{ theme, setTheme }}>
      <div className={`App ${theme}`}>
        <p>{theme}</p>
        <div>
          <ChildA />
        </div>
        <div>
          <ChildB />
        </div>
      </div>
    </themeContext.Provider>
  );
}

function ChildA() {
  const { setTheme } = React.useContext(themeContext);
  //将setTheme方法从全局变量themeContext中取出
  return (
    <div>
      <button onClick={() => setTheme("red")}>red</button>
    </div>
  );
}

function ChildB() {
  const { setTheme } = React.useContext(themeContext);
  return (
    <div>
      <button onClick={() => setTheme("blue")}>blue</button>
    </div>
  );
}

ReactDOM.render(<App />, rootElement);
  1. 初始化一个局部的全局变量themeContext

  2. themeContext.Provider value={{ theme:theme, setTheme:setTheme }} 将一个对象作为他的值

3.在 <themeContext.Provider></themeContext.Provider>标签内,以及任何子代都可以访问到这个对象

4.子组件取出此对象的setTheme方法后就可以改变themeContext的theme属性, 因此这个方法很适合用于全局切换主题颜色

5.数据更新过程是自上而下的,最外层的n发生了改变依次检查父组件,子组件,孙组件是否引用了此变量

四. useReducer

useReducer其实就是高级版的useState,他是Flux和Redux思想的一个实践

import React, { useState, useReducer } from "react";
import ReactDOM from "react-dom";

const initial = {
  n: 0
};

const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};

function App() {
  const [state, dispatch] = useReducer(reducer, initial);
  const { n } = state;
  const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };
  const onClick2 = () => {
    dispatch({ type: "add", number: 2 });
  };
  return (
    <div className="App">
      <h1>n: {n}</h1>

      <button onClick={onClick}>+1</button>
      <button onClick={onClick2}>+2</button>
    </div>
  );
}

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

他的实现分4步

  1. 创建一个初始值
const initial = {
  n: 0
};
  1. 将所有setState操作都封装到reducer中
const reducer = (state, action) => {
  if (action.type === "add") {
    return { n: state.n + action.number };
  } else if (action.type === "multi") {
    return { n: state.n * 2 };
  } else {
    throw new Error("unknown type");
  }
};
  1. 将初始值和操作传给useReducer
 const [state, dispatch] = useReducer(reducer, initial);
 //注意参数顺序,第一参数为reducer
  1. 调用dispatch,将action对象作为参数传给reducer
const onClick = () => {
    dispatch({ type: "add", number: 1 });
  };

总的来说如果你的数据的写操作过多,就可以将它封装到一起,而简单操作就可以使用useState

如何用useReducer和useContext代替Redux

模块化

五. useEffect和useLayoutEffect

useEffect见函数组件

useLayoutEffect

import React, { useState, useEffect } from "react";
import ReactDOM from "react-dom";
import "./styles.css";

const BlinkyRender = () => {
  const [value, setValue] = useState(0);

  useEffect(() => {
    document.querySelector('#x').innerText = `value: 1000`
  }, [value]);
  //页面会先出现一个0然后变成1000,使用useLayoutEffect就不会闪烁

  return (
    <div id="x" onClick={() => setValue(0)}>value: {value}</div>
  );
};

ReactDOM.render(
  <BlinkyRender />,
  document.querySelector("#root")
);

useLayoutEffect相当于在DOM更新到页面之前执行,这样useState的初始化DOM就会被覆盖,所以这个一般用在render后立马操作DOM,但是这个会延长浏览器渲染页面时间

六. useMemo

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child data={m}/>
      {/* <Child2 data={m}/> */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log('假设这里有大量代码')
  return <div>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

上述例子中,Child组件接受一个props依赖于m,但是当我们改变n的时候,App重新渲染,但是Child组件也渲染了,这是React的默认多余渲染,所以我们可以用React .memo来对他进行一个封装,这样只有当自己的依赖改变时才会重新渲染

但是这个函数有个一个bug,那就是当我们接受一个外部的函数,比如onclick,那么在App渲染之后,都会重新定义一个新的onclick,函数地址发生了改变,所以child组件还是重新渲染了

import React from "react";
import ReactDOM from "react-dom";

import "./styles.css";

function App() {
  const [n, setN] = React.useState(0);
  const [m, setM] = React.useState(0);
  const onClick = () => {
    setN(n + 1);
  };
  const onClickChild = () => {
    console.log(m);
  };
  //每次重渲染会执行此句生成一个新函数地址

  return (
    <div className="App">
      <div>
        <button onClick={onClick}>update n {n}</button>
      </div>
      <Child2 data={m} onClick={onClickChild} />
      {/* Child2 居然又执行了 */}
    </div>
  );
}

function Child(props) {
  console.log("child 执行了");
  console.log("假设这里有大量代码");
  return <div onClick={props.onClick}>child: {props.data}</div>;
}

const Child2 = React.memo(Child);

const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

useMemo

  const onClickChild = useMemo(() => {
    const fn = div => {
      console.log("on click child, m: " + m);
      console.log(div);
    };
    return fn;
  }, [m]); // 这里呃 [m] 改成 [n] 就会打印出旧的 m

将上例中的onClickChild用useMemo返回,并添加一个依赖m,这样在m改变时才会重新渲染

useCallback

const onClickChild = useCallback(
    const fn = div => {
      console.log("on click child, m: " + m);
      console.log(div);
    };
    return fn;
  , [m])

useCallback就是省略了返回函数前的箭头函数,直接返回一个函数

七. 自定义Hook

//useList Hook
import { useState, useEffect } from "react";

const useList = () => {
  const [list, setList] = useState(null);
  useEffect(() => {
    ajax("/list").then(list => {
      setList(list);
    });
  }, []); // [] 确保只在第一次运行
  return {
    list: list,
    setList: setList
  };
};
export default useList;

在这里进行一系列操作,然后将useState的读和写接口返回给外部调用

import React, { useRef, useState, useEffect } from "react";
import ReactDOM from "react-dom";
import useList from "./hooks/useList";

function App() {
  const { list, setList } = useList();
  //运行后自动发送ajax请求,得到读写接口
  return (
    <div className="App">
    </div>
  );
}
const rootElement = document.getElementById("root");
ReactDOM.render(<App />, rootElement);

甚至我们可以在自定义Hook中返回更多的操作接口

return {
    list: list,
    addItem: name => {
      setList([...list, { id: Math.random(), name: name }]);
    },
    deleteIndex: index => {
      setList(list.slice(0, index).concat(list.slice(index + 1)));
    }
  };
};

外部可以直接调用,一个字爽~~~