React Hooks 学习 - 05 模拟实现 useState、useEffect、useReducer

实现 useState 钩子函数

useState 示例

function App() {
     
  const [count, setCount] = useState(0)
  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count + 1)}>setCount</button>
    </div>
  )
}

export default App

useState 初始定义

接收一个状态初始值,返回一个数组,数组第一个元素是状态值,第二个元素是修改状态的方法

function useState(initialState) {
     
  let state = initialState
  function setState(newState) {
     
    state = newState
  }
  return [state, setState]
}

function App() {
     
  const [count, setCount] = useState(0)
  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count + 1)}>setCount</button>
    </div>
  )
}

export default App

重新渲染视图

状态变更后重新渲染视图

import ReactDOM from 'react-dom'

function useState(initialState) {
     
  let state = initialState
  function setState(newState) {
     
    console.log(newState)
    state = newState
    // 在状态更改完成后,重新渲染视图
    render()
  }
  return [state, setState]
}

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

function App() {
     
  const [count, setCount] = useState(0)
  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count + 1)}>setCount</button>
    </div>
  )
}

export default App

现在点击按钮数值变成1后,在此点击没有效果,打印结果 newState 一直是1。

这是因为 App 组件每次渲染都会重新执行 useState,重置 state 的值为 0。

在 useState 外部保存状态

为了避免组件每次渲染都重新初始化 state 的值,要将 state 存储在 useState 函数外部,先判断 state 是否有值,如果有则使用当前值,如果没有则设置为初始值。

let state

function useState(initialState) {
     
  state = state ? state : initialState
  function setState(newState) {
     
    console.log(newState)
    state = newState
    // 在状态更改完成后,重新渲染视图
    render()
  }
  return [state, setState]
}

多次调用

function App() {
     
  const [count, setCount] = useState(0)
  const [name, setName] = useState('张三')
  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count + 1)}>setCount</button>
      <br/>
      {
     name}
      <button onClick={
     () => setName('李四')}>setName</button>
    </div>
  )
}

当前显示的 count 和 name 的值都一样,因为一个 state 无法存储多个状态。

将 state 设置为一个数组,按照调用 useState 的顺序存储每次创建的状态值,这样就可以存储多个状态。

设置状态值的方法也同理。

import React from 'react'
import ReactDOM from 'react-dom'

let state = []
let setters = [] // 存储设置状态值的方法
let stateIndex = 0

function createSetter(index) {
     
  return function(newState) {
     
    if (typeof newState === 'function') {
     
      // 如果传入的是回调函数
      state[index] = newState(state[index])
    } else {
     
      state[index] = newState
    }

    // 在状态更改完成后,重新渲染视图
    render()
  }
}

function useState(initialState) {
     
  state[stateIndex] = state[stateIndex] ? state[stateIndex] : initialState

  if (!setters[stateIndex]) {
     
    // 错误:直接保存一个 set 方法,方法中使用 stateIndex 会采用最终的值
    // setters.push(function(newState) {
     
    //   state[stateIndex] = newState
    //   render()
    // })

    // 正确:声明一个函数,采用闭包的方式,保存各自状态的 index
    setters.push(createSetter(stateIndex))
  }


  const value = state[stateIndex]
  const setter = setters[stateIndex]

  stateIndex++

  return [value, setter]
}

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

function App() {
     
  const [count, setCount] = useState(0)
  const [name, setName] = useState('张三')

  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count => count + 1)}>setCount</button>
      <br/>
      {
     name}
      <button onClick={
     () => setName('李四')}>setName</button>
    </div>
  )
}

ReactDOM.render(
  <App />,
  document.getElementById('root')
);

模拟的不足

当前模拟 useState 实现原理的组件直接挂载到 root 元素下,没有嵌套其它组件。若嵌套,state 和 setters 则会出现重复数据。

React 中的 useState 基于 useReducer 实现,返回的数组的第二个元素其实是 dispatch 方法。

useState 调用时会创建一个 hook 对象并挂载到节点 Fiber 对象中,其中包括:

  • state:缓存的上一次 state 和当前 state
  • queue:dispatch 触发队列,保证多个触发 dispatch 的任务
  • next:hook 链,保证多次调用 useState 的定位

实现 useEffect 钩子函数

单次调用

import ReactDOM from 'react-dom'

/* ---------- useState ---------- */
let state = []
let setters = [] // 存储设置状态值的方法
let stateIndex = 0

function createSetter(index) {
     ...}

function useState(initialState) {
     ...}

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

/* ---------- useEffect ---------- */
// 上一次的依赖值
let prevDepsAry = []

function useEffect(callback, depsAry) {
     
  // 判断 callback 是不是函数
  if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 函数的第一个参数必须是函数')

  // 判断 depsAry 有没有被传递
  if (typeof depsAry === 'undefined') {
     
    // 没有传递
    callback()
  } else {
     
    // 判断 depsAry 是不是数组
    if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 函数的第二个参数必须是数组')

    // 将当前的依赖值和上一次的依赖值作对比 如果有变化 调用 callback
    const hasChange = depsAry.every((dep, index) => dep !== prevDepsAry[index])
    
    // 判断值是否有变化
    if (hasChange) {
     
      callback()
    }

    // 同步依赖值
    prevDepsAry = depsAry
  }
}

function App() {
     
  const [count, setCount] = useState(0)
  const [name, setName] = useState('张三')

  useEffect(() => {
     
    console.log('Hello')
  }, [count])

  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count => count + 1)}>setCount</button>
      <br/>
      {
     name}
      <button onClick={
     () => setName('李四')}>setName</button>
    </div>
  )
}

export default App

多次调用

import ReactDOM from 'react-dom'

/* ---------- useState ---------- */
let state = []
let setters = [] // 存储设置状态值的方法
let stateIndex = 0

function createSetter(index) {
     ...}

function useState(initialState) {
     ...}

function render() {
     
  // 重置 index
  stateIndex = 0
  effectIndex = 0

  ReactDOM.render(<App />, document.getElementById('root'))
}

/* ---------- useEffect ---------- */
// 上一次的依赖值
let prevDepsAry = []
let effectIndex = 0

function useEffect(callback, depsAry) {
     
  // 判断 callback 是不是函数
  if (Object.prototype.toString.call(callback) !== '[object Function]') throw new Error('useEffect 函数的第一个参数必须是函数')

  // 判断 depsAry 有没有被传递
  if (typeof depsAry === 'undefined') {
     
    // 没有传递
    callback()
  } else {
     
    // 判断 depsAry 是不是数组
    if (Object.prototype.toString.call(depsAry) !== '[object Array]') throw new Error('useEffect 函数的第二个参数必须是数组')

    // 获取上一次的依赖值
    const prevDeps = prevDepsAry[effectIndex]

    // 判断上一次的依赖值是否存在 如果不存在 调用 callback
    // 将当前的依赖值和上一次的依赖值作对比 如果有变化 调用 callback
    const hasChange = prevDeps ? depsAry.every((dep, index) => dep !== prevDeps[index]) : true
    // 判断值是否有变化
    if (hasChange) {
     
      callback()
    }

    // 同步依赖值
    prevDepsAry[effectIndex] = depsAry

    effectIndex++
  }
}

function App() {
     
  const [count, setCount] = useState(0)
  const [name, setName] = useState('张三')

  useEffect(() => {
     
    console.log('Hello')
  }, [count])

  useEffect(() => {
     
    console.log('World')
  }, [name])

  return (
    <div>
      {
     count}
      <button onClick={
     () => setCount(count => count + 1)}>setCount</button>
      <br/>
      {
     name}
      <button onClick={
     () => setName('李四')}>setName</button>
    </div>
  )
}

export default App

实现 useReducer 钩子函数

import {
      useState } from 'react'

/* ---------- useReducer ---------- */

function useReducer(reducer, initialState) {
     
  const [state, setState] = useState(initialState)

  function dispatch(action) {
     
    const newState = reducer(state, action)
    setState(newState)
  }

  return [state, dispatch]
}

function App() {
     
  function reducer(state, action) {
     
    switch (action.type) {
     
      case 'increment':
        return state + 1
      case 'decrement':
        return state - 1
      default:
        return state
    }
  }
  const [count, dispatch] = useReducer(reducer, 0)

  return (
    <div>
      {
     count}
      <button onClick={
     () => dispatch({
     type: 'increment'})}>+1</button>
      <button onClick={
     () => dispatch({
     type: 'decrement'})}>-1</button>
    </div>
  )
}

export default App

你可能感兴趣的:(react)