vue中MVVM原理及其实现
一、了解 MVVM
- MVVM - Model View ViewModel:数据,视图,视图模型。
- 三者与 Vue 的对应:view 对应 template,vm 对应 new Vue({…}),model 对应 data。
- 三者的关系:view 能够通过事件绑定的形式影响 model,model 能够通过数据绑定的模式影响到view,viewModel是把 model 和 view 连起来的连接器。
MVVM 框架的三大因素:
- 响应式:Vue 如何监听到 data 的每个属性变动。
数据劫持: 应用 Object.defineProperty(obj, 'property',{})来定义属性,将对象属性值的设置和拜访 (get,set) 都变成函数,别离在设置和获取该对象的该属性时调用执行。
- 模板引擎:Vue 的模板如何被解析,指令如何解决
- 渲染:Vue 的模板如何被渲染成 html,渲染过程是怎么的
二、实现办法
- 实现compile,进行模板的编译,包含编译元素(指令)、编译文本等,达到初始化视图的目标,并且还须要绑定好更新函数;
- 实现Observe,监听所有的数据,并对变动数据公布告诉;
- 实现watcher,作为一个中枢,接管到observe发来的告诉,并执行compile中相应的更新办法。
- 联合上述办法,向外裸露mvvm办法。
首先编辑一个html文件,如下:
<div id="app"> <input type="text" v-model="obj.name"> <div> {{obj.name}}</div> {{message}}{{obj.name}}</div><script src="index.js"></script><script> let vm = new MVVM({ el: '#app',//或document.querySelector('#app') data: { message: 'hello', obj: { name: 'susu' } } });</script>
1.创立类MVVM
class MVVM { constructor(options) { this.$el = options.el; this.$data = options.data; if (this.$el) { // 数据劫持 把对象所有的属性 减少get set 办法 new Observer(this.$data); this.proxyData(this.$data); // 用数据和元素进行编译 new Compile(this.$el, this); } } // 把对象的属性全副绑定在实例上,this.xx proxyData(data) { // defineProperty解析 : https://www.jianshu.com/p/8fe1382ba135 Object.keys(data).forEach(key => { Object.defineProperty(this, key, { get() { return data[key]; }, set(newValue) { data[key] = newValue; } }) }) }}
2.实现compile(编译模板)
1.把实在DOM移入到内存中 fragment,因为fragment在内存中,操作比拟快
2.编译 : 提取想要的元素节点 v-model 和文本节点 {{}}
3.把编译好的fragment,增加到DOM中
class Compile { constructor(el, vm) { this.el = this.isElementNode(el) ? el : document.querySelector(el); this.vm = vm; if (this.el) { // 1.把实在DOM移入到内存中 fragment,因为fragment在内存中,操作比拟快 let fragment = this.node2fragment(this.el); // 2.编译 => 提取想要的元素节点 v-model 和文本节点 {{}} this.compile(fragment); // 3.把编译好的fragment,增加到DOM中 this.el.appendChild(fragment); } } // 是否是元素节点 isElementNode(el) { //nodeType : 1 Element 代表元素节点 return el.nodeType == 1; } node2fragment(el) { // 在内存中,创立一个新的文档片段, let fragment = document.createDocumentFragment(); let firstChild; while (firstChild = el.firstChild) { fragment.appendChild(firstChild); } // 返回虚构的节点对象,节点对象蕴含所有属性和办法。 return fragment; } // 编译 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); } }) } // 编译文本 {{msg}} compileText(node) { let expr = node.textContent; //{{msg}} let reg = /\{\{([^}]+)\}\}/g; if (reg.test(expr)) { CompileUtil.text(node, this.vm, expr); } } // 编译元素 v- compileElement(node) { let attrs = node.attributes; Array.from(attrs).forEach(attr => { // 是否是指令 if (attr.name.includes('v-')) { let expr = attr.value;// message let type = attr.name.split('-')[1];//model CompileUtil[type](node, this.vm, expr); } }) }}CompileUtil = { // 文本处理 text(node, vm, expr) { let updaterFn = this.updater.textUpdater; let value = this.getTextVal(vm, expr); // 监控数据变动 eg:因为{{message}} {{obj.name}} ,所以须要循环 expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { new Watcher(vm,arguments[1],newValue => { updaterFn && updaterFn(node, this.getTextVal(vm, expr)); }) }) updaterFn && updaterFn(node, value); }, // 获取文本的key eg:message getTextVal(vm, expr) { return expr.replace(/\{\{([^}]+)\}\}/g, (...arguments) => { return this.getValue(vm, arguments[1]);// message }) }, // 获取data中对象的值 getValue(vm, expr) { let arr = expr.split('.'); //解决对象obj.name =>[obj,name] return arr.reduce((prev, next) => { return prev[next] }, vm.$data); }, // 设置data中对象的值 setValue(vm, expr, value) { let arr = expr.split('.'); //解决对象obj.name =>[obj,name] arr.reduce((prev, next, curIndex) => { if (curIndex == arr.length - 1) { return prev[next] = value; } return prev[next]; }, vm.$data); }, // 输入框解决 model(node, vm, expr) { let updaterFn = this.updater.modalUpdater; let value = this.getValue(vm, expr); node.addEventListener('input', (event) => { this.setValue(vm, expr, event.target.value); }) // 监控数据变动 new Watcher(vm,expr,newValue => { updaterFn && updaterFn(node, this.getValue(vm, expr)); }) updaterFn && updaterFn(node, value); }, updater: { textUpdater(node, value) { node.textContent = value; }, modalUpdater(node, value) { node.value = value; } }}
3.实现observe(数据监听/劫持)
vue采纳的observe + sub/pub 实现数据的劫持,通过js原生的办法Object.defineProperty()来劫持各个属性的setter,getter,在属性对应数据扭转时,公布音讯给订阅者,而后触发相应的监听回调。
为何要监听 get,而不是间接监听 set ?
- 因为 data 中有很多属性,有些被用到,有些可能不被用到
- 只有被用到的才会走 get
- 没有走到 get 中的属性,set 的时候咱们也无需关怀
- 防止不必要的从新渲染
次要内容:observe的数据对象进行递归遍历,包含子属性对象的属性,都加上 setter和getter。
class Observer { constructor(data) { this.observe(data); } // 获取data的key value observe(data) { if (!data || typeof data != 'object') return; Object.keys(data).forEach(key => { this.defineReactive(data, key, data[key]); this.observe(data[key]); }) } // 定义响应式 defineReactive(obj, key, value) { let that = this; // 每个变动的数据 都会对应一个数组,这个数组是寄存所有更新的操作 let dep = new Dep(); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get() { console.log('observe',key,Dep.target); Dep.target && dep.addSubs(Dep.target); return value; }, set(newValue) { if (value != newValue) { // 如果是对象持续劫持 that.observe(newValue); value = newValue; // 告诉更新 dep.notify(); } } }) }}
实现数据劫持后,接下来的工作怎么告诉订阅者了,咱们须要在监听数据时实现一个音讯订阅器,具体的办法是:定义一个数组,用来寄存订阅者,数据变动告诉(notify)订阅者,再调用订阅者的update办法。
增加Dep类:
class Dep { constructor() { this.subs = []; } // 增加订阅 addSubs(watcher) { this.subs.push(watcher); } // 告诉更新 notify() { this.subs.forEach(watcher => { watcher.update(); }) }}
4.实现watcher(订阅核心)
Observer和Compile之间通信的桥梁是Watcher订阅核心,其主要职责是:
1、在本身实例化时往属性订阅器(Dep)外面增加本人,与Observer建设连贯;
2、本身必须有一个update()办法,与Compile建设连贯;
3、当属性变动时,Observer中dep.notify()告诉,而后能调用本身(Watcher)的update()办法,并触发Compile中绑定的回调,实现更新。
// 观察者的目标就是给须要变动的那个元素减少一个观察者,当数据变动后执行对应的办法class Watcher { constructor(vm, expr, cb) { this.vm = vm; this.expr = expr; this.cb = cb; // 获取老的值 this.value = this.get(); } // 获取data中对象的值 getValue(vm, expr) { let arr = expr.split('.'); return arr.reduce((prev, next) => { return prev[next] }, vm.$data); } get() { Dep.target = this; let value = this.getValue(this.vm, this.expr); Dep.target = null; return value; } // 更新,内部调用的办法 update() { let newValue = this.getValue(this.vm, this.expr); if (newValue != this.value) { this.cb(newValue); } }}