在前端页面中,把 Model 用纯 JS 对象表示,View 负责显示,二者作到了最大化的分离前端
把 Model 和 View 关联起来的就是 ViewModel。ViewModel 负责把 Model 的数据同步到 View 中显示出来,还负责把 View 的修改同步回 Model。segmentfault
MVVM 的设计思想:关注 Model 的变化,让 MVVM 框架去自动更新 DOM 的状态,从而把开发者从操做 DOM 的繁琐步骤中解脱出来。数组
了解了 MVVM 思想后,本身用原生 JS 实现一个 MVVM 框架。微信
实现 MVVM 框架前先来看几个基本用法:框架
普通声明对象,定义和修改属性函数
let obj = {} obj.name = 'zhangsan' obj.age = 20
用ObjectdefineProperty
声明对象
语法:this
Object.defineProperty(obj,prop,descriptor)
obj
:要处理的目标对象prop
:要定义或修改的属性的名称descriptor
:将被定义或修改的属性描述符let obj = {} Object.defineProperty(obj,'age',{ value = 14, })
咋一看有点多此一举,这不很鸡肋嘛prototype
别急,往下看设计
descriptor
有两种形式:数据描述符和存储描述符,他们两个共有属性:code
configurable
,是否可删除,默认为false
,定义后没法修改enumerable
,是否可遍历,默认为false
,定之后没法修改configurable
设置为false
时,其内部属性没法用delete
删除;如要删除,须要把configurable
设置为true
。
let obj = {} Object.defineProperty(obj,'age',{ configurable:false, value:20, }) delete obj.age //false
enumerable
设置为false
时,其内部属性没法遍历;如需遍历,要把enumerable
设置为true
let obj = {name:'zhangsan'} Object.defineProperty(obj,'age',{ enumerable:false, value:20, }) for(let key in obj){ console.log(key) //name }
value
:该属性对应的值,默认为undefined
。writable
:当且紧当为true
时,value
才能被赋值运算符改变。默认为false
。
let obj = {} Object.defineProperty(obj,'age',{ value:10, writable:false }) obj.age = 11 obj.age //10
writable
和configurable
的区别是前者是value
可否被修改,后者是value
可否被删除。
get()
:一个给属性提供getter
的方法,默认为undefined
。set()
:一个给属性提供setter
的方法,默认为undefined
。
let obj = {} let age Object.defineProperty(obj,'age',{ get:function(){ return age }, set:function(newVal){ age = newVal } }) obj.age = 20 obj.age //20
当我调用obj.age
时,实际上是在向obj
对象要age
这个属性,它会干吗呢?它会调用obj.get()
方法,它会找到全局变量age
,获得undefined
。
当我设置obj.age = 20
时,它会调用obj.set()
方法,将全局变量age
设置为20
。
此时在调用obj.age
,获得20
。
注意:数据描述符和存储描述符不能同时存在,不然会报错
let obj = {} let age Object.defineProperty(obj,'age',{ value:10, //报错 get:function(){ return age }, set:function(newVal){ age = newVal } })
使用Object.defineProperty
来实现数据拦截,从而实现数据监听。
首先有一个对象
let data = { name:'zhangsan', friends:[1,2,3,4] }
下面写一个函数,实现对data
对象的监听,就能够在内部作一些事情
observe(data)
换句话说,就是data
内部的属性都被咱们监控的,当调用属性时,就能够在上面作些手脚,使得返回的值变掉;当设置属性时,不给他设置。
固然这样作很无聊,只是想说明,咱们能够在内部作手脚,实现咱们想要的结果。
那observe
这个函数应该怎么写呢?
function observe(data){ if(!data || typeof data !== 'object')return //若是 data 不是对象,什么也不作,直接跳出,也就是说只对 对象 操做 for(let key in data){ //遍历这个对象 let val = data[key] //获得这个对象的每个`value` if(typeof val === 'object'){ //若是这个 value 依然是对象,用递归的方式继续调用,直到获得基本值的`value` observe(val) } Object.defineProperty(data,key,{ //定义对象 configurable:true, //可删除,本来的对象就能删除 enumerable:true, //可遍历,本来的对象就能遍历 get:function(){ console.log('这是假的') //调用属性时,会调用 get 方法,因此调用属性能够在 get 内部作手脚 //return val //这里注释掉了,实际调用属性就是把值 return 出去 }, set:function(newVal){ console.log('我不给你设置。。。') //设置属性时,会调用 set 方法,因此设置属性能够在 set 内部作手脚 //val = newVal //这里注释掉了,实际设置属性就是这样写的。 } }) } }
注意两点:
let val = data[key]
时,不能用var
,由于这里须要对每一个属性进行监控,用let
每次遍历都会建立一个新的val
,在进行赋值;若是用var
,只有第一次才是声明,后面都是对一次声明val
进行赋值,遍历结束后,获得的是最后一个属性,显然这不是咱们须要的。get
方法里,return
就是前面声明的val
,这里不能用data[key]
,会报错。由于调用data.name
,就是调用get
方法时,获得的结果是data.name
,又继续调用get
方法,就随变成死循环,因此这里须要用一个变量来存储data[key]
,并将这个变量返回出去。一个典型的观察者模式应用场景——微信公众号
用代码怎么实现呢?先看逻辑:
Subject 是构造函数,new Subject()建立一个主题对象,它维护订阅该主题的一个观察者数组数组(举例来讲:Subject 是腾讯推出的公众号,new Subject() 是一个某个机构的公众号——新世相,它要维护订阅这个公众号的用户群体)
主题上有一些方法,如添加观察者addObserver
、删除观察者removeObserver
、通知观察者更新notify
(举例来讲:新世相将用户分为两组,一组是忠粉就是 addObserver,一组是黑名单就是:removeObserver,它在忠粉组能够添加用户,能够在黑名单里拉黑一些杠精,若是有福利发放,它就会统治忠粉里的用户:notify)
Observer 是构造函数,new Observer() 建立一个观察者对象,该对象有一个update
方法(举例来讲:Observer 是忠粉用户群体,new Observer() 是某个具体的用户——小王,他必需要打开流量才能收到新世相的福利推送:updata)
当调用notify
时实际上调用所有观察者observer
自身的update
方法(举例来讲:当新世相推送福利时,它会自动帮忠粉组的用户打开流量,这比较极端,只是用来举例)
function Subject(){ this.observers = [] } Subject.prototype.addObserver = function(observer){ this.observers.push(observer) } Subject.prototype.removeObserver = function(observer){ let index = this.observers.indexOf(observer) if(index > -1){ this.observers.splice(index,1) } } Subject.prototype.notify = function(){ this.observers.forEach(observer=>{ observer.update() }) } function Observer(name){ this.name = name this.update = function(){ console.log(name + ' update...') } } let subject = new Subject() //建立主题 let observer1 = new Observer('xiaowang') //建立观察者1 subject.addObserver(observer1) //主题添加观察者1 let observer2 = new Observer('xiaozhang') //建立观察者2 subject.addObserver(observer2) //主题添加观察者2 subject.notify() //主题通知观察者 /**** 输出 *****/ xiaowang update... xiaozhang update...
class Subject{ constructor(){ this.observers = [] } addObserver(observer){ this.observers.push(observer) } removeObserver(observer){ let index = this.observers.indexOf(observer) if(index > -1){ this.observers.splice(index,1) } } notify(){ this.observers.forEach(observer=>{ observer.update() }) } } class Observer{ constructor(name){ this.name = name this.update = function(){ console.log(name + ' update...') } } } let subject = new Subject() //建立主题 let observer1 = new Observer('xiaowang') //建立观察者1 subject.addObserver(observer1) //主题添加观察者1 let observer2 = new Observer('xiaozhang') //建立观察者2 subject.addObserver(observer2) //主题添加观察者2 subject.notify() //主题通知观察者 /**** 输出 *****/ xiaowang update... xiaozhang update...
ES5 和 ES6 写法效果同样,ES5 的写法更好理解,ES6 只是个语法糖
主题添加观察者的方法subject.addObserver(observer)
很繁琐,直接给观察者下方权限,给他们增长添加进忠粉组的权限
class Observer{ constructor() { this.update = function() { console.log(name + ' update...') } } subscribeTo(subject) { //只要用户订阅了主题就会自动添加进忠粉组 subject.addObserver(this) //这里的 this 是 Observer 的实例 } } let subject = new Subject() let observer = new Observer('lisi') observer.subscribeTo(subject) //观察者本身订阅忠粉分组 subject.notify() /****** 输出 *******/ lisi update...
MVVM 框架的内部基本原理就是上面这些,下一篇用代码写一遍完整的 MVVM 框架。
用原生 JS 实现 MVVM 框架MVVM 框架系列:
用原生 JS 实现 MVVM 框架2——单向绑定