blog
blog copied to clipboard
React v16 新特性
前言
最新 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 异步获取 的进展
- 官方费时开发维护中,会比最开始预想投入了更多的工作
参考链接
- https://zh-hans.reactjs.org/docs/hooks-intro.html
- https://zh-hans.reactjs.org/docs/hooks-faq.html#do-hooks-cover-all-use-cases-for-classes
- https://segmentfault.com/a/1190000017483690
- https://zhuanlan.zhihu.com/advancing-react
- https://www.robinwieruch.de/react-hooks-fetch-data