yanyue404/blog

React v16 新特性

Opened this issue · 0 comments

前言

最新 React 版本为 v16.11.0,由于 16 版本的 React 的有巨大的改变更新,此篇文章做总结性的纪录。

概述

  • 采用 Fiber diff 更新机制,生命周期重大变化
  • Suspense 代码分片
  • Hook 出现,类组件到函数式组件

Fiber

React Fiber 将应用同步 diff 更新改变为异步渲染更新。

由于异步更新会造成比较阶段阶段可能被打断,render阶段之前的生命周期方法可能引发多次执行,生命周期进行了较大调整。

React v16.3 之前的的生命周期函数 示意图:

再看看 16.3 的示意图:

Suspense

Suspense 要解决的两个问题:

  • 代码分片;
  • 异步获取数据。(还没有正式发布)

代码分片,对你的应用进行代码分割能够帮助你“懒加载”当前用户所需要的内容,能够显著地提高你的应用性能。

  • 避免一次性加载用户不直接需要的代码,动态导入
  • 减少初始化所需加载的代码包体积

实际用例 Suspense + React.lazy

import React from 'react';
import moment from 'moment';

const Clock = () => {
  <h1>{moment().format('YYYY-MM-DD HH:mm:ss')}</h1>;
};

export default Clock;
const OtherComponent = React.lazy(() => import('./Clock'));

function MyComponent() {
  return (
    <div>
      <Suspense fallback={<div>Loading...</div>}>
        <OtherComponent />
      </Suspense>
    </div>
  );
}

此代码将会在组件首次渲染时,自动导入包含 OtherComponent 组件的包。

React.lazy 接受一个函数,这个函数需要动态调用 import()。它必须返回一个 Promise,该 Promise 需要 resolve 一个 defalut export 的 React 组件。

然后应在 Suspense 组件中渲染 lazy 组件,如此使得我们可以使用在等待加载 lazy 组件时做优雅降级(如 loading 指示器等)。

原理:如果渲染组件失败会抛出错误,在外层Suspense捕获错误,得到的错误是动态导入组件的Promise类型错误时,会在resolve执行导入成功后重新 reRender

<Suspense>{show ? <OtherComponent /> : null}</Suspense>;

class Suspense extends React.Component {
  static getDerivedStateFromError(error) {
    if (isPromise(error)) {
      error.then(reRender);
    }
  }
}

Hook

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

为什么需要 Hook

  • 在组件之间复用状态逻辑很难,Hook 使我们在无需修改组件结构的情况下复用状态逻辑
  • 复杂组件变得难以理解,Hook 将组件中相互关联的部分拆分成更小的函数,整合业务逻辑,不再按照生命周期划分
  • 难以理解的 class,需要理解 JavaScript 中 this 的工作方式,绑定事件处理器

改革方向

  • 渐进策略:官方将继续为 class 组件提供支持 Hook 和现有代码可以同时工作,开发者可以渐进式地使用他们。
  • Hook:官方准备让 Hook 覆盖所有 class 组件的使用场景,在新的代码中同时使用 Hook 和 class。

useState

使用数组解构的方式组合操控 state

import React, { useState } from 'react';
function Example() {
  const [count, setCount] = useState(0);
  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

数组解构内部做了什么?

var countControl = useState('0'); // 返回一个有两个元素的数组
var initCount = countControl[0]; // 数组里的第一个值
var setNewCount = countControl[1]; // 数组里的第二个值

总结:省去了绑定事件的必须过程,不纠结 this 指向问题,复用代码

注意事项: Hooks,千万不要在 if 语句或者 for 循环语句中使用!

useEffect

副作用:在 React 组件中执行过数据获取、订阅或者手动修改过 DOM。我们统一把这些操作称为“副作用”,或者简称为“作用”。

useEffect 就是一个 Effect Hook,给函数组件增加了操作副作用的能力。它跟 class 组件中的 componentDidMount、componentDidUpdate 和 componentWillUnmount 具有相同的用途,只不过被合并成了一个 API。

很多情况下,我们希望在组件加载和更新时执行同样的操作

class Example extends React.Component {
  constructor(props) {
    super(props);
    this.state = {
      count: 0,
    };
  }

  componentDidMount() {
    document.title = `You clicked ${this.state.count} times`;
  }

  componentDidUpdate() {
    document.title = `You clicked ${this.state.count} times`;
  }

  render() {
    return (
      <div>
        <p>You clicked {this.state.count} times</p>
        <button onClick={() => this.setState({ count: this.state.count + 1 })}>
          Click me
        </button>
      </div>
    );
  }
}

使用 useEffect 重构

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

function Example() {
  const [count, setCount] = useState(0);

  useEffect(() => {
    document.title = `You clicked ${count} times`;
  });

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
}

useEffect 将书写的代码逻辑做了改变:由关心生命周期的逻辑,变成了 effect 发生在“渲染之后”这种概念,不用再去考虑“挂载”还是“更新”。

Effect只需要 componentDidMount

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, []);

Effect只需要 componentDidUpdate

只需要更新后执行,这种场景应该很少吧,方法很丑,不用,不用。

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

Effect卸载阶段

使用 Effect return 一个函数调用表示组件即将卸载阶段的调用,模拟 componentWillUnmount

function FriendStatus(props) {
  // ...
  useEffect(() => {
    // ...
    ChatAPI.subscribeToFriendStatus(props.friend.id, handleStatusChange);
    return () => {
      ChatAPI.unsubscribeFromFriendStatus(props.friend.id, handleStatusChange);
    };
  });

Effect更新阶段性能优化(简单版)

在某些情况下,每次渲染后都执行清理或者执行 effect 可能会导致性能问题。在 class 组件中,我们可以通过在 componentDidUpdate 中添加对 prevProps 或 prevState 的比较逻辑解决:

componentDidUpdate(prevProps, prevState) {
  if (prevState.count !== this.state.count) {
    document.title = `You clicked ${this.state.count} times`;
  }
}

使用 useEffect,如果某些特定值在两次重渲染之间没有发生变化,你可以通知 React 跳过对 effect 的调用,只要传递数组作为 useEffect 的第二个可选参数即可:

useEffect(() => {
  document.title = `You clicked ${count} times`;
}, [count]); // 仅在 count 更改时更新

Effect更新阶段性能优化(复杂版),模拟shouldComponentUpdate

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

自定义 Hook

  • 双向数据绑定 input 组件:
import React, { useState, useCallback } from 'react';
function useBind(init) {
  let [value, setValue] = useState(init);
  let onChange = useCallback(function(event) {
    setValue(event.currentTarget.value);
  }, []);
  return {
    value,
    onChange,
  };
}
function Page1(props) {
  let value = useBind('');
  return <input {...value} />;
}
  • 自定义一个 数据请求的 Hook
const useHackerNewsApi = () => {
  const [data, setData] = useState({ hits: [] });
  const [url, setUrl] = useState(
    'https://hn.algolia.com/api/v1/search?query=redux',
  );
  const [isLoading, setIsLoading] = useState(false);
  const [isError, setIsError] = useState(false);
  useEffect(() => {
    const fetchData = async () => {
      setIsError(false);
      setIsLoading(true);
      try {
        const result = await axios(url);
        setData(result.data);
      } catch (error) {
        setIsError(true);
      }
      setIsLoading(false);
    };
    fetchData();
  }, [url]);
  return [{ data, isLoading, isError }, setUrl];
};
function App() {
  const [query, setQuery] = useState('redux');
  const [{ data, isLoading, isError }, doFetch] = useHackerNewsApi();
  return (
    <Fragment>
      <form
        onSubmit={event => {
          doFetch(`http://hn.algolia.com/api/v1/search?query=${query}`);
          event.preventDefault();
        }}
      >
        <input
          type="text"
          value={query}
          onChange={event => setQuery(event.target.value)}
        />
        <button type="submit">Search</button>
      </form>
      ...
    </Fragment>
  );
}

Hook 规则

  • 只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook
  • 只在 React 函数中调用 Hook:在 React 的函数组件中调用 Hook,或在自定义 Hook 中调用其他 Hook
  • 可以在单个组件中使用多个 State Hook 或 Effect Hook

从 Class 迁移到 Hook

生命周期方法要如何对应到 Hook?

  • constructor:函数组件不需要构造函数。你可以通过调用 useState 来初始化 state。如果计算的代价比较昂贵,你可以传一个函数给 useState。
  • getDerivedStateFromProps:改为 在渲染时 安排一次更新。
  • shouldComponentUpdate:详见 React.memo.
  • render:这是函数组件体本身。
  • componentDidMount, componentDidUpdate, componentWillUnmount:useEffect Hook 可以表达所有这些(包括 不那么 常见 的场景)的组合。
  • componentDidCatch and getDerivedStateFromError:目前还没有这些方法的 Hook 等价写法,但很快会加上。

我该如何使用 Hook 进行数据获取?

demo 会帮助你开始理解。欲了解更多,请查阅 此文章 来了解如何使用 Hook 进行数据获取。

展望

Hook

  • React Hook 已经基本覆盖所有的 class 使用情况 [React v16.8]
  • 第三方 Hook 的支持情况
    • Redux connect() 和 React Router 等流行的 API 已经支持
    • 其它第三库支持情况需要观察

Suspense 异步获取 的进展

  • 官方费时开发维护中,会比最开始预想投入了更多的工作

参考链接