测不准

V1

2022/06/19阅读:12主题:红绯

react 学习(15)实现 useState

react 学习(15)实现 useState

前面的文章我们已经了解了 react 的渲染更新机制,包括类组件和函数组件。从本小节开始会介绍下 react 内的 hooks 的实现,毕竟现在所有的新的 react 项目基本都是用 hook 形式开发了,要成长就不能光知道这么写,还得知道为什么这么写,怎么实现的。

useState 特点

  • useState 是函数组件的状态管理器,它会返回一对值:当前状态和一个让你更新它的函数。它类似 class 组件的 this.setState,但是它不会把新的 state 和旧的 state 进行合并,而是直接用新的值替换。
  • useState 唯一的参数就是初始 state

使用

// src/index.js
function App() {
  // 初始化状态
  const [count, setCount] = React.useState(0)
  const handleClick = () => {
    // 修改状态
    setCount(count + 1)
  }
  return <div>
    <p>count: {count}</p>
    <button onClick={handleClick}>+</button>
  </div>
}

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

我们知道 useState 只会初始赋值一次,之后状态就会一直维持;执行状态改变函数,会重新执行函数组件进行渲染,页面显示新的状态。

实现

  • 第一版本
// src/react.js
import { useState } from './react-dom'

// src/react-dom.js
export function useState(initialState) {
  const state = initialState
  function setState(newState) {}
  return [state, setState]
}

这里我们大概实现了 useState 的格式,返回的状态也都对,但是如果每次都是初始值,与实际效果不符。我们需要有个变量存储起来,如果存储的有值,就返回该值,没有的话就用初始值。

引出一个问题,这里用什么存储呢?对于 js 来说,要么对象,要么数组存储。如果用 map,谁当 key 呢?一个组件中可以写多个 useState,也不可能初始值或文件名当 key;所以这里是用了数组进行存储,每执行一个 useState 对应一个索引,对于数组来说索引值是连贯的,这也就解释了为什么不能把 useStat 放到条件判断中,如果时而有该 state 时而没有,那么存储的数组索引对应关系就会乱,导致渲染异常。我们更新状态,实际上就是更新的数组中该 useState 对应的索引值对应的值而已。没理解的朋友可以多读几遍这里再往后看。

  • 第二版
// 记录状态,多次渲染保持不变
let hookStates = []
let hookIndex = 0

functoin useState(initialState) {
  // 解释了为什么第一次初始值,后面再更新就是新的状态值
  hookStates[hookIndex] = hookStates[hookIndex] || initialState
  const currentIndex = hookIndex
  function setState(newState) {
    hookStates[currentIndex] = newState
  }
  return [hookStates[hookIndex++], setState]
}

这里大家可能会有个疑问,currentIndex 是干什么的,为什么不直接用 hookIndex?我们知道 useState 第二个返回值是修改状态的函数,根据上面分析,我们实际上改的是数组中索引值的位置。由于 hookIndex 对应的是全局的所有的状态索引,执行一次就会 ++ 操作指向下一个。而 setState 修改对应的是当前执行 useState 方法对应的索引值,所以这里使用了闭包,currentIndex 与该 setState 与当前索引都是对应且不变的。如果我们这里是用 hookIndex,那么他就是定死的值 1,因为执行了一次 ++ 操作了,也丢失了索引对应关系。

从上面代码可以知道我们的组件状态实际上已经改变了,新问题又来了,页面怎么刷新呢?我们需要执行组件更新的逻辑,让函数重新执行,可以从根节点开始完成 diff 操作,实现重新渲染。

  • 第三版
// 全局方法
let scheduleUpdate

function render() {
  ...
  // 重新赋值
  scheduleUpdate = () => {
    // 这里重新赋值为 0,因为组件会重新执行,索引也需要重新开始,无节制的 ++ 操作,会溢出
    hookIndex = 0
    // 跟节点进行 diff   函数组件会重新执行,这时 hookStates 数组中的状态改变了,所以页面会改变
    compareTwoVdom(container, vdom, vdom)
  }
}

... useState
function setState(newState) {
  hookStates[currentIndex] = newState
  scheduleUpdate() // 触发刷新
}
...

切换到我们自己的库,可以实现同样的效果。大家可能有疑问,为什么 compareTwoVdom 的新旧 vdom 一样,因为我们这里只是改变函数组件的状态而已,为了重新执行渲染新值,对应函数组件的 vdom 是一样的,renderVdom 也会在递归对比中不同重新赋值,这不是 useState 考虑的事情了,我们前面已经完成了。

  • 完整修改代码
let scheduleUpdate
// 记录状态,多次渲染保持不变
let hookStates = []
let hookIndex = 0


// 虚拟dom变成 真实dom,插入到父节点容器
function render(vdom, container) {
  mount(vdom, container) // 可以自行把其他 render 方法换成 mount。否则 scheduleUpdate 会多次赋值,但是也可以忽略
  scheduleUpdate = () => {
    hookIndex = 0
    // 跟节点进行 diff   函数组件会重新执行,这时 hookStates 数组中的状态改变了,所以页面会改变
    compareTwoVdom(container, vdom, vdom)
  }
}
function mount(vdom, container) {
  // 1.
  let newDOM = createDOM(vdom);
  // 2.
  container.appendChild(newDOM);

  // 挂载完成
  if (newDOM.componentDidMount) {
    newDOM.componentDidMount();
  }
}
export function useState(initialState) {
  hookStates[hookIndex] = hookStates[hookIndex] || initialState

  let currentIndex = hookIndex // 备份索引
  // 闭包索引不会变 hookIndex 每次 ++ 会变
  function setState(newState) {
    hookStates[currentIndex] = newState
    scheduleUpdate() // 触发刷新
  }
  return [hookStates[hookIndex++], setState]
}

到这里 useState 我们就实现了,文中可能有些判断不完美,大家可以自行完善,毕竟思路已经有了。如有错误欢迎指正!

分类:

前端

标签:

React.js

作者介绍

测不准
V1