带你手写一套React Hooks好不好? 简单易懂不怕面试被问的那种(一)。

1,246 阅读5分钟

前言

最近有被面试官问到一个问题: 你能在Vue的项目里实现一套React Hooks吗?

实话讲,这个问题最难的是如何兼容Vue 和 React的渲染机制。最惨的是,我是被要求现场写代码的。(幸好,我写得一手好字,面试官都惊叹了。咦?我好像嘚瑟错地方了。)

好吧。我是想告诉你两点,简历上千万别写精通。否则,你很有可能遇见和我一样的情况。/手动狗头

手写一套React Hooks前的思考

那,如何描述React Hooks

  • 对函数型组件进行增强。让函数型组件能去完成类组件做的事情。
  • 让函数型组件可以存储状态
  • 可以拥有处理副作用的能力
  • 让开发者在不使用类组件的情况下,实现相同的功能

那,类组件有什么缺点?类组件的不足 (React Hooks 要解决的问题)

  1. 缺少逻辑复用机制

    • 为了复用逻辑增加无实际渲染效果的组件,增加了组件的嵌套层级,十分臃肿。
    • 增加了调试的难度,实际运行的效率被降低。
  2. 类组件经常会变得很复杂,难以维护。

    • 将一组相干的业务逻辑拆分到了多个生命周期函数中
    • 在一个生命周期函数里存在多个不想干的业务逻辑。
    • 类成员方法不能保证this指向的正确性
    • 当我们给一个元素绑定事件,在事件处理函数当中,我们要更改状态的时候,通常需要更正函数内部的this指向,不如this就指向undefined
    • 解决方法就是 bind 或者函数嵌套函数的方式去更改this指向。

**那,React Hooks有什么明显的特征? **

特征点React HooksClass
使用场景function 内部没什么限制,哪都能写class
更改状态const [state,setState] = useState(initState)this.setState
生命周期不存在,但可以通过Effect Hooks模拟有严格的生命周期函数
状态存储State Hooks状态变量都存放在this.state里面
状态监控单位状态变量为单位无此概念,只能在生命周期函数里写入监控代码

useState分析与实现

先看看官方提供的 useState 栗子。

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

那,如果我们再改改。例如这样:

import React, { useState } from 'react';

function Example() {
  // 声明一个新的叫做 “count” 的 state 变量
  const [count, setCount] = useState([0]);
  const [name, setName] = useState('胖子');
  const [meizhi, setMeiZhi] = useState(()=>'大佬');
  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Call me {name}</p>
      <p>{name} is meizhi ?</p>
      <button onClick={() => {
          setCount([count[0] + 1]);
          setName('胖子是渣男');
          setMeiZhi((preMeizhi)=>`女装${preMeizhi}`);
      }}>
        Click me
      </button>
    </div>
  );
}

我们,可以总结一下useState的用法.

  • 接收任意类型的入参数,可以是数组、对象、函数等。
  • 可以被多次调用,每调用一次都可以解构出不同的状态变量
  • setState 也可以传入回调函数。改回调函数会被自动传入当前对应的状态变量。
  • 被结构出来的状态变量和更改状态变量的方法是1对1绑定的。
  • 调用setState后,要重新render函数组件

开始写

// import React from 'react';
import ReactDOM from 'react-dom';
// 先把render写出来,备着。
function render () {
  ReactDOM.render(<App />, document.getElementById('root'));
}

// App 组件也先备好

function App() {
  const [count, setCount] = useState([0]);
  const [name, setName] = useState('胖子');
  const [meizhi, setMeiZhi] = useState(()=>'大佬');
  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Call me {name}</p>
      <p>{name} is meizhi ?</p>
      <button onClick={() => {
          setCount([count[0] + 1]);
          setName('胖子是渣男');
          // setMeiZhi((preMeizhi)=>`女装${preMeizhi}`);
      }}>
        Click me
      </button>
    </div>
  );
}

// step 1 , 先从结构useState(...) 开始
let state;
function useState(initState) {
  if (!state) state = initState; // 如果state为undefined,则说明是第一次调用useState。即页面没被重新render过
  let setState = (newState) => {
    if (state !== newState) {
      state = newState;
    }
    render()
  }
  return [state, setState]
}


点击Click me之前

image.png

点击Click me之后

image.png

问题很明显了,因为代码里只有一个state变量,每次调用useState返回的状态变量都是state。 我们要做的是,每次调用useState都要结构出不同的[state、setState] 再来。

let state = []; // 存放状态变量
let setters = []; // 存放更改状态变量的方法
let stateIndex = 0; // 用来绑定state[...] 和 setters[...],即状态变量和更改它的方法要保证1对1关系。


function useState (initialState) {
  state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState; // 同理。这是把状态存放到数组当中。
  setters.push(createSetter()); // 根据索引stateIndex来创建setState方法,并push到setters数组当中。
  let value = state[stateIndex]; // 从状态变量数组中取值
  let setter = setters[stateIndex]; // 从setState方法数组中取出对应的setState
 stateIndex++
  return [value, setter]; // 返回,供调用者解构。
}

function createSetter () {
  return (newState) => {
    state[stateIndex]= newState;
    render();
  };
}

看结果,是:

image.png

只有第一行变了。 说明,只有第一个状态变量的值被重新render了。 而代码里,明明已经将stateIndex交给createSetter方法了。 仔细品品,就能发现一个问题。 当你调用多次setState的时候,stateIndex都没有被保留下来。

这里通过闭包来解决这个问题。

let state = []; // 存放状态变量
let setters = []; // 存放更改状态变量的方法
let stateIndex = 0; // 用来绑定state[...] 和 setters[...],即状态变量和更改它的方法要保证1对1关系。


function useState (initialState) {
  state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState; // 同理。这是把状态存放到数组当中。
  setters.push(createSetter(stateIndex)); // 根据索引stateIndex来创建setState方法,并push到setters数组当中。
  let value = state[stateIndex]; // 从状态变量数组中取值
  let setter = setters[stateIndex]; // 从setState方法数组中取出对应的setState
 stateIndex++
  return [value, setter]; // 返回,供调用者解构。
}

function createSetter (index) {
 // 通过返回一个新方法,将传入的index保留下来。 这里就是闭包
  return function (newState) {
    state[index] = newState;
    render ();
  }
}

再看结果:

image.png

再把useState 和setState 传递的参数类型function 做一次处理。 完整代码如下:


let state = [];
let setters = [];
let stateIndex = 0;

const getStateByFn = (v, params) => {
  if (typeof v === 'function') {
    const _newState = v(params);
    if (!_newState) throw 'You must be return state'
    return _newState
  }
  return v
}

function createSetter(index) {
  return function (newState) {
    state[index] =  getStateByFn(newState,state[index])
    render();
  }
}

function useState(initialState) {
  state[stateIndex] = state[stateIndex] ? getStateByFn(state[stateIndex]) : getStateByFn(initialState);
  setters.push(createSetter(stateIndex));
  let value = state[stateIndex];
  let setter = setters[stateIndex];
  stateIndex++;
  return [value, setter];
}

function render() {
  stateIndex = 0;
  ReactDOM.render(<App />, document.getElementById('root'));
}

function App() {
  const [count, setCount] = useState([0]);
  const [name, setName] = useState('胖子');
  const [meizhi, setMeiZhi] = useState(() => '大佬');
  return (
    <div>
      <p>You clicked {count} times</p>
      <p>Call me {name}</p>
      <p>{name} is {meizhi} ?</p>
      <button onClick={() => {
        console.log('count:', count, 'name:', name, 'meizhi:', meizhi)
        setCount([count[0] + 1]);
        setName('胖子是渣男');
        setMeiZhi((preMeizhi) => `女装${preMeizhi}`);
      }}>
        Click me
      </button>
    </div>
  );
}

点击 Click me 后效果如下:

image.png

未完待续

写得很细。 如果有疑惑的地方,欢迎留言提问。

下篇会讲解useEffect的实现。