React16是一个实验版本,除了测试它的新fiber架构外,还添加了大量新功能。其他React.Suspense与React.lazy就是重磅中的重磅。
随着前端的APP化,不断集成功能,页面越来越大,bundle size以MB为单位,我们需要拆分代码,实现动态化加载非首屏功能。比日历,城市选择器,评论组件等等。
动态加载,在javascript模块加载规范中已经有草案,并且被webpack, rollup所支持
import('../components/Hello').then(Hello => {console.log(<Hello />);
});
相当于(setTimeout
模拟异步加载组件):
new Promise(resolve =>setTimeout(() =>resolve({// 来自另一个文件的函数式组件default: function render() {return <div>Hello</div>}}),3000)
).then(({ default: Hello }) => {console.log(<Hello />);
});
React16提供了lazy组件实现这个功能
var {lazy, Suspense} = React;var OtherComponent = lazy(()=>{return new Promise(resolve =>setTimeout(() =>resolve({default: function Hello() { //export defaultreturn <div>Hello</div>}}),3000))});
上面只是一个方便大量测试的例子,实际需要将 <div>Hello</div>
放到一个文件中,使用import()语法引进来,即
const OtherComponent = React.lazy(() => import('./OtherComponent'));
OtherComponent.js的源码
export default function Hello(){return <div>Hello</div>
}
接着我们实现将动态功能加进组件树,就是所谓的条件渲染。这个功能由React.Suspense提供。在框架不提供这功能的情况下,我们需要找一个父组件,将这组件动态挂上去:
class MyComponent extends Component {constructor() {super();this.state = {};// 动态加载import('./OtherComponent').then(({ default: OtherComponent }) => {this.setState({ OtherComponent });});}render() {const { OtherComponent } = this.state;return (<div>{/* 条件渲染 */}{ OtherComponent && <OtherComponent /> }</div>);}
}
但光是这样还是不行的,因为单纯的动态加载,会引起页面抖动,我们需要loading将这个突兀的显示给缓解一下。于是这render需要改成
render() {const { OtherComponent } = this.state;return (<div>{OtherComponent ? <OtherComponent />: <Loading />}</div>);}
如果有了Suspense组件,我们就简单了,不需要入侵已有的组件,并且有地方挂这个loading。
<Suspense fallback={<Loading />}><OtherComponent /></Suspense>
需要注意的是,lazy组件外面必须包着一个Suspense组件。从框架层面对用户体验提出了强制要求。
下面是一个完整的例子
<!DOCTYPE html>
<html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width">
<!--<script src="https://cdn.bootcss.com/react/16.7.0-alpha.2/umd/react.development.js"></script><script src="https://cdn.bootcss.com/react-dom/16.7.0-alpha.2/umd/react-dom.development.js"></script>
--><script src="./dist/React.js"></script> <script type='text/javascript' src="./lib/babel.js"></script></head><body><div id='root' class="root"></div><script type='text/babel'>var { lazy, Suspense } = React;
var OtherComponent = lazy(() => {return new Promise(resolve =>setTimeout(() =>resolve({default: function render(props) {//export defaultreturn <div>Hello,{props.name}</div>;}}),3000));
});
var valueRef = React.createRef()
class App extends React.Component {constructor(props){super(props)this.state = {name: "ruby"}}updateName(){var name = valueRef.current.value;this.setState({name})}render() {return (<div><p><input ref={valueRef} onChange={this.updateName.bind(this)} value={this.state.name} />{new Date-0}</p><Suspense fallback={<div>loading...</div>}><OtherComponent name={this.state.name} /></Suspense></div>);}
}ReactDOM.render(<App />, document.getElementById("root"));</script></html>
最后说一下实现,React官方是通过lazy组件主动抛错,给上面Suspense组件接住的hack方式实现的
https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberWorkLoop.js#L907-L1057
lazy组件被读取时
https://github.com/facebook/react/blob/master/packages/react-reconciler/src/ReactFiberLazyComponent.js
React官方投入了近500行代码实现的,
再看一下anujs如何实现的。
anujs的Suspense是异常简单,就是一个虚拟组件,甩手掌柜,总是渲染它的孩子。它只负责挂loading。
function Suspense(props){return props.children
}export {Suspense
}
lazy组件就是将用户函数,再包一层,封到一个LazyComponent组件中,它会根据用户的promise决定是返回上层的suspense组件的fallback属性,还是延期加载回来的组件。
import { miniCreateClass, isFn, get } from "react-core/util";
import { Component } from "react-core/Component";
import { createElement } from "react-core/createElement";
import { Suspense } from "./Suspense";var LazyComponent = miniCreateClass(function LazyComponent(props, context) {this.props = props;this.context = context;this.state = {component: null,resolved: false}var promise = props.render();if(!promise || !isFn(promise.then)){throw "lazy必须返回一个thenable对象"}promise.then( (value) =>this.setState({component: value.default,resolved: true}))}, Component, {fallback(){//返回上层Suspense组件的fallback属性var parent = Object(get(this)).returnwhile(parent){if( parent.type === Suspense){return parent.props.fallback}parent = parent.return}throw "lazy组件必须包一个Suspense组件"},render(){return this.state.resolved ? createElement(this.state.component) : this.fallback()}
});
function lazy(fn) {return function(){return createElement(LazyComponent, {render: fn})}
}
export {lazy
}
最后欢迎大家使用anujs 。一个迷你React框架,支持react16 的所有新特性,可以用于小程序中。
RubyLouvre/anugithub.comnpm i anujs