1. 說明
- compile 編譯,即模板解析器,能夠對模板中的指令和插值表示式進行解析
- observer 資料劫持,即資料監聽器,能夠對資料物件(data)的所有屬性進行監聽
- watcer 監聽者,將compile的解析結果,與observer所觀察的物件連線起來,建立關係,在observer觀察到資料物件變化時,接收通知,並更新DOM
2. 實現MVVM原理
2.1 目錄結構
2.2 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app">
<input type="text" v-model="message.a">
<div>{{b}}</div>
<ul>
<li>{{message.a}}</li>
</ul>
{{b}}
</div>
<!-- <script src="https://cdn.jsdelivr.net/npm/vue"></script> -->
<script src="./watcher.js"></script>
<script src="./observer.js"></script>
<script src="./compile.js"></script>
<script src="./mvvm.js"></script>
<script>
let vm = new MVVM({
el: '#app',
data: {
message: {
a: 'aa'
},
b: 'bb'
}
})
</script>
</body>
</html>
複製程式碼
3. mvvm
- 整合編譯和資料劫持
- 代理,使
vm.$data.message => vm.message
3.1 完整mvvm.js
class MVVM {
constructor(options) {
// 例項上的dom元素,<div id="app"></div>
this.$el = options.el;
// 例項上的所有資料,data
this.$data = options.data;
// 如果有這個dom元素,才開始
if (this.$el) {
// 資料劫持,就是對資料的所有屬性,改成set和get的方法,以至可以在資料獲取前和改變後,觸發其它方法(做點事情)
new Observer(this.$data);
this.proxyData(this.$data)
// 編譯元素,例如<input type="text" v-model="message.a">,根據message.a,找到data中對應的message.a的資料,賦值給input的value
new Compile(this.$el, this)
}
}
// proxy代理: vm.$data.message => vm.message
proxyData(data) {
Object.keys(data).forEach(key => {
Object.defineProperty(this, key, {
get() {
return data[key]
},
set(newValue) {
data[key] = newValue;
}
})
})
}
}
複製程式碼
4. compile
4.1 nodeType
nodeType 屬性返回節點型別。
-
如果節點是一個元素節點,nodeType 屬性返回 1。
-
如果節點是屬性節點, nodeType 屬性返回 2。
-
如果節點是一個文字節點,nodeType 屬性返回 3。
4.2 createDocumentFragment()
- DocumentFragments 是DOM節點。它們不是主DOM樹的一部分。通常的用例是建立文件片段,將元素附加到文件片段,然後將文件片段附加到DOM樹。在DOM樹中,文件片段被其所有的子元素所代替。
- 因為文件片段存在於記憶體中,並不在DOM樹中,所以將子元素插入到文件片段時不會引起頁面迴流(對元素位置和幾何上的計算)。因此,使用文件片段通常會帶來更好的效能。
4.3 reduce
4.3.1 說明
接收一個函式作為累加器,陣列中的每個值(從左到右)開始縮減,最終計算為一個值
4.3.2 有一個字串message.a.b
,有一個物件{"message":{"a":{"b":"我是bb"}}}
,想要找到字條串中的b,在物件中key為b,對應的value
//物件
var dataObj = {
message: {
a: {
b: '我是bb'
}
},
};
//字串
var dataStr = 'message.a.b';
// 字串轉成陣列
var dataArray = dataStr.split('.')
console.log(dataArray)
//找到字條串中的b,在物件中key為b,對應的value
var result = dataArray.reduce((prev, next) => {
return prev[next]
}, dataObj)
console.log(result)
複製程式碼
4.4 /\{\{([^}]+)\}\}/g
;
將{{a}} => a
let expr = "{{message.a.b}}"; // 取文字中的內容
let reg = /\{\{([^}]+)\}\}/g; // {{a}} {{b}} {{c}}
var result = expr.replace(reg, 'a');
console.log(result)
複製程式碼
4.5 ...運算子
function sub(...arg) {
let sum = 0;
arg.forEach(item => {
sum += item;
})
return sum;
}
var a = sub(1, 2, 3)
var b = sub(1, 2, 3, 4)
console.log(a) //6
console.log(b) //10
複製程式碼
4.6 setVal()
監聽input輸入框的值,根據<input type="text" v-model="message.a">
鍵(message.a)
,然後把值賦給vm.data裡對應的鍵(vm.data.message.a = 值)
,再更新檢視上的顯示modelUpdater
model(node, vm, expr) {
let updateFn = this.updater['modelUpdater'];
node.addEventListener('input', (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, this.getVal(vm, expr));
},
複製程式碼
setVal(vm, expr, value) { // [message,a]
expr = expr.split('.');
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return prev[next] = value;
}
return prev[next];
}, vm.$data);
},
複製程式碼
updater: {
modelUpdater(node, value) {
node.value = value;
}
}
複製程式碼
4.6 完整compile.js程式碼
class Compile {
constructor(el, vm) {
this.el = this.isElementNode(el) ? el : document.querySelector(el);
this.vm = vm;
if (this.el) {
let fragment = this.node2fragment(this.el);
this.compile(fragment);
this.el.appendChild(fragment)
}
}
isElementNode(node) {
return node.nodeType === 1;
}
isDirective(name) {
return name.includes('v-')
}
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)
}
})
}
compileText(node) {
let expr = node.textContent;
let reg = /\{\{([^}]+)\}\}/g;
if (reg.test(expr)) {
CompileUtil['text'](node, this.vm, expr)
}
}
compile(fragment) {
let childNodes = fragment.childNodes;
Array.from(childNodes).forEach(node => {
if (this.isElementNode(node)) {
this.compileElement(node)
this.compile(node)
} else {
this.compileText(node)
}
})
}
node2fragment(el) {
let fragment = document.createDocumentFragment();
let firstChild;
while (firstChild = el.firstChild) {
fragment.appendChild(firstChild);
}
return fragment;
}
}
CompileUtil = {
getVal(vm, expr) {
expr = expr.split('.');
return expr.reduce((prev, next) => {
return prev[next]
}, vm.$data)
},
getTextVal(vm, expr) {
return expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
return this.getVal(vm, argument[1])
})
},
setVal(vm, expr, value) {
expr = expr.split('.');
return expr.reduce((prev, next, currentIndex) => {
if (currentIndex === expr.length - 1) {
return prev[next] = value
}
return prev[next]
}, vm.$data)
},
model(node, vm, expr) {
let updateFn = this.updater['modelUpdater'];
let value = this.getVal(vm, expr);
new Watcher(vm, expr, (newValue) => {
updateFn && updateFn(node, this.getVal(vm, expr))
})
node.addEventListener('input', (e) => {
let newValue = e.target.value;
this.setVal(vm, expr, newValue)
})
updateFn && updateFn(node, value)
},
text(node, vm, expr) {
let updateFn = this.updater['textUpdater'];
let value = this.getTextVal(vm, expr);
expr.replace(/\{\{([^}]+)\}\}/g, (...argument) => {
new Watcher(vm, argument[1], (newValue) => {
updateFn && updateFn(node, this.getTextVal(vm, expr))
})
})
updateFn && updateFn(node, value)
},
updater: {
modelUpdater(node, value) {
node.value = value;
},
textUpdater(node, value) {
node.textContent = value;
}
}
}
複製程式碼
5. observer
5.1 this.subs=[]
以<div id="app"></div>
下面的節點為準,{{b}}
算1個,message.a
算2個
3個watcher
<div id="app">
<input type="text" v-model="message.a">
<div>{{b}}</div>
</div>
複製程式碼
addSub(watcher) {
this.subs.push(watcher)
console.log(this.subs)
}
複製程式碼
5個watcher
<div id="app">
<input type="text" v-model="message.a">
<div>{{b}}</div>
{{message.a}}
</div>
複製程式碼
5.2完整observer.js程式碼
class Observer {
constructor(data) {
this.observer(data)
}
observer(data) {
if (!data || typeof data !== 'object') {
return;
}
Object.keys(data).forEach(key => {
this.defineReactive(data, key, data[key]);
this.observer(data[key])
})
}
defineReactive(obj, key, value) {
let that = this;
let dep = new Dep();
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
Dep.target && dep.addSub(Dep.target);
return value;
},
set(newValue) {
if (newValue != value) {
that.observer(newValue)
value = newValue;
dep.notify();
}
}
})
}
}
class Dep {
constructor() {
this.subs = [];
}
addSub(watcher) {
this.subs.push(watcher);
}
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
複製程式碼
6. watcer
6.1 new Watcher()
把<div id="app"></div>
下面的節點(有expr),一個expr對應一個watcher,一個watcher後續變化都儲存到一個dep.subs[]裡
6.2 完整watcer.js程式碼
class Watcher {
constructor(vm, expr, cb) {
this.vm = vm;
this.expr = expr;
this.cb = cb;
this.value = this.get();
}
getVal(vm, expr) {
expr = expr.split('.'); // [message,a]
return expr.reduce((prev, next) => { // vm.$data.a
return prev[next];
}, vm.$data);
}
get() {
Dep.target = this;
let value = this.getVal(this.vm, this.expr);
Dep.target = null;
return value;
}
update() {
let newValue = this.getVal(this.vm, this.expr);
let oldValue = this.value;
if (newValue != oldValue) {
this.cb(newValue)
}
}
}
複製程式碼
7. 效果
GifCam錄製gif
7.1 修改資料,檢視變化
- 操作前,根據表示式
v-model="message.a"
,得到vm.data裡的資料,compile,渲染到頁面 - vm.data裡的資料變化,觸發observer.set(),
- 因為新舊資料不一樣,觸發dep.notify()
- 觸發watcher裡的this.cb(newValue)
- 觸發compile裡的CompileUtil.updater()
7.2 修改檢視,資料變化
- node.addEventListener('input'),監聽輸入框,得到新值newValue
- setVal(),使用vm.data裡的資料等於新值
- 重複上面操作