手摸手打造一个属于你自己的React Hooks

411 阅读6分钟

互联网寒冬将至,很多大厂招前端不仅对react有要求,而且要精通才可以。本文就手摸手带你从根本上理解React Hooks并写出一个自己的React Hooks。

React Hooks 原则

使用过React Hooks的同学都知道,React建议开发者在使用Hooks的时候遵循以下两个原则:

  1. 只在React函数中使用hooks。
  2. 不要在循环、判断条件或嵌套函数中使用hooks。 第一个原则的意思是,我们要在参与渲染过程的函数中使用hooks,这个应该很简单,但是第二个原则是什么一意思呢?仔细想想?如果在循环、判断条件或者嵌套函数中调用hooks的话会发生什么呢?没错,会导致hooks前后两次调用的顺序不一致。也就是说我们要保证每次渲染,Hooks的调用顺序保持一直才可以。

到这里有同学就想发出疑问了,为什么非要有这两个原则呢?其实这两个原则离不开hooks的原理。

Hooks 的原理

hooks的原理就涉及到渲染过程了,但是渲染过程又不得不提及fiber,提及fiber又要劝退一部分人。但是想要理解hooks的原理的话我认为不必非要提及fiber,接下来我将用不涉及fiber的抽象方式给你讲清楚hooks的工作方式。

拿下面的代码举例

function A() {
  console.log('A render');
  let [target, setTaget] = useState(1);
  if (target % 2 === 0) {
    return (
        <button
          onClick={() => {
            setTaget(target + 1);
          }}
        >
          target+1
        </button>
    );
  }
  return (
    <>
      <button
        onClick={() => {
          setTaget(target + 1);
        }}
      >
        target+1
      </button>
      <B></B>
    </>
  );
}
function B() {
  console.log('B render');
  let [state, setState] = useState(0);
  return (
    <>
      <button
        onClick={() => {
          setState(state + 1);
        }}
      >
        state+1
      </button>
      <div>{state}</div>
    </>
  );
}
ReactDOM.render(<A />, document.getElementById('root'));

这个代码渲染出来是这个样子的:

此时对应的虚拟DOM节点是这样的

当点击target+1按钮时会先把B组件隐藏,再次点击按钮,B组件显示。

点击state+1按钮会使下面的数字+1

然后我们想象一下我们如果进行这样的一系列操作,在hooks层面都做了什么:

1.点击state+1。

当我们点击state+1按钮的时候,我们会再得到一个虚拟DOM树,但是在得到这个虚拟DOM树的过程中,我们就需要一些思考了。

不知道你们心中有没有跟我类似的疑问,我刚开始使用useState的时候,就很郁闷,函数的上下文在运行完毕后就立即被回收了,但是为什么useState可以保存上一次渲染的状态。

其实react是通过给组件节点加上一个数组指针来记录hooks的,当组件在渲染期间,所有调用的hooks都会按照顺序地被记录,并放入这个数组中,这也是为什么我们使用hooks时需要保证hooks调用顺序保持不变,因为一但顺序发生改变,那么hooks内部的引用就会出错。

理解了这个之后接下来我们再看完整的图

这个时候就很好理解了,当我们点击state+1按钮之后,在渲染到组件B时,我们可以通过全局的这么一个指针去找到上一次渲染时hooks保留的状态,这次再按照顺序调用hooks,我们只需要在对应的保留hooks调用记录的数组上按照从头到尾的顺序找一下就好了。

2.点击target+1。

当我们点击target+1的时候,就又会发生一些变化,不过这些变化也又印证了前面讲的原理是对的。

根据代码可以知道点击target+1之后组件B会被从DOM树中移出。此时页面的虚拟DOM结构是这样的。

因为新生成的虚拟DOM树上已经没有有关组件B的内容了,因此真正渲染到真实DOM上自然也就没有了。但是这里会发现,跟随组件消失的还有保留hooks调用记录的数组,因为组件已经被卸载了,保留状态无意义了。

3.点击target+1。

再次点击target+1组件又回来了,但是是一个全新的组件,之前的状态已经无法找回了,因为保存状态的数组也是一个全新的。

此时DOM结构对比是这样的

总结

原生hooks是通过数组形式来按照hooks的调用顺序来保留调用记录和状态的。

Hooks实战

因为我这篇文章的话,目的不是教你怎么用官网的hooks API,学API的话还是去官网看,又有中文版的,多好。我是为了教会大家如何在日常工作中,根据业务逻辑封装自己的Hooks。造起来。

在开始实战前我还是想啰嗦一下,虽然我们前面理解了原理,但是还是要明白,Hooks现在有了什么能力,是个什么位置。

Hooks本质上是一个函数调用,之所以可以封装业务逻辑是因为有了原生Hooks可以记住状态并触发视图更新支持的能力(当然也支持了其他能力,只是这个最适合拿来举例),如果没有这个能力支持,那么我们自己写的Hooks就是一个函数调用,很难调动react的能力。

记录mount时间的hooks

这个简单函数的应用于埋点和行为监控,可以记录我们从页面mount到操作的时间。一般这种计时的逻辑需要在组件挂载逻辑写,有了hooks之后就可以直接把逻辑封装到里面了,这归功于useEffect的神奇魔力。

上代码

import React, { useEffect, useRef } from 'react';

const useInterval = () => {
    const time = useRef(0);
    useEffect(()=>{
        // mount逻辑
        time.current = Date.now()
    },[])

    return Date.now() - time.current
}

这样在一个组件中直接调用useInterval方法就会返回从第一次mount到再次调用的时候的毫秒数,代码很简单,但是可以反映出hooks复用逻辑的潜力。

一个抽象的逻辑复用例子

hooks在面对需要卸载的逻辑的时候,显得更加灵活和方便,我们经常因为一些需要在unmount的时候调用一些钩子而觉得很麻烦。

直接上例子,这次的例子是抽象的伪代码,只是为了让大家理解思想。

假如我们需要订阅websocket,并实时获取websocket推送的第一条信息,那么我们就可以使用hooks的能力进行封装

import React, { useEffect, useRef, useState } from 'react';

const useWebsocket = () => {
    const [data, setData] = useState(null);
    const flag = useRef(null)
    useEffect(()=>{
        // 模拟mount的时候订阅逻辑
        flag.current = subscribe('***',(data)=>{
            // 模拟数据的回调
            setData(data)
        })

        // 模拟卸载的逻辑
        return () =>{
            flag.current.unSubscribe
        }
    })
    return data
}

总结

了解原理是为了更好的使用api,hooks的最大进步就是可以灵活地复用逻辑,越大的项目对于效率的提升越明显。

另外最后加个广告,字节跳动电商部门内推招聘,校招社招都有感兴趣的可以加我微信(zs15931442916)或者扫描下方二维码。

其他部门也可以推,只是给自己部门打个广告。

不投简历也可以加好友,互相聊一聊也挺好,可以帮忙看看简历啥的,有啥学习的问题互相讨论讨论。