翻譯自:https://engineering.hexacta.c…
上一節的程式碼有一些問題:
- 每次更新都會帶來整顆虛擬DOM樹的一致性校驗;
- 狀態是全域性的(沒有私有狀態);
- 有變化發生後必須手動呼叫
render
方法以便將變化反應到頁面上。
元件可以幫我們解決上面的問題,同時還能帶來一些新特性:
- 允許自定義JSX的標籤名
- 生命週期鉤子(這一節暫不介紹這部分)
首先我們要定義一個Component
的基礎類,在建立其它元件時都要繼承該類。我們需要一個帶有props
入參和setState
方法的建構函式,setState
方法可以接收partialState
作為入參來更新元件狀態:
class Component{
constructor(props){
this.props = props;
this.state = this.state || {}
}
setState(partialState){
this.state = Object.assign({}, this.state, partialState);
}
}
我們在建立元件時都會繼承上面這個類。元件的使用方法和原生的標籤如div
或者span
一樣,直接像這樣<MyComponent />
就可以了。而且我們的createElement
也不需要做修改,元素的type
屬性可以直接取值為元件類,剩下的props
屬性也不需要特別的處理。我們需要一個方法能根據傳入的元素來建立元件的例項(稱之為公共例項,其實就是根據這個建構函式new出來的一個物件)。
function createPublicInstance(element, internalInstance){
const {type, props} = element;
const publicInstance = new type(props); // 這地方的type對應元件的建構函式
publicInstance.__internalInstance = internalInstance;
return publicInstance;
}
元件的內部例項含有元件對應的dom元素(內部例項就是前幾節我們說的例項,通過呼叫instantiate
方法生成的)。公共例項與內部例項的引用關係會被儲存著,通過這個引用關係可以找到公共例項對應的內部例項及虛擬DOM,當公共例項狀態發生變化時,我們就可以只更新發生變化的內部例項及其對應的那部分虛擬DOM:
class Component{
constructor(props){
this.props = props;
this.state = this.state || {}
}
setState(partialState){
this.state = Object.assign({}, this.state, partialState);
updateInstance(this.__internalInstance);
}
}
function updateInstance(internalInstance){
const parentDom = internalInstance.dom.parentNode;
const element = internalInstance.element;
reconcile(parentDom, internalInstance, element);
}
instantiate
方法需要做一些改造。對元件來講,我們需要先建立公共例項(先new一個組建),然後呼叫元件的render
方法來獲取元件內部的元素,最後把獲取到的元素傳遞給instantiate
方法。
function instantiate(element){
const { type, props } = element;
const isDomElement = typeof type === `string`;
if(isDomElement){ // 如果是原生的dom元素的話,直接建立例項
const isTextElement = type === TEXT_ELEMENT;
const dom = isTextElement
? document.createTextNode(``)
: document.createElement(type);
updateDomProperties(dom, [], props);
const childElements = props.children || [];
const childInstances = childElements.map(instantiate);
const childDoms = childInstances.map(childInstance => childInstance.dom);
childDoms.forEach(childDom => dom.appendChild(childDom));
const instance = { dom, element, childInstances };
return instance;
} else {// 否則先建立公共例項,然後再呼叫instantiate方法建立內部例項
const instance = {};
// 這地方的element是一個type屬性為一個建構函式的物件
const publicInstance = createPublicInstance(element, instance);
const childElement = publicInstance.render();
const childInstance = instantiate(childElement);
const dom = childInstance.dom;
Object.assign(instance, { dom, element, childInstance, publicInstance});
return instance;
}
}
元件對應的內部例項和原生dom元素對應的例項有些不一樣。元件內部例項只會擁有一個子元素,即render
方法返回的內容,而原生dom元素則可以含有多個子元素。所以對於元件內部例項來講,它們會有一個childInstance
屬性而不是一個childInstances
陣列。此外,由於在進行一致性校驗時需要呼叫元件的render
方法,所以元件內部例項會儲存對公共例項的引用(反過來公共例項也儲存著對內部例項的引用)。
接下來我們來處理下元件例項的一致性校驗。因為元件的內部例項只含有一個子元素(所有元素有一個統一的父類),只需要更新公共例項的props
屬性,執行render
方法獲取子元素,然後再進行一致性校驗就可以了。
function reconcile(parentDom, instance, element){
if(instance == null){
const newInstance = instantiate(element);
parentDom.appendChild(newInstance.dom);
return newInstance;
} else if( element == null){
parentDom.removeChild(instance.dom);
return null;
} else if(instance.element.type !== element.type){
const newInstance = instantiate(element);
parentDom.replaceChild(newInstance.dom, instance.dom);
return newInstance;
} else if(typeof element.type === `string`){
updateDomProperties(instance.dom, instance.element, props, element.props);
instance.childInstances = reconcileChildren(instance, element);
instance.element = element;
return instance;
} else {
instance.publicInstance.props = element.props;// 更新公共例項的props
const childElement = instance.publicInstance.render(); // 獲取最新的子元素
const oldChildInstance = instance.childInstance;
const childInstance = reconcile(parentDom, oldChildInstance, childElement);
instance.dom = childInstance.dom;
instance.childInstance = childInstance;
instance.element = element;
return instance;
}
}
現在,我們的Didact.js已經可以支援元件了。這裡可以線上編輯程式碼並能看到效果。
使用元件後,我們可以建立自定義的JSX標籤,並擁有了元件內部狀態,而且元件有變化時只會變更自己的那部分dom內容。
相關內容到此結束。