前言
可以說,MV*框架最核心的三個點就是
- 模板怎么轉(zhuǎn)化成代碼的虱而?
- 代碼又是怎么轉(zhuǎn)化成模板的丸冕?
- 模板的依賴和代碼中的數(shù)據(jù)是怎么響應(yīng)式關(guān)聯(lián)起來的?
這篇文章我們一起來探究一下第二個點:code->html薛窥,因為這個步驟Moon提供了現(xiàn)成的api:render胖烛。
render的使用
我們先來看看官方的用例:
new Moon({
render: function(m) {
return m('h1', {attrs: {}}, {shouldRender: false}, [m("#text", {shouldRender: false}, "Hello Moon!")]);
// same as <h1>Hello Moon!</h1>
}
});
可以看出,m(...)將會和在HTML里直接寫<h1>Hello Moon!</h1>造成一樣的效果
m函數(shù)的實現(xiàn)
Yeah诅迷,我們現(xiàn)在知道關(guān)鍵就在于這個m函數(shù)了佩番,其實它和Vue中的h函數(shù)也是類似的。
由于JS運算符優(yōu)先級的規(guī)則罢杉,會先調(diào)用m("#text", {shouldRender: false}, "Render Moon!"):
m函數(shù)傳入tag, attrs, meta, children四個參數(shù)趟畏,可以看出'#text'對應(yīng)type、{shouldRender: false}對應(yīng)attrs滩租、"Render Mooin"對應(yīng)meta赋秀、undefined對應(yīng)children利朵,返回一個createElement函數(shù)調(diào)用的結(jié)果,我們再看注釋猎莲,發(fā)現(xiàn)這個createElement函數(shù)調(diào)用完后返回的其實就是一個VNode绍弟。
同理我們可以推測出在內(nèi)層m函數(shù)執(zhí)行完之后,外層m函數(shù)調(diào)用時'h1'對應(yīng)type著洼、attrs: {}對應(yīng)attrs樟遣、{shouldRender: false}對應(yīng)meta、[m("#text", {shouldRender: false}, "Render Moon!")]對應(yīng)children身笤。
所以豹悬,這個m函數(shù)負責把code轉(zhuǎn)成VNode。
createElement
讓我們再進入createElement函數(shù):
它很簡單有木有~就是返回了只有五個鍵的一個對象液荸,而這個對象就是我們俗稱的VNode瞻佛!
從這個過程中,我們發(fā)現(xiàn)m函數(shù)其實還有一個解析組件的流程娇钱,它的判斷條件很耐人尋味:
(component = components[tag]) !== undefined
//等價于
(component = components[tag]) && component !== undefined
這里涉及到一個全局變量:components伤柄,它是在index.js里聲明的,用來存儲當前實例的組件忍弛,所以其實這里就是判斷render函數(shù)渲染的標簽是不是我們自己定義的。
如果是就會創(chuàng)建一個函數(shù)式的組件或者讓meta的component屬性引用這個組件考抄,至于細節(jié)细疚,等我們以后看到組件部分再分析。
總之川梅,m函數(shù)非常關(guān)鍵疯兼,作者也給了注釋:無論怎么樣m函數(shù)都會返回一個VNode的數(shù)據(jù):
{
type: 'h1', <= nodename
props: {
attrs: {'id': 'someId'}, <= regular attributes
dom: {'textContent': 'some text content'} <= only for DOM properties added by directives,
directives: {'m-mask': ''} <= any directives
},
meta: {}, <= metadata used internally
children: [], <= any child nodes
}
調(diào)用m的前奏
了解完m函數(shù)的實現(xiàn)細節(jié)后,我們執(zhí)行代碼贫途,程序會先進入Moon里定義$render:
//defineProperty其實就是this[$render]=options.render
defineProperty(this, "$render", options.render, noop);
繼續(xù)走下去吧彪,我們就進入了init和mount函數(shù),可以看出它設(shè)置了當前實例的$el丢早、$destroyed姨裸、$template、$render屬性(可選)怨酝,然后調(diào)用build函數(shù)和觸發(fā)mounted鉤子傀缩。
在build函數(shù)里調(diào)用render和m
到build里就是進入正片了:
render實際上是調(diào)用$render(也就是用戶傳進去的render):
dom變量存儲了render生成的(實際上是m生成的)新VNode、old存儲當前的Node(可能是原生DOM節(jié)點也有可能是VNode)农猬,接著調(diào)用patch函數(shù)對兩個node做一些事情赡艰。
patch:vnode->html
進入patch函數(shù),可以發(fā)現(xiàn)old對應(yīng)#app4這個DOM節(jié)點斤葱,vnode對應(yīng)render生成的VNode節(jié)點慷垮,parent是body元素揖闸。
因為old沒有meta屬性,所以不是VNode料身。(VNode有meta屬性)于是接著判斷old是不是Node類型汤纸。
很明顯,是hydrate函數(shù)把VNode變成一個原生DOM的newNode的惯驼。
之后還對比了newNode和old蹲嚣,從例子中可以看出顯然h1和app4不一樣,這時當前實例的$el就被替換成了vnode中的元素祟牲,當前moon實例也被替換掉了隙畜。原先#app4的位置就變成了h1元素。
再看看沒進入的那個流程:即old有meta屬性時说贝,也就是old也是一個VNode時(DOM樹上的node沒有meta屬性)议惰,這個時候會直接使用一個createNodeFromVNode和replaceChild方法更改html元素,關(guān)于這兩個方法乡恕,我們后面會提到言询。
hydrate:真正的執(zhí)行者
hydrate函數(shù)的執(zhí)行流程非常復雜,所以我畫了個流程圖:
這里的node即之前patch函數(shù)中的old節(jié)點傲宜,vnode即render函數(shù)生成的節(jié)點运杭,parent是實例的根元素。
可以看出它是先獲取了node的nodeName函卒,然后進入一個非常復雜的判斷:這一系列的判斷目的只有一個辆憔,那就是把vnode的內(nèi)容轉(zhuǎn)化到瀏覽器DOM上去。
在我們的例子中报嵌,node是原生的#app4DOM節(jié)點虱咧,vnode是render生成的h1虛擬節(jié)點,所以需要調(diào)用createNodeFromVNode函數(shù)锚国。
node和vnode的hydrate
這個過程會涉及到createNodeFromVNode腕巡、diffProps、replaceChild血筑、Moon.destroy绘沉、Moon.off、callHook豺总、extractAttrs梆砸、addEventListeners、removeChild等方法
-
createNodeFromVNode根據(jù)vnode對象創(chuàng)建一個真實node:
先根據(jù)vnode的類型創(chuàng)建一個元素,類型是文本或SVG的話就創(chuàng)建文本或SVG元素园欣。
如果只有一個子元素的時候帖世,直接混入。有多個子元素的時候需要迭代調(diào)用appendChild函數(shù)并把子VNode也通過createNodeFromVNode轉(zhuǎn)化成DOM的Node。
最后根據(jù)vnode.meta.eventListeners給這個Node添加事件監(jiān)聽邏輯日矫、通過diffProps設(shè)置屬性(包括attr赂弓、prop、directive)哪轿、Hydrate(也就是把node作為vnode.meta的一個屬性盈魁,方便兩者同步更新)。
可以說這個hydrate讓vnode和node你中有我窃诉,我中有你了杨耙。 -
diffProps
接下來讓我們看看diffProps是如何設(shè)置屬性的:
進入以后我們發(fā)現(xiàn)其實是對attrs、props進行diff并且執(zhí)行相關(guān)的directive飘痛,關(guān)于attr和props的區(qū)別不明白的讀者可以自行搜索一下珊膜,簡單來說就是attr存在于html元素上,props存在于DOM元素上宣脉。
2.1. 對于attr(作者注釋為Node Props)车柠,vnode只是一個參照物,始終操縱node塑猖,如果node有vnode沒有就remove這個attr竹祷,如果node沒有vnode有就add這個attr。
2.2. 對于prop(作者注釋為DOM Props)羊苟,會從vnode.props.dom中拿出prop去匹配node塑陵,如果沒有匹配到就給node加上。
2.3. 對于directive蜡励,會從vnode.props.directives拿出directive去匹配directives令花,如果匹配到了就執(zhí)行相關(guān)指令。 -
replaceChild
這個函數(shù)會替換一個子元素巍虫,其實就是封裝了dom原生提供的replaceChild彭则。
它會先銷毀當前moon的實例(componentInstance)鳍刷,然后用newNode替換oldNode占遥,最后通過createComponentFromVNode對里面存在的組件進行轉(zhuǎn)化(如果有的話)。
3.1 destroy
順便看下銷毀實例的函數(shù):
里面非常簡潔:移除事件監(jiān)聽->移除dom引用->$destroy標志位置為真->調(diào)用destroy鉤子
3.2 off
再來看看怎么移除事件監(jiān)聽的:
產(chǎn)生了三條分支:事件名參數(shù)為空->移除所有事件输瓜;回調(diào)名參數(shù)為空->移除所有事件回調(diào)瓦胎;參數(shù)都存在->移除這個特定的事件回調(diào) -
callHook
順理成章地會走調(diào)用鉤子的函數(shù)
兩步搞定:獲取這個鉤子->當前實例調(diào)用這個鉤子方法
-
extractAttrs
把當前node的attrs抽離出來,返回一個attrs對象尤揣。
-
addEventListeners
這個需要在render函數(shù)的meta參數(shù)里傳入一個eventListeners對象才可以觸發(fā):
eventListeners: { "click": [function() { console.log("click") },function() { console.log("click2") } ], "dblclick": [function() { console.log("double") }] }
它首先會先聲明一個addHandler函數(shù)搔啊,這是個閉包函數(shù),然后遍歷eventListeners逐個調(diào)用這個addHandler函數(shù)北戏。
在addHandler里面又聲明了一個handle函數(shù)负芋,它的目的就是把某個具體事件相關(guān)的回調(diào)全部調(diào)用一遍。至于這些回調(diào)嗜愈,都存儲在handle.handlers里旧蛾。
這些原來聲明的回調(diào)數(shù)組就轉(zhuǎn)化成了一個handle函數(shù)莽龟,最后把這個handle函數(shù)才是node的實際事件回調(diào)。 -
removeChild
這個函數(shù)和replaceChild類似锨天,都是封裝了原生的同名api:
因為原來的node有子元素毯盈,所以就需要移除。hydrate的后半部分也是圍繞子元素展開的:首先把vnode的child和node的child獲取到病袄,然后廣度遍歷node的child和vchild搂赋,逐個和vchild進行hydrate
總結(jié)
之后,調(diào)用棧就沿著hydrate->patch->build->mount->init一步步回退益缠,結(jié)束了整個過程脑奠。也就是說,code->html轉(zhuǎn)開就是code->vnode->node->html的過程左刽。