前言
React 自发布 v16 版本以来已经有半年了, 至今最新的是 v16.3. 从 v16 开始增加了较多新的 API. 相较于之前纯净的 API 设计, 变化可以说是非常大了. 可以看出 facebook 的 React 团队已经解决了之前的大多数问题, 现在开始为 React 设计新的 API , 增加新功能了. 得益于重写 React 底层为 Fiber 架构, v16 包含了许多实用的新特性, 并且也有一些 "break-change", 之后的版本肯定更多. 在官方博客 https://doc.react-china.org/blog/ 上的介绍虽然很全, 但是由于翻译不及时, 查阅不变, 而且有的地方难于理解. 故在此通过一些代码做简要介绍, 也为自己查阅 API 做记录.
仓库中代码包含了本人的最佳实践, 阅读文章之前, 希望你对 React 和 ES6 有一定了解. 当然如果不会, 也可以选择性看看. 如有例子不合适或谬误, 欢迎 提出意见 https://github.com/fengkfengk/learn-react-next/issues .
代码仓库
代码已托管到 github 仓库 https://github.com/fengkfengk/learn-react-next 上, 里面有对应新特性子的 demo, 欢迎 star ! 没错我就是来骗赞的
开始
由于准备长期更新, 现在的是 v16 版. 所以克隆到本地以后先 cd react16, 再 npm i, npm start . 访问 localhost:9000 即可看到 demo(忽略我丑陋的 CSS). 左边有几个路由, 分别对应了几个新特性和相应的 demo.
ErrorBoundary (v16.0)
React v16 之前, 如果渲染中出现错误, 整个页面会直接崩掉. 如果对 React 足够了解, 可能会知道一个秘而不宣的 API:
unstable_handleError
. 此函数可用于捕获页面错误, 然而由于文档没记录, 知道的开发者也寥寥无几. 现在我们有了新的, 官方的, 稳定的 API: componentDidCatch. 就像 try catch 一样, 可用于捕获 render 过程中的错误, 常用于捕获错误并渲染不同的页面, 避免整个页面崩溃.
演示
demo 中, 第一个是自增按钮, 增加到 5 会抛出渲染错误, 如果此时处于不捕获模式, 页面崩溃, 只能刷新页面恢复正常.
相信这是每一个 React 页面仔的噩梦吧(包括我), 应该尽力避免此种情况发生. 切换到捕获模式后, 组件启用新的 componentDidCatch API. 此时发生错误不会崩, 会显示备用页面.
代码
新特性的主要代码在
/src/ErrorBoundary/ErrorHandler.jsx
下. 本来如果不用切换模式, 一直捕获错误的话, ErrorHander 应该长成这样:
- 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 https://doc.react-china.org/docs/error-boundaries.html
Portal (v16.0)
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 https://doc.react-china.org/docs/portals.html .
Fragment(v16.0) & StrictMode(v16.3)
这两个静态组件均挂载在 React 包下, 通过 React.Fragment 和 React.StrictMode 可访问到.
Fragment 静态组件, v16.0 推出, 用于将多个 React render 返回值包裹成一个具有顶级元素的 element. 之前如果我们需要返回多个元素, 一定要在外面包一层 < div></div > 或其他的元素, React 还会将其渲染成真实 DOM; 或直接返回一个相应的数组(React v16.0 支持), 但是非常丑陋, 并且必须附带 key 属性, 即使用不到.
现在新的 Fragment 仅用于包裹, 并不会生成对应 DOM 了, 就像普通的 jsx 一样, 也不需要 key 属性了, 还是非常不错的新功能. 官方文档: Fragments https://doc.react-china.org/docs/fragments.html
StrictMode 于 v16.3 推出. 顾名思义, 即严格模式, 可用于在开发环境下提醒组件内使用不推荐写法和即将废弃的 API(该版本废弃了三个生命周期钩子). 与 Fragment 相同, 并不会被渲染成真实 DOM. 官方文档严格模式 https://doc.react-china.org/docs/strict-mode.html 里详细介绍了会在哪些情况下发出警告. 对于我们开发者来说, 及时弃用不被推荐的写法即可规避这些警告.
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' 一样应用越来越广泛.
createRef (v16.3)
v15 版本 ref
之前版本, 如果想取得某个 Element 的 Ref, 有两种方式可选:
字符串形式:
- <input ref="input" />
- => this.refs.input
回调函数形式:
- <input ref={input => (this.input = input)} />
- => this.input
其中字符串形式, 由于存在种种问题 ( https://github.com/facebook/react/pull/8333#issuecomment-271648615 , 八卦下: 这哥们就是 redux 作者)而不被推荐, 具体内容就是:
需要内部追踪 ref 的 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 中, 因此这也是个问题.
不可组合(其实没看太懂, 大意是如果一个库将传进来的 children 给了 ref, 那么开发者将无法传递另一个 ref 给 children. https://github.com/facebook/react/issues/8734 ).
因此现在常用函数形式, 几乎没有确定, 唯一的遗憾是需要新建函数; 如果放入 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 (v16.3)
之前是没有 ForwardRef 这种概念的, 这是专门为高阶组件获取 Ref 而设计. 官方文档 (英文)Forwarding Refs https://doc.react-china.org/docs/forwarding-refs.html 的例子掺杂了许多对高阶组件(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 包裹, 并且可以传递第二个参数 ref;
无状态组件中的返回值可将 ref 作为 props 传入.
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 来实现相同功能, 而且用的是新的生命周期函数实现, 将在下次说新的生命周期钩子时详细讲述.
来源: https://juejin.im/post/5ad949e36fb9a07aa92544e4