React 17 的重要更新

React 17 版本没有产生新的特性,是一个为了后续版本升级更方便、安全的垫脚石。其中比较重要升级点如下:

  • 事件委托。从挂在到 document 上,变更为挂在到 rootNode (React应用挂载的根节点)上。
    React 17 的事件委托
    这使得当存在多个不同版本的 React 树嵌套,或者嵌入到其他程序构建的应用程序中时,不会破坏 e.stopPropagation()
    另外,React 17 的事件传递更接近原生 DOM 。具体表现为在 React 16 及更早版本中,即使你在 React 事件中调用了 e.stopPropagation()document 监听器器仍然会接收到,这是因为原生事件已经处于 document 层级了。在 React 17 中传递将会按要求停止,不会再触发 document 对应的事件。
  • 新的 JSX 转换。与 Babel 合作,提供一种全新版本的 JSX 转换方式——无需额外导入 React 。

    1
    2
    3
    4
    5
    import React from 'react';

    function App() {
    return <h1>Hello World</h1>;
    }

    旧的 JSX 转换方式会将它转成下列代码:

    1
    2
    3
    4
    5
    import React from 'react';

    function App() {
    return React.createElement('h1', null, 'Hello world');
    }

    而在 React 17 中,源码可以写成这样:

    1
    2
    3
    function App() {
    return <h1>Hello World</h1>;
    }

    新的 JSX 转换方式会将它转成下列代码:

    1
    2
    3
    4
    5
    6
    // Inserted by a compiler (don't import it yourself!)
    import {jsx as _jsx} from 'react/jsx-runtime';

    function App() {
    return _jsx('h1', { children: 'Hello world' });
    }

    在升级到 React 17 过程中,如果想批量移除无用的 React 导入,可以在你的项目中执行 npx react-codemod update-react-imports ,即可批量移除:

    1
    2
    3
    4
    5
    import React from 'react';

    function App() {
    return <h1>Hello World</h1>;
    }

    将会被替换成

    1
    2
    3
    function App() {
    return <h1>Hello World</h1>;
    }

    如果使用了其他如 Hook 的 React 导入:

    1
    2
    3
    4
    5
    6
    import React from 'react';

    function App() {
    const [text, setText] = React.useState('Hello World');
    return <h1>{text}</h1>;
    }

    将会被替换成

    1
    2
    3
    4
    5
    6
    import { useState } from 'react';

    function App() {
    const [text, setText] = React.useState('Hello World');
    return <h1>{text}</h1>;
    }
  • 移除了事件池
    React 在旧浏览器中为了性能而重用不同事件之间的事件对象,并在它们之间将所有事件字段设置为null。在 React 16 及更早版本,你必须调用 e.persist() 才能正确使用事件,否则需要提前读取需要的属性。而现代浏览器中,这个性能优化并没有作用,所以 React 17 已经彻底移除了“事件池”

  • 异步执行 useEffect 清理函数
    此前 useEffect 在组件卸载时,是同步执行的,类似于 componentWillUnmount ,这对于大型应用程序来说并不理想,因为它会减慢大屏过渡的速度。在 React 17 中这个过程将会变成异步执行,即在更新已渲染完成后执行。若想要保持同步执行,那么可以使用 useLayoutEffect 来代替。当然,不必担心的是, React 17 会保证在执行任何新的副作用之前执行完所有副作用的清理函数。
    当然这个改动会有潜在的问题,当组件中使用了 ref 来执行一些清理操作,如下代码所示:

    1
    2
    3
    4
    5
    6
    useEffect(() => {
    someRef.current.someSetupMethod();
    return () => {
    someRef.current.someCleanupMethod();
    };
    });

    由于 ref 的值是可变的,所以在异步调用时,这个 ref 的值已经被设置为 null 了,也就意味着这个清理函数并无法执行。那么解决方案除了使用上述的 useLayoutEffect 同步执行以外,还可以这样:

    1
    2
    3
    4
    5
    6
    7
    useEffect(() => {
    const instance = someRef.current;
    instance.someSetupMethod();
    return () => {
    instance.someCleanupMethod();
    };
    });

    将 ref 的值的引用赋值到一个新的变量,那么在清理函数执行时,可以保证这个引用还存在在内存中,可以调用到里面的方法。

  • render 中返回 undefined 时一致的报错表现
    此前, React 只在 class 组件或函数组件中做了“返回 undefined”的检查,错误遗漏了 forwardRefmemo 组件, React 17 中已经将这个错误修复了。如果有意返回空内容,可以返回 null
  • 更好的组件堆栈跟踪。报错时,不再是简单的 JS 堆栈,而是可以显示出具体的 React 组件堆栈信息,并可以快速定位到。