React 16.0之后的改动还是很大的,除了新增加了很多新特性之外,还明确表示未来会增加async render,增加async render之后,将会在17.0的版本完全废除当前版本的三个生命周期,对于已经习惯现在写法的小伙伴来说感觉有点方(至少我有点方),所以还是提前熟悉一下,做好升级的准备吧~
个人觉得升级是必然的事情,所以,还是提前准备一下,做好升级准备!
我技术没有大牛的水平,所以我写文章并不是为了吸引人,一方面是记录自己新学的东西,写出来觉得自己的理解也会加深;另一方面是让比我还入门的人找到个非常合适的入门文章。我喜欢配上一些Demo,这样不太明白的人才能看懂,受教人群不一样,大牛可以去看官方文档说明,小白可以看看demo感受一下新特性~ Demo地址 Demo大概长这个样子:
V16.0
16.0算是一个大版本,增加了很多新特性,解决了很多痛点问题~比如,可以render字符串和数组,比如增加了Fragment,这些在使用中都有效减少了dom节点的数量;还有可以使用portals将新节点插入在任何其他非父节点的dom节点下,对于modal,message等插件是福音;还有增加了error boundary,如果使用的好你再也不会在项目里看到满屏红色或者崩溃了,哈哈~
render多类型
16.0以后,react的render函数增加了几种类型,包括字符串和数组类型。
render() {
//不需要再把所有的元素绑定到一个单独的元素中了
return [
// 别忘记加上key值
<li key="A"/>First itemli>,
<li key="B"/>Second itemli>,
<li key="C"/>Third itemli>,
];
}
// 也可以用下面这种写法
// 不需要再把所有的元素绑定到一个单独的元素中了
render() {
const arr = ['Adams', 'Bill', 'Charlie'];
const Arr = () => (arr.map((item, index) => <p key={index}>{item}</p>));
return <Arr />
}
复制代码
从上图可以看出,解决了以往必须在外层包一个父元素div的限制,有效的减少了不必要的dom元素。
React.Fragment
解决的痛点问题与上面数组是相同的,不过个人感觉更加优雅,首先不需要加上key,其次就是增加一个不渲染的空标签看起来更加的整体,因为以前已经习惯了JSX语法需要一个父标签,这种写法更符合习惯。但是在16.0里提到了Fragment,而更详细的介绍是在16.2版本里,之所以放在这里说因为和返回数组解决的痛点是类似的~ 下面例子来自官网:
// 一个Table组件,里面嵌套了columns组件
class Table extends React.Component {
render() {
return (
<table>
<tr>
<Columns />
</tr>
</table>
);
}
}
// columns组件
class Columns extends React.Component {
render() {
return (
<div>
<td>Hello</td>
<td>World</td>
</div>
);
}
}
复制代码
上面设计符合react,组件式划分,但是最后渲染出来却不是最佳的,因为columns的最外层嵌套了一层没用的div标签。这个问题存在于16.0之前。
有了Fragment以后,很好的解决问题:
import React, { Fragment } from 'react';
class Columns extends React.Component {
render() {
return (
<Fragment>
<td>Hello</td>
<td>World</td>
</Fragment>
);
}
}
// Fragment的语法糖
<>
<td>Hello</td>
<td>World</td>
</>
两个空标签
复制代码
这块糖有点苦,官方明明说的是语法糖,但是我试了,编译通不过,并且官方也特意说明了可能使用该语法糖会出现问题,但是给出的解决办法我都试了,还是不成功,可能配置的不对吧,有谁配置好了可以留言告诉我一下,不过无伤大雅,我倒是觉得语法糖也不一定必须使用。
Error Boundary
什么是Error Boundary?
单一组件内部错误,不应该导致整个应用报错并显示空白页,而Error Boundaries解决的就是这个问题。
在以前的React版本中,如果某一个组件内部出现异常错误,会导致整个项目崩溃直接显示空白页或者error红页,很不友好。error boundary就是解决这个问题的。
Error Boundary本质上是一个组件
按照我的个人理解,error boundary本质上就是一个组件,只不过组件内部多出现了一个生命周期,componentDidCatch,在这个生命周期里面,它会捕捉本组件下的所有子组件抛出的异常错误,包括堆栈信息。
class ErrorBoundary extends React.Component {
constructor(props) {
super(props);
this.state = { hasError: false };
}
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
render() {
if (this.state.hasError) {
// You can render any custom fallback UI
return <h1>Something went wrong.</h1>;
}
return this.props.children;
}
}
// 使用起来就跟普通组件一样
<ErrorBoundary>
<ChildCompA />
<ChildCompB />
...
</ErrorBoundary>
复制代码
上面代码是官网给出的例子,在ErrorBoundary组件内,定义state={ hasError: false },在componentDidCatch内部捕捉到error,然后动态渲染组件,如果出现异常使用提前定义好的替换组件代替发生异常的组件,这样整个页面只有发生异常的部分被替换不影响其他内容的展示。
Portals
有些元素需要被挂载在更高层级的位置。最典型的应用场景:当父组件具有overflow: hidden或者z-index的样式设置时,组件有可能被其他元素遮挡,这个时候你就可以考虑要不要使用Portal使组件的挂载脱离父组件。
Portals 提供了一种很好的将子节点渲染到父组件以外的 DOM 节点的方式。
render() {
// React does *not* create a new div. It renders the children into `domNode`.
// `domNode` is any valid DOM node, regardless of its location in the DOM.
return ReactDOM.createPortal(
this.props.children,
domNode,
);
}
复制代码
一般而言,组件在装载的时候会就近装载在该组件最近的父元素下,而现在你可以使用Portal将组件渲染到任意一个已存在的dom元素下,这个dom元素并不一定必须是组件的父组件。
Portals的应用 —— Modal,message等消息提示
Portals的事件冒泡
从上图可以看出来,弹窗的父组件应该是挂载在#app这个dom下面的,通过portals,我们将modal框挂载在#portal_modal这个dom下了。虽然最后的modal组件没有挂载在整个应用所在的#app下,但是portals创建的组件里面的事件依然会冒泡给它自身的父组件,父组件可以捕获到被挂载在#portal_modal节点下面的modal的点击事件。
class PortalsComp extends Component {
constructor(props) {
super(props);
this.state = { showModal: false, clickTime: 0 };
}
handleShow = () => {
this.setState({ showModal: true });
}
handleHide = () => {
this.setState({ showModal: false });
}
handleClick = () => {
let { clickTime } = this.state;
clickTime += 1;
this.setState({ clickTime });
}
render() {
const protalModal = this.state.showModal ? (
<PortalModal>
<ModalContent hideModal={this.handleHide} />
</PortalModal>
) : null;
return (
<div className={s.portalContainer} onClick={this.handleClick}>
<div>该组件被点击了: {this.state.clickTime}次</div>
<Button onClick={this.handleShow} type='primary'>点我弹出Modal</Button>
{protalModal}
</div>
);
}
}
export default PortalsComp;
复制代码
从上图可以看出来,portals的组件虽然挂载在其他dom下,但是父组件依然可以捕获到modal的冒泡事件,打开和关闭,父组件显示点击次数为2。
V16.3
废弃的几个生命周期
- componentWillMount
- componentWillReceiveProps
- componentWillUpdate
这三个生命周期之中,componentWillReceiveProps平时用的频率还是特别多的,所以对于以前的项目,可能升级会是一种麻烦事,但是说是废弃,但是其实在整个V16版本,还都是可以使用的,只不过会抛出警告,而且官方会建议使用的时候加上前缀UNSAFE_。 componentWillReceiveProps ---> UNSAFE_componentWillReceiveProps
为什么要废弃这三个生命周期
React16.0之前的生命周期设计如下图:
可以看到从开始到结束,这些生命周期的设计可以捕捉到组件的每一个state和props的改变,并没有任何逻辑上的问题,而且对于我们来说写法已经形成习惯,如果废弃肯定是费力不讨好的事情。那么为啥官方还是要皮这么一下呢? 虽然我英文不好,但是还是大致看了一下,意思呢,首先就是说这三个API经常被滥用和误用,再者就是在未来版本中,要引入async render(异步渲染),而在异步渲染的场景下,这些生命周期里面的代码会在未来的React版本里存在缺陷,因此就抛弃了。 这三个API存在的问题: React v16.3 版本新生命周期函数浅析及升级方案这里讲的很清楚。为了弥补不足新增了两个生命周期
static getDerivedStateFromProps
触发时间:在组件构建之后(虚拟dom之后,实际dom挂载之前) ,以及每次获取新的props之后。
每次接收新的props之后都会返回一个对象作为新的state,返回null则说明不需要更新state. 配合componentDidUpdate,可以覆盖componentWillReceiveProps的所有用法。
// before
componentWillReceiveProps(nextProps) {
if (nextProps.flag !== this.props.flag) {
this.setState({ flag: nextProps.flag }, () => {
if (nextProps.flag) {
this.doSmething();
}
});
}
}
// 在16.3之后的版本使用,react推荐下面这种写法,否则eslint可能会提示警告
UNSAFE_componentWillReceiveProps(nextProps) {
// your code
}
// after
static getDrivedStateFromProps(nextProps, prevState) {
if (nextProps.flag !== prevState.flag) {
// 更新state
return {
flag: nextProps.flag
}
}
// 不更新state
return null;
}
// state更新过后需要做的事放在componentDidUpdate里
componentDidUpdate(prevProps, prevState) {
if (prevState.flag !== this.props.flag) {
this.doSomething();
}
}
复制代码
写法与之前相比要麻烦了一些,但是处理逻辑上应该是更清晰了。在 componentWillReceiveProps 中,一般会进行两件事,第一、判断this.props与nextProps的异同,然后更新组件state;第二、根据state的变化更新组件或者执行一些回调函数。在以前的写法里,这两件事我们都需要在 componentWillReceiveProps 中去做。而在新版本中,官方将两件事分配到了两个不同的生命周期 getDerivedStateFromProps 与 componentDidUpdate 中去做,使得组件整体的更新逻辑更为清晰,getDerivedStateFromProps里面进行state的更新,componentDidUpdate里做更新之后的各种回调。而且在 getDerivedStateFromProps 中还禁止了组件去访问 this.props(static方法,获取不到组件的this),强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去做其他一些让组件自身状态变得更加不可预测的事情。
还是需要适应,虽然习惯了以前的写法,但是现在这种性能要更好。而且毕竟以后会废弃。
这里解决了我的很久一个困惑,组件最后的更新过程其实是: componentWillReceiveProps(static getDerivedStateFromProps)判断state的变化 ---> shouldComponentUpdate判断是否进行更新 render阶段会根据diff算法来生成需要更新的虚拟dom结构 ---> 更新虚拟dom ---> 虚拟dom更新完毕立刻调用componentDidUpdate ---> 最后完成渲染。
因为官方给出的定义是,componentDidUpdate是在组件dom更新结束之后立即调用,那么这个更新结束我理解的就是dom已经更新完毕渲染好了,但是我在componentDidUpdate里面调用了alert,发现其实进入该生命周期之后,其实dom还未发生变化,但是页面上的dom未发生变化,而componentDidUpdate获取dom的时候值确实正确的,可能这里是虚拟dom和真实dom不同步的关系吧,总之就是,在componentDidUpdate里面可以获取dom节点的操作,获取的值也是更新完毕的,下面的例子也是这样的。
getSnapshotBeforeUpdate ---- 针对对dom的一些操作
触发时间: update发生的时候,在render之后,在组件dom渲染之前。
返回一个值,作为componentDidUpdate的第三个参数。 配合componentDidUpdate, 可以覆盖componentWillUpdate的所有用法。
componentWillUpdate存在的问题
- 与componentWillReceiveProps类似,同样在一层更新过程中可能会被调用多次,这样就会造成里面的回调函数可能会执行多次,浪费性能。
- 在React17引入async render之后,render阶段和commit阶段可能并不是同步连贯的,因此,componentDidUpdate和componentWillUpdate获取到的Dom可能是不同的,这样就会导致读取到的dom元素的状态是不安全的。
getSnapshotBeforeUpdate配合componentDidUpdate来保证状态的一致
getSnapshotBeforeUpdate的发生时间在render之后,组件dom渲染之前,这样可以保证此时读取的dom和componentDidUpdate的dom是一致的。
getSnapshotBeforeUpdate不是静态方法,里面可以读取this.props和this.state等信息,并且调用之后应该返回一个值作为componentDidUpdate的第三个参数
static getDerivedStateFromProps(nextProps, prevState) {
if (nextProps.disabled !== prevState.disabled) {
return {
disabled: nextProps.disabled
};
}
return null;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
return this.props.disabled;
}
componentDidUpdate(prevProps, prevState, snapshot) {
if (!snapshot) {
// 如果snapshot是false,获取焦点
this.domRef.focus();
}
}
render() {
return (
<div>
<input ref={(ref) => this.domRef = ref} disabled={this.state.disabled} />
</div>
);
}
复制代码
V16.4
修复了getDerivedStateFromProps的bug,为了更好地兼容即将到来的异步渲染
这个点我还没太弄明白,因为我准备写的时候就已经是16.4了,也不太知道这个bug会导致什么影响,不过看了一些文章,大概意思是下面这样: 参考文章:React16.4 新特性
React这次更新修复了getDerivedStateFromProps这个生命周期的触发节点, 在之前, 它触发的方式和旧生命周期getDerivedStateFromProps类似, 都是在被父组件re-render的时候才会触发,并且本组件的setState的调用也不会触发
这种方式在之前同步渲染的时候是没有问题的, 但是为了支持新的还未启用的fiber异步渲染机制, 现在, getDerivedStateFromProps在组件每一次render的时候都会触发,也就是说无论是来自父组件的re-render, 还是组件自身的setState, 都会触发getDerivedStateFromProps这个生命周期。
要理解为什么react修复了这个生命周期的触发方式, 我们首先得了解react的异步渲染机制
react异步渲染
要理解react异步渲染的机制, 我们首先要说一说react之前是如何进行渲染。
在react16之前, 组件的渲染都是同步进行的, 也就是说从constructor开始到componentDidUpdate结束, react的运行都是没有中断的, 生命周期开始之后就会运行到其结束为止, 这样带来的一个缺点就是,如果组件嵌套很深, 渲染时间增长了之后, 一些重要的, 高优先级的操作就会被阻塞, 例如用户的输入等, 这样就会造成体验上的不友好。
在之后即将到来的异步渲染机制中, 会允许首先解决高优先级的运行,同时会暂停当前的渲染进程,当高优先级的进程结束之后, 再返回继续运行当前进程, 这样会大大的提高react的流畅度,给用户带来更好的体验
而这次修复getDerivedStateFromProps, 正是为了保证与即将到来的异步渲染模式的兼容。
复制代码
React pointer events
pointer events是HTML5规范的WEB API,它主要目的是用来将鼠标(Mouse)、触摸(touch)和触控笔(pen)三种事件整合为统一的API。
如果你的应用涉及到指针的相关事件,那么这个API还是很有用的,不过这个API的兼容性不怎么样,基本主流浏览器的最新版本才支持,从React增加了这个pointer events事件来看,说明React官方还是很看重这个API的,我觉得兼容性肯定满满的会越来越好。
因为兼容性不太好,所以官方的建议是使用的时候配合第三方的polyfill来用。
React提供的pointer events
- onPointerDown
- onPointerMove
- onPointerUp
- onPointerCancel
- onGotPointerCapture
- onLostPointerCapture
- onPointerEnter
- onPointerLeave
- onPointerOver
- onPointerOut
因为平时接触较少,所以没怎么用过,就用官方Demo给大家看看吧,一定要升级到14以上哦,否则没有这些属性,感兴趣的深入研究研究,毕竟这篇文章目的就是让自己了解一下新特性~
官方demo效果如下:
【坑来了】:我自信满满的升级到Firefox和chrome到最新版本,然后把官方demo跑了一下,但是WTF?是下面这样的结果。。。
很明显,这些属性依然不能被支持,也可能是我自己的问题?不清楚了,反正就是不能用。然后呢,我就查呗,让我查到了这个东东 ——react-pointable
,
// 首先,安装包
yarn add react-pointable
// 然后代码变成下面
import Pointable from 'react-pointable';
...
<Pointable
style={circleStyle}
onPointerDown={this.onDown}
onPointerMove={this.onMove}
onPointerUp={this.onUp}
onPointerCancel={this.onUp}
onGotPointerCapture={this.onGotCapture}
onLostPointerCapture={this.onLostCapture}
/>
复制代码
OK,可以动了!等一下~官方Demo摁住和松开的时候会变颜色,这里没变颜色,打开控制台发现还是有两个报错: 嗯,原来是6个,现在变成了两个,说明还是解决了一部分问题,这是为啥呢,原来官方文档说了:它支持的事件如下,但是并不支持官方Demo里面的onGotPointerCapture和onLostPointerCapture。这个包就是一种polyfill吧,按照我的理解,它最后渲染出来的效果就是官方代码那个样子。 然后看下运行效果:
原本我想提个issue来的,O(∩_∩)O哈哈~,但是发现好像有人提了,反正暂时不支持就对了。也可能是我配置的不对?因为官方demo确实可以运行,而且浏览器版本也都支持pointer events事件,如果有大牛给我解答还是万分感谢的~
总结
升级react日后应该是必然的事情,所以提前了解一下还是有帮助的,作为使用者暂时不做深入分析,当然,我也分析不明白,单纯从更新角度来写几个demo给大家看一下变化,应该还挺清楚的~感谢阅读!