前言
組件化是前端生產(chǎn)力提升的另一大變革,之前jq時(shí)代大家都是一份代碼復(fù)制來復(fù)制去,對(duì)代碼復(fù)用性和可維護(hù)性非常不友好。現(xiàn)在MV*框架全部是支持組件的翼闹,只不過使用上略有區(qū)別。
Component
首先蒋纬,讓我們注冊一個(gè)具有template橄碾、props和自定義事件的組件:
Moon.component("my-component", {
// options
template: `<h3>This is a Component {{content}} 計(jì)數(shù)器{{count}}!
<button m-on:click='increment'>計(jì)數(shù)器</button></h3>`,
props: ['content'],
data: function() {
return {
count: 0
}
},
methods: {
increment: function() {
this.set("count", this.get("count") + 1);
this.emit("increment");
}
}
});
它是一個(gè)計(jì)數(shù)器組件,擁有content這個(gè)從父組件傳來的prop颠锉,也有increment這個(gè)事件可以發(fā)射到父組件以便相互通信。
讓我們打個(gè)斷點(diǎn)看看里面發(fā)生了什么:
可以看出史汗,我們傳了name和options兩個(gè)參數(shù)用于注冊組件琼掠。首先,初始化了組件的options停撞,然后讓MoonComponent繼承Moon瓷蛙,并重寫了init方法(主要是為了處理props數(shù)據(jù))。
之后把MoonComponent和options掛載到全局的components[name]上戈毒,返回MoonComponent構(gòu)造函數(shù)艰猬。
html->code過程與組件
組件既然是繼承自Moon實(shí)例,自然也少不了html->tokens->ast->code->vnode->node->html的過程埋市。拿我們這個(gè)例子來說也是一樣:
<my-component content="'父組件內(nèi)容'" m-on:increment="incrementTotal"></my-component>
這段html自然也會(huì)先被轉(zhuǎn)成code冠桃。只不過組件對(duì)于編譯過程中來說暫時(shí)還沒有什么特別的。
code->html與組件
組件html被稀里糊涂轉(zhuǎn)成code了道宅,之后解析code的過程就變得麻煩許多食听。
第一個(gè)與之相遇的就是m函數(shù):
可見它會(huì)先判斷此組件是否是函數(shù)式組件胸蛛,是的話就按函數(shù)式的方法返回一段函數(shù)式組件vnode,不是的話先掛載到meta上去再創(chuàng)建vnode樱报。
函數(shù)式組件
函數(shù)式組件沒有任何狀態(tài)葬项,也沒有生命周期方法。它只是一個(gè)接收參數(shù)的函數(shù)迹蛤。創(chuàng)建函數(shù)式組件的代碼其實(shí)很好懂:
var createFunctionalComponent = function(props, children, functionalComponent) {
var options = functionalComponent.options;
var attrs = props.attrs;
var data = options.data;
if (data === undefined) {
data = {};
}
// Merge data with provided props
var propNames = options.props;
if (propNames === undefined) {
data = attrs;
} else {
for (var i = 0; i < propNames.length; i++) {
var prop = propNames[i];
data[prop] = attrs[prop];
}
}
// Call render function
return functionalComponent.options.render(m, {
data: data,
slots: getSlots(children)
});
}
它其實(shí)就是將一個(gè)擁有attrs(props會(huì)被合并進(jìn)去)和data的組件直接render民珍,至于slot,我們稍后再講盗飒。
createComponentFromVNode
它區(qū)別于createNodeFromVNode嚷量,專門用于產(chǎn)生組件vnode的node:
可以看出,它分四步走:
- 合并prop到data屬性上去
- 擴(kuò)展vnode的事件監(jiān)聽到組件實(shí)例上去
- 獲取slot和實(shí)際掛載元素并build產(chǎn)生node
- 混入vnode和node然后返回實(shí)際node
appendChild/removeChild/replaceChild
在這期間它可能還會(huì)遇見一些vnode層面上的增/刪/替操作:
// appendChild Check for Component
var component = null;
if ((component = vnode.meta.component) !== undefined) {
createComponentFromVNode(node, vnode, component);
}
// removeChild Check for Component
var componentInstance = null;
if ((componentInstance = node.__moon__) !== undefined) {
// Component was unmounted, destroy it here
componentInstance.destroy();
}
// Check for Component
var componentInstance = null;
if ((componentInstance = oldNode.__moon__) !== undefined) {
// Component was unmounted, destroy it here
componentInstance.destroy();
}
// Replace It
parent.replaceChild(newNode, oldNode);
// replaceChild Check for Component
var component = null;
if ((component = vnode.meta.component) !== undefined) {
createComponentFromVNode(newNode, vnode, component);
}
增的情況最簡單箩兽,如果有組件就創(chuàng)建一個(gè)組件的node津肛,刪的情況則是有組件就銷毀掉。替的話則是結(jié)合了增和刪的情況汗贫,先刪后增身坐。
hydrate
在build->patch的最后幾步,還會(huì)遇到hydrate里處理組件的代碼:
// Check for Component
if (vnode.meta.component !== undefined) {
// Diff the Component
diffComponent(node, vnode);
// Skip diffing any children
return node;
}
也就是diff組件
diffComponent
diff組件做的事情也很簡單:
若當(dāng)前node沒有被掛載落包,直接創(chuàng)建一個(gè)node部蛇。否則獲取當(dāng)前組件實(shí)例,對(duì)node的props和vnode的attrs進(jìn)行diff咐蝇,diff成功就標(biāo)記componentChanged標(biāo)志位為真并在最后build一次這個(gè)實(shí)例涯鲁。
還有一件事情要處理:如果vnode還有子元素,就要給組件實(shí)例加上slot有序。
slot與getSlots
slot是組件內(nèi)嵌的元素抹腿,思想來源于shadow dom
Moon中關(guān)于slot的關(guān)鍵函數(shù)式getSlots:
var getSlots = function(children) {
var slots = {};
// Setup default slots
var defaultSlotName = "default";
slots[defaultSlotName] = [];
// No Children Means No Slots
if (children.length === 0) {
return slots;
}
// Get rest of the slots
for (var i = 0; i < children.length; i++) {
var child = children[i];
var childProps = child.props.attrs;
var slotName = "";
var slotValue = null;
if ((slotName = childProps.slot) !== undefined) {
slotValue = slots[slotName];
if (slotValue === undefined) {
slots[slotName] = [child];
} else {
slotValue.push(child);
}
delete childProps.slot;
} else {
slots[defaultSlotName].push(child);
}
}
return slots;
}
可以發(fā)現(xiàn)它傳入的是children參數(shù),先會(huì)建立一個(gè)默認(rèn)的slots旭寿,然后從children里取每個(gè)child警绩,如果slot是不是具名slot就放進(jìn)默認(rèn)slot里。
getSlots使用處
- createFunctionalComponent
return functionalComponent.options.render(m, {
data: data,
slots: getSlots(children)
});
創(chuàng)建函數(shù)式組件的時(shí)候slot會(huì)被提取出來放到創(chuàng)建組件的options里去盅称。
- createComponentFromVNode
componentInstance.$slots = getSlots(vnode.children);
類似創(chuàng)建函數(shù)式組件肩祥,創(chuàng)建實(shí)例組件也是把slot提取出來掛載到實(shí)例的$slots屬性上。
- diffComponent
// If it has children, resolve any new slots
if (vnode.children.length !== 0) {
componentInstance.$slots = getSlots(vnode.children);
componentChanged = true;
}
diff的過程中也會(huì)修改組件實(shí)例缩膝,可以發(fā)現(xiàn)與createComponentFromVNode類似混狠,是把slot提取出來掛載到實(shí)例的$slots屬性上。
- generateNode
else if (node.type === "slot") {
parent.meta.shouldRender = true;
parent.deep = true;
var slotName = node.props.name;
return ("instance.$slots[\"" + (slotName === undefined ? "default" : slotName.value) + "\"]");
}
generate的過程中也會(huì)處理slot疾层,處理方式自然是把它變成code了将饺。
總結(jié)
組件化對(duì)我們前端開發(fā)工程化、提高代碼復(fù)用性和可維護(hù)性起到了革命式的作用,從此各種UI組件庫如雨后春筍似的冒了出來俯逾,對(duì)我們前端解放生產(chǎn)力來說是非常大的一個(gè)進(jìn)步贸桶。相信有一天,HTML5會(huì)支持原生定義組件和Slot桌肴。