前言
- 本篇看一下Suspense和lazy到底啥玩意。
示例
- lazy和suspense的组合有点意思,一般用来做代码分割用,但里面如何实现的?我只知道分割部分是用webpack的import语句写的,毕竟这个是用户自己写的,里面感觉像是啥都没干一样。这就有点像webpack的cssloader,你感觉cssloader好像啥都没干,毕竟你写的本来就是css或者被预处理器转成了css,那么要cssloader干啥?实际上cssloader写的相当复杂。
import React, { Component,lazy,Suspense } from 'react';
import reactDom from 'react-dom';
import {HashRouter,Route,Link}from 'react-router-dom'
class App extends Component{
render(){
return(
<HashRouter>
<Suspense fallback={null}>
<Link to='/'>111</Link>
<Link to='/2'>222</Link>
<Route path="/" exact component={lazy(() =>import("./a"))}></Route>
<Route path="/2" exact component={lazy(() =>import("./b"))}></Route>
</Suspense>
</HashRouter>
)
}
}
reactDom.render(
<App></App>
,document.getElementById('root')
)
源码
- lazy:
function lazy(ctor) {
var lazyType = {
$$typeof: REACT_LAZY_TYPE,
_ctor: ctor,
// React uses these fields to store the result.
_status: -1,
_result: null
};
{
// In production, this would just set it on the object.
var defaultProps;
var propTypes;
Object.defineProperties(lazyType, {
defaultProps: {
configurable: true,
get: function () {
return defaultProps;
},
set: function (newDefaultProps) {
error('React.lazy(...): It is not supported to assign `defaultProps` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');
defaultProps = newDefaultProps; // Match production behavior more closely:
Object.defineProperty(lazyType, 'defaultProps', {
enumerable: true
});
}
},
propTypes: {
configurable: true,
get: function () {
return propTypes;
},
set: function (newPropTypes) {
error('React.lazy(...): It is not supported to assign `propTypes` to ' + 'a lazy component import. Either specify them where the component ' + 'is defined, or create a wrapping component around it.');
propTypes = newPropTypes; // Match production behavior more closely:
Object.defineProperty(lazyType, 'propTypes', {
enumerable: true
});
}
}
});
}
return lazyType;
}
- suspense居然就一type:
exports.Suspense = REACT_SUSPENSE_TYPE;
- 我只想说mmp,要知道这个流程只能看一下他到底怎么工作的。
- 可以在渲染前打印下,发现suspense就是做成了个vdom
type: Symbol(react.suspense)
props: {fallback: null, children: Array(4)}
$$typeof: Symbol(react.element)
- 而lazy也做成了个vdom:
component:
$$typeof: Symbol(react.lazy)
_ctor: () => {…}
_result: ƒ Aaa(props)
_status: 1
defaultProps: (...)
propTypes: (...)
get defaultProps: ƒ ()
set defaultProps: ƒ (newDefaultProps)
get propTypes: ƒ ()
set propTypes: ƒ (newPropTypes)
__proto__: Object
exact: true
path: "/"
- 所以看来,要了解原理,又得去看令人崩溃的react-dom。。。
- 打断点看一下lazy相关:
function mountLazyComponent(_current, workInProgress, elementType, updateExpirationTime, renderExpirationTime) {
if (_current !== null) {
// A lazy component only mounts if it suspended inside a non-
// concurrent tree, in an inconsistent state. We want to treat it like
// a new mount, even though an empty version of it already committed.
// Disconnect the alternate pointers.
_current.alternate = null;
workInProgress.alternate = null; // Since this is conceptually a new fiber, schedule a Placement effect
workInProgress.effectTag |= Placement;
}
var props = workInProgress.pendingProps; // We can't start a User Timing measurement with correct label yet.
// Cancel and resume right after we know the tag.
cancelWorkTimer(workInProgress);
var Component = readLazyComponentType(elementType); // Store the unwrapped component in the type.
workInProgress.type = Component;
var resolvedTag = workInProgress.tag = resolveLazyComponentTag(Component);
startWorkTimer(workInProgress);
var resolvedProps = resolveDefaultProps(Component, props);
var child;
switch (resolvedTag) {
case FunctionComponent:
{
{
validateFunctionComponentInDev(workInProgress, Component);
workInProgress.type = Component = resolveFunctionForHotReloading(Component);
}
child = updateFunctionComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);
return child;
}
case ClassComponent:
{
{
workInProgress.type = Component = resolveClassForHotReloading(Component);
}
child = updateClassComponent(null, workInProgress, Component, resolvedProps, renderExpirationTime);
return child;
}
case ForwardRef:
{
{
workInProgress.type = Component = resolveForwardRefForHotReloading(Component);
}
child = updateForwardRef(null, workInProgress, Component, resolvedProps, renderExpirationTime);
return child;
}
case MemoComponent:
{
{
if (workInProgress.type !== workInProgress.elementType) {
var outerPropTypes = Component.propTypes;
if (outerPropTypes) {
checkPropTypes(outerPropTypes, resolvedProps, // Resolved for outer only
'prop', getComponentName(Component), getCurrentFiberStackInDev);
}
}
}
child = updateMemoComponent(null, workInProgress, Component, resolveDefaultProps(Component.type, resolvedProps), // The inner type can have defaults too
updateExpirationTime, renderExpirationTime);
return child;
}
}
var hint = '';
{
if (Component !== null && typeof Component === 'object' && Component.$$typeof === REACT_LAZY_TYPE) {
hint = ' Did you wrap a component in React.lazy() more than once?';
}
} // This message intentionally doesn't mention ForwardRef or MemoComponent
// because the fact that it's a separate type of work is an
// implementation detail.
{
{
throw Error("Element type is invalid. Received a promise that resolves to: " + Component + ". Lazy element type must resolve to a class or function." + hint);
}
}
}
function readLazyComponentType(lazyComponent) {
initializeLazyComponentType(lazyComponent);
if (lazyComponent._status !== Resolved) {
throw lazyComponent._result;
}
return lazyComponent._result;
}
function initializeLazyComponentType(lazyComponent) {
if (lazyComponent._status === Uninitialized) {
lazyComponent._status = Pending;
var ctor = lazyComponent._ctor;
var thenable = ctor();
lazyComponent._result = thenable;
thenable.then(function (moduleObject) {
if (lazyComponent._status === Pending) {
var defaultExport = moduleObject.default;
{
if (defaultExport === undefined) {
error('lazy: Expected the result of a dynamic import() call. ' + 'Instead received: %s\n\nYour code should look like: \n ' + "const MyComponent = lazy(() => import('./MyComponent'))", moduleObject);
}
}
lazyComponent._status = Resolved;
lazyComponent._result = defaultExport;
}
}, function (error) {
if (lazyComponent._status === Pending) {
lazyComponent._status = Rejected;
lazyComponent._result = error;
}
});
}
}
-
可以看见,这个thenable就是我们在lazy中写的webpack的import,还没加载时是个pending的promise,然后存到_result上。就是把我们写的存到vdom属性上。又加了个then后的内容和状态,到时候代码来了,状态显示仍未处理,那就把真正结果放到_result上。
-
当拉完代码后webpack的promise会调then,就是走一遍这里,这时这个vnode的_result就是有值的状态,同样也会进initializeLazyComponentType方法,但是这里已经被标记处理过了,然后就会直接退出来返回_result结果。
-
此时mountLazyComponent中工作的fiber就会取得Component结果,然后存到type上,resolveDefaultProps是判断有无默认属性,判断这个默认导出到底是什么组件,再后面几个判断没啥用,最后就是更新属性,得到当前工作fiber的最新状态,也就是拿到了懒加载后最终要渲染的fiber的样子。
-
如果一直没拉完代码当然就一直没人调then,页面一直维持fallback样子,有人调then或者rej就继续走调度。
-
判断是suspense的tag会走这个逻辑:
function updateSuspenseComponent(current, workInProgress, renderExpirationTime) {
var mode = workInProgress.mode;
var nextProps = workInProgress.pendingProps; // This is used by DevTools to force a boundary to suspend.
{
if (shouldSuspend(workInProgress)) {
workInProgress.effectTag |= DidCapture;
}
}
var suspenseContext = suspenseStackCursor.current;
var nextDidTimeout = false;
var didSuspend = (workInProgress.effectTag & DidCapture) !== NoEffect;
if (didSuspend || shouldRemainOnFallback(suspenseContext, current)) {
// Something in this boundary's subtree already suspended. Switch to
// rendering the fallback children.
nextDidTimeout = true;
workInProgress.effectTag &= ~DidCapture;
} else {
// Attempting the main content
if (current === null || current.memoizedState !== null) {
// This is a new mount or this boundary is already showing a fallback state.
// Mark this subtree context as having at least one invisible parent that could
// handle the fallback state.
// Boundaries without fallbacks or should be avoided are not considered since
// they cannot handle preferred fallback states.
if (nextProps.fallback !== undefined && nextProps.unstable_avoidThisFallback !== true) {
suspenseContext = addSubtreeSuspenseContext(suspenseContext, InvisibleParentSuspenseContext);
}
}
}
suspenseContext = setDefaultShallowSuspenseContext(suspenseContext);
pushSuspenseContext(workInProgress, suspenseContext); // This next part is a bit confusing. If the children timeout, we switch to
// showing the fallback children in place of the "primary" children.
// However, we don't want to delete the primary children because then their
// state will be lost (both the React state and the host state, e.g.
// uncontrolled form inputs). Instead we keep them mounted and hide them.
// Both the fallback children AND the primary children are rendered at the
// same time. Once the primary children are un-suspended, we can delete
// the fallback children — don't need to preserve their state.
//
// The two sets of children are siblings in the host environment, but
// semantically, for purposes of reconciliation, they are two separate sets.
// So we store them using two fragment fibers.
//
// However, we want to avoid allocating extra fibers for every placeholder.
// They're only necessary when the children time out, because that's the
// only time when both sets are mounted.
//
// So, the extra fragment fibers are only used if the children time out.
// Otherwise, we render the primary children directly. This requires some
// custom reconciliation logic to preserve the state of the primary
// children. It's essentially a very basic form of re-parenting.
if (current === null) {
// If we're currently hydrating, try to hydrate this boundary.
// But only if this has a fallback.
if (nextProps.fallback !== undefined) {
tryToClaimNextHydratableInstance(workInProgress); // This could've been a dehydrated suspense component.
} // This is the initial mount. This branch is pretty simple because there's
// no previous state that needs to be preserved.
if (nextDidTimeout) {
// Mount separate fragments for primary and fallback children.
var nextFallbackChildren = nextProps.fallback;
var primaryChildFragment = createFiberFromFragment(null, mode, NoWork, null);
primaryChildFragment.return = workInProgress;
if ((workInProgress.mode & BlockingMode) === NoMode) {
// Outside of blocking mode, we commit the effects from the
// partially completed, timed-out tree, too.
var progressedState = workInProgress.memoizedState;
var progressedPrimaryChild = progressedState !== null ? workInProgress.child.child : workInProgress.child;
primaryChildFragment.child = progressedPrimaryChild;
var progressedChild = progressedPrimaryChild;
while (progressedChild !== null) {
progressedChild.return = primaryChildFragment;
progressedChild = progressedChild.sibling;
}
}
var fallbackChildFragment = createFiberFromFragment(nextFallbackChildren, mode, renderExpirationTime, null);
fallbackChildFragment.return = workInProgress;
primaryChildFragment.sibling = fallbackChildFragment; // Skip the primary children, and continue working on the
// fallback children.
workInProgress.memoizedState = SUSPENDED_MARKER;
workInProgress.child = primaryChildFragment;
return fallbackChildFragment;
} else {
// Mount the primary children without an intermediate fragment fiber.
var nextPrimaryChildren = nextProps.children;
workInProgress.memoizedState = null;
return workInProgress.child = mountChildFibers(workInProgress, null, nextPrimaryChildren, renderExpirationTime);
}
} else {
// This is an update. This branch is more complicated because we need to
// ensure the state of the primary children is preserved.
var prevState = current.memoizedState;
if (prevState !== null) {
// wrapped in a fragment fiber.
var currentPrimaryChildFragment = current.child;
var currentFallbackChildFragment = currentPrimaryChildFragment.sibling;
if (nextDidTimeout) {
// Still timed out. Reuse the current primary children by cloning
// its fragment. We're going to skip over these entirely.
var _nextFallbackChildren2 = nextProps.fallback;
var _primaryChildFragment2 = createWorkInProgress(currentPrimaryChildFragment, currentPrimaryChildFragment.pendingProps);
_primaryChildFragment2.return = workInProgress;
if ((workInProgress.mode & BlockingMode) === NoMode) {
// Outside of blocking mode, we commit the effects from the
// partially completed, timed-out tree, too.
var _progressedState = workInProgress.memoizedState;
var _progressedPrimaryChild = _progressedState !== null ? workInProgress.child.child : workInProgress.child;
if (_progressedPrimaryChild !== currentPrimaryChildFragment.child) {
_primaryChildFragment2.child = _progressedPrimaryChild;
var _progressedChild2 = _progressedPrimaryChild;
while (_progressedChild2 !== null) {
_progressedChild2.return = _primaryChildFragment2;
_progressedChild2 = _progressedChild2.sibling;
}
}
} // Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
var _treeBaseDuration = 0;
var _hiddenChild = _primaryChildFragment2.child;
while (_hiddenChild !== null) {
_treeBaseDuration += _hiddenChild.treeBaseDuration;
_hiddenChild = _hiddenChild.sibling;
}
_primaryChildFragment2.treeBaseDuration = _treeBaseDuration;
} // Clone the fallback child fragment, too. These we'll continue
// working on.
var _fallbackChildFragment2 = createWorkInProgress(currentFallbackChildFragment, _nextFallbackChildren2);
_fallbackChildFragment2.return = workInProgress;
_primaryChildFragment2.sibling = _fallbackChildFragment2;
_primaryChildFragment2.childExpirationTime = NoWork; // Skip the primary children, and continue working on the
// fallback children.
workInProgress.memoizedState = SUSPENDED_MARKER;
workInProgress.child = _primaryChildFragment2;
return _fallbackChildFragment2;
} else {
// No longer suspended. Switch back to showing the primary children,
// and remove the intermediate fragment fiber.
var _nextPrimaryChildren = nextProps.children;
var currentPrimaryChild = currentPrimaryChildFragment.child;
var primaryChild = reconcileChildFibers(workInProgress, currentPrimaryChild, _nextPrimaryChildren, renderExpirationTime); // If this render doesn't suspend, we need to delete the fallback
// children. Wait until the complete phase, after we've confirmed the
// fallback is no longer needed.
// TODO: Would it be better to store the fallback fragment on
// the stateNode?
// Continue rendering the children, like we normally do.
workInProgress.memoizedState = null;
return workInProgress.child = primaryChild;
}
} else {
// The current tree has not already timed out. That means the primary
// children are not wrapped in a fragment fiber.
var _currentPrimaryChild = current.child;
if (nextDidTimeout) {
// Timed out. Wrap the children in a fragment fiber to keep them
// separate from the fallback children.
var _nextFallbackChildren3 = nextProps.fallback;
var _primaryChildFragment3 = createFiberFromFragment( // It shouldn't matter what the pending props are because we aren't
// going to render this fragment.
null, mode, NoWork, null);
_primaryChildFragment3.return = workInProgress;
_primaryChildFragment3.child = _currentPrimaryChild;
if (_currentPrimaryChild !== null) {
_currentPrimaryChild.return = _primaryChildFragment3;
} // Even though we're creating a new fiber, there are no new children,
// because we're reusing an already mounted tree. So we don't need to
// schedule a placement.
// primaryChildFragment.effectTag |= Placement;
if ((workInProgress.mode & BlockingMode) === NoMode) {
// Outside of blocking mode, we commit the effects from the
// partially completed, timed-out tree, too.
var _progressedState2 = workInProgress.memoizedState;
var _progressedPrimaryChild2 = _progressedState2 !== null ? workInProgress.child.child : workInProgress.child;
_primaryChildFragment3.child = _progressedPrimaryChild2;
var _progressedChild3 = _progressedPrimaryChild2;
while (_progressedChild3 !== null) {
_progressedChild3.return = _primaryChildFragment3;
_progressedChild3 = _progressedChild3.sibling;
}
} // Because primaryChildFragment is a new fiber that we're inserting as the
// parent of a new tree, we need to set its treeBaseDuration.
if (workInProgress.mode & ProfileMode) {
// treeBaseDuration is the sum of all the child tree base durations.
var _treeBaseDuration2 = 0;
var _hiddenChild2 = _primaryChildFragment3.child;
while (_hiddenChild2 !== null) {
_treeBaseDuration2 += _hiddenChild2.treeBaseDuration;
_hiddenChild2 = _hiddenChild2.sibling;
}
_primaryChildFragment3.treeBaseDuration = _treeBaseDuration2;
} // Create a fragment from the fallback children, too.
var _fallbackChildFragment3 = createFiberFromFragment(_nextFallbackChildren3, mode, renderExpirationTime, null);
_fallbackChildFragment3.return = workInProgress;
_primaryChildFragment3.sibling = _fallbackChildFragment3;
_fallbackChildFragment3.effectTag |= Placement;
_primaryChildFragment3.childExpirationTime = NoWork; // Skip the primary children, and continue working on the
// fallback children.
workInProgress.memoizedState = SUSPENDED_MARKER;
workInProgress.child = _primaryChildFragment3;
return _fallbackChildFragment3;
} else {
// Still haven't timed out. Continue rendering the children, like we
// normally do.
workInProgress.memoizedState = null;
var _nextPrimaryChildren2 = nextProps.children;
return workInProgress.child = reconcileChildFibers(workInProgress, _currentPrimaryChild, _nextPrimaryChildren2, renderExpirationTime);
}
}
}
}
- 这玩意真长,注释都写下面内容比较乱,不过这个分2部分,就是挂载和更新,就是看current是不是null,current是页面上状态,上半部分是未渲染出来时走的fallback逻辑,大概意思就是在当前的工作fiber的孩子的sibiling那用fragment新增个元素,然后存个pendingProps等待属性,这个pendingProps就是我们写在fallback里面的内容,在等待期间会渲染这个,而当前工作fiber的大儿子以及其子节点,就是我们所要真实渲染的元素。
- current不是null也就是更新逻辑,还得判断个nextDidTimeout,看是不是仍超时,如果仍超时,做个fallback渲染。
- 判断nextDidTimeout的下半段注释写了:
No longer suspended. Switch back to showing the primary children,
// and remove the intermediate fragment fiber.
- 不再推迟,换成大儿子进行渲染。大儿子就是要渲染的子节点。
- 所以这个suspense是个分步渲染的玩意,不管下面组件是不是能立即获取。里面只要进行更新,要走它调度,先甩给你个fallback,再看后面。
总结
- lazy有点相当于给promise后面搞个更新,suspense相当于搞个分段渲染。
附加
- 看看司徒正美大佬做的react咋实现suspense和lazy的:
function Suspense(props){
return props.children
}
export {
Suspense
}
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.children();
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)).return
while(parent){
if( parent.type === Suspense){
return parent.props.fallback
}
parent = parent.return
}
throw "lazy组件必须包一个Suspense组件"
},
render: function f2(){
return this.state.resolved ? createElement(this.state.component, this.props) : this.fallback()
}
});
function lazy(render) {
return function (props) {
return createElement(LazyComponent, props, render );
};
}
export {
lazy
}
- 相当精简啊,直接用找父亲方式找有没有suspensetype然后渲染fallback就行了,当webpack调then,直接setState刷新。