自己實現MVVM(Vue源碼解析)
前言
本文會帶大家手動實現一個雙向綁定過程(僅僅涵蓋一些簡單的指令解析,如: v-text , v-model ,插值),當然借鑒的是Vue1的源碼,相信大家在閱讀完本文后對Vue1會有一個更好的理解, 源代碼 放到了github,由于本人水平有限,理解不到位的地方還請大家指出。
MVVM
MVVM 使開發可以更加關注于數據,減少了很大的工作量,也使代碼可讀性,可維護性更高, MVVM 核心的思想就是視圖是狀態的函數: View = ViewModel(Model) ,所以當Model發生改變時,ViewModel會來操作View來怎么做,而非是自己寫代碼來做。無論是雙向綁定還是單向綁定,都是符合 MVVM 思想的。Vue提倡的是雙向綁定,也就是允許View到Model的變化,其實這個場景出現在的也就是表單操作上, 看個例子 ,例子中分別利用了Vue和React實現了一下表單 value 變化,影響頁面與其相關的 dom 節點發生變化, 可以發現的是雙向綁定的Vue是 input 的 value 發生變化則 h1 的 innerText 就發生了變化,變化是由View->Model,而提倡單向數據流的 React 需要手動監聽事件,事件觸發后,更改Model的值,從而使 input 的 value 發生了變化。看了Vue的源碼后不難發現Vue的雙向綁定的實現也就是在表單元素上添加了 input 事件,可以說雙向綁定是單向綁定的一個語法糖。
實現思路
上圖是一個大體的流程,下面按照流程來實現下:
-
利用 observer 對 data 進行了監聽,并且提供訂閱某個數據項的變化的能力
這點的實現,需要借助的是 Object.defineProperty() 來為對象的屬性綁定 get/set 特性(由于利用了 Object.defineProperty() ,所以Vue不支持ie8), observer 需要將 data 的所有屬性都綁定 get/set ,很容易想到的就是利用遞歸來實現 。
-
利用 Compile 對模板進行解析
這點實現的是將我們的模板轉化為 html ,過程中會將數據與View中的節點相關聯起來,最終會將編譯好的 html 頁面替換到頁面上。首先來看解析,首先從根節點開始,根據不同的節點類型采用不同的解析方式:
function compileNode(node, vm) {
const type = node.nodeType;
if (type === 1 && !isScript(node)) {
compileElement(node, vm);
} else if (type === 3 && node.data.trim()) {
compileTextNode(node, vm);
} else {
return null;
}
}
對于文本節點來說,可能存在情況只有兩種:
-
與數據不相關不用操作
-
含有插值,需要與數據進行關聯
-
{{}} 文本插值
-
{{{}}} 純 html 插值
-
利用下面正就可以將插值找出:
/\{\{\{(.*?)\}\}\}|\{\{(.*?)\}\}/g
采用下面函數來對文本節點的內容解析:
function parseText(node) {
var text = node.wholeText;
if (!tagRE.test(text)) {
return void 0;
}
const tokens = [];
var lastIndex = tagRE.lastIndex = 0,
match, index, html, value;
while (match = tagRE.exec(text)) {
index = match.index;
if (index > lastIndex) {
tokens.push({
value: text.slice(lastIndex, index)
})
}
html = htmlRE.test(match[0]);
value = html ? match[1] : match[2];
tokens.push({
value: value,
tag: true,
html: html
});
lastIndex = index + match[0].length;
}
if (lastIndex < text.length) {
tokens.push({
value: text.slice(lastIndex)
})
}
return tokens;
}
返回了 tokens ,里面存儲了每一個塊內容,一個插值or一個普通文本, tag 來標記是否為插值, html 來標記是否為純 html 插值。遍歷返回的 tokens ,根據不同的類型,來采用不同的方式將其添加到其父節點上:
function compileTextNode(node, vm) {
const tokens = parseText(node);
if (tokens == null) return void 0;
var frag = document.createDocumentFragment();
tokens.forEach(token => {
var el;
if (token.tag) {
if (token.html) {
el = document.createDocumentFragment();
el.$parent = node.parentNode;
el.$oneTime = true;
dirCollection["html"](el, vm, token.value);
} else {
el = document.createTextNode(" ");
dirCollection["text"](el, vm, token.value);
}
} else {
el = document.createTextNode(token.value);
}
el && frag.appendChild(el);
});
return replace(node, frag);
}
dirCollection 是一個指令集合,也就是決定了如何初始化以及如何更新該節點。對于 nodeType 為 1 的節點來說,指令全部存儲在其屬性中,遍歷屬性,假若指令中含有 v-html,v-model,v-text ,則停止遍歷其子樹,直接將調用相應指令即可,否則,則需要遍歷其子節點,對其子節點應用 compileNode 進行解析:
function compileNodeList(nodes, vm) {
for (let val of nodes) {
compileNode(val, vm);
}
}
function compileElement(node, vm) {
var flag = false;
const attrs = Array.prototype.slice.call(node.attributes);
attrs.forEach((val) => {
const name = val.name,
value = val.value;
if (dirRE.test(name)) {
var dir;
// 事件指令
if (
(dir = name.match(eventRE)) &&
(dir = dir[1])
) {
dirCollection["eventDir"](node, dir, vm, value);
} else {
dir = name.match(dirRE)[1];
dirCollection[dir](node, vm, value);
}
// 指令中為v-html or v-text or v-model終止遞歸
flag = flag ||
name === vhtml ||
name === vtext;
node.removeAttribute(name);
}
});
const childs = node.childNodes;
if (!flag && childs && childs.length) {
compileNodeList(childs, vm);
}
}
在 dirCollections 中還會做的就是將數據與View的 dom 節點相關聯,利用的就是 Dep 與 Watcher ,頁面上每一個與數據相關聯的節點都含有一個 Watcher ,當數據發生變化是 Watcher 用于計算,是否需要更新該節點;數據的每一個屬性都有一個 Dep ,當該屬性發生變化時, Dep 會通知與該數據相關聯的 Watcher 來進行計算是否需要更新對應頁面。 Dep代碼 , Watcher代碼 。
-
異步更新隊列
異步更新隊列,是一個優化,將更新 dom 的操作變為異步的,放到下一個事件循環來做,這樣做可以減少不必要的 dom 更新,看下面情況:
vm.value++;
vm.value++;
vm.value++;
三次數據改變,假若同步更新的話,則每次數據改變會立即更新 dom ,而異步更新的話,可以先將更新推入一個隊列中,由于是異步,也可以保證每一個 Watcher 只被推入到一次,這樣就避免了不必要的更新,異步更新主要利用的是 nextTick ,這個函數會優先使用 Promise ,不兼容則利用 MutationObserver ,再不兼容的話會利用 setTimeout 。
寫在后面
看過了Vue的源碼不得不感嘆Vue的優美,而Vue2又增加了虛擬dom,這樣就可以做到服務端渲染,給了我們更多的可能!
來自:https://segmentfault.com/a/1190000007741904