使用React Hook后的一些体会

rock-roll 2019-06-26 原文

使用React Hook后的一些体会

一、前言

距离React Hook发布已经有一段时间了,笔者在之前也一直在等待机会来尝试一下Hook,这个尝试不是像文档中介绍的可以先在已有项目中的小组件和新组件上尝试,而是尝试用Hook的方式构建整个项目,正好新的存储项目启动了,需要一个新的基于web的B/S管理系统,机会来了。在项目未进入正式开发前的时间里,笔者和小伙伴们对官方的Hook和Dan以及其他优秀开发者的关于Hook的文档和文章都过了至少一遍,当时的感觉就是:之前学的又没用了,新的一套又来了。目前这个项目已经成功搭起来了,主要组件和业务已具规模,UT也对应完成了。是时候写一下对Hook使用后的初步体会了,在这里,笔者不会做太多太深入的Hook API和原理讲解,因为很多其他优秀的文章可以已经讲得足够多了。再者因为虽然重构了项目,但代码组织方式可能还不是最Hook的方式。本文内容大多为笔者认为使用Hook最需要明白的地方。

 

二、怎么替代之前的生命周期方法?

这个问题在笔者粗略地过了一遍Hook的API后自然而然地产生了,因为毕竟大多数关注Hook新特性的开发者们,都是从生命周期的开发方式方式过来的,从 createClass 到ES2015的 class ,再到Hook。很少有人是从Hook出来才使用React的。这也就是说,大家在使用初期,都会首先用生命周期的思维模式来探究Hook的使用,就像我们对英语没熟练使用之前,英文对话都是先在心里准备出中文语句,在心里翻译出英文语句再说出来。笔者已有3年的生命周期方式的开发经验,惯性的思维改变起来最为困难。

笔者在之前使用生命周期的方式开发组件时,使用最多的、对要实现业务最依赖的生命周期是 componentDidMount 、 componentWillReceiveProps 、 shouldComponentUpdate 。

对于 componentDidMount 的替代方式很简单: useEffect(() => {/* code */}, []); ,使用 useEffect hook,依赖给空数组就行,空数组在这里表示有依赖的存在,但依赖实际上又为空,会是这个hook在初次render完成的时候调用一次足矣。如果有需要在组件卸载的生命周期内 componentWillUnmount 干的事情,只需要在 useEffect 内部返回一个函数,并在这个函数内部做这些事情即可。但要记住的时候,考虑到函数的Capture Value的特性,对值的获取等情况与生命周期方法的表现并非完全一致。

对于 componentWillReceiveProps 这个生命周期。首先这里说说笔者自己的历史原因。在React16.3版本以后,生命周期API被大幅修改,16.4又在16.3上改了一把,为了后期的Async Render的出现,原有的 componentWillReceiveProps 被预先重命名为unsafe方法,并引入了 getDerivedStateFromPorps 的静态方法,为了不重构项目,笔者把React和对应打包工具都停留在了16.2和适配16.2的版本。现有的Hook文档也忽略了怎么替代 componentWillReceiveProps 。其实这个生命周期的替代方式最为简单,因为像 useEffect 、 useCallback 、 useMemo 等hook都可以指定依赖,当依赖变化后,回调函数会重新执行,或者返回一个根据依赖产生的新的函数,或者返回一个根据依赖产生的新的值。

对于 shouldComponentUpdate 来说,它和 componentWillReceiveProps 的替换方式其实差不多。说实话,笔者在项目中,至少是在目前跑在PC浏览器的项目中,不太经常使用这个生命周期。因为在目前的业务中,从redux导致的props更新基本都有数据变化进而导致有视图更新的需要,可能从触发父到子的prop更新的时候,会出现不太必要的冲渲染需要,这个时候可能需要这个生命周期对当前和历史状态进行判断。也就是说,如果对于某个组件来说,差不多每次的props变化大概率可能是值真的变了,其实做比较是无意义的,因为比较也需要耗时,特别是数据量较大的情况。最后耗时去比较了,结果还是数据发生了变化,需要冲渲染,那么这是很操蛋的。所有说不能滥用 shouldComponentUpdate ,真的要看业务情况而定,在PC上多几次小范围的无意义的重渲染对性能影响不是很大,但在移动端的影响就很大,所以得看时机情况来决定。

Hook带来的改变,最重要的应该是在组织一个组件代码的时候,在思维方式上的变化,这也是官方文章中有提到的:”忘记你已经学会的东西”,所以我们在熟悉Hook以后,在书写组件逻辑的时候应该不要先考虑生命周期是怎么实现这个业务的,再转成Hook的实现,这样一来,一是还停留在生命周期的方式上,二是即便实现了业务功能,可能也不是很Hook的最优方式。所以,是时候用Hook的方式来思考组件的设计了。

 

三、不要忘记依赖、不要打乱Hook的顺序

先说Hook的顺序,在很多文章中,都有介绍Hook的基本实现或模拟实现原理,笔者这里不再多讲,有兴趣可以自行查看。总结来说就是,Hook实现的时候依赖于调用索引,当某个Hook在某一次渲染时因条件不满足而未能被调用,就会造成调用索引的错位,进而导致结果出错。这是和Hook的实现方式有关的原因,只要记住Hook不能书写在 if 等条件判断语句内部即可。

对于某个hook的依赖来说,一定要记住写,因为函数式组件是没有 componentWillReceive 、 shouldComponentUpdate 生命周期的。任何在重渲染时,一个函数是否需要重新创建、一个值是否需要重新计算,都和依赖有关系,如果依赖变了,就需要计算,没变就不需要计算,以节省重渲染的成本。这里特别需要注意的是函数依赖,因为函数内部可能会使用到 state 和 props 。比如,当你在 useEffect 内部引用了某些 state 和 props ,你可能会很容易的查看到,但是不太容易查看到其内部调用的其他函数是否也用到了 state 和 props 。所以函数的依赖一定不要忘记写。当然官方的CRA工具已经集成了ESlint配置,来帮我们检测某个hook是否存在有遗漏的依赖没有写上。PS. 这里我也推荐大家使用CRA进行项目初始化,并eject出配置文件,这样可以按照我们的业务要求自定义修改配置,然后将一些框架代码通过yeoman打包成generator,这样我们就有了自己的种子项目生成器,当开新项目的时候,可以进行快速的初始化。

 

四、Cpature Value特性

捕获值的这个特性并非函数式组件特有,它是函数特有的一种特性,函数的每一次调用,会产生一个属于那一次调用的作用域,不同的作用域之前不受影响。笔者看过的有关Hook的文档中,大多都引述过这个经典的例子:

function App (){
    const [count, setCount] = useState(0);
    
    function increateCount (){
        setCount(count + 1);
    }
    
    function showCount (){
        setTimeout(() => console.log(`你点击了${count}次`), 3000);
    }
    
    return (
        <div>
            <p>点击了{count}次</p>
            <button onClick={increateCount}>增加点击次数</button>
            <button onClick={showCount}>显示点击次数</button>
        </div>
    );
}

当我们点击了一次”增加点击次数”按钮后,再点击”显示点击次数”按钮,在大约3s后,我们可以看到点击次数会在控制台输上出来,在这之前我们再次点击”增加点击次数”按钮。3s后,我们看到控制台上输出的是1,而我们期望的是2。当你第一次接触Hook的时候看到这个结果,你一定会大吃一惊,WTF?

可以惊,但不要慌,听我细细道来:

1. 当App函数组件初次渲染完后,生成了第一个scope。在这个scope中, count 的值为0。

2. 我们第一次点击”增加点击次数”按钮的时候,调用了 setCount 方法,并将 count 的值加1,触发了重渲染,App组件函数因重渲染的需要而被重新调用,生成了第二个scope。在这个scope中,count为1。页面也更新到最新的状态,显示”点击了1次”。

3. 紧接着我们点击了”显示点击次数”按钮,将调用 showCount 方法,延迟3s后显示 count 的值。请注意这里,我们这次操作是在第二次渲染生成的这个scope(第二个scope)中进行的,而在这个scope中, count 的值为1。

4. 在3s的异步宏任务还未被推进主线程执行之前,我们又再次点击了”增加点击次数”按钮,再次调用了 setCount 方法,并加 count 的值再次加1,又触发了重渲染,App组件函数因重渲染的需要而被重新调用,生成了第三个scope。在这个scope中,count为2。页面也更新到最新的状态,显示”点击了2次”。

5. 3s到了以后,主线程也出于空闲状态,之前压入异步队列的宏任务被推入主线程中执行,重要的地方来了,这个异步任务所处的作用域是属于第二个scope,也就是说它会使用那一次渲染scope的 count 值,也就是1。而不是和界面最新的渲染结果2一样。

当你使用类组件来实现这个小功能并进行相同操作的时候,在控制台得到的结果都不同,但是在界面上最终的结果是一致的。在类组件中,我们在是生命周期方法 componentDidMount 、 componentDidUpdate 通过 this.state 去获取状态,得到的一定是其最新的值。这就是最大的不同之处,也是让初学者很困惑,很容易踩入坑中的地方,当然这个坑并不是说函数式组件和Hook设计上的问题,而是我们对其的不了解,进而导致使用上的错误和对结果的误判,进而导致代码出现BUG。

Capture Value这个特性在Hook的编码中一定要记住,并且理解。

如果说想要跳出每个重渲染产生的scope会固化自己的状态和值的特性,可以使用Hook API提供的 useRef hook,让所有的渲染scope中的某个状态,都指向一个统一的值的一个Key(API中采用current)。这个对象是引用传递的,ref的值记录在这个Key中,我们并不直接改变这个对象本身,而是通过修改其的一个Key来修记录的值。让每次重渲染生成的scope都保持对同一个对象的引用,来跳出Cpature Value带来的限制。

 

五、Hook的优势

在Hook的官方文档和一些文章中也提到了类组件的一些不好的地方,比如:HOC的多层嵌套,HOC和Render Props也不是太理想的复用代码逻辑,有关状态管理的逻辑代码很难在组件之间复用、一个业务逻辑的实现代码被放到了不同的生命周期内、ES2015与类有关语法和this指向等困扰初级开发者的问题等都有提到。还有像上一段落中提到的一些问题一样。这些都是需要改革和推动的地方。

这里笔者对HOC的多层嵌套确实觉得很恶心,因为笔者之前的项目就是这样的,一旦进入开发者工具的React Dev Tool的Tab,犹如地狱般的connect、asyncLoad就出现了,你会发现每个和Redux有关的组件都有一个connect,做了代码分割以后,异步加载的组件都有一个asyncLoad(虽然后面可以用原生的 lazy 和 suspense 替代),很多因使用HOC而带来的负面影响,对强迫症患者来说这不可接受,只能不看了之。

而对于类组件生命周期的开发方式来说,一个业务逻辑的实现,需要多个生命周期的配合,也就是逻辑代码会被放到多个生命周期内部,在一个组件比较稍微庞大和复杂以后,维护起来较为困难,有些时候可能会忘记修改某个地方,而采用Hook的方式来实现就比较好,可以完全封装在一个自定hook内部,需要的组件引入这个hook即可,还可以做到逻辑的复用。比如这个简单的需求:在页面渲染完成后监听一个浏览器网络变化的事件,并给出对应提示,在组件卸载后,我们再移除这个监听,通常使用生命周期的实现方式为:

class App (){
    browserOnline () {
        notify('浏览器网络已恢复正常!');  
    }   

    browserOffline () {
        notify('浏览器发生网络异常!');  
    }  

    componentDidMount (){
        window.addEventListener('online', this.browserOnline);
        window.addEventListener('offline', this.browserOffline);
    }  

    componentWillUnmount (){
        window.removeEventListener('online', this.browserOnline);
        window.removeEventListener('offline', this.browserOffline);
    }
}

使用Hook方式实现:

function useNetworkNotification (){
    const browserOnline = () => notify('浏览器网络已恢复正常!');

    const browserOffline = () => notify('浏览器发生网络异常!');

    useEffect(() => {
        window.addEventListener('online', browserOnline);
        window.addEventListener('offline', browserOffline);

        return () => {
            window.removeEventListener('online', browserOnline);
            window.removeEventListener('offline', browserOffline);
        };
    }, []);
}
function App (){
    useNetworkNotification();
}    

function AnotherComp (){
    useNetworkNotification();
}

所以,采用Hook实现的代码不仅管理起来方便(无需将相关的代码散布到不同的生命周期方法内),可以封装成自定义的hook,便于逻辑的在不同组件间复用,组件在使用的时候也不需要关注其内部的实现方式。这仅仅是实现了一个很简单功能的例子,如果项目变得更加复杂和难以维护,通过自定义Hook的方式来抽象逻辑有助于代码的组织质量。

 

六、为啥会推动Hook

笔者认为上个段落中提到的函数式组件配合Hook相较于类组件配合生命周期方法是存在有一定优势的。再者,React团队最开始发布Hook的时候,其实是顶着很大的压力的,因为这对于开发者来说实在就是以前的白学了,除了底层某些思想不变外,上层API全部变完。笔者最开始了解Hook后,最直接感受就是这东西是不是在给后面的Async Render填坑用的,为啥会这么说呢?因为React的这种更新机制就是全部树做Diff然后更新patch。而Vue是依赖收集方式的,数据变化后,哪些地方需要更新是明确的,所以更新是精准的。React的这种设计机制,就导致更新的成本很高,即便有虚拟树,但是一旦应用很庞大以后,遍历新旧虚拟树做Diff也是很耗时的,并且没有Async Render前,一旦开启协调,就只能一条路走到底,代码又不能控制JS引擎的函数调用栈,在主线程长时间运行脚本又不归还控制权,会阻塞线程造成界面友好度下降,特别是当应用运行在移动端设备等性能不太强的计算机上时效果特别显著。而基于Fiber的链表式树结构可以模拟出函数调用栈,并能够由代码控制工作的开始和暂停,可以有效解决上述问题,但它会破坏原本完整的生命周期方式,因为一个协调的任务,可能会放在不同的线程空闲时间内去完成,进而导致一个生命周期可能会被调用多次,导致实际运行的结果并不像代码书写的那样,这也是在16.3及以后版本将某些生命周期重命名为unsafe的原因。生命周期基本废掉了,虽然后来引入了一些静态方法用来解决一些问题,但存在感太低了,基本都属于过度阶段的产物。生命周期废了,就需要有东西来替代,并支持Async Render的实现,Hook这种模式就是一个不错的选择。当然这可能并不全面,或者说的不绝对正确,但笔者认为是有这个原因的。

 

七、单元测试

笔者目前的项目对稳定性要求高,属于LTS类型,不像创业型的互联网项目,可能上线几个月就下了,所以UT是必须的。笔者给新项目的模块写单元测试的时候,比较完好的支持Hook的Enzyme3.10版本在8天前才发布:(。从目前测试的体验来看,相对于类组件时代确实有进步。在类组件时代,除了生命周期外,其他的一切基本都靠HOC来完成,这就造成了我们在测试的时候,必须套上HOC,而当测试组件业务逻辑的时候,又必须扒开之前套上的HOC,找到里面的真实组件,再进行各种模拟和打桩操作。而函数式组件是没有这个问题的,有Hook加持后,一切都是扁平化的,总之就是好测。当然类组件时代也有好处,就是能够访问instance,但对于函数组件来说,无法从函数外面访问函数作用域内的东西。

 

八、总结

就像官方团队的文章中写道的一样:“如果你太不能够接受Hook,我们还是能够理解的,但请你至少不要去喷它,可以适当宣传一下。”。我们还是可以大胆尝试一下Hook的,至少现在2019年年中的时候,因为在这个时间点,一切有关Hook的支持和文档应该都比去年年底甚至是年初的时候更加完善了,虽然可能还不是太完全,但至少官方还在继续摸索,社区也很活跃,造轮子的人也很多。之前也有消息说Vue3.0大版本也会出Hook,哈哈,又是一片腥风血雨。总之,风口来了,能折腾的、喜欢折腾的就跟着风吹呗。入门简单,但完全、彻底地掌握和熟练运用,还是需要时间的。

 

发表于 2019-06-26 08:53 james·von 阅读() 评论() 编辑 收藏

 

版权声明:本文为rock-roll原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://www.cnblogs.com/rock-roll/p/11002093.html

使用React Hook后的一些体会的更多相关文章

  1. React脚手架create-react-app+elementUI使用

    一、介绍 1、create-react-app是FaceBook官方发布了一个无需配置的、用于快速构建开发环境 […]...

  2. react的生命周期需要知道的。

    有关React生命周期: 1、组件生命周期的执行次数是什么样子的??? 只执行一次: constructor、 […]...

  3. React 系列教程2:编写兰顿蚂蚁演示程序

    简介 最早接触兰顿蚂蚁是在做参数化的时候,那时候只感觉好奇,以为是很复杂的东西。因无意中看到生命游戏的 Rea […]...

  4. redux源码解析(深度解析redux)

    redux源码解析 1、首先让我们看看都有哪些内容       2、让我们看看redux的流程图      S […]...

  5. react 函数子组件(Function ad Child Component)

    今天学习了react中的函数子组件的概念,然后在工作中得到了实际应用,很开心,那么好记性不如烂笔头,开始喽~ […]...

  6. 从 0 到 1 实现 React 系列 —— 组件和 state|props

    组件即函数 在上一篇 JSX 和 Virtual DOM 中,解释了 JSX 渲染到界面的过程并实现了相应代码 […]...

  7. H5 hybrid开发-前端资源本地化方案纪要

    H5 hybrid-前端资源本地化方案纪要 就整个行业来说,大前端是趋势,现阶段,native方面除了一些偏C […]...

  8. 使用 React hooks 转化 class 的一些思考

    Hooks 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以 […]...

随机推荐

  1. 架构设计:负载均衡层设计方案之负载均衡技术总结篇

    架构设计:负载均衡层设计方案之负载均衡技术总结篇 1、概述 通过前面文章的介绍,并不能覆盖负载均衡层的所有技术 […]...

  2. 国产化之路-安装WEB服务器

    专题目录 国产化之路-统信UOS操作系统安装国产化之路-国产操作系统安装.net core 3.1 sdk国产 […]...

  3. [区块链] 拜占庭将军问题 [BFT]

    背景:   拜占庭将军问题很多人可能听过,但不知道具体是什么意思。那么究竟什么是拜占庭将军问题呢? 本文从最通 […]...

  4. Spring Boot 2.0 教程 – 配置详解

    Spring Boot 可以通过properties文件,YAML文件,环境变量和命令行参数进行配置。属性值可 […]...

  5. vue UI库iview源码解析(2)

    上篇问题 在上篇《iview源码解析(1)》中的index.js 入口文件的源码中有一段代码有点疑惑: /** […]...

  6. 重拾c++第四天(7):函数相关

    1、引用变量: int a; int &b = a; //引用变量 指向同一地址,必须在初始化时定义, […]...

  7. 个人开发者上架Android应用市场

    背景 前阵子开发了一个面向大众的应用,作为开发者,还是蛮期待自己的应用能上架应用市场,毕竟获得用户和得到用户的 […]...

  8. android7.0以上https抓包(无需root)

    一、声明 大家都知道android7.0以上, 有android的机制不在信任用户证书,导致https协议无法 […]...

展开目录

目录导航