前言
- 本篇看一下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刷新。