这一次请记住React Hook

这一次请记住React Hook

技术杂谈小彩虹2021-08-25 20:44:07170A+A-

因工作经历的关系,到目前为止自己在react开发的经验上面不是很足,在接触到react hook的当下,关于react hook书面上表达的那些优势没有切身的感受和实际的体会,一直没有认真体会react hook“真香”在哪里?这篇文章我会试着从自身不多的理解和经验上去剖析react hook的优势,并在实践中去验证这些,以及什么场景下适合使用hook。

一般介绍一个新的东西(知识),分三个部分:是什么?为什么?怎么用?

React Hook是什么?

官方解释:Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。

翻译一下:从现在开始,react函数式组件不再是无状态组件,你可以通过hook直接让函数式组件拥有class组件的能力。

为什么要用React Hook?

看官方的意思:不是替代class组件吧,也至少是想引导用户把函数式的组件写起来。那么为什么要往这个方面引导呢?

class组件有什么问题?

  • 随时间推移,组件臃肿难以维护和复用
  • 声明周期里的函数比较乱
  • class的特殊书写形式(this绑定)
  • class编译会变大,性能不好,热加载不稳定
  • class自身复杂度高

其中复用是最棘手的问题,也是hook出现的最根本的原因。

没有hook的时候,我们用什么办法实现class组件的复用?

  • 属性渲染
  • 高阶组件

这两种方式都受限于class组件的本身问题,也不能根本解决提升组件复用性的问题,只是提供了复用的手段。

函数式组件结合hook的方式,让组件可以有自己的状态,并且可以高复用,页面里class组件减少也会更好维护。

怎么就体现了好的复用性呢?具体的优势有哪些?

  • 函数组件没有生命周期
  • 不存在特殊的书写方式
  • hook的注入让其拥有class组件的能力,但同时又有简洁的书写方式
  • 复用的本质是那些封装起来的hook能力,例如自定义的hook,那是一部分独立的功能组件

仔细回味一下,在实际开发中是不是碰到过不得已将函数式组件更改为class组件,从而进入到了难为维护和复用的世界里。

怎么使用React Hook?

常规用法

先看一个例子,就不从一个简单的使用api开始了,直接看封装Hook的玩法

const useUserList = () => {
    const [loading, setLoading] = useState(false);
  const [users, setUsers] = useState([]);
  const loadUsers = async params => {
    setLoading(true);
    setUsers([]);
    const users = await loadUsers('/request', params);
    setUsers(users);
    setLoading(false);
  };
  const addUsers = useCallback(
    user => setUsers(users => users.concat(user)),
    []
  );
  const deleteUsers = useCallback(
    use => setUsers(users => without(users, user)),
    []
  );
  return [users, {loading, loadUsers, addUsers, deleteUsers}];
}

上面是一段简单的封装hook操作,业务逻辑中经常会使用到。

先体会一下这个hook的功能:

  • 提供用户列表
  • 提供加载、添加和删除操作
  • 还有加载状态,这可能影响页面的行为

这是在干什么?管理状态和关联的行为,是状态和行为的封装,这是复用的本质,未来只要用到用户方面的数据和操作复用这个hook好了。所以看到这里,复用的不是UI,而是能力。

如何合理使用

当然上面的书写方式对我来说还没能感受到“真香”的现场,只是换了一个方式抽离逻辑的书写,那么试着分析看看上面的例子,在我看来,更加合理的使用是让hook发挥其更强的复用性价值,如果做到这一点那么Hook才是最有意义的。看看下面的硬核现场分析。

从上面这个例子看,封装是状态和关联的行为的打包,那么可以理解hook就是“给我一些变量和方法,我给你封装一个hook”。从最底层通用的角度翻译成代码如下:

export const useMethods = (initialValue, methods) => {
  const [value, setValue] = useState(initialValue);
  const boundMethods = useMemo(
    () => Object.entries(methods).reduce(
        (methods, [name, fn]) => {
        const method = (...args) => {
          setValue(value => fn(value, ...args));
        };
        methods[name] = method;
        return methods;
      },
      {}
    ),
    [methods]
  )
  return [value, boundMethods];
}

上述只是最底层的hook封装办法,想要解决业务逻辑还需要其他通用Hook来调用。

既然Hook是解决状态和关联行为的复用能力,那么从状态和行为两个方面下手:状态是变量,变量是简单的数据结构,而行为其实是过程,是一些更为通用Hook和方法的组合。

拿例子里用到的User数组类型来对数据结构进行封装,看代码

const arrayMethods = {
    push(list, item) {
    return list.concat(item)
  },
  pop(list) {
    return list.slice(0, -1);
  },
  slice(list, start, end) {
    return list.slice(start, end);
  }
  ...
}
export const useArray = (initialValue = []) => {
    assert(Array.isArray(initialValue), 'initialValue must be an array');
  return useMethods(initialValue, arrayMethods);
}

再来看看行为的封装,拿上面的例子的请求数据进行封装

const useTaskPending = task => {
  const [pending, setPending] = useState(false);
  const taskWithPending = useCallback(
    async (...args) => {
        setPending(true);
      const result = await task(...args);
      setPending(false);
      return result;
    }, 
    [task, setPending]
  );
  
  return [taskWithPending, pending];
}

除了过程的封装,还要针对结果进行状态同步管理

const useTaskPendingState = (task, state) => {
    const [taskWithPending, pending] = useTaskPending(task);
  const callAndStore = useCallback(
    async () => {
      const result = await taskWithPending();
      state(result);
    },
    [taskWithPending, state]
  )
  return [callAndStore, pending];
}

现在我们将上述例子里的hook过程经过数据结构和过程封装以后,代码修改成如下的结果:

const useUserList = () => {
    const [users, {push, pop, slice}] = useArray([]);
  const [load, pending] = useTaskPendingState(getListUser, state);
  
  return [users, {pending, load, addUser: push}]
}

再看看上述的表达,极简到震惊脸。而且useArray和useTaskPendingState可以重复被使用,越是通用底层的Hook,越是复用性强。

品到这里,可以深刻感受到hook的“香”在哪里,只要你理解到Hook解决的问题是什么?本质是复用什么?针对这个复用我们可以极致到什么程度?

其实目前业务代码里不太可能按照上述分析的方式进行书写,但是可以通过分析感受到Hook的使用原则,以及面对工作里业务逻辑怎么思考Hook的使用和封装。现阶段,能够写出第一个例子的情况已经很优秀了。

Hook的实现原理

hook的使用上面有两个原则

  • 不要再循环、条件判断和嵌套函数里使用
  • 只在react的函数式组件里使用

原理部分拿useState举例:状态管理都是关于数组

先看useState怎么工作的?

function renderFunction() {
    const [firstName, setFirstName] = useState('qingming');
  const [lastName, setLastName] = useState('dong');
  
  return (
    <Button onClick={() => setFirstName('damu')}>SET</Button>
  )
}

代码逻辑不解释,那么React怎么去工作的呢?注意上面的黄色文字,模拟实现步骤:

  • 创建两个空数组,state一个,setter一个,一个下标变量cursor
  • 第一次渲染
    • cursor = 0 state = ['first'] setters = [setFirstName]
    • cursor = 1 state = ['first', 'last'] setters = [setFirstName, setLastName]
  • 第二次渲染(第N次渲染)每一次重新渲染cursor都会重置为0
    • cursor = 0 state = ['first'] setters = [setFirstName]
    • cursor = 1 state = ['first', 'last'] setters = [setFirstName, setLastName]
  • 事件处理
    • setFirstName('damu')
    • 找到setter里对应的方法,记录当前的cursor
    • 找到state里对应cursor的状态值
    • 替换state里对应位置的值

useState底层实现大概可以如下:

let state = []
let setters = []
let cursor = 0
let firstRun = true
// 实现cursor与setter关联,调用方法直接找到cursor对象的state进行修改
function createSetter(cursor) {
    return function setterWithCursor(newVal) {
    state[cursor] = newVal
  }
}
export function useState(initValue){
  // 初次渲染进行数组的更新,其他情况下不需要操作数组
    if (firstRun) {
    state.push(initValue)
    setters.push(createSetter(cursor))
    firstRun = false
  }
  
  const value = state(cursor)
  const setter = setters(cursor)
  
  cursor++
  return [value, setter];
}

最后解释一下hook为什么不能再循环、条件语句和函数嵌套里使用。拿条件语句来看

let firstRender = true;
function RenderFunctionComponent() {
  let initName;
  
  if(firstRender){
    [initName] = useState("qingming");
    firstRender = false;
  }
  const [firstName, setFirstName] = useState(initName);
  const [lastName, setLastName] = useState("dong");
  return (
    <Button onClick={() => setFirstName("damu")}>Fred</Button>
  );
}

将上面的代码逻辑,使用useState底层实现来翻译一遍是这样的:

  • 第一次执行
    • cursor = 0 state = ['first'] setters = [setter_0]
    • cursor = 1 state = ['first', 'first'] setters = [setter_0, setFirstName]
    • cursor = 2 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]
  • 第二次执行,不再执行条件语句,直接执行两次初始化
    • cursor = 0 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]
    • cursor = 1 state = ['first', 'first', 'last'] setters = [setter_0, setFirstName, setLastName]

此时我们的firstName = lastName = 'first',而我们的代码意思确实firstName = 'first' !== lastName = 'last'。

由此可见,数组管理的严谨性,为什么不灵活管理这些下标呢?可能有性能问题吗?看未来会不会变化? 相信梳理到这里,针对hook你应该理解了,更多Hook api和实现底层快去实践和学习吧。

点击这里复制本文地址 以上内容由权冠洲的博客整理呈现,请务必在转载分享时注明本文地址!如对内容有疑问,请联系我们,谢谢!

支持Ctrl+Enter提交

联系我们| 本站介绍| 留言建议 | 交换友链 | 域名展示
本站资源来自互联网收集,仅供用于学习和交流,请遵循相关法律法规,本站一切资源不代表本站立场,如有侵权、后门、不妥请联系本站删除

权冠洲的博客 © All Rights Reserved.  Copyright quanguanzhou.top All Rights Reserved
苏公网安备 32030302000848号   苏ICP备20033101号-1
本网站由 提供CDN/云存储服务

联系我们