从实现到理解 React Hook 使用规则

写在前面html

阅读hook的官方文档 能够看到它的一些使用限制react

平时开发的时候,咱们会遵循这些使用规则,可是一般咱们不太理解其中的缘由,当咱们尝试在循环或者条件语句中调用hook的时候, 浏览器就会抛出相似的错误。git

接下来 ,让咱们尝试手动实现如下useState和useEffect这两个Hook。经过理解其设计原则来理解其使用规则.github

useState实现数组

参考以下计数器浏览器

setState须要返回一个[变量,函数]元组,而且在每次 调用函数时,自动调用render方法更新视图bash

function App() {
  const [num, setNum] = useState < number > 0;

  return (
    <div>
      <div>num: {num}</div>
      <button onClick={() => setNum(num + 1)}>+ 1</button>
    </div>
  );
}

复制代码
// Example 1 这么作是有bug的!
function useState(initialValue) {
    var _val = initialValue
    function setState(newVal) {
      _val = newVal
      render()
    }
    return [_val, setState] // 直接对外暴露_val
  }
  var [foo, setFoo] = useState(0)
  console.log(foo) // logs 0 不须要进行函数调用
  setFoo(1) // 在useState做用域内给_val赋值
  console.log(foo) // logs 0 - 糟糕!!
复制代码

这里实现的setState每次更新state后,并不能拿到最新的state,由于组件内部的state值,已经state的值等于解构赋值时的初始值,后续没有从新赋值给解构出来的变量,所以不会更新。闭包

为了达到更新state的目的,而这个state又必须是一个变量而不是一个函数,咱们考虑把useState自己放在一个闭包中。函数

// Example 2
const MyReact = (function() {
    let _val // 将咱们的状态保持在模块做用域中
    return {
      render(Component) {
        const comp = Component()
        comp.render()
        return comp
      },
      useState(initialValue) {
        _val = _val || initialValue // 每次运行都从新赋值
        function setState(newVal) {
          _val = newVal
        }
        return [_val, setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      return {
          click : ()=>{b(a+1)},
          render:()=> {console.log(a)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
复制代码

MyReact是容许用来渲染react组件的一个对象,当经过setState更新状态后,每次渲染都会从新执行MyReact.useState ,以此拿到MyReact闭包内的最新数据。更新state到视图oop

此时咱们的state只支持一个usestate 咱们经过将存放状态的变量_val改形成数组和使用当前所在的usestate索引curr使其支持多个useState

// Example 3
const MyReact = (function() {
    let _val = [] // 将咱们的状态保持在模块做用域中
    let curr = 0
    return {
      render(Component) {
      // 每次更新视图都须要重置curr
        curr = 0
        const comp = Component()
        comp.render()
        return comp
      },
      useState(initialValue) {
      // 注意这里须要保存curr到本地变量currenCursor,不然当用户setState时,curr已经不是咱们所须要的值
        const currenCursor = curr;
        _val[currenCursor] = _val[currenCursor] || initialValue; // 检查是否渲染过
        function setState(newVal) {
          _val[currenCursor] = newVal
        }
        ++curr; 
        return [_val[currenCursor], setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      var [c,d] =  MyReact.useState(6)
      return {
          click : ()=>{b(a+1),d(c+1)},
          render:()=> {alert(a + '+' +c)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
复制代码

至此 这个功能更像React中的useState Hook了

那么如何解释,不能再循环或者条件语句中使用useState呢

假设有以下代码

let tag = true;

function App() {
  const [num, setNum] = useState < number > 0;

  // 只有初次渲染,才执行
  if (tag) {
    const [unusedNum] = useState < number > 1;
    tag = false;
  }

  const [num2, setNum2] = useState < number > 2;

  return (
    <div>
      <div>num: {num}</div>
      <div>
        <button onClick={() => setNum(num + 1)}>加 1</button>
        <button onClick={() => setNum(num - 1)}>减 1</button>
      </div>
      <hr />
      <div>num2: {num2}</div>
      <div>
        <button onClick={() => setNum2(num2 * 2)}>扩大一倍</button>
        <button onClick={() => setNum2(num2 / 2)}>缩小一倍</button>
      </div>
    </div>
  );
}
复制代码

第一次循环时 拿到的state&对应的cursor以下

state - setState中的cursor

num -0

unusedNum - 1

num2 - 2

第二次循环时得到的对应关系以下

state - setState中的cursor

num - 0

num2 - 1

此时的setNum2指向的对象不正确,天然update后返回的state也不正确。

实际上,react使用的是单向链表维护hook的前后顺序和内容,但缘由是一致的,咱们能够看看react实际源码

ReactFiberHooks.js中定义了初始化时, firstWorkInProgressHook 和 workInProgressHook这两个全局变量,观察全部的hook实现

firstWorkInProgressHook指向第一个hook,workInProgressHook 指向当前正在处理的hook

每一个hook的结构能够看开头的Hook type定义

function mountWorkInProgressHook(): Hook {
  const hook: Hook = {
    memoizedState: null,

    baseState: null,
    queue: null,
    baseUpdate: null,

    next: null,
  };

  if (workInProgressHook === null) {
    // This is the first hook in the list
    firstWorkInProgressHook = workInProgressHook = hook;
  } else {
    // Append to the end of the list
    workInProgressHook = workInProgressHook.next = hook;
  }
  return workInProgressHook;
}
复制代码

考虑下面的代码

let condition = true;
const [state1,setState1] = useState(0);
if(condition){
    const [state2,setState2] = useState(1);
    condition = false;
}
const [state3,setState3] = useState(2);
复制代码

初始化时 执行了如下流程

  • 初始时,组件还未渲染时,firstWorkInProgressHook = workInProgressHook = null;
  • firstWorkInProgressHook = workInProgressHook = Hook{state1}
  • firstWorkInProgressHook = workInProgressHook = Hook{state2}
  • firstWorkInProgressHook = workInProgressHook = Hook{state3}

能够用下图理解这个结构

memoizedState 存储当前Hook的结果,next指向下一个hook,每一个hook对象中保存了当前hook的状态 ,和指向下一个hook的属性。

第二次渲染时,condition不符合,只剩下两个hook 渲染调用update方法,update方法对每一个hook分别执行了updateWorkInProgressHook()

function updateWorkInProgressHook(): Hook {
  if (nextWorkInProgressHook !== null) {
    // There's already a work-in-progress. Reuse it. workInProgressHook = nextWorkInProgressHook; nextWorkInProgressHook = workInProgressHook.next; currentHook = nextCurrentHook; nextCurrentHook = currentHook !== null ? currentHook.next : null; } else { // Clone from the current hook. invariant( nextCurrentHook !== null, 'Rendered more hooks than during the previous render.', ); currentHook = nextCurrentHook; const newHook: Hook = { memoizedState: currentHook.memoizedState, baseState: currentHook.baseState, queue: currentHook.queue, baseUpdate: currentHook.baseUpdate, next: null, }; if (workInProgressHook === null) { // This is the first hook in the list. workInProgressHook = firstWorkInProgressHook = newHook; } else { // Append to the end of the list. workInProgressHook = workInProgressHook.next = newHook; } nextCurrentHook = currentHook.next; } return workInProgressHook; } 复制代码

currentHook 指向当前代码中正在处理的hook,

workInProgressHook 指向以前维护的链表中对应的当前正在处理的hook

firstWorkInProgressHook 对应维护的链表结构对应的hook头

更新时 第一个currentHook指向的next为原先的第三个hook,而且赋值给链表结构中的第二个workInProgressHook对象,致使返回的state和setstate对象都是错误的!

useEffect实现

useEffect 称为组件的反作用,他相对于直接将逻辑写在函数组件顶层的优势是,能够在依赖未更新的时候不重复执行。

在Example3 的基础上加上useeffect的部分

_val 中 useEffect对应的内容为传入的依赖数组

每次执行时 对比以前的依赖和如今的值是否不一样,而且更新依赖值,

若是更新 执行callback,而且更新依赖的最新值,不然什么都不作

// Example 4
const MyReact = (function() {
    let _val = [] // 将咱们的状态保持在模块做用域中
    let curr = 0
    return {
      render(Component) {
        curr = 0
        const comp = Component()
        comp.render()
        return comp
      },
      // + effect部分
      useEffect(callback, depArray) {
        const currenCursor = curr;
        const hasNoDeps = !depArray
        const deps = _val[currenCursor] // type: array | undefined
        const hasChangedDeps = deps ? !depArray.every((el, i) => el === deps[i]) : true
        if (hasNoDeps || hasChangedDeps) {
          callback()
          _val[currenCursor] = depArray
        }
        curr++ // 本hook运行结束
      },
      // - effect部分
      useState(initialValue) {
        const currenCursor = curr;
        _val[currenCursor] = _val[currenCursor] || initialValue; // 检查是否渲染过
        function setState(newVal) {
          _val[currenCursor] = newVal
        }
        ++curr; 
        return [_val[currenCursor], setState]
      }
    }
  })()
  function Counter(){
      var [a,b] =  MyReact.useState(0)
      MyReact.useEffect(() => {
        alert('effect:' +  a)
      }, [a])
      return {
          // 触发effect
          click : ()=>{b(a+1)},
          // 不触发effect
          noop: () => b(a),
          render:()=> {alert(a)}
      }
  }
  let App = MyReact.render(Counter)
  App.click()
  App = MyReact.render(Counter)
  App.noop()
  App = MyReact.render(Counter)
复制代码

以上的逻辑解释了,为何useeffect在不传参数的时候,会在每次rerender时执行,而在传[]时,只在初始化时执行一次。

总结

  • 本文使用数组 + cursor维护多个hooks的状态,使用模块闭包实现状态值的更新。
  • 文章中使用的demo是简化版的实现,而且在实际react中使用的是单向链表维护的hooks,demo为最简单实现,方便理解,useState 和 useEffect 特性洗
  • 欢迎批评 欢迎指正 🐶

参考文章

www.netlify.com/blog/2019/0…

github.com/SunShinewyf…

做者

做者主页

相关文章
相关标签/搜索