让我们手动实现 React Hooks

让我们手动实现 React Hooks

正在找工作,有没有大佬带带我的?

brambles:坐标深圳,贵司还缺前端吗?zhuanlan.zhihu.com图标

前言

本人是个 React 小白,用了快一个月时间 React 了,感觉还挺好用的。最近刚好看到了新的 React Hooks API 貌似讨论挺激烈,就顺便看了一眼,简单说下感觉。

  1. 方便了很多
  2. 更方便组件拆得更小
  3. 更方便把数据、业务逻辑拆到组件外面

基本上就是真香预定了。

不过呢,最让我惊讶的是,我一直以为 React 以及其生态是标榜「纯」的,居然还能把副作用玩那么6,这是我没有想到的。


所以呢,我当然是自己手撸了一个出来:

bramblex/react-hooksgithub.com图标

先讲讲副作用

「纯」是什么,React 系的小伙伴并不会陌生,但与「纯」相对的副作用,很多小伙伴就开始犯迷糊了。为什么会有「纯」与「不纯」的区别,为什么在「函数式」的领域里面,大家会那么在乎纯呢?

其实吧,道理很简单。以一个函数为例,想想我们执行一个函数的过程。

你以为的函数执行过的公式是:

函数 + 参数 = 返回值

但上面这个公式是错误的,只有纯函数的情况下才满足上面公式,一般情况下都是如下公式:

函数 + 参数 + 环境 = 返回值 + 环境'


一个纯函数意味着,这个函数的执行既不会被外部的环境所改变,也无法改变外部的环境。

通过副作用运行时注入状态

副作用这东西,就跟 goto,eval 等等各种规范里面严禁使用的东西一样,具有很强的自由度和很强不可控属性,用的好可以实现各种奇技淫巧,用的不好坑死人不偿命。我们这里讲的就是副作用的奇技淫巧之一,如何通过副作用,给函数注入状态。

我们先抛开 React 独立从一个函数的角度来看这件事情,我们要现在有一个 useState函数和一个render 函数,现在要给 render 函数用 useState 注入状态,让他每一次执行都不一样,怎么做呢?

let state

function useState(defaultState) {
  function setState(newState) {
    state = newState
  }

  if (!state) {
    state = defaultState  
  }
  return [state, setState]
}

function render() {
  const [state, setState] = useState(0)
  console.log(state)
  setState(state + 1)
}

这时候我们只需要在外面存在一个变量,就能记录这个函数的状态了,第一次执行初始化状态,第二次执行就已经有状态了。

尝试多个状态

上面我们实现的函数只能设置一个状态,我们如何设置多个状态,同时还能在函数执行的时候恢复状态呢?

这时候情况稍微就复杂一些了,我们需要多加一个计数器,用于记录多个状态的序列,并且每次执行结束或者执行开始需要把计数器置为0。

useState的顺序是一个序列,我们用一个 key 为 number 的 map 来记录 useState的序列。

最后我们写一个函数包装一下我们具体的render 函数。

let states = {}
let currentNu = 0

function useState(defaultState) {
  const nu = currentNu++

  function setState(newState) {
    states[nu] = newState
  }

  if (!states[nu]) {
    states[nu] = defaultState
  }

  return [states[nu], setState]
}
 
function withState(func) {
  return (...args) => {
    currentNu = 0
    return func(...args)
  }
}

const render = withState(
  function render() {
    const [state, setState] = useState(0)
    const [state1, setState1] = useState(1)
    const [state2, setState2] = useState(2)

    console.log(state, state1, state2)
    setState(state + 1)
    setState1(state1 + 2)
    setState2(state2 + 3)
  }
)



状态堆与上下文栈

一个函数的例子我们解决了,那多个函数怎么办呢?多个函数怎么办呢?多个函数最复杂的情况,是相互调用,那么相互调用就会打乱我们记录的useState顺序,怎么办?

我们先看看函数互相调用为什么就没有打乱计算机里面的状态呢?原因是函数调用的时候会变生一个函数栈,那我们就用函数该有的解决方式——栈来解决问题。

同样的,在函数调用的时候,函数会把状态等东西存在堆空间里,我们也建一个堆来保存状态,这个堆直接用函数闭包存起来就行了。


let contextStack = []

function useState(defaultState) {
  const context = contextStack[contextStack.length - 1]
  const nu = context.nu++
  const { states } = context

  function setState(newState) {
    states[nu] = newState
  }

  if (!states[nu]) {
    states[nu] = defaultState
  }

  return [states[nu], setState]
}
 
function withState(func) {
  const states = {}
  return (...args) => {
    contextStack.push({ nu: 0, states })
    const result = func(...args)
    contextStack.pop()
    return result
  }
}

const render = withState(
  function render() {
    const [state, setState] = useState(0)

    render1()

    console.log('render', state)
    setState(state + 1)
  }
)

const render1 = withState(
  function render1() {
    const [state, setState] = useState(0)

    console.log('render1', state)
    setState(state + 2)
  }
)

这样,我们哪怕递归,都不会搞乱我们程序应有的状态了。


结合React

上面,我已经讲完了实现的基本原理,那么最后就是结合 React 。不过,我们包装函数生成的不再是另一个函数,而是一个 React 组件。因为 React 组件自身能够保存个管理状态,我们可以直接用,避免了我们要手动管理状态的麻烦。这里的React 组件,和之前分配一个堆的意义是一样的。

代码的话就直接看下面吧,我就就不一一写了。

bramblex/react-hooksgithub.com图标

到这里,大家对于给一个函数注入状态的技巧大家学会了吗?

编辑于 2019-10-08

文章被以下专栏收录