MVVM 原理
常见的面试问题:
- Vue 数据绑定的原理?
- MVVM 数据绑定的原理?
- Vue 双向数据绑定的原理?
- Vue 数据响应式原理?
- 数据响应式原理?

当前比较流行的前端框架都是采用的 MVVM 的方式:
什么是 MVVM?
简单一句话:数据驱动视图。
介绍
感受 MVVM
- 传统的 DOM 操作方式
- 模板引擎方式
- 数据驱动视图方式(MVVM)
什么是 MVVM
简单一句话:数据驱动视图

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| <template> <div>{{ message }}</div> </template>
<script> export default { data() { return { message: "Hello World", }; }, }; </script>
|
- Model(M):普通的 JavaScript 对象,例如 Vue 实例中的 data
- View(V):视图
- ViewModel(VM):Vue 实例
- 负责数据和视图的更新
- 它是 Model 数据 和 View 视图通信的一个桥梁
JavaScript 数据劫持
- 数据劫持?
- Observer 数据观察
- 数据拦截器
如何实现修改一个对象成员就修改了 DOM?
1 2 3 4 5 6 7 8 9 10 11
| const data = { message: "Hello World", };
data.message = "hello";
document.querySelector("xxx").style.xxx = "xxx";
|
答案是:JavaScript 数据劫持,或者说是 JavaScript 对象属性拦截器。
什么是数据劫持(属性拦截器)?
说白了就是:观察数据的变化。
- Object.defineProperty
- ECMAScript 5 中的一个 API
- Vue 1 和 Vue 2 中使用的都是 Object.defineProperty
- Proxy
- ECMAScript 6 中的一个 API
- 即将升级的 Vue 3 会升级使用 Proxy
- Proxy 比 Object.defineProperty 性能要更好
Object.defineProperty
参考资料:
Object.defineProperty() 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法
1
| Object.defineProperty(obj, prop, descriptor);
|
参数:
返回值:
被传递给函数的对象。
描述符
configurable
当且仅当该属性的 configurable 为 true 时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。默认为 false。
enumerable
当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中。默认为 false。
value
该属性对应的值。可以是任何有效的 JavaScript 值(数值,对象,函数等)。默认为 undefined。
writable
当且仅当该属性的writable为true时,value才能被赋值运算符改变。默认为 false。
get
一个给属性提供 getter 的方法,如果没有 getter 则为 undefined。当访问该属性时,该方法会被执行,方法执行时没有参数传入,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。
默认为 undefined。
set
一个给属性提供 setter 的方法,如果没有 setter 则为 undefined。当属性值修改时,触发执行该方法。该方法将接受唯一参数,即该属性新的参数值。
默认为 undefined。
|
configurable |
enumerable |
value |
writable |
get |
set |
| 数据描述符 |
Yes |
Yes |
Yes |
Yes |
No |
No |
| 存取描述符 |
Yes |
Yes |
No |
No |
Yes |
Yes |
如果一个描述符不具有 value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value 或 writable)和(get 或 set)关键字,将会产生一个异常。
示例
需求:
1 2 3 4 5 6 7 8 9 10 11 12 13
| const data = { name: "张三", age: 18, };
data.name;
data.name = xxx;
data.age = xxx;
|
实现:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32
| const data = {};
let _name = ""; let _age = 0;
Object.defineProperty(data, "name", { configurable: false, enumerable: true, set(value) { _name = value; }, get() { return _name; }, });
Object.defineProperty(data, "age", { configurable: false, enumerable: true, set(value) { _age = value; }, get() { return _age; }, });
|
事件发布/订阅
1 2 3 4 5
| bus.$on("事件类型", 处理函数);
bus.$emit("事件类型", 处理函数);
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| function EventEmitter() { this.subs = { }; }
EventEmitter.prototype.$on = function (eventType, callback) { this.subs[eventType] = this.subs[eventType] || []; this.subs[eventType].push(callback); };
EventEmitter.prototype.$emit = function (eventType, ...args) { const subs = this.subs[eventType]; if (subs) { subs.forEach((callback) => { callback(...args); }); } };
|
DOM 操作
原理实现
示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <title>MVVM原理分析</title> </head> <body> <div id="app"> <h3 v-text="msg"><span>哈哈...</span></h3> <input type="text" v-model="msg" /> <button v-on:click="sayHi">按钮</button> </div> <script src="https://gcore.jsdelivr.net/npm/vue/dist/vue.js"></script> <script> var vm = new Vue({ el: "#app", data: { msg: "学习MVVM原现分析!", }, methods: { sayHi() { this.msg = "修改了数据"; }, }, }); </script> </body> </html>
|
VM 模型
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| function Vue(options) { this.$options = options;
this.$el = typeof options.el === "string" ? document.querySelector(options.el) : options.el;
this.$data = options.data || {}; this.$methods = options.methods || {};
Object.keys(this.$data).forEach((key) => { Object.defineProperty(this, key, { configurable: false, enumerable: true, get() { console.log("get from vue..."); return this.$data[key]; }, set(val) { console.log("set from vue..."); this.$data[key] = val; }, }); });
new Observe(this.$data);
new Compile(this.$el, this); }
|
数据劫持
劫持 VM 模型中初始的数据,监听数据的访问
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| function Observe(data) { if (!data || typeof data !== "object") return;
this.data = data;
Object.keys(data).forEach((key) => { this.walk(key, data[key]); }); }
Observe.prototype.walk = function (key, val) { Object.defineProperty(this.data, key, { configurable: false, enumerable: true, set(newVal) { if (newVal === val) return; val = newVal; watcher.$emit(key, newVal); }, get() { return val; }, }); };
|
编译模板
对 el 所对应的 DOM 节点的所有节点进行遍历操作,查找出所以包含指令或插值的节点,然后进行订阅监听,实现 DOM 的更新。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| const watcher = new Watcher();
function Compile(el, vm) { this.vm = vm;
if (el.nodeType !== 1) return;
this.compileElement(el); }
Compile.prototype.compileElement = function (el) { let childNodes = el.childNodes;
if (!childNodes) return;
Array.from(childNodes).forEach((node) => { let text = node.textContent, reg = /(\{\{(.*)\}\})/;
if (node.nodeType === 3 && reg.test(text)) { node.textContent = text.replace(RegExp.$1, this.vm[RegExp.$2]);
watcher.$on(RegExp.$2, (newVal) => { node.textContent = text.replace(RegExp.$1, newVal); }); } else if (node.nodeType === 1) { this.compile(node); }
this.compileElement(node); }); };
Compile.prototype.compile = function (node) { let attrs = node.attributes;
Array.from(attrs).forEach((attr) => { let attrName = attr.name;
if (attrName.indexOf("v-") === 0) { let exp = attr.value; let dir = attrName.slice(2);
node.removeAttribute(attrName);
if (dir.indexOf("on") === 0) { let type = dir.split(":")[1], handler = this.vm.$methods[exp].bind(this.vm);
return node.addEventListener(type, handler); }
directives[dir] && directives[dir](node, exp, this.vm);
watcher.$on(exp, (newVal) => { directives[dir] && directives[dir](node, exp, this.vm); }); } }); };
|
订阅/发布
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function Watcher(sub) { this.subs = {}; }
Watcher.prototype.$on = function (sub, cb) { this.subs[sub] = this.subs[sub] || []; this.subs[sub].push(cb); };
Watcher.prototype.$emit = function (sub, newVal) { this.subs[sub].forEach((cb) => { cb(newVal); }); };
|
推荐阅读