1. 说明
- compile 编译,即模板解析器,能够对模板中的指令和插值表达式进行解析
- observer 数据劫持,即数据监听器,能够对数据对象(data)的所有属性进行监听
- watcer 监听者,将compile的解析结果,与observer所观察的对象连接起来,建立关系,在observer观察到数据对象变化时,接收通知,并更新DOM
gitbub
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;// 实例上的所有数据,datathis.$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的valuenew Compile(this.$el, this)}}// proxy代理: vm.$data.message => vm.messageproxyData(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,对应的valuevar 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) //6console.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.areturn 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里的数据等于新值
- 重复上面操作