[关闭]
@gyyin 2019-12-28T23:36:34.000000Z 字数 11649 阅读 349

详解 React16 最新特性

慕课专栏


1. 前言

React 进入 v16 版本后有了翻天覆地的变化,不仅是 api 上面,甚至连底层渲染都做了很大的改动,今天来聊一聊 React v16 的那些新特性。
React v16 是从2017年9月26日发布了第一个版本,截止到今天(2019年12月23日)目前 React 已经到了 16.12.0 版本,可以在 GitHub 上查看版本更新信息:releases

2. 要求新的 JS 环境

React 16 依赖于 Map 和 Set 以及 requestAnimationFrame。如果你还在使用没有原生提供这些功能的旧版浏览器和设备(例如 <IE11),则可能需要包含 polyfill。

3. 组件可以返回数组和字符串

  1. render() {
  2. return [
  3. <li>111</li>
  4. <li>222</li>
  5. <li>333</li>
  6. ]
  7. }
  8. render() {
  9. return 'hello, world';
  10. }

4. 错误边界处理

如果错误在构造函数、生命周期或者渲染的时候抛出。那么该组件会被卸载,不会影响到其他组件。
如果 class 组件定义了生命周期方法 static getDerivedStateFromError()componentDidCatch() 中的任何一个(或两者),它就成为了 Error boundaries。通过生命周期更新 state 可让组件捕获树中未处理的 JavaScript 错误并展示降级 UI。

  1. // static getDerivedStateFromError
  2. class ErrorBoundary extends React.Component {
  3. constructor(props) {
  4. super(props);
  5. this.state = { hasError: false };
  6. }
  7. static getDerivedStateFromError(error) {
  8. // 更新 state 使下一次渲染可以显降级 UI
  9. return { hasError: true };
  10. }
  11. render() {
  12. if (this.state.hasError) {
  13. // 你可以渲染任何自定义的降级 UI
  14. return <h1>Something went wrong.</h1>;
  15. }
  16. return this.props.children;
  17. }
  18. }
  19. // componentDidCatch
  20. class ErrorBoundary extends React.Component {
  21. constructor(props) {
  22. super(props);
  23. this.state = { hasError: false };
  24. }
  25. componentDidCatch(error) {
  26. // 更新 state 使下一次渲染可以显降级 UI
  27. this.setState({ hasError: true })
  28. }
  29. render() {
  30. if (this.state.hasError) {
  31. // 你可以渲染任何自定义的降级 UI
  32. return <h1>Something went wrong.</h1>;
  33. }
  34. return this.props.children;
  35. }
  36. }

5. ReactDOM.createPortal

createPortal 允许你将组件挂载到任意的 DOM 节点下面,这样就可以实现将当前的组件挂载到父组件 DOM 层次之外的地方。
举个例子,比如之前所有的组件都被挂载了 ReactDOM.render 的第二个参数节点下面,如果想要实现一个模态框,不得不通过 CSS 定位的方式让蒙层覆盖住当前页面。

  1. const Modal = (props) => {
  2. const styles = {
  3. position: 'fixed',
  4. top: 0,
  5. left: 0,
  6. right: 0,
  7. bottom: 0,
  8. backgroundColor: 'gray'
  9. }
  10. return (
  11. <div style={styles}>{props.children}</div>
  12. )
  13. }
  14. // 调用
  15. <Modal>
  16. <h1>hello, world</h1>
  17. </Modal>

但是如果父元素设置了 oveflow: hidden,那么就不得不突破父元素的容器。虽然之前 React 提供了 unstable_renderSubtreeIntoContainer 方法,但这是个不稳定的 api。因此在 React v16.0 提供了 createPortal 这个特性,允许你将组件挂载到任意 DOM 之下。

createPortal 存在于 react-dom 这个库中,并非在 React 中。

在 html 文件中预留出一个 DOM 节点,和 ReactDOM 插入的节点同级。

  1. <body>
  2. <div id="app"></div> // 用于 ReactDOM.render 插入
  3. <div id="modal"></div>
  4. </body>

将 Modal 组件中的内容插入到提前创建好的 #modal 节点里面,这样就可以做到脱离原来应用的父容器 #app

  1. class Modal extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.root = document.querySelector("#modal");
  5. this.wrap = document.createElement("div");
  6. }
  7. componentDidMount() {
  8. this.root.appendChild(this.wrap);
  9. }
  10. componentWillUnmount() {
  11. this.root.removeChild(this.wrap);
  12. }
  13. render() {
  14. return ReactDOM.createPortal(
  15. this.props.children,
  16. this.wrap
  17. )
  18. }
  19. }

6. renderToNodeStream

v16.0 对服务端渲染进行了一些新的变动,提供了新的 ReactDOM.hydrate 方法用于在服务端环境下代替 ReactDOM.render,原来的 ReactDOM.render 用于客户端渲染。但 ReactDOM.render 依然可以使用,这个改动是向下兼容的。
v16.0 还提供了新的 renderToNodeStream 方法。renderToString 方法会将生成字符串到页面渲染一气呵成,如果组件比较大,同步的过程就会花费很多时间,带来的用户体验不够好。

  1. // using Express
  2. import { renderToString } from "react-dom/server"
  3. import MyPage from "./MyPage"
  4. app.get("/", (req, res) => {
  5. res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  6. res.write("<div id='content'>");
  7. res.write(renderToString(<MyPage/>));
  8. res.write("</div></body></html>");
  9. res.end();
  10. });

renderToNodeStream 允许使用可读流的形式将 string 传给 response 对象,一般在 Express、Koa、Nest 等 NodeJS 框架中,response 对象都是一个可写流。这样就不需要等待 html 字符串全部渲染成功后再返回给客户端,可以做到边读边渲染,用户也不需要等待很长时间后才看到页面显示出来。

  1. // using Express
  2. import { renderToNodeStream } from "react-dom/server"
  3. import MyPage from "./MyPage"
  4. app.get("/", (req, res) => {
  5. res.write("<!DOCTYPE html><html><head><title>My Page</title></head><body>");
  6. res.write("<div id='content'>");
  7. const stream = renderToNodeStream(<MyPage/>);
  8. stream.pipe(res, { end: false });
  9. stream.on('end', () => {
  10. res.write("</div></body></html>");
  11. res.end();
  12. });
  13. });

7. setState 的一些变动

  1. 当传给 setState 一个 null 的时候,不会再触发更新。
  2. setState 的第二个参数(回调函数)会在执行完 componentDidMount 或者 componentDidUpdate 之后立即执行,而不是像原来一样,在所有组件渲染完之后再触发。
  3. 当用 <B /> 来替换 <A />的时候,B.componentWillMount 总是在 A.componentWillUnmount 之前执行,而以前在某些场景下执行顺序是相反的。

8. Fragment

类似前面直接返回数组,React v16.2 提供了 Fragment 组件,它允许直接将返回元素,而不需要用父节点来包裹。

  1. const App = () => (
  2. <React.Fragment>
  3. <h1>hello</h1>
  4. <h1>world</h1>
  5. </React.Fragment>
  6. )

除此之外,React 还提供了语法糖 <></> 来快速使用 React.Fragment。

  1. const App = () => (
  2. <React.Fragment>
  3. <h1>hello</h1>
  4. <h1>world</h1>
  5. </React.Fragment>
  6. )

9. 新的 Context

react Context 提供了一种新的组件通信方式,用于解决子孙组件通信需要多次传递 props(简直就是套娃啊)。
我们先来看一下老的 Context api 的用法。
一般需要你在祖先组件里面提供 Context,声明一个 Context 对象类型,并且实现 getChildContext 方法,返回一个 Context 对象。

  1. class Parent extends Component {
  2. // 声明 Context 对象类型
  3. static childContextTypes = {
  4. name: PropTypes.string,
  5. eat: PropTypes.func
  6. }
  7. // 返回 Context 对象
  8. getChildContext () {
  9. return {
  10. name: 'xiaoming',
  11. eat: () => 'eat'
  12. }
  13. }
  14. render () {
  15. return <Child />
  16. }
  17. }

然后子孙组件接收到这个 Context 之后,需要通过 static contextTypes 声明需要接收的 Context 对象类型和属性之后,直接通过 this.context 来访问。

  1. class Child extends Component {
  2. static contextTypes = {
  3. name: PropTypes.string,
  4. eat: PropTypes.func
  5. }
  6. render() {
  7. const {
  8. name,
  9. eat
  10. } = this.context;
  11. // ...
  12. }
  13. }

而在 React v16.3 中,对 Context 这个 api 进行了重新设计。具体就是增加了 createContext 这个方法,可以来创建一个 Context 对象,这个对象包含了 ProviderConsumer 两个组件,可以理解为“生产者”和“消费者”。

9.1 createContext

createContext 接收一个默认值,返回一个 Context 对象。

  1. const ColorContext = React.createContext('red');

当 React 渲染一个订阅了这个 Context 对象的组件,这个组件会从组件树中离自身最近的那个匹配的 Provider 中读取到当前的 context 值。

只有当组件所处的树中没有匹配到 Provider 时,其 defaultValue 参数才会生效。这有助于在不使用 Provider 包装组件的情况下对组件进行测试。

注意:将 undefined 传递给 Provider 的 value 时,消费组件的 defaultValue 不会生效。

9.2 Context.Provider

  1. <MyContext.Provider value={/* 某个值 */}>

每一个 Context 对象都会返回一个 Provider组件,它允许消费组件来订阅 context 的变化。

Provider 接收一个 value 属性,传递给消费组件。一个 Provider 可以和多个消费组件有对应关系。同时,多个 Provider 也可以嵌套使用,里层的会覆盖外层的数据。

Providervalue 值发生变化时,它内部的所有消费组件都会重新渲染。Provider 及其内部 consumer 组件都不受制于 shouldComponentUpdate 函数,因此当 consumer 组件在其祖先组件退出更新的情况下也能更新。

9.3 Context.Consumer

  1. <MyContext.Consumer>
  2. {value => /* 基于 context 值进行渲染*/}
  3. </MyContext.Consumer>

每一个 Context 对象也都会返回一个 Consumer组件,这个组件也可以订阅到 context 变更。
Consumer 组件需要接收一个函数当做子元素(function as a child),现在一般称为 render props
这个函数接收当前的 context 值,返回一个 React 节点。传递给函数的 value 值等同于往上组件树离这个 context 最近的 Provider 提供的 value 值。
如果没有对应的 Provider,value 参数就是传递给 createContext() 的 defaultValue。

9.4 contextType

除了使用 Consumer 组件来获取到 context 的值,还可以继续通过设置 static contextType 来在组件中通过 this.context 的形式拿到 context 值。
和老的 contextType 区别在于,现在的 contextType 应该是一个 context 对象。

  1. const ThemeContext = React.createContext('light');
  2. class Child extends Component {
  3. static contextType = ThemeContext;
  4. render() {
  5. const value = this.context;
  6. // ...
  7. }
  8. }

9.5 嵌套多个 Context

Context 还允许你使用多个 context 嵌套。以 React 官网这个为例:

  1. // Theme context,默认的 theme 是 “light” 值
  2. const ThemeContext = React.createContext('light');
  3. // 用户登录 context
  4. const UserContext = React.createContext({
  5. name: 'Guest',
  6. });
  7. class App extends React.Component {
  8. render() {
  9. const {signedInUser, theme} = this.props;
  10. // 提供初始 context 值的 App 组件,可以嵌套多个 Provider
  11. return (
  12. <ThemeContext.Provider value={theme}>
  13. <UserContext.Provider value={signedInUser}>
  14. <Layout />
  15. </UserContext.Provider>
  16. </ThemeContext.Provider>
  17. );
  18. }
  19. }
  20. function Layout() {
  21. return (
  22. <div>
  23. <Sidebar />
  24. <Content />
  25. </div>
  26. );
  27. }
  28. // 一个组件可能会消费多个 context,多个 render props 嵌套
  29. function Content() {
  30. return (
  31. <ThemeContext.Consumer>
  32. {theme => (
  33. <UserContext.Consumer>
  34. {user => (
  35. <ProfilePage user={user} theme={theme} />
  36. )}
  37. </UserContext.Consumer>
  38. )}
  39. </ThemeContext.Consumer>
  40. );
  41. }

10. hooks

hooks 是 React v16.8 引入的特性,它允许你在函数组件中使用 state 和某些特性。React 官方认为,类组件会让人难以理解,比如为什么要在事件函数里面重新绑定 this?在在多数情况下,也不可能将类组件拆分为更小的粒度,因为状态逻辑无处不在。这也给测试带来了一定挑战。
React Hooks 可以解决组件代码逻辑复用的问题,通过编写自定义 Hook 就能将一些通用逻辑封装起来。也可以解决多个状态嵌套的问题,比如我们上面的多个 context 对象嵌套,也可以编写自定义 Hook 将其平铺开。

10.1 useState

useState 接收一个值或者一个函数,它返回一个数组,数组包括传入的值、修改值的方法。

  1. const App = () => {
  2. const [count, setCount] = useState(0);
  3. return <h1 onClick={() => setCount(count + 1)}>{count}<h1>
  4. }

每次点击当前这个 h1,就会调用 setCount 方法来将 count 增加1,同时这个组件将会重新渲染。
你可能会问那每次渲染不就重新执行 useState 了吗?那 count 岂不是又变成了0?事实上不是这样的,React 会记录执行 useState 时候的下标,在下次渲染的时候会取前一次缓存起来的值,而非重新初始化。

正是因为如此,所以 React 官方建议只在最顶层使用 Hook,不要在循环,条件或嵌套函数中调用 Hook。

当 useState 接收一个函数的时候,函数返回值就是当前的 state 值。

  1. const App = () => {
  2. const [count, setCount] = useState(() => 0);
  3. return <h1 onClick={() => setCount(count + 1)}>{count}<h1>
  4. }

10.2 useEffect

由于函数组件只是一个函数,无法处理具有副作用(网络请求、DOM 操作等等)的场景,所以常常需要用到生命周期的钩子,这就意味着要用类组件。React 提供了 useEffect 方法来在函数组件中解决这个问题。useEffect 接收一个可能有副作用代码的函数。

以一个绑定 scroll 事件的组件为例,scroll 事件就是副作用,在普通函数组件中是无法处理这种副作用的,更何况 scroll 事件还需要手动清除。

  1. class App extends Component {
  2. onScroll() {}
  3. componentDidMount() {
  4. window.addEventListener('scroll', this.onScroll);
  5. }
  6. componentWillUnmout() {
  7. window.removeEventListener('scroll', this.onScroll);
  8. }
  9. }

但是如果使用了 useEffect,这个代码会变得异常简单。

  1. const App = () => {
  2. useEffect(() => {
  3. const onScroll = () => {};
  4. window.addEventListener('scroll', onScroll);
  5. return () => {
  6. window.removeEventListener('scroll', onScroll);
  7. }
  8. }, [])
  9. }

来一起理解一下 useEffect,它接收两个参数,第一个参数是一个函数,第二个参数是一个数组(也可以不传)。

如果第二个参数什么也不传,那么第一个参数就会在组件每一次渲染之后执行,而其返回函数则会在组件卸载前执行,就类似于 componentDidMount、componentDidUpdate 和 componentWillUnmout。

如果第二个参数传了一个空数组,那么第一个参数只会在组件第一次渲染之后执行,返回函数则也是在卸载前执行,就类似于 componentDidMount 和 componentWillUnmout。

除此之外,如果第二个参数传的是一个有值的数组,那么这个函数就只会在这个值改变后才执行。如果这个值没有变化,不管组件渲染多少次,这个函数都不会执行。

  1. const App = (props) => {
  2. useEffect(() => {
  3. console.log(props.name); // 每次传入新的 name 时就会打印
  4. }, [props.name])
  5. }

这种场景下的 useEffect 非常像 vue 里面的 watch,我们也可以基于此实现一个 watch。

  1. const watch = (value, cb) => {
  2. useEffect(cb, [value])
  3. }
  4. watch(props.name, () => console.log(props.name))

10.3 自定义 Hook

前面说过,对于通用的逻辑来说,可以编写自定义的 Hook 来提供给组件使用。一般自定义 Hook 命名都是以 use 开头,不遵循的话,由于无法判断某个函数是否包含对其内部 Hook 的调用,React 将无法自动检查你的 Hook 是否违反了 Hook 规则。

接下来就带你手把手实现一个 Hook。假设有这么一种场景,我的页面中有很多类似弹层的组件,如果点击弹层之外的区域,就需要关闭这个弹层,但是如果点击这个弹层,就不应该关闭。

所以我们需要对整个页面绑定点击事件,但需要排除当前这个组件的 DOM,这个功能就可以写成自定义 Hook 来复用。

  1. const useClickAway = (ref, cb) => {
  2. useEffect(() => {
  3. const clickHandler = (e) => {
  4. if (!e.target.contains(ref)) {
  5. cb();
  6. }
  7. }
  8. document.body.addEventListener('click', clickHandler);
  9. return () => {
  10. document.body.removeEventListener('click', clickHandler);
  11. }
  12. }, [])
  13. }

使用的时候,只要传入组件的 ref 和 关闭方法就行了。

  1. const App = () => {
  2. const [open, setOpen] = useState(true);
  3. const ref = createRef();
  4. useClickAway(ref, () => setOpen(false));
  5. return open ? <Modal ref={ref} /> : null;
  6. }

11. ref

在 React v16.3 之前,如果想要获取到 Ref,一般有这么两种方法。
直接规定一个 ref 的名字:

  1. class App extends Component {
  2. componentDidMount() {
  3. const ref = this.refs.container
  4. }
  5. render() {
  6. return <div ref="container"></div>
  7. }
  8. }

或者传给 ref 属性一个函数,来动态设置 Ref。

  1. class App extends Component {
  2. container = null;
  3. componentDidMount() {
  4. const ref = this.container
  5. }
  6. render() {
  7. return <div ref={r => this.container = r}></div>
  8. }
  9. }

React v16.3 提供了新的 createRef 和 forwardRef 来操作 Ref。

11.1 createRef

React.createRef 可以获取到 Ref 对象。

  1. class MyComponent extends React.Component {
  2. constructor(props) {
  3. super(props);
  4. this.inputRef = React.createRef();
  5. }
  6. render() {
  7. return <input type="text" ref={this.inputRef} />;
  8. }
  9. componentDidMount() {
  10. this.inputRef.current.focus();
  11. }
  12. }

11.2 forwardRef

React.forwardRef 会创建一个 React 组件,这个组件能够将其接受的 ref 属性转发给子组件。被 React.forwardRef 包裹的函数组件,第二个参数就是 Ref 对象。

  1. const FancyButton = React.forwardRef((props, ref) => (
  2. <button ref={ref} className="FancyButton">
  3. {props.children}
  4. </button>
  5. ));
  6. // 你可以直接获取到 button 的 ref
  7. const ref = React.createRef();
  8. <FancyButton ref={ref}>Click me!</FancyButton>;

forwardRef 一般用于以下两种场景:
1、转发 Ref 到 DOM 组件
一般来说,我们在使用组件的时候,不关心其内部实现细节,防止组件过度依赖其他组件的 DOM 结构。
但是在例如封装的 input 组件中,常常会需要管理输入框的焦点,这就需要能够获取到 input 的 ref。
2、在高阶组件中转发 Ref
如果我的多个组件都使用了某个高阶组件,但又需要获取到 Ref 对象,该怎么办?ref 属性并不能通过 props 来传下去,这样只能通过 forwardRef 来转发这个高阶组件的 ref。
具体实现思路就是,在组件外创建一个 Ref 对象,将这个对象通过 forwardRef 转发给这个高阶组件,高阶组件内部将这个 ref 当做 props 传给被包裹的组件的 ref 属性,这样就获取到了隐藏在高阶组件中的被包裹组件的 ref。

  1. function logProps(Component) {
  2. class LogProps extends React.Component {
  3. render() {
  4. const {forwardedRef, ...rest} = this.props;
  5. // 将自定义的 prop 属性 “forwardedRef” 定义为 ref
  6. return <Component ref={forwardedRef} {...rest} />;
  7. }
  8. }
  9. // 注意 React.forwardRef 回调的第二个参数 “ref”。
  10. // 我们可以将其作为常规 prop 属性传递给 LogProps,例如 “forwardedRef”
  11. // 然后它就可以被挂载到被 LogProps 包裹的子组件上。
  12. return React.forwardRef((props, ref) => {
  13. return <LogProps {...props} forwardedRef={ref} />;
  14. });
  15. }

12. Suspense / lazy

React v16.6 中新增了 lazy 和 Suspense 两个属性,主要用于代码分割。

12.1 lazy

React.lazy 函数能让你像渲染常规组件一样处理动态引入(的组件)。

  1. const OtherComponent = React.lazy(() => import('./OtherComponent'));

React.lazy 接受一个函数,这个函数需要使用动态 import。它返回一个 Promise,这个 Promise 需要 resolve 一个默认导出的 React 组件。
因此,如果你的 OtherComponent 如果不是默认导出的话,就只能使用一个中间文件来重新导出为默认模块。
这个代码会在第一次渲染的时候再导入 OtherComponent 这个文件。

12.2 Suspense

lazy 组件需要包裹在 Suspense 组件中渲染,这样允许我们在等待 lazy 组件加载的时候进行一些占位处理,比如在加载成功之前,给其设置一个占位的 Loading 组件。
fallback 是一个 ReactElement,会在加载成功前渲染。

  1. const OtherComponent = React.lazy(() => import('./OtherComponent'));
  2. function MyComponent() {
  3. return (
  4. <div>
  5. <Suspense fallback={<div>Loading...</div>}>
  6. <OtherComponent />
  7. </Suspense>
  8. </div>
  9. );
  10. }

注意:React.lazy 和 Suspense 还不支持服务端渲染。如果你想要在使用服务端渲染的应用中使用,推荐使用 Loadable Components 这个库。

13. memo

memo 等效于 PureComponent,后者是用于类组件,前者被提供用来函数组件。memo 可以帮助对 props 进行浅比较,减少一些不必要的 render。

  1. const App = React.memo((props) => {
  2. return <h1>{props.name}</h1>
  3. })

14. 推荐阅读

  1. whats-new-with-server-side-rendering-in-react-16
  2. 精读《React16 新特性》
添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注