// 模板內容
<div id="app">
<input type="text" v-model="message"> {{message}} <div>{{a.b}}</div> </div>
// vue腳本
let vm = new Vue({
el: '#app',
data: {
message: '我是message',
a: {
b: '個人a.b'
}
}
})
複製代碼
看到上面的代碼,使用過vue的同窗能知道頁面的渲染結果會以下圖所示:vue
那他是如何進行渲染的呢,咱們帶着問題來進入正題。node
class Vue {
constructor(options) {
// 掛載可用數據到實例上
this.$el = options.el;
this.$data = options.data;
// 若是含有模板就去編譯
if (this.$el) {
// 用數據和元素進行編譯
new Compile(this.$el, this);
}
}
}
複製代碼
以上代碼就是對new Vue時傳遞的參數el和data進行存儲,再利用Compile來對編譯模板。git
新建一個compile.js的文件,並建立Compile類github
class Compile{
constructor(el ,vm) {
this.el = this.isElememtNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
// 把須要操做的dom先放到內存中
let fragment = this.node2fragment(this.el);
// 編譯:提取元素節點的v-model和文本節點{{}}
this.compile(fragment);
// 把編譯完成的元素放到頁面中
this.el.appendChild(fragment);
}
}
}
複製代碼
因爲Vue中的el是能夠傳遞選擇器和元素節點的,咱們這裏也對el作了相應的處理。數組
判斷用戶傳遞的el是不是元素節點,若是是元素節點使用,若是是選擇器,就獲取元素後進行使用。app
// isElememtNode
isElememtNode(node) {
return node.nodeType === 1;
}
複製代碼
node2fragment(el) {
// 建立文檔碎片
let fragment = document.createDocumentFragment();
let firstChild;
while(firstChild = el.firstChild) {
// 把dom元素移入到fragment
fragment.appendChild(firstChild);
}
return fragment;
}
複製代碼
這樣咱們就獲得了fragment,接下來的處理,咱們只須要對fragment進行處理便可。dom
compile(fragment) {
// 獲取fragment的全部子元素
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElememtNode(node)) {
// 編譯元素
this.compileElement(node);
// 遞歸執行
this.compile(node);
} else {
this.compileText(node);
}
})
}
複製代碼
獲取全部子元素後,分別針對是元素節點和文本節點的狀況進行處理,須要指出的一點就是,元素節點內部可能還有子元素, 因此咱們以當前子節點爲參數遞歸執行compile。mvvm
咱們再分別來看一下compileElement和compileText兩個方法函數
// 編譯文本節點
compileText(node) {
let expr = node.textContent;
// 匹配開頭是{{結尾是}}而且中間不存在}的值
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr);
}
}
複製代碼
其中用到的正則:ui
/\{\{([^}]+)\}\}/g;
複製代碼
若是對這個正則不理解,咱們能夠配合圖來理解一下
他實現的功能就是匹配開頭是 {{ 結尾是 }} 而且中間不存在 } 的字符串模板。
獲得字符串模板以後咱們就能夠vm實例中取到對應的值,具體的處理,咱們分離到CompileUtil中來實現。
若是是元素節點,咱們須要考慮的就是其存在指令的狀況(本篇文章只講述v-model的狀況)
咱們分爲三步來實現該功能
// 編譯元素節點
compileElement(node) {
let attrs = node.attributes; // 獲取當前節點的屬性
Array.from(attrs).forEach(attr => {
let attrName = attr.name;
// 若是是指令進行數據處理
if (this.isDirective(attrName)) {
let expr = attr.value;
let [,type] = attrName.split('-');
CompileUtil[type](node, this.vm, expr)
}
})
}
// 若是是v-開頭,咱們就認爲他是指令
isDirective(name) {
return name.startsWith('v-');
}
複製代碼
以上compileText和compileElement兩個方法中,具體的處理方式都使用到了CompileUtil這個輔助類,咱們能夠來看一下其代碼實現。
咱們先來看對於text的處理。
通過以上的處理,咱們會拿到相似於{{XXX}}的字符串,有了這個字符串,咱們還須要下面幾步:
上面須要處理的一個難點是:咱們的須要的值多是對象中的對象,相似於{{a.b.c}},解決方案爲:先把字符串分隔成數組,再使用reduce每次都取到下一個key,最後利用key取到對應對象的值。
// 編譯所需的輔助方法
CompileUtil = {
getVal(vm, expr) { // 獲取實例上對應的數據
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
getTextVal(expr, vm) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
// expr: {{XXX}}
// arguments[1]是XXX
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) { // 文本處理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(expr, vm);
updateFn && updateFn(node, value);
},
updater: {
textUpdater(node, value) {
node.textContent = value;
}
}
}
複製代碼
處理完了text,再來看如何處理指令
在上面的compileElement方法中,咱們判斷了節點屬性是不是指令,若是是指令咱們就拿到具體的指令,例如v-model咱們就拿到model,到這裏,咱們還須要如下幾步:
爲了實現以上需求,咱們給CompileUtil新增model方法
model(node, vm ,expr) { // v-model處理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
複製代碼
對應的modelUpdater:
modelUpdater(node, value) {
node.value = value;
}
複製代碼
完整的CompileUtil代碼以下
// 編譯所需的輔助方法
CompileUtil = {
getVal(vm, expr) { // 獲取實例上對應的數據
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next];
}, vm.$data);
},
getTextVal(expr, vm) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => {
return this.getVal(vm, arguments[1]);
});
},
text(node, vm, expr) { // 文本處理
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(expr, vm);
updateFn && updateFn(node, value);
},
model(node, vm ,expr) { // v-model處理
let updateFn = this.updater['modelUpdater'];
updateFn && updateFn(node, this.getVal(vm, expr));
},
updater: {
textUpdater(node, value) {
node.textContent = value;
},
modelUpdater(node, value) {
node.value = value;
}
}
}
複製代碼
到這裏,文本節點和v-model指令的編譯都已經完成。
this.el.appendChild(fragment);
複製代碼
到這裏,一個基礎的編譯環節就宣告完成,打開頁面就能獲得期待的渲染結果了👏👏👏
斗膽發文,歡迎吐槽和指正。
附上完整代碼示例,期待與您共同進步:github.com/Ljhhhhhh/mv…