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); }); };
|
推荐阅读