React自发布v16版本以来已经有半年了,至今最新的是v16.3。从 v16 开始增长了较多新的API。相较于以前纯净的API设计,变化能够说是很是大了。能够看出 facebook 的 React 团队已经解决了以前的大多数问题,如今开始为 React 设计新的 API 、增长新功能了。得益于重写 React 底层为Fiber架构,v16 包含了许多实用的新特性,而且也有一些 “break-change”,以后的版本确定更多。在官方博客上的介绍虽然很全,可是因为翻译不及时,查阅不变,并且有的地方难于理解。故在此经过一些代码作简要介绍,也为本身查阅 API 作记录。html
仓库中代码包含了本人的最佳实践,阅读文章以前, 但愿你对 React 和 ES6 有必定了解。固然若是不会,也能够选择性看看。若有例子不合适或谬误,欢迎 提出意见。前端
代码已托管到 github仓库 上,里面有对应新特性子的 demo,欢迎 star !没错我就是来骗赞的react
因为准备长期更新,如今的是 v16 版。因此克隆到本地之后先 cd react16
,再npm i
, npm start
。访问 localhost:9000 便可看到 demo(忽略我丑陋的 CSS )。左边有几个路由,分别对应了几个新特性和相应的 demo。git
React v16 以前,若是渲染中出现错误,整个页面会直接崩掉。若是对 React 足够了解,可能会知道一个秘而不宣的API: unstable_handleError
。此函数可用于捕获页面错误,然而因为文档没记录,知道的开发者也寥寥无几。如今咱们有了新的、官方的、稳定的 API: componentDidCatch
。就像 try catch
同样,可用于捕获 render
过程当中的错误,经常使用于捕获错误并渲染不一样的页面,避免整个页面崩溃。github
demo 中,第一个是自增按钮,增长到5会抛出渲染错误,若是此时处于不捕获模式,页面崩溃,只能刷新页面恢复正常。npm
相信这是每个 React 页面仔的噩梦吧(包括我),应该尽力避免此种状况发生。切换到捕获模式后,组件启用新的componentDidCatch
API。此时发生错误不会崩,会显示备用页面。redux
新特性的主要代码在 /src/ErrorBoundary/ErrorHandler.jsx
下。原本若是不用切换模式,一直捕获错误的话,ErrorHander
应该长成这样:api
import React from 'react'
class FakeHandler extends React.Component {
state = {
hasError: false
}
// 新的生命周期钩子
componentDidCatch(error, info) {
this.setState({
hasError: true
})
}
// 重置状态,与新 API 无关
reset = () => {
this.setState({
hasError: false
})
this.props.reset()
}
render() {
// 显示备用页面的核心代码,如有错误显示备用页面
return this.state.hasError ? (
<React.Fragment> <p>页面渲染发生错误,这是备用页面,可打开 console 查看错误</p> <div> <button onClick={this.reset}>点击此处重置</button> </div> </React.Fragment> ) : ( React.Children.only(this.props.children) ) } } 复制代码
重点在 componentDidCatch
这一句,若是捕获错误了把当前state.hasError
设为true
,render
里判断下是否有错误再渲染,能够作到备用页面的显示,这也是经常使用的 componentDidCatch
处理手法,能够做为经典范例。数组
componentDidCatch
能够接受两个参数: 抛出的错误error
和 错误信息的 info
,如今的info只包含了调用栈的信息,感受用处不大,由于发生错误时React老是会打印堆栈。可能之后会加入新信息,拭目以待。架构
因为此方法能够放在任意组件内,所以能够在页面不一样地方定制化备用页。
Note:今生命周期函数没法捕获渲染外的错误,如如下错误没法捕获,会正常渲染。
class A extends React.Component {
render() {
// 此错误没法被捕获,渲染时组件正常返回 `<div></div>`
setTimeout(() => {
throw new Error('error')
}, 1000)
return (
<div></div>
)
}
}
复制代码
仓库中的代码因为须要切换捕获模式以演示区别,所以出现了组件的继承写法,若是不熟悉,请好好体会。不过实际项目中遇到继承的机会仍是不多的,此种方法经常使用于覆盖某组件的生命周期函数。
官网文档已有中文版,更多详情请参阅 Error Boundaries
API为 ReactDOM.createPortal
。能够简单的理解为“传送门”,便可以直接渲染在父组件之外的任意 DOM 节点,经常使用于弹出框、提示框等,而且支持事件冒泡,行为彻底与子组件一致。demo 代码在src/Portal
下。注意此方法并不能为所欲为调用,只有在组件的 render 方法调用,并做为合法element
的代替返回。
Note: 新的 API 挂载在 react-dom 下,并非 React 包内。
代码示例:
import React from 'react'
import { createPortal } from 'react-dom'
class Dialog extends React.Component {
render() {
// 必定要 return
return createPortal((
<div></div>
), document.querySelector('#dialog'))
}
}
复制代码
渲染的实际 DOM 如图,即便整个应用都在 div#app
下,createPortal 依然能在以外的 div#poral
下渲染 Element。
很是简单,可是要注意不能滥用,就像 ref 同样,尽可能把 react 能作的都交给 react 处理。淡然此 API 作弹出框的时候很是好用,对作基本弹窗组件的前端们简直就是福音。更多请参考官方中文文档 Portals。
这两个静态组件均挂载在 React 包下,经过React.Fragment
和React.StrictMode
可访问到。
Fragment静态组件,v16.0 推出,用于将多个 React render
返回值包裹成一个具备顶级元素的element
。以前若是咱们须要返回多个元素,必定要在外面包一层<div></div>
或其余的元素,React 还会将其渲染成真实 DOM;或直接返回一个相应的数组(React v16.0支持),可是很是丑陋,而且必须附带key
属性,即便用不到。
如今新的 Fragment 仅用于包裹,并不会生成对应 DOM 了,就像普通的jsx同样,也不须要key
属性了,仍是很是不错的新功能。官方文档:Fragments
StrictMode 于 v16.3 推出。顾名思义,即严格模式,可用于在开发环境下提醒组件内使用不推荐写法和即将废弃的 API(该版本废弃了三个生命周期钩子)。与 Fragment 相同,并不会被渲染成真实 DOM。官方文档严格模式里详细介绍了会在哪些状况下发出警告。对于咱们开发者来讲,及时弃用不被推荐的写法便可规避这些警告。
Fragment 和 StrictMode 代码示例在src/NewComponent
下:
import React, {Fragment, StrictMode} from 'react'
const FragmentItem = props => new Array(5).fill(null).map((k, i) => (
<Fragment key={i}> <p>这是第{i}项</p> <p>{i} * {i} = {i * i}</p> </Fragment>
))
class OldLifecycleProvider extends React.Component {
// 如下三个函数在 React v16.3 已不被推荐,将来的版本会废弃。
componentWillMount() {
console.log('componentWillMount')
}
componentWillUpdate() {
console.log('componentWillUpdate')
}
componentWillReceiveProps() {
console.log('componentWillReceiveProps')
}
render() {
return (
<FragmentItem></FragmentItem>
)
}
}
export default class NewComponent extends React.Component {
state = {
propFlag: 2
}
// 使 OldLifecycleProvider 进入 componentWillReceiveProps 函数
componentDidMount() {
this.setState({
propFlag: 1
})
}
render() {
return (
<StrictMode> <OldLifecycleProvider propFlag={this.state.propFlag}></OldLifecycleProvider> </StrictMode>
)
}
}
复制代码
渲染层级为:NewComponent -> OldLifecycleProvider -> FragmentItem
,能够看到在 React dev tool下依然能够看到多层结构(Fragment并无显示,比较遗憾,但愿 dev tool 新版本能修复这个问题),但渲染出的 DOM 层级仍是扁平的,直接挂载在 div.view
下。
另外,因为故意在 StrictMode 下使用了三个即将废弃的API,打开 console ,可看到以下错误提醒:
Note: 项目可直接使用StrictMode,没必要检测是否为开发环境,由于只在开发环境起做用。
若是很是注重项目代码将来的可升级性,甚至能够在最顶层用 StrictMode 包裹。但其实除此以外,若是项目稳定,开启此模式对开发人员没有一点好处,甚至还有额外的迁移工做,所以不建议在已开始项目使用;但对代码重构有很是大的好处,可随时提醒开发人员即将废弃的 API 以便迁移。相信在 React 生态中会与 JS 的 'use strict'
同样应用愈来愈普遍。
以前版本,若是想取得某个 Element 的 Ref,有两种方式可选:
<input ref="input" />
=> this.refs.input
<input ref={input => (this.input = input)} />
=> this.input
其中字符串形式,因为存在种种问题 (issue ,八卦下:这哥们就是 redux 做者)而不被推荐,具体内容就是:
this
取值,会使 React 稍稍变慢;this
与你想象的并不一致:import React from 'react'
class Children extends React.Component {
componentDidMount() {
// <h1></h1>
console.log('children ref', this.refs.titleRef)
}
render() {
return (
<div> {this.props.renderTitle()} </div>
)
}
}
export default class Parent extends React.Component {
// 放入子组件渲染
renderTitle = () => (
<h1 ref='titleRef'>{this.props.title}</h1>
)
componentDidMount() {
// undefined
console.log('parent ref:', this.refs.titleRef)
}
render() {
return (
<Children renderTitle={this.renderTitle}></Children>
)
}
}
复制代码
由于字符串形式的 ref 绑定的 this 是根据渲染时而定,而不是声明时而定,有点像 js 中函数的 做用域 和 this 的区别。但做为 React 组件,咱们老是但愿声明时将 ref绑定在当前声明的 Component 中,所以这也是个问题。
所以如今经常使用函数形式,几乎没有肯定,惟一的遗憾是须要新建函数;若是放入render里,会影响性能;若是放在 class 下,又白白多了一个业务无关函数。可是如今咱们有了新的 API:createRef。基本用法:
class A extends React.Component {
inputRef = React.createRef()
componentDidMount() {
// 注意 current
this.inputRef.current.focus()
}
render() {
return (
<input type="text" ref={this.inputRef}></input>
)
}
}
复制代码
经过 this.inputRef.current
便可获取。this.inputRef
实际上是个原型为 Object.prototype
的对象,并且目前为止只有一个 current
键,对应的值是取得的 ref。看来 React 团队已经预留好接口,接下来的版本会为 Ref 增长新功能了。
相较于字符串形式,createRef
既在编码中提早声明须要获取 Ref,又能够避免字符串形式的种种硬伤;而像对于函数形式,能够少写一个函数,可是不够灵活,实际编码中可能仍是须要函数形式,这也是 React 文档中将函数形式列为高级技巧的缘由。所以做为开发者,须要作到彻底避免字符串形式,尽可能使用createRef
,把函数形式列为备选;而在 v16.3 版本中,看到 createRef
,无脑取 current
就好了。
以前是没有ForwardRef
这种概念的,这是专门为高阶组件获取 Ref 而设计。官方文档(英文)Forwarding Refs 的例子掺杂了许多对高阶组件(HOC)的介绍和理解,不够纯净,不利于初步理解ForwardRef,原本挺简单的一个概念被复杂化了,下面用简单例子例子说明其基本用法:
import React from 'react'
// 高阶组件,注意返回值用 `React.forwardRef` 包裹
// 里面的无状态组件接收第二个参数:ref
const paintRed = Component => React.forwardRef(
// 此例中,ref 为 ForwardRef 中的 textRef
(props, ref) => (
<Component color='red' ref={ref} {...props}></Component>
)
)
class Text extends React.Component {
// 仅用于检测是否取到 ref
value = 1
render() {
const style = {
color: this.props.color
}
return (
<p style={style}> 我是红色的! </p>
)
}
}
const RedText = paintRed(Text)
export default class ForwardRef extends React.Component {
textRef = React.createRef()
componentDidMount() {
// value = 1
console.log(this.textRef.current.value)
}
render() {
// 若是没有 forwardRef,那么这个ref只能获得 `RedText`,而不是里面的 `Text`
return (
<RedText ref={this.textRef}></RedText>
)
}
}
复制代码
今后例子看出,forwardRef 主要针对高阶组件编写者,用法流程以下:
forwardRef里的参数只能是无状态组件,那若是高阶组件返回值不是个无状态函数,是个有生命周期函数的 class 呢?React 官方文档中已有这样的例子,即在外面包一层无状态组件,即:
const paintRed = Component => (() => {
// 新增 `componentDidMount`
class WhatEver extends React.Component {
static displayName = `PaintRed(${Component.displayName || Component.name || Unkown})`
componentDidMount() {
console.log('Mounted!')
}
render() {
// textRef 即为最外层的 ref
const { textRef, ...props } = this.props
return (
<Component color='red' ref={textRef} {...props}></Component>
)
}
}
const forwardRef = React.forwardRef(
// 这里再将 ref 的值做为普通 props 传递便可
(props, ref) => (
<WhatEver textRef={ref} {...props}></WhatEver>
)
)
return forwardRef
})()
复制代码
众所周知,React 的 props 有两个是私有的:key 和 ref,这二者是不能做为普通props传递给子组件的。然而今后例子能够看出,forwardRef 功能是:包裹的无状态组件能够接收 ref 做为第二个参数,而且能够传递下去。此时 ref 依然是 props 里面私有的,仍是没法从 props 取出,依然没有打破原来的设计。
若是不用 createRef,而是用原来的两种形式,都是正常的。
这个 API 给个人感受是用的不是不少,实际中必定要用高阶组件的里面的 ref 状况很是少,并且大部分均可以经过 react 普通 api 解决,但总算是解决了一个原来的盲点,所以只能算是聊胜于无的新功能。但其实文档中也提到了,大部分须要使用 forwardRef 的时候均可以用其余方式解决。如在上面的源码仓库中,有个稍稍复杂的 forwardRef 的 demo,但其实仍是能够不用 forwardRef 来实现相同功能,并且用的是新的生命周期函数实现,将在下次说新的生命周期钩子时详细讲述。