本節咱們看如何更新組件。在上一節也反覆提到renderComponent這個方法了,這節直接從它入手吧。它位於src/vdom/component.js
文件中。javascript
從參數來看,咱們會驚訝它居然會有這麼多參數,原來咱們只看到它有兩個參數,第二個爲數字。第一個參數不用說,是組件的實例。java
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(); }
這個函數出現的對象與關係太多了,究竟某某是某某的什麼,看下圖就知了。node
咱們須要知道,組件render後可能產生普通虛擬DOM與子組件,而只有普通虛擬DOM才能轉化爲真實DOM。組件的實例經過_component
與_parentComponent
聯結在一塊,方便上下回溯。而實例老是保存着最後轉化出來的真實DOM(base, 也叫initialBase)。base上保存着最上面的那個組件實例,也就是_component,此外,爲了方便比較,它的構造器也放在DOM節點上。react
renderComponent這個方法主要處理組件更新時的鉤子,及創建父子組件間的聯繫。數組
這個方法的參數的起名也很奇葩,若是改爲dom
renderComponent(componentInstance, renderModel, isRenderByReactDOM, isRenderChildComponent)
則好理解些。顯示preact的做者不太想知道其奧祕,所以源碼的註釋也不多不多。異步
好了,咱們看setComponentProps方法,它在renderComponent用了兩次。函數
//更新已有的子組件實例 setComponentProps(inst, childProps, SYNC_RENDER, context, false); //新舊子組件的類型不一致,用新組件的實例進行替換 setComponentProps(inst, childProps, NO_RENDER, context, false);
setComponentProps的源碼this
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
出來的。spa
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); }