全面了解 React 新功能: Suspense 和 Hooks

LawsonArlene 发布于1年前
0 条问题

悄悄的, React v16.7 发布了。 React v16.7: No, This Is Not The One With Hooks.

全面了解 React 新功能: Suspense 和 Hooks

最近我也一直在关注这两个功能,就花些时间就整理了一下资料, 在此分享给大家, 希望对大家有所帮助。

引子

为什么不推荐在 componentwillmount 里最获取数据的操作呢?

这个问题被过问很多遍了, 前几天又讨论到这个问题, 就以这个作为切入点吧。

有些朋友可能会想, 数据早点获取回来,页面就能快点渲染出来呀, 提升用户体验, 何乐而为不为?

这个问题, 简单回答起来就是, 因为是可能会 调用多次 。

要深入回答这个问题, 就不得不提到一个React 的核心概念: React Fiber .

一些必须要先了解的背景

React Fiber

React Fiber 是在 v16 的时候引入的一个全新架构, 旨在解决 异步渲染 问题。

新的架构使得使得 React 用异步渲染成为可能,但要注意,这个改变只是让异步渲染成为 可能 。

但是React 却并没有在 v16 发布的时候立刻开启,也就是说,React 在 v16 发布之后依然使用的是 同步渲染 。

不过,虽然异步渲染没有立刻采用,Fiber 架构还是打开了通向新世界的大门,React v16 一系列新功能几乎都是基于 Fiber 架构。

说到这, 也要说一下 同步渲染 和 异步渲染 .

同步渲染 和 异步渲染

同步渲染

我们都知道React 是facebook 推出的, 他们内部也在大量使用这个框架,(个人感觉是很良心了, 内部推动, 而不是丢出去拿用户当小白鼠), 然后就发现了很多问题, 比较突出的就是 渲染问题 。

他们的应用是比较复杂的, 组件树也是非常庞大, 假设有一千个组件要渲染, 每个耗费1ms, 一千个就是1000ms, 由于javascript 是单线程的, 这 1000ms 里 CPU 都在努力的干活, 一旦开始,中间就不会停。 如果这时候用户去操作, 比如输入, 点击按钮, 此时页面是没有响应的。 等更新完了, 你之前的那些输入就会啪啪啪一下子出来了。

这就是我们说的 页面卡顿 , 用起来很不爽, 体验不好。

这个问题和设备性能没有多大关系, 归根结底还是 同步渲染机制 的问题。

目前的React 版本(v16.7), 当组件树很大的时候,也会出现这个问题, 逐层渲染, 逐渐深入,不更新完就不会停 。

函数调用栈如图所示:

全面了解 React 新功能: Suspense 和 Hooks

因为JavaScript单线程的特点,每个同步任务不能耗时太长,不然就会让程序不会对其他输入作出相应,React的更新过程就是犯了这个禁忌,而React Fiber就是要改变现状。

异步渲染

Fiber 的做法是:分片。

把一个很耗时的任务分成很多小片,每一个小片的运行时间很短,虽然总时间依然很长,但是在每个小片执行完之后,都给其他任务一个执行的机会,这样唯一的线程就 不会被独占 ,其他任务依然有运行的机会。 而 维护每一个分片的数据结构, 就是Fiber 。

用一张图来展示Fiber 的碎片化更新过程:

全面了解 React 新功能: Suspense 和 Hooks

中间每一个波谷代表深入某个分片的执行过程,每个波峰就是一个分片执行结束交还控制权的时机。

更详细的信息可以看: Lin Clark - A Cartoon Intro to Fiber - React Conf 2017

在React Fiber中,一次更新过程会分成多个分片完成,所以完全有可能 一个更新任务还没有完成,就被另一个更高优先级的更新过程打断 ,这时候,优先级高的更新任务会优先处理完,而 低优先级更新任务所做的工作则会完全作废,然后等待机会重头再来 。

因为一个更新过程可能被打断,所以React Fiber一个更新过程被分为两个阶段: render phase and commit phase .

两个重要概念: render phase and commit phase

有了Fiber 之后, react 的渲染过程不再是一旦开始就不能终止的模式了, 而是划分成为了两个过程: 第一阶段和第二阶段, 也就是官网所谓的 render phase and commit phase 。

在 Render phase 中, React Fiber会找出需要更新哪些DOM,这个阶段是可以被打断的, 而到了第二阶段commit phase, 就一鼓作气把DOM更新完,绝不会被打断。

两个阶段的分界点

这两个阶段, 分界点 是什么呢?

其实是 render 函数 。 而且, render 函数 也是属于 第一阶段 render phase 的 。

那这两个 phase 包含的的生命周期函数有哪些呢?

render phase :

  • componentWillMount
  • componentWillReceiveProps
  • shouldComponentUpdate
  • componentWillUpdate

commit phase :

  • componentDidMount
  • componentDidUpdate
  • componentWillUnmount

全面了解 React 新功能: Suspense 和 Hooks

因为第一阶段的过程会被打断而且“重头再来”,就会造成意想不到的情况。

比如说,一个低优先级的任务A正在执行,已经调用了某个组件的componentWillUpdate函数,接下来发现自己的时间分片已经用完了,于是冒出水面,看看有没有紧急任务,哎呀,真的有一个紧急任务B,接下来React Fiber就会去执行这个紧急任务B,任务A虽然进行了一半,但是没办法,只能完全放弃,等到任务B全搞定之后,任务A重头来一遍,注意,是重头来一遍,不是从刚才中段的部分开始,也就是说,componentWillUpdate函数会被再调用一次。

在现有的React中,每个生命周期函数在一个加载或者更新过程中绝对只会被调用一次; 在React Fiber中,不再是这样了,第一阶段中的生命周期函数在一次加载和更新过程中可能会被多次调用! 。

这里也可以回答文行开头的那个问题了, 当然, 在异步渲染模式没有开启之前, 你可以在 willMount 里做ajax (不建议)。 首先,一个组件的 componentWillMount 比 componentDidMount 也早调用不了几微秒,性能没啥提高,而且如果开启了异步渲染, 这就难受了。 React 官方也意识到了这个问题,觉得有必要去劝告(威胁, 阻止)开发者 不要在render phase 里写有副作用 的代码了(副作用:简单说就是做本函数之外的事情,比如改一个全局变量, ajax之类)。

static getDerivedStateFromProps(nextProps, prevState) {
  //根据nextProps和prevState计算出预期的状态改变,返回结果会被送给setState
}

新的静态方法

为了减少(避免?)一些开发者的骚操作,React v16.3,干脆引入了一个新的生命周期函数 getDerivedStateFromProps , 这个函数是一个 static 函数,也是一个 纯函数 ,里面 不能通过 this 访问到当前组件 (强制避免一些有副作用的操作),输入只能通过参数,对组件渲染的影响只能通过返回值。目的大概也是让开发者逐步去适应异步渲染。

我们再看一下 React v16.3 之前的的生命周期函数 示意图:

全面了解 React 新功能: Suspense 和 Hooks

再看看16.3的示意图:

全面了解 React 新功能: Suspense 和 Hooks

上图中并包含全部React生命周期函数,另外在React v16发布时,还增加了一个 componentDidCatch ,当异常发生时,一个可以捕捉到异常的 componentDidCatch 就排上用场了。不过,很快React觉着这还不够,在v16.6.0又推出了一个新的捕捉异常的生命周期函数 getDerivedStateFromError 。

如果异常发生在 render阶段 ,React就会调用 getDerivedStateFromError ,如果异常发生在第 commit阶段 ,React会调用 componentDidCatch 。 这个异常可以是任何类型的异常, 捕捉到这个异常之后呢, 可以做一些补救之类的事情。

componentDidCatch 和 getDerivedStateFromError 的 区别

componentDidCatch 和 getDerivedStateFromError 都是能捕捉异常的,那他们有什么区别呢?

我们之前说了两个阶段, render phase 和 commit phase .

render phase 里产生异常的时候, 会调用 getDerivedStateFromError ;

在 commit phase 里产生异常大的时候, 会调用 componentDidCatch 。

严格来说, 其实还有一点区别:

componentDidCatch 是不会在 服务器端渲染 的时候被调用的 而 getDerivedStateFromError 会。

背景小结

啰里八嗦一大堆, 关于背景的东西就说到这, 大家只需要了解什么是Fiber: ‘ 哦, 这个这个东西是支持异步渲染的, 虽然这个东西还没开启’。

然后就是渲染的两个阶段: renderphase 和 commit phase .

  • render phase 可以被打断 , 大家不要在此阶段做一些有 副作用 的操作,可以放心在 commit phase 里做。
  • 然后就是生命周期的调整, react 把你有可能在render phase 里做的有副作用的函数都改成了static 函数, 强迫开发者做一些纯函数的操作。

现在我们进入正题: Suspense 和 Hooks 。

正题

suspense

Suspense要解决的两个问题:

  1. 代码分片;
  2. 异步获取数据。

刚开始的时候, React 觉得自己只是管视图的, 代码打包的事不归我管, 怎么拿数据也不归我管。 代码都打到一起, 比如十几M, 下载就要半天,体验显然不会好到哪里去。

可是后来呢,这两个事情越来越重要, React 又觉得, 嗯,还是要掺和一下,是时候站出来展现真正的技术了。

Suspense 在v16.6的时候 已经解决了代码分片的问题,异步获取数据还没有正式发布。

先看一个简单的例子:

import React from "react";
import moment from "moment";
 
const Clock = () => <h1>{moment().format("MMMM Do YYYY, h:mm:ss a")}</h1>;

export default Clock;

假设我们有一个组件, 是看当前时间的, 它用了一个很大的第三方插件, 而我想只在用的时候再加载资源,不打在总包里。

再看一段代码:

// Usage of Clock
const Clock = React.lazy(() => {
  console.log("start importing Clock");
  return import("./Clock");
});

这里我们使用了React.lazy, 这样就能实现代码的懒加载。 React.lazy 的参数是一个function, 返回的是一个promise. 这里返回的是一个import 函数, webpack build 的时候, 看到这个东西, 就知道这是个分界点。 import 里面的东西可以打包到另外一个包里。

真正要用的话, 代码大概是这个样子的:

<Suspense fallback={<Loading />}>
  { showClock ? <Clock/> : null}
</Suspense>

showClock 为 true, 就尝试render clock, 这时候, 就触发另一个事件: 去加载clock.js 和它里面的 lib momment。

看到这你可能觉得奇怪, 怎么还需要用个<Suspense> 包起来, 有啥用, 不包行不行。

哎嗨, 不包还真是不行。 为什么呢?

前面我们说到, 目前react 的渲染模式还是同步的, 一口气走到黑, 那我现在画到clock 这里, 但是这clock 在另外一个文件里, 服务器就需要去下载, 什么时候能下载完呢, 不知道。 假设你要花十分钟去下载, 那这十分钟你让react 去干啥, 总不能一直等你吧。 Suspens 就是来解决这个问题的, 你要画clock, 现在没有,那就会抛一个异常出来,我们之前说

componentDidCatch 和 getDerivedStateFromProps, 这两个函数就是来抓子组件 或者 子子组件抛出的异常的。

子组件有异常的时候就会往上抛,直到某个组件的 getDerivedStateFromProps 抓住这个异常,抓住之后干嘛呢, 还能干嘛呀, 忍着。 下载资源的时候会抛出一个promise, 会有地方(这里是suspense)捕捉这个promise, suspense 实现了getDerivedStateFromProps, getDerivedStateFromProps 捕获到异常的时候, 一看, 哎, 小老弟,你来啦,还是个promise, 然后就等这个promise resole, resolve 完成之后呢,它会尝试重新画一下子组件。这时候资源已经到本地了, 也就能画成功了。

用伪代码 大致实现一下:

getDerivedStateFromError(error) {
   if (isPromise(error)) {
      error.then(reRender);
   }
}

以上大概就是Suspense 的原理, 其实也不是很复杂,就是利用了 componentDidCatch 和 getDerivedStateFromError, 其实刚开始在v16的时候, 是要用componentDidCatch 的, 但它毕竟是commit phase 里的东西, 还是分出来吧, 所以又加了个getDerivedStateFromError来实现 Suspense 的功能。

这里需要注意的是 reRender 会渲染suspense 下面的所有子组件。

异步渲染什么时候开启呢, 根据介绍说是在19年的第二个季度随着一个小版本的升级开启, 让我们提前做好准备。

做些什么准备呢?

  • render 函数之前的代码都检查一边, 避免一些有副作用的操作

到这, 我们说完了Suspense 的一半功能, 还有另一半: 异步获取数据。

目前这一部分功能还没正式发布。 那我们获取数据还是只能在commit phase 做, 也就是在componentDidMount 里 或者 didUpdate 里做。

就目前来说, 如果一个组件要自己获取数据, 就必须实现为一个类组件, 而且会画两次, 第一次没有数据, 是空的, 你可以画个loading, didMount 之后发请求, 数据回来之后, 把数据setState 到组件里, 这时候有数据了, 再画一次,就画出来了。

虽然是一个很简答的功能, 我就想请求个数据, 还要写一堆东西, 很麻烦, 但在目前的正式版里, 不得不这么做。

但以后这种情况会得到改善, 看一段示例:

import {unstable_createResource as createResource} from 'react-cache';

const resource = createResource(fetchDataApi);

const Foo = () => {
  const result = resource.read();
  return (
    <div>{result}</div>
  );

// ...

<Suspense>
   <Foo />
</Suskpense>};

代码里我们看不到任何譬如 async await 之类的操作, 看起来完全是同步的操作, 这是什么原理呢。

上面的例子里, 有个 resource.read() , 这里就会调api, 返回一个promise, 上面会有suspense 抓住, 等resolve 的时候,再画一下, 就达到目的了。

到这,细心的同学可能就发现了一个问题, resource.read(); 明显是一个有副作用的操作, 而且 render 函数又属于render phase, 之前又说, 不建议在 render phase 里做有副作用的操作, 这么矛盾, 不是自己打脸了吗。

这里也能看出来React 团队现在还没完全想好, 目前放出来测试api 也是以 unstable_ 开头的, 不用用意还是跟明显的: 让大家不要写class的组件 ,Suspense 能很好的支持函数式组件。

hooks

React v16.7.0-alpha 中第一次引入了 Hooks 的概念, 为什么要引入这个东西呢?

有两个原因:

  1. React 官方觉得 class组件太难以理解,OO(面向对象)太难懂了
  2. React 官方觉得 , React 生命周期太难理解。

最终目的就是, 开发者不用去理解class, 也不用操心生命周期方法。

但是React 官方又说, Hooks的目的并不是消灭类组件。此处应手动滑稽。

回归正题, 我们继续看Hooks, 首先看一下 官方的API

全面了解 React 新功能: Suspense 和 Hooks

乍一看还是挺多的, 其实有很多的Hook 还处在实验阶段,很可能有一部分要被砍掉, 目前大家只需要熟悉的, 三个就够了:

  • useState
  • useEffect
  • useContext

useState

举个例子来看下, 一个简单的counter :

// 有状态类组件
class Counter extends React.Component {
   state = {
      count: 0
   }
   
   increment = () => {
       this.setState({count: this.state.count + 1});
   }
   
   minus = () => {
       this.setState({count: this.state.count - 1});
   }
   
   render() {
       return (
           <div>
               <h1>{this.state.count}</h1>
               <button onClick={this.increment}>+</button>
               <button onClick={this.minus}>-</button>
           </div>
       );
   }
}
// 使用useState Hook
const Counter = () => {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(count + 1);
  
  return (
    <div>
        <h1>{count}</h1>
        <button onClick={increment}>+</button>
    </div>
  );
};

这里的Counter 不是一个类了, 而是一个函数。

进去就调用了useState, 传入 0,对state 进行初始化,此时count 就是0, 返回一个数组, 第一个元素就是 state 的值,第二个元素是更新 state 的函数。

// 下面代码等同于: const [count, setCount] = useState(0);
  const result = useState(0);
  const count = result[0];
  const setCount = result[1];

利用 count 可以读取到这个 state,利用 setCount 可以更新这个 state,而且我们完全可以控制这两个变量的命名,只要高兴,你完全可以这么写:

const [theCount, updateCount] = useState(0);

因为 useState 在 Counter 这个函数体中,每次 Counter 被渲染的时候,这个 useState 调用都会被执行,useState 自己肯定不是一个纯函数,因为它要区分第一次调用(组件被 mount 时)和后续调用(重复渲染时),只有第一次才用得上参数的初始值,而后续的调用就返回“记住”的 state 值。

读者看到这里,心里可能会有这样的疑问:如果组件中多次使用 useState 怎么办?React 如何“记住”哪个状态对应哪个变量?

React 是完全根据 useState 的 调用顺序 来“记住”状态归属的,假设组件代码如下:

const Counter = () => {
  const [count, setCount] = useState(0);
  const [foo, updateFoo] = useState('foo');
  
  // ...
}

每一次 Counter 被渲染,都是第一次 useState 调用获得 count 和 setCount,第二次 useState 调用获得 foo 和 updateFoo(这里我故意让命名不用 set 前缀,可见函数名可以随意)。

React 是渲染过程中的“上帝”,每一次渲染 Counter 都要由 React 发起,所以它有机会准备好一个 内存记录 ,当开始执行的时候,每一次 useState 调用对应 内存记录上一个位置 ,而且是 按照顺序来记录的 。React 不知道你把 useState 等 Hooks API 返回的结果赋值给什么变量,但是它也不需要知道,它只需要按照 useState 调用顺序记录就好了。

你可以理解为会有一个槽去记录状态。

正因为这个原因, Hooks,千万不要在 if 语句或者 for 循环语句中使用!

像下面的代码,肯定会出乱子的:

const Counter = () => {
    const [count, setCount] = useState(0);
    if (count % 2 === 0) {
        const [foo, updateFoo] = useState('foo');
    }
    const [bar, updateBar] = useState('bar');
 // ...
}

因为条件判断,让每次渲染中 useState 的调用次序不一致了,于是 React 就错乱了。

useEffect

除了 useState,React 还提供 useEffect,用于支持组件中增加副作用的支持。

在 React 组件生命周期中如果要做有副作用的操作,代码放在哪里?

当然是放在 componentDidMount 或者 componentDidUpdate 里,但是这意味着组件必须是一个 class。

在 Counter 组件,如果我们想要在用户点击“+”或者“-”按钮之后把计数值体现在网页标题上,这就是一个修改 DOM 的副作用操作,所以必须把 Counter 写成 class,而且添加下面的代码:

componentDidMount() {
  document.title = `Count: ${this.state.count}`;
}

componentDidUpdate() {
  document.title = `Count: ${this.state.count}`;
}

而有了 useEffect,我们就不用写一个 class 了,对应代码如下:

import { useState, useEffect } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);
  
  useEffect(() => {
    document.title = `Count: ${this.state.count}`;
  });

  return (
    <div>
       <div>{count}</div>
       <button onClick={() => setCount(count + 1)}>+</button>
       <button onClick={() => setCount(count - 1)}>-</button>
    </div>
  );
};

useEffect 的参数是一个函数,组件每次渲染之后,都会调用这个函数参数,这样就达到了 componentDidMount 和 componentDidUpdate 一样的效果。

虽然本质上,依然是 componentDidMount 和 componentDidUpdate 两个生命周期被调用,但是现在我们关心的不是 mount 或者 update 过程,而是“after render”事件,useEffect 就是告诉组件在“渲染完”之后做点什么事。

读者可能会问,现在把 componentDidMount 和 componentDidUpdate 混在了一起,那假如某个场景下我只在 mount 时做事但 update 不做事,用 useEffect 不就不行了吗?

其实,用一点小技巧就可以解决。 useEffect 还支持第 二个可选参数 ,只有同一 useEffect 的两次调用第二个参数不同时,第一个函数参数才会被调用. 所以,如果想模拟 componentDidMount,只需要这样写:

useEffect(() => {
    // 这里只有mount时才被调用,相当于componentDidMount
  }, [123]);

在上面的代码中,useEffect 的第二个参数是 [123],其实也可以是任何一个常数,因为它永远不变,所以 useEffect 只在 mount 时调用第一个函数参数一次,达到了 componentDidMount 一样的效果。

useContext

在前面介绍“提供者模式”章节我们介绍过 React 新的 Context API,这个 API 不是完美的,在多个 Context 嵌套 的时候尤其麻烦。

比如,一段 JSX 如果既依赖于 ThemeContext 又依赖于 LanguageContext,那么按照 React Context API 应该这么写:

<ThemeContext.Consumer>
    {
        theme => (
            <LanguageContext.Cosumer>
                language => {
                    //可以使用theme和lanugage了
                }
            </LanguageContext.Cosumer>
        )
    }
</ThemeContext.Consumer>

因为 Context API 要用 render props,所以用两个 Context 就要用两次 render props,也就用了两个函数嵌套,这样的缩格看起来也的确过分了一点点。

使用 Hooks 的 useContext,上面的代码可以缩略为下面这样:

const theme = useContext(ThemeContext);
const language = useContext(LanguageContext);
// 这里就可以用theme和language了

这个useContext把一个需要很费劲才能理解的 Context API 使用大大简化,不需要理解render props,直接一个函数调用就搞定。

但是,useContext也并不是完美的,它会造成意想不到的重新渲染,我们看一个完整的使用useContext的组件。

const ThemedPage = () => {
    const theme = useContext(ThemeContext);
    
    return (
       <div>
            <Header color={theme.color} />
            <Content color={theme.color}/>
            <Footer color={theme.color}/>
       </div>
    );
};

因为这个组件ThemedPage使用了 useContext ,它很自然成为了Context的一个 消费者 ,所以,只要Context的值发生了变化,ThemedPage就会被 重新渲染 ,这很自然,因为不重新渲染也就没办法重新获得theme值,但现在有一个大问题,对于ThemedPage来说,实际上只依赖于theme中的color属性,如果只是theme中的size发生了变化但是color属性没有变化,ThemedPage依然会被重新渲染,当然,我们通过给Header、Content和Footer这些组件添加shouldComponentUpdate实现可以减少没有必要的重新渲染,但是上一层的ThemedPage中的JSX重新渲染是躲不过去了。

说到底, useContext 需要一种表达方式告诉React:“我没有改变,重用上次内容好了。”

希望Hooks正式发布的时候能够弥补这一缺陷。

Hooks 带来的代码模式改变

上面我们介绍了 useState 、 useEffect 和 useContext 三个最基本的 Hooks,可以感受到,Hooks 将大大简化使用 React 的代码。

首先我们可能不再需要 class了,虽然 React 官方表示 class 类型的组件将继续支持,但是,业界已经普遍表示会迁移到 Hooks 写法上,也就是放弃 class,只用函数形式来编写组件。

对于 useContext,它并没有为消除 class 做贡献,却为消除 render props 模式做了贡献。很长一段时间,高阶组件和 render props 是组件之间共享逻辑的两个武器,但如同我前面章节介绍的那样,这两个武器都不是十全十美的,现在 Hooks 的出现,也预示着高阶组件和 render props 可能要被逐步取代。

但读者朋友,不要觉得之前学习高阶组件和 render props 是浪费时间,相反,你只有明白 React 的使用历史,才能更好地理解 Hooks 的意义。

可以预测,在 Hooks 兴起之后,共享代码之间逻辑会用函数形式,而且这些函数会以 use- 前缀为约定,重用这些逻辑的方式,就是在函数形式组件中调用这些 useXXX 函数。

例如,我们可以写这样一个共享 Hook useMountLog,用于在 mount 时记录一个日志,代码如下:

const useMountLog = (name) => {
    useEffect(() => {
        console.log(`${name} mounted`);    
    }, [123]);
}

任何一个函数形式组件都可以直接调用这个 useMountLog 获得这个功能,如下:

const Counter = () => {
    useMountLog('Counter');
    
    ...
}

对了,所有的 Hooks API 都只能在函数类型组件中调用,class 类型的组件不能用,从这点看,很显然,class 类型组件将会走向消亡。

如何用Hooks 模拟旧版本的生命周期函数

Hooks 未来正式发布后, 我们自然而然的会遇到这个问题, 如何把写在旧生命周期内的逻辑迁移到Hooks里面来。下面我们就简单说一下,

模拟整个生命周期中只运行一次的方法

useMemo(() => {
  // execute only once
}, []);

我们可以看到useMemo 接收两个参数, 第一个参数是一个函数, 第二个参数是一个数组。

这里有个地方要注意, 就是, 第二个参数的数组里的元素和上一次执行useMemo的第二个参数的数组的元素 完全一样的话,那就表示没有变化, 就不用执行第一个参数里的函数了。 如果有不同, 说明有变化, 就执行。

上面的例子里, 我们只传入了一个空数组, 不会有变化, 也就是只会执行一次。

模拟shouldComponentUpdate

const areEqual = (prevProps, nextProps) => {
   // 返回结果和shouldComponentUpdate正好相反
   // 访问不了state
}; 
React.memo(Foo, areEqual);

模拟componentDidMount

useEffect(() => {
    // 这里在mount时执行一次
}, []);

模拟componentDidUpdate

const mounted = useRef();
useEffect(() => {
  if (!mounted.current) {
    mounted.current = true;
  } else {
    // 这里只在update是执行
  }
});

模拟componentDidUnmount

useEffect(() => {
    // 这里在mount时执行一次
    return () => {
       // 这里在unmount时执行一次
    }
}, []);

未来的代码形势

Hooks 未来发布之后, 我们的代码会写成什么样子呢? 简单设想一下:

// Hooks之后的组件逻辑重用形态

const XXXX = () => {
  const [xx, xxx, xxxx] = useX();
  
  useY();
  
  const {a, b} = useZ();
  

  return (
    <>
     //JSX
    </>
  );
};

内部可能用各种Hooks, 也可能包含第三方的Hooks。 分享Hooks 就是实现代码重用的一种形势。 其实现在已经有人在做这方面的工作了: useHooks.com , 有兴趣的朋友可以去看下。

Suspense 和 Hooks 带来的改变

Suspense 和 Hooks 发布后, 会带来什么样的改变呢? 毫无疑问, 未来的组件, 更多的将会是函数式组件。

原因很简单, 以后大家分享出来的都是Hooks,这东西只能在函数组件里用啊, 其他地方用不了,后面就会自然而然的发生了。

但函数式组件和函数式编程还不是同一个概念。 函数式编程必须是纯的, 没有副作用的, 函数式组件里, 不能保证, 比如那个resource.read(), 明显是有副作用的。

关于好坏

既然这两个东西是趋势, 那这两个东西到底好不好呢 ?

个人理解, 任何东西都不是十全十美。 既然大势所趋, 我们就努力去了解它,学会它, 努力用它好的地方, 避免用不好的地方。

React 发布路线图

最新的消息: https://reactjs.org/blog/2018...

  • React 16.6 with Suspense for Code Splitting (already shipped)
  • A minor 16.x release with React Hooks (~Q1 2019)
  • A minor 16.x release with Concurrent Mode (~Q2 2019)
  • A minor 16.x release with Suspense for Data Fetching (~mid 2019)

明显能够看到资源在往 Suspense 和 Hooks 倾斜。

结语

看到这, 相信大家都Suspense 和 Hooks 都有了一个大概的了解了。

收集各种资料花费了挺长时间,大概用了两三天写出来,中间参考了很多资料, 一部分是摘录到了上面的内容里。

在这里整理分享一下, 希望对大家有所帮助。

才疏学浅, 难免会有纰漏, 欢迎指正:)。

参考资料

查看原文: 全面了解 React 新功能: Suspense 和 Hooks

  • blackcat
  • brownbutterfly
  • orangedog
  • lazyrabbit
  • crazypeacock
  • goldenmouse
  • greencat
需要 登录 后回复方可回复, 如果你还没有账号你可以 注册 一个帐号。