前言
目前前端最火的几个框架vue,angular等都是基于MVVM框架思想,那么什么是MVVM框架,和它的双向数据绑定都是如何实现的呢?
什么是MVVM?
MVVM是一种发开代码的组织和设计思想,说白了就是框架,它跟MVC是同一个物种,也可以说是从MVC演化到MVP,再在MVP的基础进化为MVVM。
在了解MVVM之前,我们有必要了解它的祖先。MVC是指Model(数据模型),View(视图),Controller(控制器),这些我们都很好理解,而MVP的P指的是Presenter。它跟Controlller有点相似,不同的是,它是用户触发View上的绑定的Dom事件后,View将修改通知给Presenter来完成后续的操作(更新数据或者视图)。而在MVC模式下,用户是直接操作Controller,以更改url上的hash发送请求的方式。所以很明显,Presenter跟View是双向绑定的。
回到MVVM,VM指的是ViewModel。ViewModel是把Present拆分为多个小的指令步骤(directive),它将View和Model双向绑定,用户操作修改View,ViewModel驱动Model进行更新,相同的,Model数据被更改,ViewModel检测到并驱动View更新。
那么Model数据更改是如何被ViewModel检测到的呢?
数据变更检测
在MVVM模式下,在view→model的方向,用View层通过触发一些元素的事件,例如input的onchange事件,将修改通知给ViewModel,然后ViewModel再操作Model,这很容易理解。
那么Model→view这个方向上的通知如何实现呢?那就需要数据变更检测机制,它能够检测到Model的变化,并通过ViewModel更新View层。它有很多实现形式,特别是ES6 新特性的出现,丰富了实现数据对象的变更检测的方式。
下面介绍四种方式:
- 手动触发绑定
- 脏检测机制
- 数据对象劫持
- Proxy
数据变更检测四种方式
手动触发绑定
这种方式比较直接,思路是通过在数据对象上定义get()和set()方法,改变数据后手动触发这两个方法来获取和设置。Angular正是通过这种方式进行view层操作更新的。
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
| <!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>数据变更检测之手动触发绑定</title> </head> <body> <input type="text" q-value="value" id="input"> <div> <span q-text="value" id="el"></span> </div>
<script>
let elems = [document.getElementById("el"), document.getElementById("input")];
let data = { value: 'hello' };
let directive = { text: function (text) { this.innerHTML = text; }, value: function(value) { this.value = value; } };
if(document.addEventListener) { elems[1].addEventListener('keyup', function(e) { ViewModelSet('value', e.target.value); }, false); }else { elems[1].attachEvent('onkeyup', function(e) { ViewModelSet('value', e.target.value); }, false); }
scan(); setTimeout(function() { ViewModelSet('value', 'hello AlexL'); }, 1000)
function scan() { for(let elem of elems) { elem.directive = []; for(let attr of elem.attributes) { if(attr.nodeName.indexOf('q-') > -1) { directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]); elem.directive.push(attr.nodeName.slice(2)); } } } }
function ViewModelSet(key, value) { data[key] =value; scan(); }
</script> </body> </html>
|
脏检测机制
脏检测机制的基本原理是,在viewmodel对象的某个属性值发生变化时找到与这个属性值相关的所有元素,然后进行数据比较,如果变化就调用指令,重新扫描并渲染这个元素。
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 81 82 83 84 85 86
| <!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>数据变更检测之脏数据检测</title> </head> <body> <input type="text" id="input" qg-event="value" qg-bind="value"> <div> <span id="el" qg-event="text" qg-bind="value"></span> </div> <script> let elems = [document.getElementById('el'), document.getElementById('input')]; let data = { value: 'hello' };
let directive = { text: function(str) { this.innerHTML = str; }, value: function(str) { this.setAttribute('value', str); } };
scan(elems);
if(document.addEventListener) { elems[1].addEventListener('keyup',function(e) { data.value = e.target.value; $digest(e.target.getAttribute('qg-bind')); }, false); }else { elems[1].attachEvent('onkeyup', function(e) { data.value = e.target.value; $digest(e.target.getAttribute('qg-bind')); }, false); }
setTimeout(function() { data.value = 'nice job!'; $digest('value'); }, 5000)
function scan() { for(let elem of elems) { elem.directive = []; } }
function $digest(value) { let list = document.querySelectorAll('[qg-bind=' + value + ']'); digest(list); }
function digest(elems) { for(let i=0, len=elems.length; i<len; i++) { let elem = elems[i]; for(let j=0, len=elem.attributes.length; j<len; j++) { let attr = elem.attributes[j]; if(attr.nodeName.indexOf('qg-event') > -1) { let datakey = elem.getAttribute('qg-bind') || undefined; if(elem.directive[attr.nodeValue] !== data[datakey]) { directive[attr.nodeValue].call(elem, data[datakey]); elem.directive[attr.nodeValue] = data[datakey]; } } } } }
</script> </body> </html>
|
数据对象劫持
基本思路是使用Object.defineProperty和Object.defineProperies对viewmodel数据对象进行属性get()和set()的监听,当有数据读取和赋值操作的时候则扫面节点,运行指定对应节点的Directive指令,viewmodel通过等号赋值就可以了。
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 81 82 83 84
| <!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>数据变更检测之对象劫持</title> </head> <body> <input type="text" q-value="value" id="input"> <div> <span q-text="value" id="el"></span> </div>
<script> let elems = [document.getElementById('el'), document.getElementById('input')]; let data = { value: 'hello' };
let directive = { text: function(str) { this.innerHTML = str; }, value: function(str) { this.setAttribute('value', str); } };
let bValue; scan();
defineGetAndSet(data, 'value');
if(document.addEventListener) { elems[1].addEventListener('keyup',function(e) { data.value = e.target.value; }, false); }else { elems[1].attachEvent('onkeyup', function(e) { data.value = e.target.value; }, false); }
setTimeout(function() { data.value = 'hello 木木夕'; }, 2000);
function scan() { for(let elem of elems) { elem.directive = []; for(let attr of elem.attributes) { if(attr.nodeName.indexOf('q-') > -1) { directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]); elem.directive.push(attr.nodeName.slice(2)); } } } }
function defineGetAndSet(obj, propName) { Object.defineProperty(obj, propName, { get: function() { return bValue; }, set: function(newValue) { bValue = newValue; scan(); }, enumerable: true, configurable: true }) }
</script> </body> </html>
|
ES6 Proxy
ES6的新特性Proxy,可以在已有对象的基础上重新定义一个对象,并重新定义对象原型上的方法,包括get()和set()。
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 81
| <!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>数据变更之proxy对象劫持</title> </head>
<body> <label>请输入<input type="text" q-value="value" id="input"></label> <div> <span q-text="value" id="el"></span> </div>
<script>
let elems = [document.getElementById('el'), document.getElementById('input')]; let directive = { text: function (str) { this.innerHTML = str; }, value: function (str) { this.setAttribute('value', str); } };
let data = new Proxy({ value: 'my name is AlexL' }, { get: function (target, key, receiver) { return target.value; }, set: function (target, key, value, receiver) { target.value = value; scan(); return target.value; } });
scan();
if (document.addEventListener) { elems[1].addEventListener('keyup', function (e) { data.value = e.target.value; }, false); } else { elems[1].attachEvent('onkeyup', function (e) { data.value = e.target.value; }, false); }
setTimeout(function () { data.value = 'hello 木木夕'; }, 2000);
function scan() { for (let elem of elems) { elem.directive = []; for (let attr of elem.attributes) { if (attr.nodeName.indexOf('q-') > -1) { directive[attr.nodeName.slice(2)].call(elem, data[attr.nodeValue]); elem.directive.push(attr.nodeName.slice(2)); } } } } </script> </body>
</html>
|
结语
实现一个MVVC框架远远没这么简单,里面涉及很多性能优化等机制,在上诉的四种方法中,在数据变更时,都把整个Dom结构扫描了,在渲染Dom时连没变化的也重新渲染,这样是很耗性能的,在Angular,vue这样的框架中,用到了virtual dom来最小化的操作dom,这样就可以解决这个问题。
但是,也无可避免的操作了Dom,我们知道浏览器的Dom操作其实是很慢的,有没有在完全不操作dom的情况下,渲染视图呢?其实是有的,MNV*框架就可以办到,它适用于移动端的hybird app,通过定义的协议来调用原生方法来渲染视图,来达到跟原生体验非常接近的效果 。关于MNV*框架的讨论不在本章的范畴,有兴趣的同学可以去自行了解。