React最新16.9,支持组件性能评估!

avatar
@腾讯科技(深圳)有限公司

概述

React团队在8月9日发布了最新的16.9版本,本文简单介绍一下在新版本中都有哪些变更和需要注意的地方。

关键变更如下:

  • 在16.9版本中使用componentWillMountcomponentWillReceivePropscomponentWillUpdate将会收到React发出的警告。
  • 为大型React应用提供React.Profiler以进行性能评估
  • 使用javascript:形式的url,React将抛出warning,并且这种写法在未来的主要版本中会被禁止。
  • 废弃 Factory 组件
  • 用于测试的 act()方法正式支持异步

Unsafe 生命周期

在16.3版本时,React团队就讨论过这三个生命周期潜在的问题,并且在16.3版本中将加入UNSAFE_前缀作为他们的别名,按照当时定下的计划,将会在16.9中抛出warning,并且在17.0的大版本中彻底移除componentWillMount这三个生命周期。

  • componentWillMount → UNSAFE_componentWillMount
  • componentWillReceiveProps → UNSAFE_componentWillReceiveProps
  • componentWillUpdate → UNSAFE_componentWillUpdate
对于老项目来说意味着什么?

其实没什么太大的影响,官方保证即便在17.0中,使用UNSAFE_的生命周期也可以正常使用,也只是生命周期函数名字变更了而已。想要在老项目升级时避免抛出warning,可以手动变更函数名。当然官方为也可以使用官方提供的工具codemod来一键变更:

cd your_project
npx react-codemod rename-unsafe-lifecycles

开发团队也可以在项目中加入严格模式(Strict Mode)<React.StrictMode>来禁止使用这类有潜在风险的生命周期。

使用React.Profiler进行性能评估

在这次React 16.9更新中,提供了一种通过编程的方式来收集测量的代码的方式,<React.Profiler>通常在大型的React项目中会使用到。

它可以作为一个节点添加在React应用的任意一处,并且能评估React 应用程序渲染的频率以及渲染的 “成本”。其目的是帮助标识应用程序中渲染缓慢的部分,并可能会更易于进行 memoization 等优化

render(
  <App>
    <Profiler id="Navigation" onRender={callback}>
      <Navigation {...props} />
    </Profiler>
    <Main {...props} />
  </App>
);

Profiler可以在多处使用,也可以嵌套使用。它接受两个参数id和onRender,onRender会在React更新的commit阶段,也就是内部更新的最后一个阶段,在这个阶段React会将所有的更新变现,反馈到DOM上去。在onRender触发时也会带回来一些关于本次更新的性能参数:

  • id, 用于区分多个Pofiler,由props传入
  • phase, 值为 "mount" 或者 "update" ,表示当前组件树是第一次挂载(mount)还是处于更新周期(update)
  • actualDuration, 当前组件树更新所花费的时间,使用了一些组件的缓存方法例如React.memo可以看到较为明显的减少
  • baseDuration, 初始挂载组件树的时间,可以理解为没有任何优化情况下的渲染所花费的时间
  • startTime, 本轮更新的初始时间戳
  • commitTime, 本轮更新的结束时间戳(到达commit阶段截止)
  • interactions 本轮更新的调度堆栈

有了如上组件更新的回调信息,我们可以更加精细地判断使用的优化方法所带来的收益。

需要注意的是Profiler即便是一个轻量级的组件,但是依然会有性能和计算开销,不推荐在生产环境使用。

用于测试的 act()方法正式支持异步

react官方提供了一个用于测试组件的内置库react-dom/test-utils,为了更好地在测试环境模仿浏览器和用户的真实行为以及应社区的意愿为背景下,官方团队赋予act()异步调用和集中处理state变更的能力。 在以前的版本中,act()中写异步代码(异步状态更新)将会抛出如下警告

An update to SomeComponent inside a test was not wrapped in act(...).

在 React 16.9 中, act() 也支持了异步函数, 并且可以使用await

await act(async () => {
  // ...
});

React团队是非常推荐大家为自己组件提供测试用例的,在这篇文章中提供了一些测试技巧和应用场景以及使用act()的地方,也包括对hooks的测试场景,比如测试一个hook的事件:

import React, { useState } from "react";

export default function Toggle(props) {
  const [state, setState] = useState(false);
  return (
    <button
      onClick={() => {
        setState(previousState => !previousState);
        props.onChange(!state);
      }}
      data-testid="toggle"
    >
      {state === true ? "Turn off" : "Turn on"}
    </button>
  );
}

测试用例如下

import React from "react";
import { render, unmountComponentAtNode } from "react-dom";
import { act } from "react-dom/test-utils";

import Toggle from "./toggle";

let container = null;
beforeEach(() => {
  // setup a DOM element as a render target
  container = document.createElement("div");
  // container *must* be attached to document so events work correctly.
  document.body.appendChild(container);
});

afterEach(() => {
  // cleanup on exiting
  unmountComponentAtNode(container);
  container.remove();
  container = null;
});

it("changes value when clicked", () => {
  const onChange = jest.fn();
  act(() => {
    render(<Toggle onChange={onChange} />, container);
  });

  // get a hold of the button element, and trigger some clicks on it
  const button = document.querySelector("[data-testid=toggle]");
  expect(button.innerHTML).toBe("Turn off");

  act(() => {
    button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
  });

  expect(onChange).toHaveBeenCalledTimes(1);
  expect(button.innerHTML).toBe("Turn on");

  act(() => {
    for (let i = 0; i < 5; i++) {
      button.dispatchEvent(new MouseEvent("click", { bubbles: true }));
    }
  });

  expect(onChange).toHaveBeenCalledTimes(6);
  expect(button.innerHTML).toBe("Turn on");
});

这些示例采用了原生 DOM API,但也可以使用 React Testing Library来减少样板代码。它的许多方法已经通过 act() 进行了实现

弃用 javascript: 形式的不安全 URL

a标签的href如果使用javascript:的写法,在16.9版本中继续使用这种写法React将会抛出警告

const userProfile = {
  website: "javascript: alert('you got hacked')",
};
// This will now warn:
<a href={userProfile.website}>Profile</a>

并且该写法将会在未来的主要版本中会抛出错误,也就是将会禁止这种易产生安全漏洞的写法

废弃 Factory 组件

在 Babel还没有成为 JavaScript Class 的主流编译工具以前,可以在React中采用"factory" 的写法来创建组件,该组件使用 render 方法返回一个对象

function FactoryComponent() {
  return { render() { return <div />; } }
}

这种方式令人迷惑,因为它看起来像函数组件 ,然而它并不是。 React支持它会导致库变大且变慢。因此,在 16.9 中正在弃用此模式,并在遇到警告时输出警告。如果项目中依赖了此组件,可以通过添加 FactoryComponent.prototype=React.Component.prototype 来做兼容。

changlog

React

  • 提供 <React.Profiler> API 实现以编程的方式进行性能评估。(@bvaughn in #15172)

  • 删除 unstable_ConcurrentMode 以支持 unstable_createRoot。(@acdlite in #15532)

React DOM

  • 弃用以 UNSAFE_* 开头的旧生命周期方法。(@bvaughn in #15186 and @threepointone in #16103)

  • 弃用 javascript: 形式的 URL。(@sebmarkbage in #15047)

  • 弃用不常用的 "module pattern" (factory) 组件。(@sebmarkbage in #15145)

  • 在video 组件上添加对 disablePictureInPicture 属性的支持。(@eek in #15334)

  • 为 embed 添加对 onLoad 事件的支持。(@cherniavskii in #15614)

  • 为在 DevTools 中编辑 useState 的 state 提供支持。(@bvaughn in #14906)

  • 为在 DevTools 中切换 Suspense 提供支持。(@gaearon in #15232)

  • 当 setState 在 useEffect 中循环调用时,发出警告。(@gaearon in #15180)

  • 修复内存泄露。(@paulshen in #16115)

  • 修复 Suspense 包裹的组件中使用 findDOMNode 发生崩溃的问题。(@acdlite in #15312)

  • 修复因为刷新太晚而导致 pending effect 的情况。(@acdlite in #15650)

  • 修复警告信息中不正确的参数顺序。(@brickspert in #15345)

  • 修复当存在 !important 样式时,隐藏 Suspense 降级节点的问题。(@acdlite in #15861 and #15882)

  • 提高 hydration 的性能。(@bmeurer in #15998)

React DOM Server

  • 修复 camelCase 自定义 CSS 属性名称的错误输出。(@bedakb in #16167)

  • React Test Utilities and Test Renderer

  • 添加 act(async()=>...) 来测试异步状态更新。(@threepointone in #14853)

  • 添加对不同渲染器嵌套 act 的支持。(@threepointone in #16039 and #16042)

  • 在严格模式下,如果副作用函数在 act 之外被调用,就会发出警告。(@threepointone in #15763 and #16041)

  • 当在错误的渲染器中使用 act 时发出警告。(@threepointone in #15756)


关注【IVWEB社区】公众号获取每周最新文章,通往人生之巅!