Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

React v16 新特性 #98

Open
yanyue404 opened this issue Nov 13, 2019 · 0 comments
Open

React v16 新特性 #98

yanyue404 opened this issue Nov 13, 2019 · 0 comments

Comments

@yanyue404
Copy link
Owner

yanyue404 commented Nov 13, 2019

前言

最新 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 异步获取 的进展

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

参考链接

@yanyue404 yanyue404 changed the title React V16 使用探索 React v16 使用探索 Nov 13, 2019
@yanyue404 yanyue404 changed the title React v16 使用探索 React v16 新特性 Jan 5, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

1 participant