专栏名称: 前端外刊评论
最新、最前沿的前端资讯,最有深入、最干前端相关的技术译文。
目录
相关文章推荐
前端早读课  ·  【第3453期】圈复杂度在转转前端质量体系中的应用 ·  22 小时前  
前端早读课  ·  【第3451期】前端 TypeError ... ·  2 天前  
51好读  ›  专栏  ›  前端外刊评论

React 16 新特性全解(上)

前端外刊评论  · 公众号  · 前端  · 2019-03-04 07:00

正文

前言

本次系列分上下两篇文章,上主要介绍从v16.0~ 16.4的新特性,下主要介绍16.5~16.8。下面就开始吧~

本篇文章较长预计需要15min(当然主要是因为demo太多),大家可以搞点瓜子边啃边看。最好能留出一只手自己在codePen上自己调试一下。

目录

v16.0

  1. render 支持返回数组和字符串 演示

  2. Error Boundary

  3. createPortal

  4. 支持自定义 DOM 属性

  5. Fiber

  6. 提升SSR渲染速度

  7. 减小文件体积

v16.1

react-call-return

v16.2

Fragment

v16.3

  1. 生命周期函数的更新

  2. createContext

  3. createRef

  4. forwardRef

  5. strict Mode

下面就开始吧~

v16.0

主要特性:

一、render可以返回字符串,数组,数字

React 15: 只可以返回单一组件,也就是说即使你返回的是一个string,也需要用div包住。

  1. function MyComponent() {

  2. return (



  3. hello world



  4. );

  5. }

React 16: 支持返回这五类:React elements, 数组和Fragments,Portal,String/numbers,boolean/null。

  1. class Example extends React.Component {

  2. render() {

  3. return [


  4. first element

  5. ,


  6. second element

  7. ,

  8. ];

  9. }

  10. }

注意:无论返回的形式是怎么样的,都要保持render是一个纯函数。所以要求我们不要改state的状态,同时不要直接跟浏览器直接交互,让它每次调用生成的结果都是一致的。

二、Error boundary(错误边界)

React 15:渲染过程中有出错,直接crash整个页面,并且错误信息不明确,可读性差

  1. class BuggyCounter extends React.Component {

  2. constructor(props) {

  3. super(props);

  4. this.state = { counter: 0 };

  5. this.handleClick = this.handleClick.bind(this);

  6. }


  7. componentWillMount() {

  8. throw new Error('I am crash');

  9. }


  10. handleClick() {

  11. this.setState(({counter}) => ({

  12. counter: counter + 1

  13. }));

  14. }


  15. render() {

  16. if (this.state.counter === 5) {

  17. // Simulate a JS error

  18. throw new Error('I crashed!');

  19. }

  20. return

  21. # {this.state.counter}

  22. ;

  23. }

  24. }


  25. function App() {

  26. return (




  27. This is an example of error boundaries in React 16.




  28. Click on the numbers to increase the counters.



  29. The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.



  30. ---


  31. These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.


  32. ---



  33. );

  34. }


  35. ReactDOM.render (

  36. ,

  37. document.getElementById('root')

  38. );

demo地址

比如上面这个App,可以看到子组件BuggyCounter出了点问题,在没有Error Boundary的时候,整个App都会crash掉,所以显示白屏。

React 16:用于捕获子组件树的JS异常(即错误边界只可以捕获组件在树中比他低的组件错误。),记录错误并展示一个回退的UI。

捕获范围:

  1. 渲染期间

  2. 生命周期内

  3. 整个组件树构造函数内

如何使用:

  1. // 先定一个组件ErrorBoundary

  2. class ErrorBoundary extends React.Component {

  3. constructor(props) {

  4. super(props);

  5. this.state = { error: null, errorInfo: null };

  6. }


  7. componentDidCatch(error, errorInfo) {

  8. // Catch errors in any components below and re-render with error message

  9. this.setState({

  10. error: error,

  11. errorInfo: errorInfo

  12. })

  13. // You can also log error messages to an error reporting service here

  14. }


  15. render() {

  16. // 有错误的时候展示回退

  17. if (this.state.errorInfo) {

  18. // Error path

  19. return (



  20. ## Something went wrong.


  21. {this.state.error && this.state.error.toString()}



  22. {this.state. errorInfo.componentStack}



  23. );

  24. }

  25. // 正常的话,直接展示组件

  26. return this.props.children;

  27. }

  28. }


  29. class BuggyCounter extends React.Component {

  30. constructor(props) {

  31. super(props);

  32. this.state = { counter: 0 };

  33. this.handleClick = this.handleClick.bind(this);

  34. }


  35. componentWillMount () {

  36. throw new Error('I am crash');

  37. }


  38. handleClick() {

  39. this.setState(({counter}) => ({

  40. counter: counter + 1

  41. }));

  42. }


  43. render() {

  44. if (this.state.counter === 5) {

  45. // Simulate a JS error

  46. throw new Error('I crashed!');

  47. }

  48. return

  49. # {this.state.counter}

  50. ;

  51. }

  52. }


  53. function App() {

  54. return (




  55. This is an example of error boundaries in React 16.




  56. Click on the numbers to increase the counters.



  57. The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.



  58. ---


  59. These two counters are inside the same error boundary. If one crashes, the error boundary will replace both of them.


  60. ---



  61. );

  62. }


  63. ReactDOM.render(

  64. ,

  65. document.getElementById('root')

  66. );

demo演示:

可以看到加上Error Boundary之后,除了出错的组件,其他的地方都不受影响。 而且它很清晰的告诉我们是哪个组件发生了错误。

注意事项:

Error Boundary无法捕获下面的错误:

1、事件函数里的错误

  1. class MyComponent extends React.Component {

  2. constructor(props) {

  3. super(props);

  4. this.state = { error: null };

  5. this.handleClick = this.handleClick.bind(this);

  6. }


  7. handleClick() {

  8. try {

  9. // Do something that could throw

  10. } catch (error) {

  11. this.setState({ error });

  12. }

  13. }


  14. render() {

  15. if (this.state.error) {

  16. return

  17. # Caught an error.


  18. }

  19. return

  20. Click Me


  21. }

  22. }

上面的例子中,handleClick方法里面发生的错误,Error Boundary是捕获不道德。因为它不发生在渲染阶段,所以采用try/catch来捕获。

2、异步代码(例如setTimeout 或 requestAnimationFrame 回调函数)

  1. class A extends React.Component {

  2. render() {

  3. // 此错误无法被捕获,渲染时组件正常返回 ``

  4. setTimeout(() => {

  5. throw new Error('error')

  6. }, 1000)

  7. return (


  8. )

  9. }

  10. }

3、服务端渲染

因为服务器渲染不支持Error Boundary

4、Error Boundary自身抛出来的错误 (而不是其子组件)

那这里还遗留一个问题?错误边界放在哪里。一般来说,有两个地方:

1、可以放在顶层,告诉用户有东西出错。但是我个人不建议这样,这感觉失去了错误边界的意义。因为有一个组件出错了,其他正常的也没办法正常显示了

2、包在子组件外面,保护其他应用不崩溃。

三、react portal

在介绍这个新特性之前,我们先来看看为什么需要portal。在没有portal之前,如果我们需要写一个Dialog组件,我们会这样写。

  1. ...


  2. { needDialog ? : null }

问题:

1、最终渲染产生的html存在于JSX产生的HTML在一起,这时候dialog 如果需要position:absolute 控制位置的话,需要保证dialog 往上没有position:relative 的干扰。

2、层级关系不清晰,dialog实际是独立在app之外的。

所以这时候Portal降临。

Portal可以帮助我们在JSX中跟普通组件一样直接使用dialog, 但是又可以让dialog内容层级不在父组件内,而是显示在独立于原来app在外的同层级组件。

如何使用:

HTML:

  1. // 这里为我们定义Dialog想要放入的位置

JS:

  1. // These two containers are siblings in the DOM

  2. const appRoot = document.getElementById('app-root');

  3. const modalRoot = document.getElementById('modal-root');


  4. // Let's create a Modal component that is an abstraction around

  5. // the portal API.

  6. class Modal extends React.Component {

  7. constructor(props) {

  8. super(props);

  9. // Create a div that we'll render the modal into. Because each

  10. // Modal component has its own element, we can render multiple

  11. // modal components into the modal container.

  12. this .el = document.createElement('div');

  13. }


  14. componentDidMount() {

  15. // Append the element into the DOM on mount. We'll render

  16. // into the modal container element (see the HTML tab).

  17. // 这边会将我们生成的portal element插入到modal-root里。

  18. modalRoot.appendChild(this.el);

  19. }


  20. componentWillUnmount() {

  21. // Remove the element from the DOM when we unmount

  22. modalRoot.removeChild(this.el);

  23. }


  24. render() {

  25. // Use a portal to render the children into the element

  26. return ReactDOM.createPortal(

  27. // Any valid React child: JSX, strings, arrays, etc.

  28. this.props.children,

  29. // A DOM element

  30. this.el,

  31. );

  32. }

  33. }


  34. // The Modal component is a normal React component, so we can

  35. // render it wherever we like without needing to know that it's

  36. // implemented with portals.

  37. class App extends React.Component {

  38. constructor(props) {

  39. super(props);

  40. this.state = {showModal: false};


  41. this.handleShow = this.handleShow.bind(this );

  42. this.handleHide = this.handleHide.bind(this);

  43. }


  44. handleShow() {

  45. this.setState({showModal: true});

  46. }


  47. handleHide() {

  48. this.setState({showModal: false});

  49. }


  50. render() {

  51. // Show a Modal on click.

  52. // (In a real app, don't forget to use ARIA attributes

  53. // for accessibility!)

  54. const modal = this.state.showModal ? (





  55. With a portal, we can render content into a different

  56. part of the DOM, as if it were any other React child.



  57. This is being rendered inside the #modal-container div.

  58. Hide modal



  59. ) : null;


  60. return (



  61. This div has overflow: hidden.

  62. Show modal

  63. {modal}



  64. );

  65. }

  66. }


  67. ReactDOM.render(, appRoot);

Example: Portalscodepen.io 没有portal生成与有portal的时候生成的层级关系如下:

Example: Portalscodepen.io 可以很清楚的看到,使用portal之后,modal不在嵌在app-root里。

四、自定义DOM属性

React 15:忽略未标准化的html 和 svg属性

React 16:去掉了这个限制

为什么要做这个改动呢?两个原因:

  1. 不能用自定义属性,对于非标准(proposal阶段)新属性还有其他框架(Angular)很不友好

  2. React 15之所以可以过滤掉非标准的属性,是因为他们维护了一个白名单的文件(放在bundle size 里)。而随着时间的增加,标准化的属性越来越多,意味着要一直维护这个文件,同时这个文件也会越来越大,增加bundle的体积。

所以还不如去掉这个限制。

演示 可以看到自定义属性已经生效了。

五、优化SSR

具体优化了下面五个方面:

  1. 生成更简洁的HTML

  2. 宽松的客户端一致性校验

  3. 无需提前编译

  4. react 16服务端渲染速度更快

  5. 支持流式渲染

1、生成更简洁的HTML

先看下面的HTML,react 15与react 16的服务端分别会生成什么。

  1. renderToString(



  2. This is some server-generatedHTML.

  3. );

react15:

有data-reactid, text noded ,react-text各种属性。

  1. This is some server-generatedHTML.

react 16:

  1. This is some server-generatedHTML.

可以看到,react 16去掉了很多属性,它的好处很明显:增加易读性,同时很大程度上减少html的文件大小。

2、宽松的客户端一致性校验

react 15:会将SSR的结果与客户端生成的做一个个字节的对比校验 ,一点不匹配发出waring同时就替换整个SSR生成的树。

react 16:对比校验会更宽松一些,比如,react 16允许属性顺序不一致,而且遇到不匹配的标签,还会做子树的修改,不是整个替换。

注意点: react16不会自动fix SSR 属性跟client html属性的不同,但是仍然会报waring,所以我们需要自己手动去修改。

3、无需提前编译

react 15:如果你直接使用SSR,会有很多需要检查procee.env的地方,但是读取在node中读取process.env是很消耗时间的。所以在react 15的时候,需要提前编译,这样就可以移除 process.env的引用。

react 16:只有一次检查process.env的地方,所以就不需要提前编译了,可以开箱即用。

4、react 16服务端渲染速度更快

为什么呢? 因为react 15下,server client都需要生成vDOM,但是其实在服务端, 当我们使用renderToString的时候,生成的vDom就会被立即抛弃掉, 所以在server端生成vDom是没有意义的。 5、支持流式渲染

会提升首个字节到的速度,不过我试了一下,会闪屏,所以我不太推荐使用 除非我们的页面做到一个空的框架先来,内容在填充

新的API server: renderyToNodeStream, renderToStaticNodeStream (renderToString, renderToStaticMarkup) client: hydyate

如何使用:

React 15:

  1. // server:

  2. // using Express client

  3. import { renderToString } from "react-dom/server"

  4. import MyPage from "./MyPage"

  5. app.get("/", (req, res) => {

  6. res.write("My Page");

  7. res.write("

  8. ");

  9. res.write(renderToString());

  10. res.write("

  11. ");

  12. res.end();

  13. });


  14. // client

  15. import { render } from "react-dom"

  16. import MyPage from "./MyPage"

  17. render(, document.getElementById("content"));

React 16:

其实就是吧client端的render改成hydrate。

  1. // client

  2. import { hydrate } from "react-dom"

  3. import MyPage from "./MyPage"

  4. hydrate(, document.getElementById("content"));

当然,现在依然兼容render,但是17之后不再兼容,所以还是直接用hydrate好一点。

注意事项:不支持ErrorBoundary 跟Portal,所以需要直出的页面就不能用了。

五、减小了32%bundle的体积

React 库大小从 20.7kb(压缩后 6.9kb)降低到 5.3kb(压缩后 2.2kb)

ReactDOM 库大小从 141kb(压缩后 42.9kb)降低到 103.7kb(压缩后 32.6kb)

React + ReactDOM 库大小从 161.7kb(压缩后 49.8kb)降低到 109kb(压缩后 43.8kb)

六、Fiber

由于Fiber不是新的API,是react对于对比更新的一种新算法,它影响着生命周期函数的变化跟异步渲染。需要详细了解的同学可以戳下面的链接,这应该是我看过最易懂得解释Fiber得视频。https://www.youtube.com/watch?v=VLAqywvHpD0www.youtube.com

v16.1

react-call-return

这就是一个库,平时用的比较少,所以暂时不讲。

v16.2

主要特性:Fragement

React 15:render函数只能接受一个组件,所以一定要外层包一层


React16:可以通过Fragement直接返回多个组件。

  1. render () {

  2. return (

  3. <>



  4. );

  5. }

但是这样看起来,似乎可以用v16.0 return一个数组搞定。

但是返回数组是有缺点的,比如:这段html

  1. Some text.


  2. ## A heading


  3. More text.


  4. ## Another heading


  5. Even more text.

用Fragment写,方便又快捷:

  1. render() {

  2. return (

  3. // Extraneous div element :(


  4. Some text.


  5. ## A heading


  6. More text.


  7. ## Another heading


  8. Even more text .


  9. );

  10. }

用数组写.... 一言难尽(什么,你还没看出来有什么区别!下面我来带你)

  1. render() {

  2. return [

  3. "Some text.",


  4. ## A heading

  5. ,

  6. "More text.",


  7. ## Another heading

  8. ,

  9. "Even more text."

  10. ];

  11. }

缺点:

  • 数组里的子节点必须要用逗号分离

  • 数组里的子节点必须要带key防止waring

  • string类型要用双引号括住

所以,Fragement还是很大程度上给我们提供了便利。

注意点:

<> 不支持写入属性,包括keys。如果你需要keys,你可以直接使用 (但是Fragment也只可以接受keys这一个属性,将来会支持更多)

  1. function Glossary(props) {

  2. return (


  3. {props.items. map(item => (

  4. // Without the `key`, React will fire a key warning

  5. {item.term}{item.description}

  6. ))}


  7. );

  8. }

官方演示

16.3

一、新的生命周期函数

由于异步渲染的改动,有可能会导致componentWillMount, componentWillReceiveProps,componentWillUpdate ,所以需要抛弃三个函数。

由于这是一个很大的改变会影响很多现有的组件,所以需要慢慢的去改。 目前react 16 只是会报waring,在react 17你就只能在前面加"UNSAFE_" 的前缀 来使用。不能不说react团队真是太贴心了,他们还写了一个脚本自动帮你加上 这些前缀。疯狂打call~

同时新加了两个生命周期函数来替代他们,分别是:

getDerivedStateFromProps:这个方法用于替代componentWillReceiveProps,相关内容可以看这篇文章,但是大多数情况下,都不需要用到这两种方法。 因为你都可以用其他办法来替代。

而getSnapshotBeforeUpate使用的场景很少,这里就不介绍了。

二、新的context API

1、context 就是可以使用全局的变量,不需要一层层pass props下去,比如主题颜色

  1. // Context lets us pass a value deep into the component tree

  2. // without explicitly threading it through every component.

  3. // Create a context for the current theme (with "light" as the default).

  4. const ThemeContext = React.createContext('light');


  5. class App extends React.Component {

  6. render() {

  7. // Use a Provider to pass the current theme to the tree below.

  8. // Any component can read it, no matter how deep it is.

  9. // In this example, we're passing "dark" as the current value.

  10. return (


  11. );

  12. }

  13. }


  14. // A component in the middle doesn't have to

  15. // pass the theme down explicitly anymore.

  16. function Toolbar(props) {

  17. return (








请到「今天看啥」查看全文