共计 5252 个字符,预计需要花费 14 分钟才能阅读完成。
在前端页面中,把 Model 用纯 JS 对象表示,View 负责显示,两者做到了最大化的分离
把 Model 和 View 关联起来的就是 ViewModel。ViewModel 负责把 Model 的数据同步到 View 中显示出来,还负责把 View 的修改同步回 Model。
MVVM 的设计思想:关注 Model 的变化,让 MVVM 框架去自动更新 DOM 的状态,从而把开发者从操作 DOM 的繁琐步骤中解脱出来。
了解了 MVVM 思想后,自己用原生 JS 实现一个 MVVM 框架。
实现 MVVM 框架前先来看几个基本用法:
Object.defineProperty
普通声明对象,定义和修改属性
let obj = {}
obj.name = ‘zhangsan’
obj.age = 20
用 ObjectdefineProperty 声明对象语法:
Object.defineProperty(obj,prop,descriptor)
obj:要处理的目标对象
prop:要定义或修改的属性的名称
descriptor:将被定义或修改的属性描述符
let obj = {}
Object.defineProperty(obj,’age’,{
value = 14,
})
咋一看有点画蛇添足,这不很鸡肋嘛
别急,往下看
描述符
descriptor 有两种形式:数据描述符和存储描述符,他们两个共有属性:
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],并将这个变量返回出去。
观察者模式
一个典型的观察者模式应用场景——微信公众号
不同的用户(我们把它叫做观察者:Observer)都可以订阅同一个公众号(我们把它叫做主体:Subject)
当订阅的公众号更新时(主体),用户都能收到通知(观察者)
用代码怎么实现呢?先看逻辑:
Subject 是构造函数,new Subject() 创建一个主题对象,它维护订阅该主题的一个观察者数组数组(举例来说:Subject 是腾讯推出的公众号,new Subject() 是一个某个机构的公众号——新世相,它要维护订阅这个公众号的用户群体)
主题上有一些方法,如添加观察者 addObserver、删除观察者 removeObserver、通知观察者更新 notify(举例来说:新世相将用户分为两组,一组是忠粉就是 addObserver,一组是黑名单就是:removeObserver,它在忠粉组可以添加用户,可以在黑名单里拉黑一些杠精,如果有福利发放,它就会统治忠粉里的用户:notify)
Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法(举例来说:Observer 是忠粉用户群体,new Observer() 是某个具体的用户——小王,他必须要打开流量才能收到新世相的福利推送:updata)
当调用 notify 时实际上调用全部观察者 observer 自身的 update 方法(举例来说:当新世相推送福利时,它会自动帮忠粉组的用户打开流量,这比较极端,只是用来举例)
ES5 写法:
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…
ES6 写法:
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——单向绑定