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里的数据等于新值
- 重复上面操作