本节我们看如何更新组件。在上一节也反复提到renderComponent这个方法了,这节直接从它入手吧。它位于src/vdom/component.js
文件中。
从参数来看,我们会惊讶它竟然会有这么多参数,原来我们只看到它有两个参数,第二个为数字。第一个参数不用说,是组件的实例。
export function renderComponent(component, opts, mountAll, isChild) {
//如果它是_disable状态,立即返回,
if (component._disable) return;
//开始取出它前后的props, state,context, base
//base是这个组件的render方法生成的虚拟DOM最后转化出来的真实DOM
//如果有这个真实DOM,说明它已经mount了,现在是处于更新状态
let props = component.props,
state = component.state,
context = component.context,
previousProps = component.prevProps || props,
previousState = component.prevState || state,
previousContext = component.prevContext || context,
isUpdate = component.base,
nextBase = component.nextBase,
//真实DOM
initialBase = isUpdate || nextBase,
//这个变早比较难理,它是component的render方法生成的虚拟DOM的type函数再实例化出来的子组件,相当于一个组件又return出另一个组件。通常情况下,组件会return出来的虚拟DOM的type为一个字符串,对应div, p, span这些真实存在的nodeName。而type为函数时,它就是一个组件。
initialChildComponent = component._component,
skip = false,
rendered, inst, cbase;
// 如果是更新状态,会经过shouldComponentUpdate,componentWillUpdate钩子
if (isUpdate) {
component.props = previousProps;
component.state = previousState;
component.context = previousContext;
if (opts!==FORCE_RENDER
&& component.shouldComponentUpdate
&& component.shouldComponentUpdate(props, state, context) === false) {
skip = true;
}
else if (component.componentWillUpdate) {
component.componentWillUpdate(props, state, context);
}
component.props = props;
component.state = state;
component.context = context;
}
//GC
component.prevProps = component.prevState = component.prevContext = component.nextBase = null;
component._dirty = false;
if (!skip) {
//mount与update都要调用render方法,这时与官方react有点不一样,官方react是没有传参,可能是早期官方文档没有规范render的参数吧。而后来的官方源码上,render是没有参数的。这个参数不应该preact来背。
rendered = component.render(props, state, context);
// 如果用户定义了getChildContext,那么用它与context生成孩子的context
if (component.getChildContext) {
context = extend(extend({}, context), component.getChildContext());
}
let childComponent = rendered && rendered.nodeName,
toUnmount, base;
//判定render出来的虚拟DOM是否还是一个组件
if (typeof childComponent==='function') {
// set up high order component link
let childProps = getNodeProps(rendered);
inst = initialChildComponent;
//如果前后两次的子组件的类型都一致,并且key也一样,则用setComponentProps方法更新这个子组件
if (inst && inst.constructor===childComponent && childProps.key==inst.__key) {
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
}
else {
//否则要替换原来的组件
//toUnmount用来标识一会儿要进行unmount操作
toUnmount = inst;
//实例化另一个组件
component._component = inst = createComponent(childComponent, childProps, context);
//刷新真实DOM
inst.nextBase = inst.nextBase || nextBase;
inst._parentComponent = component;
//更新子组件的属性,这里面调用WillRecieveProps钩子
setComponentProps(inst, childProps, NO_RENDER, context, false);
//异步渲染子组件,这招比较妙,这里你可以看到isChild参数的作用
renderComponent(inst, SYNC_RENDER, mountAll, true);
}
base = inst.base;
}
else {
//如果这次render出来的不是组件,而是普通虚拟DOM,
cbase = initialBase;
// destroy high order component link
toUnmount = initialChildComponent;
if (toUnmount) {
cbase = component._component = null;
}
if (initialBase || opts===SYNC_RENDER) {
if (cbase) cbase._component = null;
base = diff(cbase, rendered, context, mountAll || !isUpdate, initialBase && initialBase.parentNode, true);
}
}
//如果元素节点不同,并且组件实例也不是一个
if (initialBase && base!==initialBase && inst!==initialChildComponent) {
let baseParent = initialBase.parentNode;
if (baseParent && base!==baseParent) {
baseParent.replaceChild(base, initialBase);
if (!toUnmount) {
initialBase._component = null;
recollectNodeTree(initialBase, false);
}
}
}
if (toUnmount) {
unmountComponent(toUnmount);
}
//重写真实DOM
component.base = base;
if (base && !isChild) {
let componentRef = component,
t = component;
//由于组件能返回组件,可能经过N次render后才能返回一个能转换成为真实DOM的普通虚拟DOM,这些组件通过_parentComponent链接在一起,它们都是共享同一个真实DOM(base), 这时我们需要为这些组件都重写base属性
while ((t=t._parentComponent)) {
(componentRef = t).base = base;
}
//在真实DOM上保存最初的那个组件与组件的构造器
//在真实DOM上保存这么多对象其实是不太好的实现,因为会导致内存泄露,因此才有了recollectNodeTree这个方法
base._component = componentRef;
base._componentConstructor = componentRef.constructor;
}
}
//如果是异步插入进行组件的单个render或者是ReactDOM.render,这些组件实例都会先放到mounts数组中。
if (!isUpdate || mountAll) {
mounts.unshift(component);
}
else if (!skip) {
//更新完毕,调用componentDidUpdate,afterUpdate钩子
if (component.componentDidUpdate) {
component.componentDidUpdate(previousProps, previousState, previousContext);
}
if (options.afterUpdate){
options.afterUpdate(component);
}
}
//调用setState, forceUpdate钩子
if (component._renderCallbacks!=null) {
while (component._renderCallbacks.length) component._renderCallbacks.pop().call(component);
}
//执行其他组件的更新或插入,diffLevel为一个全局变量
if (!diffLevel && !isChild) flushMounts();
}
这个函数出现的对象与关系太多了,究竟某某是某某的什么,看下图就知了。
我们需要知道,组件render后可能产生普通虚拟DOM与子组件,而只有普通虚拟DOM才能转化为真实DOM。组件的实例通过_component
与_parentComponent
联结在一块,方便上下回溯。而实例总是保存着最后转化出来的真实DOM(base, 也叫initialBase)。base上保存着最上面的那个组件实例,也就是_component,此外,为了方便比较,它的构造器也放在DOM节点上。
renderComponent这个方法主要处理组件更新时的钩子,及建立父子组件间的联系。
这个方法的参数的起名也很奇葩,如果改成
renderComponent(componentInstance, renderModel, isRenderByReactDOM, isRenderChildComponent)
则好理解些。显示preact的作者不太想知道其奥秘,因此源码的注释也很少很少。
好了,我们看setComponentProps方法,它在renderComponent用了两次。
//更新已有的子组件实例
setComponentProps(inst, childProps, SYNC_RENDER, context, false);
//新旧子组件的类型不一致,用新组件的实例进行替换
setComponentProps(inst, childProps, NO_RENDER, context, false);
setComponentProps的源码
export function setComponentProps(component, props, opts, context, mountAll) {
if (component._disable) return;
//_disable状态下阻止用户
component._disable = true;
if ((component.__ref = props.ref)) delete props.ref;
if ((component.__key = props.key)) delete props.key;
if (!component.base || mountAll) {
//如果没有插入到DOM树或正在被ReactDOM.render渲染
if (component.componentWillMount) component.componentWillMount();
}
else if (component.componentWillReceiveProps) {
//如果是在更新过程中
component.componentWillReceiveProps(props, context);
}
//下面依次设置provProps, props, prevContext, context
if (context && context!==component.context) {
if (!component.prevContext) component.prevContext = component.context;
component.context = context;
}
if (!component.prevProps) component.prevProps = component.props;
component.props = props;
component._disable = false;
//=====================
if (opts!==NO_RENDER) {
if (opts===SYNC_RENDER || options.syncComponentUpdates!==false || !component.base) {
renderComponent(component, SYNC_RENDER, mountAll);
}
else {
enqueueRender(component);
}
}
if (component.__ref) component.__ref(component);
}
最后看 createComponent,这是创建一个组件实例。React的组件有三种,经典组件,纯组件,无状态组件,前两种都是类的形式,可以归为一种,最后一种是普通函数。但在src/vdom/component-recycler.js
我们看到它们都是new
出来的。
export function createComponent(Ctor, props, context) {
let list = components[Ctor.name],
inst;
//类形式的组件
if (Ctor.prototype && Ctor.prototype.render) {
inst = new Ctor(props, context);
Component.call(inst, props, context);
}else {//无状态组件
inst = new Component(props, context);
inst.constructor = Ctor;
inst.render = doRender;
}
if (list) {
for (let i=list.length; i--; ) {
if (list[i].constructor===Ctor) {
inst.nextBase = list[i].nextBase;
list.splice(i, 1);
break;
}
}
}
return inst;
}
我们再看一下doRender,这时恍然大悟,原来preact是统一所有组件以后更新都要通过render方法生成它的普通虚拟DOM或子组件。
function doRender(props, state, context) {
return this.constructor(props, context);
}
此外,preact还通过collectComponent来回收它的真实DOM,然后在createComponent中重复利用。这是它高效的缘由之一。
const components = {};
export function collectComponent(component) {
let name = component.constructor.name;
(components[name] || (components[name] = [])).push(component);
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。