React Hooks 之 useState 浅谈

222 阅读7分钟

一、Hooks的基本理念

1、理念的提出

Sebastian Markbåge 首先提出了Hooks的理念,由Team成员落地。此人主修心理学

2、Hoos的理念

  • Hoos践行了“代数效应”的思想:在函数式编程中把副作用从函数调用中剥离出去。

  • 代数效应的本质就是在正常的程序流程中,允许我们停下来,去做另外一件事,做完之后,我们可以再从被打断的地方继续往下执行,而另外的那件事,可以是同步的也可以是异步的,它的执行过程与我们当前的流程无关,我们只关心它的结果。

  • 而useState就是利用了代数效应的思想,函数组件不能拥有状态,也不需要管理状态,当通过useState定义一个状态的时候,相当于perform语法,终止程序,当React处理完这个状态的时候,再恢复程序。将副作用从函数组件中剥离。

二、什么是Hooks

  • React官方一直都提倡使用函数组件,但是有时候需要使用state或者其他一些功能时,只能使用类组件,因为函数组件没有实例,也没有生命周期,只有类组件有

  • Hooks是React16.8新增的特性,它可以让你在函数组件中使用state以及其他的React特性。

  • 在React中,凡是以use开头的API都是Hooks

三、常用的Hook

1、useState

  • 一个钩子,相当于可以让你“钩入”React的特性。

  • useState是一个允许在函数组件中添加state的Hook

用法:

const [count, setCount] = useState(initailState)

count是定义的变量,setCount是更改这个变量的方法,更改可以引起DOM的重新渲染。

initailState为初始值,可以是一个变量也可以是一个函数,这个函数需要返回一个变量。

注意:useState不能在条件语句中声明

2、useEffect

这是一个可以在函数组件中执行副作用的Hook

用法:

useEffect(fun, deps)
  • fun接收一个函数,这个函数可能在不同的生命周期被调用

  • deps如果为undefined,那么useEffect会在组件挂载后以及更新后被调用

  • deps如果为空数组[],那么useEffect只会在组件挂载后被调用

  • deps如果为包含state的数组,那么useEffect会在该state改变时被调用

四、useState简易版实现

let isMount = true // 是否是首次渲染
let workInProgressHook = null // 存储当前组件的hook链表

// 每一个函数组件对应一个fiber
const fiber = {
  stateNode: App, // 对应组件本身
  memoizedState: null, // 保存相应的hook数据,这是一个链表的头指针
  // updateQueue: null, // 存放useEffect的effect
}

function mountWorkInProgressHook () {
  const hook = {
    memoizedState: null, // hook的数据存在这里
    next: null, // 指向下一个hook
    queue: { // 创建一个新的链表来更新队列,用来存放更新(setXXX())
      pending: null // 最近一个等待执行的更新
    },
  }

  // workInProgressHook 指向当前组件的Hook链表
  if (!workInProgressHook) {
    // 如果当前组件的Hook链表为空,那么就将刚刚新建的Hook作为Hook链表的第一个节点
    fiber.memoizedState = workInProgressHook = hook
  } else {
    // 如果不为空,则将新建的hook添加到链表的下一个,即next指向
    workInProgressHook = workInProgressHook.next = hook
  }

  return workInProgressHook
}

function useState (initialState) {
  let hook

  if (isMount) {
    // 首次渲染
    hook = mountWorkInProgressHook()
    hook.memoizedState = initialState
  } else {
    hook = workInProgressHook
    workInProgressHook = workInProgressHook.next
  }

  let baseState = hook.memoizedState // 上一次数据
  if (hook.queue.pending) {
    let firstUpdate = hook.queue.pending.next // 第一个update

    // 遍历环装链表 计算新的状态
    do {
      let action = firstUpdate.action
      action = typeof action === 'function' ? action(baseState) : action
      baseState = action
      firstUpdate = firstUpdate.next
    } while (firstUpdate !== hook.queue.pending.next)

    hook.queue.pending = null
  }

  hook.memoizedState = baseState // 更新hook的值

  return [baseState, dispatchAction.bind(null, hook.queue)]
}

function dispatchAction (queue, action) {
  // 为当前更新操作新建update对象
  const update = {
    action,
    next: null
  }

  const pending = queue.pending
  if (pending === null) {
    // 如果更新队列为空,那么将当前的更新作为更新队列的第一个节点,并且指向自身,以保证是一个循环链表
    update.next = update
  } else {
    // 如果队列不为空,那么就将当前的更新对象插入到列表的头部
    update.next = pending.next
    // 链表的尾结点指向最新的头结点,以保持为一个循环链表
    pending.next = update
    // update0 -> update0
    // update0 -> update1 -> update0
    // update0 -> update1 -> update2 -> update0
    // queue.pending -> update2(最新的更新操作)
  }

  queue.pending = update
  // schedule()
  setTimeout(() => {
    schedule()
  }, 0) // 触发一次更新
}

function schedule () {
  workInProgressHook = fiber.memoizedState
  const app = fiber.stateNode() // 触发了组件的 render
  isMount = false // 首次渲染之后就改成成update状态
  return app
}

function App () {
  // 如果多个useState的话,必须按照固定的顺序声明,不能写在条件语句中,因为条件会变化,会导致hook的顺序改变
  const [num, setNum] = useState(0)
  const [num1, setNum1] = useState(0)

  console.log('isMount?', isMount)
  console.log('num:', num)
  console.log('num1:', num1)

  return {
    onClick () {
      setNum(x => x + 1)
      setNum1(num => num + 10)
    }
  }
}

window.app = schedule() // 首次渲染

useState实现原理:

  1. 首先调用App()函数,调用第一个useState方法,进入useState函数体。
  2. 因为是首次渲染,所以创建一个新的hook。
  3. 判断workInProgressHook(当前组件所有hook链表的头指针)是否为null,如果为null,则将新创建的hook作为第一个节点,此时workInProgressHook指向第一个hook,并且fiber的memoizedState也指向第一个hook。
  4. 然后进行数据更新(其实源码中并不会在这里进行状态更新,源码中会分为mountState和updateState)。
  5. 首次使用useState,hook中的queue队列还是空的,所以不做数据更新。
  6. 返回一个数组,第一项是数据值,第二项是更新该数据的action。
  7. 调用第二个useState方法,同样创建一个新的hook。
  8. 但此时workInProgressHook不为null,它指向的是上一个hook,所以,将workInProgressHook的next指针指向新的hook,也就是上一个hook的next指向新的hook,然后将workInProgressHook也就是头指针指向新的hook。
  9. 同样不需要更新数据,并且返回[baseState, dispatchAction.bind(null, hook.queue)]。
  10. 此时将isMount改为false。

上面的过程为函数组件挂载时发生的事

  1. 此时调用app.onClick(),触发了第一个hook的dispatchAction,其中的action是新的值。
  2. 进入dispatchAction函数,首先创建一个update对象,其中包含action和next指向。
  3. 然后判断这个hook的pending,也就是更新操作链表是不是空的,如果是空的,那么就将这个update对象指向自己,形成一个循环链表,并且让queue.pending(头指针,指向当前执行的更新操作)指向这个update。
  4. 将更新操作添加到链表后,执行一次组件渲染,执行schedule()。
  5. 在schedule中,将fiber中的链表指针指向workInProgressHook,此时为第一个hook,然后执行App()函数,也就是fiber中的指针指向的永远是第一个hook。
  6. 在App函数中,执行第一个useState,此时isMount为false,所以为更新数据。
  7. 将hook指向workInProgressHook也就是第一个hook,(所以这里一定不能将useState放在条件语句中,如果这里第一个执行的不是第一个hook,那么数据将会出错)。
  8. 然后workInProgressHook指向下一个hook。
  9. 开始更新数据,将当前hook中的数据给baseState保存,然后判断该数据是否需要更新,即判断hook的链表中是否有update,如果没有,则还将baseState赋值给hook。
  10. 如果链表中有操作的update,则让保存第一个操作指针给firstUpdate,接下来循环这些操作。
  11. 首先拿到操作需要更新的action,这个action支持单纯的值或者一个函数。
  12. 然后将新值赋给baseState,然后查看下一个指向的操作是不是自己,如果是则结束循环,如果还有操作,那么则进行值的覆盖。
  13. 更新完值后,对操作的循环列表清空。
  14. 最后更新hook中的数据,并将该数据返回。
  15. 第二个hook同理,也将数据返回给到App组件中的变量并且按需渲染DOM。

挂载的时候第一次执行:

调用app.onClick():

onClick中进行了两次set,在这个简易版的useState中会进行两次渲染,而React中进行多次set,会整合在一起执行一次渲染,提升性能。

完~