自从去年9月份 React 团队发布了 v16.0 版本开始,到18年3月刚发布的 v16.3 版本,React 陆续推出了多项重磅新特性,并改进了原有功能中反馈呼声很高的一些问题,例如 render 方法内单节点层级嵌套问题,提供生命周期错误捕捉,组件指定 render 到任意 DOM 节点 (Portal) 等能力,以及最新的 Context API 和 Ref API。我们在对以上新特性经过一段时间的使用过后,通过本文进行一些细节分享和总结。
一、render 方法优化为了符合 React 的 component tree 和 diff 结构设计,在组件的 render() 方法中顶层必须包裹为单节点,因此实际组件设计和使用中总是需要注意嵌套后的层级变深,这是 React 的一个经常被人诟病的问题。比如以下的内容结构就必须再嵌套一个 div 使其变为单节点进行返回:
render() { return ( <div> 注: <p>产品说明一</p> <p>产品说明二</p> </div> ); }现在在更新 v16 版本后,这个问题有了新的改进,render 方法可以支持返回数组了:
render() { return [ "注:", <p key="t-1">产品说明一</h2>, <p key="t-2">产品说明二</h2>, ]; }这样确实少了一层,但大家又继续发现代码还是不够简洁。首先 TEXT 节点需要用引号包起来,其次由于是数组,每条内容当然还需要添加逗号分隔,另外 element 上还需要手动加 key 来辅助 diff。给人感觉就是不像在写 JSX 了。
于是 React v16.2 趁热打铁,提供了更直接的方法,就是 Fragment:
render() { return ( <React.Fragment> 注: <p>产品说明一</p> <p>产品说明二</p> </React.Fragment> ); }可以看到是一个正常单节点写法,直接包裹里面的内容。但是 Fragment 本身并不会产生真实的 DOM 节点,因此也不会导致层级嵌套增加。
另外 Fragment 还提供了新的 JSX 简写方式 <></>:
render() { return ( <> 注: <p>产品说明一</p> <p>产品说明二</p> </> );}看上去是否舒服多了。不过注意如果需要给 Fragment 添加 key prop,是不支持使用简写的(这也是 Fragment 唯一会遇到需要添加props的情况):
<dl> {props.items.map(item => ( // 要传key用不了 <></> <Fragment key={item.id}> <dt>{item.term}</dt> <dd>{item.description}</dd> </Fragment> ))} </dl> 二、错误边界 (Error Boundaries)错误边界是指以在组件上定义 componentDidCatch 方法的方式来创建一个有错误捕捉功能的组件,在其内嵌套的组件在生命过程中发生的错误都会被其捕捉到,而不会上升到外部导致整个页面和组件树异常 crash。
例如下面的例子就是通过一个 ErrorBoundary 组件对其内的内容进行保护和错误捕捉,并在发生错误时进行兜底的UI展示:
class ErrorBoundary extends Component { constructor(props) { super(props); this.state = { error: null }; } componentDidCatch(error, {componentStack} ) { this.setState({ error, componentStack, }); } render() { if (this.state.error) { return ( <> <h1>报错了.</h1> <ErrorPanel {...this.state} /> </> ); } return this.props.children; } } export default function App(){ return ( <ErrorBoundary> <Content /> </ErrorBoundary> ); }需要注意的是错误边界只能捕捉生命周期中的错误 (willMount / render 等方法内)。无法捕捉异步的、事件回调中的错误,要捕捉和覆盖所有场景依然需要配合 window.onerror、Promise.catch、 try/catch 等方式。
三、React.createPortal()这个 API 是用来将部分内容分离式地 render 到指定的 DOM 节点上。不同于使用 ReactDom.render 新创建一个 DOM tree 的方式,对于要通过 createPortal() “分离”出去的内容,其间的数据传递,生命周期,甚至事件冒泡,依然存在于原本的抽象组件树结构当中。
class Creater extends Component { render(){ return ( <div onClick={() => alert("clicked!") }> <Portal> <img src=http://www.likecs.com/{myImg} /> </Portal> </div> ); } } class Portal extends Component { render(){ const node = getDOMNode(); return createPortal( this.props.children, node ); } }例如以上代码,
四、Context API