在Preact源码阅读(二),我们看了初始化阶段的diff算法,preact采用了深度diff的算法,由根节点开始,深度遍历子节点,完成比较及DOM的创建、插入。本章准备分析setState的基本函数功能,并基于此,分析preact更新阶段的流程。
1. setState功能
setState是我们使用最频繁的函数,在React框架里,我们通过setState更新数据,从而触发UI的渲染。setState的api如下:updater可以为object或类似(state, props)的函数。
setState(updater, [callback]);
Preact里setState的实现如下:
- 设置_nextState。_nextState不存在时,初始化_nextState=assign({}, this.state),nextState为浅拷贝this.state。
- update为函数时,调用update(s, this.props),注此处state传递的为_nextState。
- update存在时,浅拷贝到_nextState中;update不存在,过滤此次无效更新。
- 当前VNOde存在时,将callback放到_renderCallbacks中,将此次更新放到enqueueRender队列中。
Component.prototype.setState = function(update, callback) {
// _nextState不存在时,设置s=this._nextSat
let s;
if (this._nextState != null && this._nextState !== this.state) {
s = this._nextState;
} else {
s = this._nextState = assign({}, this.state);
}
if (typeof update == 'function') {
update = update(s, this.props);
}
if (update) {
assign(s, update);
}
// Skip update if updater function returned null
if (update == null) return;
if (this._vnode) {
if (callback) this._renderCallbacks.push(callback);
enqueueRender(this);
}
};
我们可以看到setState的功能主要是将更新的state放到_nextState里,并将更新放到渲染队列中。enqueueRender的功能主要是什么那,其主要负责渲染的队列管理,例如setState的同步合并、多级组件的渲染顺序,我们看一下enqueueRender的具体功能。
2. 更新机制
在我们写React代码时,我们经常会这样写,在这段代码里,我们用setState更新了多个state,Preact是如何在一次更新完成渲染的,这就是enqueueRender的功能了,我们首先看下Preact的enqueueRender的功能执行顺序。
this.setState({
count: count + 1,
});
...
this.setState({
pre: pre + 1,
});
Preact的函数调用如下图:
- setState调用,将更新放到enqueueRender队列。
- defer(process)的调用,nextTick执行渲染队列。
- 调用renderComponent完成UI的更新。
2.1 enqueueRender
enqueueRender函数功能主要是更新队列的状态,只有当组件未更新(_diry=false)且待更新组件数目为0时,才触发组件的更新。Preact用_dirty标记组件的更新状态, _dirty=true比较当前组件处于更新中,这主要是将组件的多个更新合并成一次。使用process_rerenderCount标记当前待更新的组件数目,大于1时就不触发渲染。
Preact支持自定义更新方式,可以通过options.deounceRendering自定义更新的方式,例如我们可以定义options.debounceRendering = window.requestAnimationFrame的形式,定义队列的更新方式。
export function enqueueRender(c) {
// 组件未更新(_dirty=false)且待更新组件为0
if (
(!c._dirty &&
(c._dirty = true) &&
rerenderQueue.push(c) &&
!process._rerenderCount++) ||
// 自定义更新方式,触发UI更新
prevDebounce !== options.debounceRendering
) {
// prevDebounce为自定义更新方式
prevDebounce = options.debounceRendering;
(prevDebounce || defer)(process);
}
}
Preact提供的更新方式为defer,defer的定义如下, Promise存在时,defer为Promise().then()、不存在时为setTimeout。defer(process)意味着将在下一个Eventloop周期,执行process,触发diff、组件的更新。
const defer =
typeof Promise == 'function'
? Promise.prototype.then.bind(Promise.resolve())
: setTimeout;
2.2 Process
process的函数定义如下,其具体的功能如下:
- 设置process._rerenderCount = rerenderQueue.length,标记当前渲染的数目。
- rerenderQueue基于_depth升序排序,更新从子组件开始更新,由下向上的更新。
- 队列循环处理,调用renderComponent,完成组件的更新。
function process() {
let queue;
while ((process._rerenderCount = rerenderQueue.length)) {
queue = rerenderQueue.sort((a, b) => a._vnode._depth - b._vnode._depth);
rerenderQueue = [];
queue.some(c => {
if (c._dirty) renderComponent(c);
});
}
}
process._rerenderCount = 0;
process主要是执行队列的更新及重置,Preact将按照由下向上的方式执行组件的更新,从而完成一次周期的更新。
2.3 renderComponent
renderComponent主要负责单个组件的更新,其主要功能是调用diff完成节点的更新。
- 调用diff完成节点的diff及dom的更新。parentDom存在时,调用diff完成节点的更新、dom的生成。
- 调用commitRoot, 完成_renderCallback的调用,包括生命周期、setState callback。
- 父节点DOM的指向变更。当newDom != oldDom时,例如if/else导致的节点变更时,我们需要更新dom的指向。
function renderComponent(component) {
let vnode = component._vnode,
oldDom = vnode._dom,
parentDom = component._parentDom;
if (parentDom) {
let commitQueue = [];
const oldVNode = assign({}, vnode);
oldVNode._original = oldVNode;
// 调用diff完成节点的更新及dom的生成
let newDom = diff(
parentDom,
vnode,
oldVNode,
component._globalContext,
parentDom.ownerSVGElement !== undefined,
null,
commitQueue,
oldDom == null ? getDomSibling(vnode) : oldDom
);
// _renderCallbcks的调用
commitRoot(commitQueue, vnode);
// parentDom的指向变更
if (newDom != oldDom) {
updateParentDomPointers(vnode);
}
}
}
updateParentDomPointers的功能如下,将从当前diff的节点开始,向上修改_dom/_component.base的指向。
function updateParentDomPointers(vnode) {
// 父节点不为null其为组件时,修改base的指向。
if ((vnode = vnode._parent) != null && vnode._component != null) {
vnode._dom = vnode._component.base = null;
for (let i = 0; i < vnode._children.length; i++) {
let child = vnode._children[i];
if (child != null && child._dom != null) {
// 指向第一个不为null的child节点
vnode._dom = vnode._component.base = child._dom;
break;
}
}
return updateParentDomPointers(vnode);
}
}
3. 总结
本章主要分析了setState及Preact的更新机制,Preact setState将待更新的组件push到更新队列,在NextTick完成组件Vnode、UI的更新。Preact的异步更新机制,可以很好的减少渲染的次数,从而减少DOM的更新频率,从而提高UI的渲染效率。