多组件更新
多组件是我们常用的场景,若我们更新其中的某组件,react如何实现高效更新呢?
class Child1 extends Component{state={count: 1}onClickFn = ()=>{this.setState({ count: this.state.count + 1 })}componentDidUpdate(){console.log('didUpdate Child1');}render(){return <div onClick={ this.onClickFn }>child1{ this.state.count }</div>}}class Child2 extends Component{state={count: 1}componentDidUpdate(){console.log('didUpdate Child2');}onClickFn = ()=>{this.setState({ count: this.state.count + 1 })}render(){return <div onClick={ this.onClickFn } >child2{ this.state.count }</div>}}class App extends Component {componentDidUpdate(){console.log('didUpdate');}render() {return (<div id="appId">Hello World!<Child1></Child1><Child2></Child2></div>)}}ReactDOM.render(<App/>, document.getElementById('root'));
点击组件的click事件,触发组件的setState,调用enqueueSetState。
enqueueSetState: function enqueueSetState(inst, payload, callback) {var fiber = get(inst);var currentTime = requestCurrentTime();var expirationTime = computeExpirationForFiber(currentTime, fiber);var update = createUpdate(expirationTime);update.payload = payload;if (callback !== undefined && callback !== null) {update.callback = callback;}flushPassiveEffects();enqueueUpdate(fiber, update);scheduleWork(fiber, expirationTime);//}
在 scheduleWork 中 scheduleWorkToRoot 为当前fiber添加expirationTime,同时为fiber 父级的childExpirationTime 添加 expirationTime,找到并返回ReactRoot(fiberRoot)。
function scheduleWork(fiber, expirationTime) {var root = scheduleWorkToRoot(fiber, expirationTime);if (root === null) { return; }//...markPendingPriorityLevel(root, expirationTime);// 为 root 添加 expirationTime 等if (!isWorking || isCommitting$1 ||nextRoot !== root) {var rootExpirationTime = root.expirationTime;requestWork(root, rootExpirationTime);}if (nestedUpdateCount > NESTED_UPDATE_LIMIT) {nestedUpdateCount = 0;}}
在 markPendingPriorityLevel 为当前root添加 expirationTime 等值,其它的和事件派发类似,执行完事件方法接着执行 performSyncWork 等方法。更新的高效性和 expirationTime 有无有关。
workLoop
最初的nextUnitOfWork 为 createWorkInProgress 创建的ReactRoot(fiberRoot)。更新的高效处理在 beginWork 中的 bailoutOnAlreadyFinishedWork中。
beginWork
在 执行到更新的 fiber 之前,workInProgress.expirationTime = 0。
function beginWork(current$$1, workInProgress, renderExpirationTime) {var updateExpirationTime = workInProgress.expirationTime;if (current$$1 !== null) {var oldProps = current$$1.memoizedProps;var newProps = workInProgress.pendingProps;if (oldProps === newProps && !hasContextChanged() &&updateExpirationTime < renderExpirationTime) {//....return bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime);}}workInProgress.expirationTime = NoWork; // NoWork = 0;//..创建与更新}
在scheduleWork 下 scheduleWorkToRoot 中为 fiber的父级添加 childExpirationTime。这就让bailoutOnAlreadyFinishedWork 中 childExpirationTime < renderExpirationTime 为 false,进而执行cloneChildFibers。
function bailoutOnAlreadyFinishedWork(current$$1, workInProgress, renderExpirationTime) {cancelWorkTimer(workInProgress);if (current$$1 !== null) {workInProgress.firstContextDependency = current$$1.firstContextDependency;}if (enableProfilerTimer) {stopProfilerTimerIfRunning(workInProgress);}var childExpirationTime = workInProgress.childExpirationTime;// expirationTimeif (childExpirationTime < renderExpirationTime) {return null;} else {cloneChildFibers(current$$1, workInProgress);return workInProgress.child;}}
这样就能让更新的fiber的 上级 都进行cloneChildFibers,进而检查组件的更新。
cloneChildFibers
function cloneChildFibers(current$$1, workInProgress) {!(current$$1 === null || workInProgress.child === current$$1.child) ?invariant(false, 'Resuming work not yet implemented.') : void 0;if (workInProgress.child === null) { return; }var currentChild = workInProgress.child;// 克隆fiber,currentChild.pendingProps为当前fiber上的属性var newChild = createWorkInProgress(currentChild,currentChild.pendingProps,currentChild.expirationTime);workInProgress.child = newChild;newChild.return = workInProgress;while (currentChild.sibling !== null) {currentChild = currentChild.sibling;newChild = newChild.sibling = createWorkInProgress(currentChild,currentChild.pendingProps,currentChild.expirationTime);newChild.return = workInProgress;}newChild.sibling = null;}
cloneChildFibers 直接使用 前一次渲染的结果,此处使用 currentChild.pendingProps ,这让 beginWork 中 oldProps === newProps 为true。
复用
随着workLoop的执行,会逐渐检查所有子fiber,beginWork 中 oldProps === newProps && !hasContextChanged() && updateExpirationTime < renderExpirationTime 条件会过滤是否更新。符合条件的则会调用 bailoutOnAlreadyFinishedWork检查 childExpirationTime 是否有值,若childExpirationTime 有值则克隆并返回 child ;若childExpirationTime 无值则返回null ,进而忽略此fiber。这样就只更新调用 setState 的fiber。若无更新在completeUnitOfWork也会复用实例。
调用setState的fiber,我们用newFiber表示。通过 过滤 及复用fiber 属性,这样可以 高效的找到newFiber,然后再得到 此fiber下 要更新的元素。在workLoop中,循环 newFiber后,还要循环newFiber中的子元素,而 newFiber 中的子元素有些是符合 beginWork 中的更新条件, 但有些子元素的 oldProps === newProps 为true(基本上是文本)。
以上面的例子为例,点击 Child1 组件触发click 调用setState,更新时只有Child1 会调用 updateClassComponent 更新,其它 reactElement 调用bailoutOnAlreadyFinishedWork。Child1 中的‘child1’文本,符合beginWork条件,也会调用bailoutOnAlreadyFinishedWork。
建议
fiber是 React Tree中的’dom’。不同reactElemnt转化为fiber时,fiber之间只是属性不同。用组件封装不会减慢运行速度,相反使用恰当的话,可以提升运行速度。
