本节我们看如何更新组件。在上一节也反复提到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,context) === false) { skip = true; } else if (component.componentWillUpdate) { component.componentWillUpdate(props,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,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,false); } else { //否则要替换原来的组件 //toUnmount用来标识一会儿要进行unmount操作 toUnmount = inst; //实例化另一个组件 component._component = inst = createComponent(childComponent,context); //刷新真实DOM inst.nextBase = inst.nextBase || nextBase; inst._parentComponent = component; //更新子组件的属性,这里面调用WillRecieveProps钩子 setComponentProps(inst,NO_RENDER,false); //异步渲染子组件,这招比较妙,这里你可以看到isChild参数的作用 renderComponent(inst,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,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,false); //新旧子组件的类型不一致,用新组件的实例进行替换 setComponentProps(inst,false);
setComponentProps的源码
export function setComponentProps(component,props,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,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,mountAll); } else { enqueueRender(component); } } if (component.__ref) component.__ref(component); }
最后看 createComponent,这是创建一个组件实例。React的组件有三种,经典组件,纯组件,无状态组件,前两种都是类的形式,可以归为一种,最后一种是普通函数。但在src/vdom/component-recycler.js
我们看到它们都是new
出来的。
export function createComponent(Ctor,context) { let list = components[Ctor.name],inst; //类形式的组件 if (Ctor.prototype && Ctor.prototype.render) { inst = new Ctor(props,context); Component.call(inst,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,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); }原文链接:https://www.f2er.com/react/303417.html