盘点 React 16.0 ~ 16.5 主要更新及其应用
目录
0. 生命周期函数的更新
1. 全新的 Content API
2. React Strict Mode
3. Portal
4. Refs
5. Fragment
6. 其他
7. 总结
生命周期函数的更新
随着 React 16.0 发布, React 采用了新的内核架构 Fiber,在新的架构中它将更新分为两个阶段:Render Parse 和 Commit Parse, 也由此引入了 getDerivedStateFromProps 、 getSnapshotBeforeUpdate 及 componentDidCatch 等三个生命周期函数。同时,也将 componentWillUpdate、componentWillReceiveProps 和 componentWillUpdate 标记为不安全的方法。
新生命周期函数图例
新增
static getDerivedStateFromProps(nextProps, prevState)getSnapshotBeforeUpdate(prevProps, prevState)componentDidCatch(error, info)
标记为不安全
componentWillMount(nextProps, nextState)componentWillReceiveProps(nextProps)componentWillUpdate(nextProps, nextState)
static getDerivedStateFromProps(nextProps, prevState)
根据 getDerivedStateFromProps(nextProps, prevState) 的函数签名可知: 其作用是根据传递的 props 来更新 state。它的一大特点是 无副作用 : 由于处在 Render Phase 阶段,所以在每次的更新都要触发, 故在设计 API 时采用了静态方法,其好处是单纯 —— 无法访问实例、无法通过 ref 访问到 DOM 对象等,保证了单纯且高效。值得注意的是,其仍可以通过 props 的操作来产生副作用,这时应该将操作 props 的方法移到 componentDidUpdate 中,减少触发次数。
例:
state = { isLogin: false }
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.isLogin !== prevState.isLogin){
return {
isLogin: nextProps.isLogin
}
}
return null
}
componentDidUpdate(prevProps, prevState){
if(!prevState.isLogin && prevProps.isLogin) this.handleClose()
}
但在使用时要非常小心,因为它不像 componentWillReceiveProps 一样,只在父组件重新渲染时才触发,本身调用 setState 也会触发。官方提供了 3 条 checklist, 这里搬运一下:
- 如果改变
props的同时,有副作用的产生(如异步请求数据,动画效果),这时应该使用componentDidUpdate - 如果想要根据
props计算属性,应该考虑将结果 memoization 化,参见 memoization - 如果想要根据
props变化来重置某些状态,应该考虑使用受控组件
配合 componentDidUpdate 周期函数,getDerivedStateFromProps 是为了替代 componentWillReceiveProps 而出现的。它将原本 componentWillReceiveProps 功能进行划分 —— 更新 state 和 操作/调用 props,很大程度避免了职责不清而导致过多的渲染, 从而影响应该性能。
getSnapshotBeforeUpdate(prevProps, prevState)
根据 getSnapshotBeforeUpdate(prevProps, prevState) 的函数签名可知,其在组件更新之前获取一个 snapshot —— 可以将计算得的值或从 DOM 得到的信息传递到 componentDidUpdate(prevProps, prevState, snapshot) 周期函数的第三个参数,常常用于 scroll 位置的定位。摘自官方的示例:
class ScrollingList extends React.Component {
constructor(props) {
super(props)
// 取得dom 节点
this.listRef = React.createRef()
}
getSnapshotBeforeUpdate(prevProps, prevState) {
// 根据新添加的元素来计算得到所需要滚动的位置
if (prevProps.list.length < this.props.list.length) {
const list = this.listRef.current
return list.scrollHeight - list.scrollTop
}
return null
}
componentDidUpdate(prevProps, prevState, snapshot) {
// 根据 snapshot 计算得到偏移量,得到最终滚动位置
if (snapshot !== null) {
const list = this.listRef.current
list.scrollTop = list.scrollHeight - snapshot
}
}
render() {
return <div ref={this.listRef}>{/* ...contents... */}</div>
}
}
componentDidCatch(error, info)
在 16.0 以前,错误捕获使用 unstable_handleError 或者采用第三方库如 react-error-overlay 来捕获,前者捕获的信息十分有限,后者为非官方支持。而在 16.0 中,增加了 componentDidCatch周期函数来让开发者可以自主处理错误信息,诸如展示,上报错误等,用户可以创建自己的Error Boundary 来捕获错误。例:
···
componentDidCatch(error, info) {
// Display fallback UI
this.setState({ hasError: true });
// You can also log the error to an error reporting service
logErrorToMyService(error, info);
}
···
此外,用户还可以采用第三方错误追踪服务,如 Sentry、Bugsnag 等,保证了错误处理效率的同时也极大降级了中小型项目错误追踪的成本。
Bugsnag 错误追踪截图
标记为不安全 componentWillMount、componentWillReceiveProps、componentWillUpdate
componentWillMount
componentWillMount 可被开发者用于获取首屏数据或事务订阅。
开发者为了快速得到数据,将首屏请求放在 componentWillMount中。实际上在执行 componentWillMount时第一次渲染已开始。把首屏请求放在componentWillMount 的与否都不能解决首屏渲染无异步数据的问题。而官方的建议是将首屏放在 constructor 或 componentDidMount中。
此外事件订阅也被常在 componentWillMount 用到,并在 componentWillUnmount 中取消掉相应的事件订阅。但事实上 React 并不能够保证在 componentWillMount 被调用后,同一组件的 componentWillUnmount 也一定会被调用。另一方面,在未来 React 开启异步渲染模式后,在 · 被调用之后,组件的渲染也很有可能会被其他的事务所打断,导致 componentWillUnmount 不会被调用。而 componentDidMount 就不存在这个问题,在 componentDidMount 被调用后,componentWillUnmount 一定会随后被调用到,并根据具体代码清除掉组件中存在的事件订阅。
对此的升级方案是把 componentWillMount 改为 componentDidMount 即可。
componentWillReceiveProps、componentWillUpdate
componentWillReceiveProps 被标记为不安全的原因见前文所述,其主要原因是操作 props 引起的 re-render。与之类似的 componentWillUpdate 被标记为不安全也是同样的原因。除此之外,对 DOM 的更新操作也可能导致重新渲染。
对于 componentWillReceiveProps 的升级方案是使用 getDerivedStateFromProps 和 componentDidUpdate 来代替。 对于 componentWillUpdate 的升级方案是使用 componentDidUpdate 代替。如涉及大量的计算,可在 getSnapshotBeforeUpdate 完成计算,再在 componentDidUpdate 一次完成更新。
通过框架级别的 API 来约束甚至限制开发者写出更易维护的 Javascript 代码,最大限度的避免了反模式的开发方式。
全新的 Context API
在 React 16.3 之前,Context API 一直被官方置为不推荐使用(don’t use context),究其原因是因为老的 Context API 作为一个实验性的产品,破坏了 React 的分形结构。同时在使用的过程中,如果在穿透组件的过程中,某个组件的 shouldComponentUpdate 返回了 false, 则 Context API 就不能穿透了。其带来的不确定性也就导致被不推荐使用。随着 React 16.3 的发布,全新 Context API 成了一等 API,可以很容易穿透组件而无副作用,官方示例代码:
// 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 (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
)
}
}
// A component in the middle doesn't have to
// pass the theme down explicitly anymore.
function Toolbar(props) {
return (
<div>
<ThemedButton />
</div>
)
}
function ThemedButton(props) {
// Use a Consumer 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".
return (
<ThemeContext.Consumer>{theme => <Button {...props} theme={theme} />}</ThemeContext.Consumer>
)
}
其过程大概如下:
- 通过
React.createContext创建 Context 对象 - 在父组件上,使用
<ThemeContext.Provider/>来提供 Provider - 在需要消费的地方,使用
<ThemeContext.Consumer/>以函数调用的方式{theme => <Button {...props} theme={theme} />}获得 Context 对象的值。
Context API 与 Redux
在状态的管理上,全新的 Context API 完全可以取代部分 Redux 应用,示例代码:
const initialState = {
theme: 'dark',
color: 'blue',
}
const GlobalStore = React.createContext()
class GlobalStoreProvider extends React.Component {
render() {
return (
<GlobalStore.Provider value={{ ...initialState }}>{this.props.children}</GlobalStore.Provider>
)
}
}
class App extends React.Component {
render() {
return (
<GlobalStoreProvider>
<GlobalStore.Consumer>
{context => (
<div>
<div>{context.theme}</div>
<div>{context.color}</div>
</div>
)}
</GlobalStore.Consumer>
</GlobalStoreProvider>
)
}
}
全新的 Context API 带来的穿透组件的能力对于需要全局状态共享的场景十分有用,无需进入额外的依赖就能对状态进行管理,代码简洁明了。
React Strict Mode
React StrictMode 可以在开发阶段发现应用存在的潜在问题,提醒开发者解决相关问题,提供应用的健壮性。其主要能检测到 4 个问题:
- 识别被标志位不安全的生命周期函数
- 对弃用的 API 进行警告
- 探测某些产生副作用的方法
- 检测是否采用了老的 Context API
使用起来也很简单,只要在需要被检测的组件上包裹一层 React StrictMode ,示例代码 React-StrictMode:
class App extends React.Component {
render() {
return (
<div>
<React.StrictMode>
<ComponentA />
</React.StrictMode>
</div>
)
}
}
若出现错误,则在控制台输出具体错误信息:
React Strict Mode 控制台输出
Portal
由 ReactDOM 提供的 createPortal 方法,允许将组件渲染到其他 DOM 节点上。这对大型应用或者独立于应用本身的渲染很有帮助。其函数签名为 ReactDOM.createPortal(child, container), child 参数为任意的可渲染的 React Component,如 element、sting、fragment 等,container 则为要挂载的 DOM 节点.
以一个简单的 Modal 为例, 代码见 Portal Modal :
import React from 'react'
import ReactDOM from 'react-dom'
const modalRoot = document.querySelector('#modal')
export default class Modal extends React.Component {
constructor(props) {
super(props)
this.el = document.createElement('div')
}
componentDidMount() {
modalRoot.appendChild(this.el)
}
componentWillUnmount() {
modalRoot.removeChild(this.el)
}
handleClose = () => [this.props.onClose && this.props.onClose()]
render() {
const { visible } = this.props
if (!visible) return null
return ReactDOM.createPortal(
<div>
{this.props.children}
<span onClick={this.handleClose}>[x]</span>
</div>,
this.el
)
}
}
具体过程就是使用了 props 传递 children后, 使用 ReactDOM.createPortal, 将 container 渲染在其他 DOM 节点上的过程。
Refs
虽然 React 使用 Virtual DOM 来更新视图,但某些时刻我们还要操作真正的 DOM ,这时 ref属性就派上用场了。
React.createRef
React 16 使用了 React.createRef 取得 Ref 对象,这和之前的方式还是有不小的差别,例:
// before React 16
···
componentDidMount() {
// the refs object container the myRef
const el = this.refs.myRef
// you can also using ReactDOM.findDOMNode
// const el = ReactDOM.findDOMNode(this.refs.myRef)
}
render() {
return <div ref="myRef" />
}
···
···
// React 16+
constructor(props) {
super(props)
this.myRef = React.createRef()
}
render() {
return <div ref={this.myRef} />
}
···
React.forwardRef
另外一个新特性是 Ref 的转发, 它的目的是让父组件可以访问到子组件的 Ref,从而操作子组件的 DOM。 React.forwardRef 接收一个函数,函数参数有 props 和 ref。看一个简单的例子,代码见 Refs:
const TextInput = React.forwardRef((props, ref) => (
<input type="text" placeholder="Hello forwardRef" ref={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 (
<form onSubmit={this.handleSubmit}>
<TextInput ref={inputRef} />
<button type="submit">Submit</button>
</form>
)
}
}
这个例子使用了 React.forwardRef 将 props 和 ref 传递给子组件,直接就可以在父组件直接调用。
Fragment
在向 DOM 树批量添加元素时,一个好的实践是创建一个document.createDocumentFragment,先将元素批量添加到 DocumentFragment 上,再把 DocumentFragment 添加到 DOM 树,减少了 DOM 操作次数的同时也不会创建一个新元素。
和 DocumentFragment 类似,React 也存在 Fragment 的概念,用途很类似。在 React 16 之前,Fragment 的创建是通过扩展包 react-addons-create-fragment 创建,而 React 16 中则通过 <React.Fragment></React.Fragment> 直接创建 'Fragment'。例如:
render() {
return (
<React.Fragment>
<ChildA />
<ChildB />
<ChildC />
</React.Fragment>
)
}
如此,我们不需要单独包裹一层无用的元素(如使用<div></div>包裹),减少层级嵌套。 此外,还一种精简的写法:
render() {
return (
<>
<ChildA />
<ChildB />
<ChildC />
</>
)
}
其他
ReactDOM 的 render 函数可以数组形式返回 React Component
render(){
return [
<ComponentA key='A' />,
<ComponentB key='B' />,
]
}
移除内建的react-with-addons.js, 所有的插件都独立出来
之前常用的react-addons-(css-)transition-group,react-addons-create-fragment,react-addons-pure-render-mixin、react-addons-perf 等,除部分被内置,其余全部都独立为一个项目,使用时要注意。
盘点 React 16.0 ~ 16.5 主要更新及其应用的更多相关文章
- React 特性剪辑(版本 16.0 ~ 16.9)
Before you're going to hate it, then you're going to love it. Concurrent Render(贯穿 16) 在 18年的 JSConf ...
- 安装MYSQL详细教程 版本:mysql-installer-community-5.7.16.0 免安装版本和安装版本出现错误的解决
一.版本的选择 之前安装的Mysql,现在才来总结,好像有点晚,后台换系统了,现在从新装上Mysql,感觉好多坑,我是来踩坑,大家看到坑就别跳了,这样可以省点安装时间,这个折腾了两天,安装了好多个版本 ...
- 增加VirtualBox虚拟机的磁盘空间大小(Host:Win7 VirtualBox5.0.16 VM:Win10)
1 前言 网上关于增加VirtualBox虚拟机的磁盘空间大小的文章非常非常多,这里我之所以再写一篇,是因为在参照这些文章做的时候,由于VirtualBox的版本更新以及其他一些环境问题,碰到到一些问 ...
- Navicat Premium 简体中文版 12.0.16 以上版本国外官网下载地址(非国内)
国内Navicat网址是:http://www.navicat.com.cn 国外Navicat网址是:http://www.navicat.com 国外的更新比国内的快,而且同一个版本,国内和国外下 ...
- CentOS 6.5 搭建 .NET 环境, Mono 5.16.0 + Jexus 5.8
最近有这样一个打算,就是准备把以前的有一个.NET 网站部署在Linux 下面,正好试试 .NET 跨平台的功能,为后续研究 .netCore 方向准备. 搭建环境: CentOS 6.5 + Mon ...
- PowerDesign 16.0 生成的SQL Server2000 数据库脚本时MS_Description不存在的问题解决
根据网上查询到的资料,找到了解决方法,原文出自:http://www.cnblogs.com/24tt/p/5047257.html PowerDesign 16.0 生成的Script语句,Sql2 ...
- ChemOffice Professional 16.0新增了哪些功能
ChemOffice Professional 16.0是为终极化学和生物组件设计,可满足化学家和生物学家的需求.ChemOffice Professional帮助科学家有效地检索数据库,包括SciF ...
- mysql8.0.16操作记录
mysql8.0.16操作记录 2.1.登录 -uroot -p'AnvcTMagdLarwNV3CKaC' mysql: [Warning] Using a password on the comm ...
- 【SQL必知必会笔记(1)】数据库基础、SQL、MySQL8.0.16下数据库、表的创建及数据插入
文章目录 1.数据库基础 1.1 数据库(database) 1.2 表(table) 1.3 列和数据类型 1.4 行 1.5 主键 2.什么是SQL 3.创建后续练习所需数据库.表(MySQL8. ...
随机推荐
- Hashmap的Hash()
JDK7: public final int hashCode() { return Objects.hashCode(getKey()) ^ Objects.hashCode( ...
- angular学习笔记(2)- 前端开发环境
angular1学习笔记(2)- 前端开发环境 1.代码编辑工具 2.断点调试工具 3.版本管理工具 4.代码合并和混淆工具 5.依赖管理工具 6.单元测试工具 7.集成测试工具 常见的前端开发工具 ...
- 硬件工程师必会电路模块之MOS管应用
实际工程应用中常用的MOS管电路(以笔记本主板经典电路为例): 学到实际系统中用到的开关电路模块以及MOS管非常重要的隔离电路(结合IIC的数据手册和笔记本主板应用电路): MOS管寄生体二极管,极性 ...
- VMWare 下安装 MSDN版 MS-DOS 6.22
最近有些怀旧,刚从孔夫子旧书网淘回一本<Borland 传奇>,里面讲到了很多DOS时代的经典软件,特别想尝试一下~比如:Turbo Pascal.SideKick.Borland C/C ...
- asp.net mvc 实战化项目之三板斧
laravel实战化项目之三板斧 spring mvc 实战化项目之三板斧 asp.net mvc 实战化项目之三板斧 接上文希望从一张表(tb_role_info 用户角色表)的CRUD展开asp. ...
- Quartz小记(一):Elastic-Job - 分布式定时任务框架
Elastic-Job是ddframe中dd-job的作业模块中分离出来的分布式弹性作业框架.去掉了和dd-job中的监控和ddframe接入规范部分.该项目基于成熟的开源产品Quartz和Zooke ...
- 使用多个项目生成Xml文件来显示帮助文档
终于到这了,我们首先将Product单独作为一个项目 WebAPI2PostMan.WebModel 并引用他,查看文档如下. 你会发现,你的注释也就是属性的描述没有了.打开App_Data/XmlD ...
- 解析 .Net Core 注入——注册服务
在学习 Asp.Net Core 的过程中,注入可以说是无处不在,对于 .Net Core 来说,它是独立的一个程序集,没有复杂的依赖项和配置文件,所以对于学习 Asp.Net Core 源码的朋友来 ...
- MySQL 服务常用操作命令
1.MySQL 服务设置 在使用 mysql.server 命令之前需要将路径 /usr/local/mysql/support-files 添加到系统环境变量中. export PATH=$PATH ...
- Java基础(三)面向对象(下)
接口 接口中成员修饰符是固定的: 成员常量:public static final 成员函数:public abstract 通过接口间接实现了多重继承 接口的特点 接口是对外暴露的规则 接口是程序的 ...