React 16 新特性全解(上)

本次系列分上下两篇文章,上主要介绍从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包住。

function MyComponent() {
  return (
    
hello world
); }

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

class Example extends React.Component {
  render() {
    return [
      
first element
,
second element
, ]; } }

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

二、Error boundary(错误边界)

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

class BuggyCounter extends React.Component {
  constructor(props) {
    super(props);
    this.state = { counter: 0 };
    this.handleClick = this.handleClick.bind(this);
  }
  
  componentWillMount() {
    throw new Error('I am crash');
  }
  
  handleClick() {
    this.setState(({counter}) => ({
      counter: counter + 1
    }));
  }
  
  render() {
    if (this.state.counter === 5) {
      // Simulate a JS error
      throw new Error('I crashed!');
    }
    return 

{this.state.counter}

; } } function App() { return (

This is an example of error boundaries in React 16.

Click on the numbers to increase the counters.
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.


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


); } ReactDOM.render( , document.getElementById('root') );

demo地址

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

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

捕获范围:

  1. 渲染期间

  2. 生命周期内

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

如何使用:

// 先定一个组件ErrorBoundary
class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null, errorInfo: null };
  }
  
  componentDidCatch(error, errorInfo) {
    // Catch errors in any components below and re-render with error message
    this.setState({
      error: error,
      errorInfo: errorInfo
    })
    // You can also log error messages to an error reporting service here
  }
  
  render() {
    // 有错误的时候展示回退
    if (this.state.errorInfo) {
      // Error path
      return (
        

Something went wrong.

{this.state.error && this.state.error.toString()}
{this.state.errorInfo.componentStack}
); } // 正常的话,直接展示组件 return this.props.children; } } class BuggyCounter extends React.Component { constructor(props) { super(props); this.state = { counter: 0 }; this.handleClick = this.handleClick.bind(this); } componentWillMount() { throw new Error('I am crash'); } handleClick() { this.setState(({counter}) => ({ counter: counter + 1 })); } render() { if (this.state.counter === 5) { // Simulate a JS error throw new Error('I crashed!'); } return

{this.state.counter}

; } } function App() { return (

This is an example of error boundaries in React 16.

Click on the numbers to increase the counters.
The counter is programmed to throw when it reaches 5. This simulates a JavaScript error in a component.


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


); } ReactDOM.render( , document.getElementById('root') );

可以看到加上Error Boundary之后,除了出错的组件,其他的地方都不受影响。

React 16 新特性全解(上)_第1张图片

而且它很清晰的告诉我们是哪个组件发生了错误。

注意事项:

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

1、事件函数里的错误

class MyComponent extends React.Component {
  constructor(props) {
    super(props);
    this.state = { error: null };
    this.handleClick = this.handleClick.bind(this);
  }

  handleClick() {
    try {
      // Do something that could throw
    } catch (error) {
      this.setState({ error });
    }
  }

  render() {
    if (this.state.error) {
      return 

Caught an error.

} return
Click Me
} }

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

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

class A extends React.Component {
     render() {
        // 此错误无法被捕获,渲染时组件正常返回 `
` setTimeout(() => { throw new Error('error') }, 1000) return (
) } }

3、服务端渲染

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

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

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

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

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

三、react portal

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

...
{ needDialog ? : null }

问题:

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

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

所以这时候Portal降临。

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

如何使用:

HTML:

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

JS:

// These two containers are siblings in the DOM
const appRoot = document.getElementById('app-root');
const modalRoot = document.getElementById('modal-root');

// Let's create a Modal component that is an abstraction around
// the portal API.
class Modal extends React.Component {
  constructor(props) {
    super(props);
    // Create a div that we'll render the modal into. Because each
    // Modal component has its own element, we can render multiple
    // modal components into the modal container.
    this.el = document.createElement('div');
  }

  componentDidMount() {
    // Append the element into the DOM on mount. We'll render
    // into the modal container element (see the HTML tab).
    // 这边会将我们生成的portal element插入到modal-root里。
    modalRoot.appendChild(this.el);
  }

  componentWillUnmount() {
    // Remove the element from the DOM when we unmount
    modalRoot.removeChild(this.el);
  }
  
  render() {
    // Use a portal to render the children into the element
    return ReactDOM.createPortal(
      // Any valid React child: JSX, strings, arrays, etc.
      this.props.children,
      // A DOM element
      this.el,
    );
  }
}

// The Modal component is a normal React component, so we can
// render it wherever we like without needing to know that it's
// implemented with portals.
class App extends React.Component {
  constructor(props) {
    super(props);
    this.state = {showModal: false};
    
    this.handleShow = this.handleShow.bind(this);
    this.handleHide = this.handleHide.bind(this);
  }

  handleShow() {
    this.setState({showModal: true});
  }
  
  handleHide() {
    this.setState({showModal: false});
  }

  render() {
    // Show a Modal on click.
    // (In a real app, don't forget to use ARIA attributes
    // for accessibility!)
    const modal = this.state.showModal ? (
        //注意~~~~~~~~~~~~~这里可以自行加上这个调试
        // 
        
With a portal, we can render content into a different part of the DOM, as if it were any other React child.
This is being rendered inside the #modal-container div.
//
) : null; return (
This div has overflow: hidden. {modal}
); } } ReactDOM.render(, appRoot);

Example: Portalscodepen.ioReact 16 新特性全解(上)_第2张图片

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

React 16 新特性全解(上)_第3张图片
React 16 新特性全解(上)_第4张图片

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

四、自定义DOM属性

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

React 16:去掉了这个限制

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

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

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

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

React 16 新特性全解(上)_第5张图片

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

五、优化SSR

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

  1. 生成更简洁的HTML

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

  3. 无需提前编译

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

  5. 支持流式渲染

1、生成更简洁的HTML

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

renderToString(  
 
This is some server-generated HTML.
);

react15:

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

This is some server-generated HTML.

react 16:

This is some server-generated HTML.

可以看到,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是没有意义的。

React 16 新特性全解(上)_第6张图片 图片来源(https://hackernoon.com/whats-new-with-server-side-rendering-in-react-16-9b0d78585d67)

5、支持流式渲染 (renderyToNodeStream)

使用流式渲染会提升首个字节到(TTFB)的速度。但是什么是流式渲染呢?

可以理解为内容以一种流的形式传给前端。所以在下一部分的内容被生成之前,开头的内容就已经被发到浏览器端了。这样浏览器就可以更早的编译渲染文件内容。

// using Express
import { renderToNodeStream } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("My Page");
  res.write("
"); const stream = renderToNodeStream(); stream.pipe(res, { end: false }); stream.on('end', () => { res.write("
"); res.end(); }); });

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

如何使用:

React 15:

// server:
// using Express client
import { renderToString } from "react-dom/server"
import MyPage from "./MyPage"
app.get("/", (req, res) => {
  res.write("My Page");
  res.write("
"); res.write(renderToString()); res.write("
"); res.end(); }); // client import { render } from "react-dom" import MyPage from "./MyPage" render(, document.getElementById("content"));

React 16:

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

// client
import { hydrate } from "react-dom"
import MyPage from "./MyPage"
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直接返回多个组件。

render() {
 return (
    <>
      
      
      
    
  );
}

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

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

Some text.

A heading

More text.

Another heading

Even more text.

用Fragment写,方便又快捷:

render() {
  return (
 // Extraneous div element :(
    
      Some text.
      

A heading

More text.

Another heading

Even more text.
); }

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

render() {
 return [
  "Some text.",
  

A heading

, "More text.",

Another heading

, "Even more text." ]; }

缺点:

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

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

  • string类型要用双引号括住

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

注意点:

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

function Glossary(props) {
 return (
    
{props.items.map(item => ( // Without the `key`, React will fire a key warning
{item.term}
{item.description}
))}
); }

好了,相信看到这里,大家都很想睡觉了。坚持一下,还有五个点就讲完了~。16.3

一、新的生命周期函数

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

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

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

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

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

二、新的context API

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

// Context lets us pass a value deep into the component tree
// without explicitly threading it through every component.
// Create a context for the current theme (with "light" as the default).
const ThemeContext = React.createContext('light');

class App extends React.Component {
  render() {
 // Use a Provider to pass the current theme to the tree below.
 // Any component can read it, no matter how deep it is.
 // In this example, we're passing "dark" as the current value.
 return (
      
        
      
    );
  }
}

// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
 return (
    
); } class ThemedButton extends React.Component { // Assign a contextType to read the current theme context. // React will find the closest theme Provider above and use its value. // In this example, the current theme is "dark". static contextType = ThemeContext; render() { return

但是需要谨慎使用,因为这会让你的组件复用性变差。一般来说,如果你只是想避免需要传很多次props的话,可以直接使用component composition(就是通过props自己传给指定的)会更好。例如:

function Page(props) {
 const user = props.user;
 // 简单来说就是直接父组件将props传下去
 const userLink = (
    
      
    
  );
 return ;
}

// Now, we have:

// ... which renders ...

// ... which renders ...

// ... which renders ...
{props.userLink}

什么场景下需要用context? 一些相同的data需要被大多的component用到,并且还是在不同的层级里。 一般用于主题,存储数据等。

三、createRef API

react15 的时候提供了两种refs的方法:string 跟 callback string:

class MyComponent extends React.Component {
  constructor(props) {
 super(props);

 
  }
 // 通过this.refs.textInput 来获取

  render() {
 return ;
  }
}
callback:
class MyComponent extends React.Component {
  constructor(props) {
 super(props);

 
  }
 // 通过this.textInput 来获取

  render() {
 return  this.textInput = element} />;
  }
}

由于用string的方式会导致一些潜在的问题,所以之前推荐使用callback。但是用string的方法明显方便一点啊喂~

所以react 团队get到了大家的需求,又出了一个新的api 可以用string的方式而且还没有缺点, 真是可喜可贺,可口可乐。

class MyComponent extends React.Component {
  constructor(props) {
 super(props);

 this.inputRef = React.createRef();
  }

  render() {
 return ;
  }

  componentDidMount() {
 this.inputRef.current.focus();
  }
}

使用场景:

  1. 用于操作focus, text 选择,media playback

  2. 触发即时动画

  3. 与第三方组件结合

注意事项:

1、functional component 是不能传ref属性的,因为他们没有instance

function MyFunctionComponent() {
 return ;
}

class Parent extends React.Component {
  constructor(props) {
 super(props);
 this.textInput = React.createRef();
  }
  render() {
 // 这个不能工作
 return (
      
    );
  }
}

但是!只要你要引用的对象是DOM元素或者是class component, 那你可以在functional component里可以使用ref属性

function CustomTextInput(props) {
 // textInput must be declared here so the ref can refer to it
  let textInput = React.createRef();

 function handleClick() {
    textInput.current.focus();
  }

 return (
    
); }

简而言之:functional component里可以使用refs 但是不能把ref属性给它本身。

四、forwardRef API

使用场景:父组件需要将自己的引用传给子组件

const TextInput = React.forwardRef((props, ref) => (
  
))

const inputRef = React.createRef()

class App extends Component {
  constructor(props) {
 super(props)
 
 this.myRef = React.createRef()
  }

  handleSubmit = event => {
    event.preventDefault()
 
    alert('input value is:' + inputRef.current.value)
  }
 
  render() {
 return (
      
) } } const FancyButton = React.forwardRef((props, ref) => ( )); // You can now get a ref directly to the DOM button: const ref = React.createRef(); Click me!;

这样我们就可以直接用this.ref 拿到对button的引用。如果你写的是一个高阶组件,那么推荐使用forwardAPI 将ref传给下面的component。

五、strictMode component

严格模式用来帮助开发者发现潜在问题的工具。就像Fragment 一样,它不会render任何的DOM 元素。注意:只有在development模式下才能用。

它可以帮助我们:

  1. 识别出使用不安全生命周期的组件

  2. 对使用string ref进行告警

  3. 对使用findDOMNode进行告警

  4. 探测某些产生副作用的方法

  5. 对使用弃用context API进行警告

还会有更多的功能在后续版本加进来。

使用:

function ExampleApplication() {
 return (
    
); }

你可能感兴趣的:(React 16 新特性全解(上))