MVVM 设计模式,是由 MVC、MVP 等设计模式进化而来。
通过以上的MVVM模式图,我们可以看出最核心的就是ViewModel,它主要的作用:对View中DOM元素的监听和对Model中的数据进行绑定,当View变化会引起Modal中数据的改动,Model中数据的改动会触发View视图重新渲染,从而达到数据双向绑定的效果,该效果也是Vue最为核心的特性。
下面介绍如何实现一个完整MVVM框架,首先介绍一下一个核心api,Object.defineProperty的使用。
Object.defineProperty()
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
语法:
Object.defineProperty(obj, prop, descriptor)
返回值:
被传递给函数的对象
使用方法:
var obj = {};
obj.name = 'hunger';
obj['age'] = 3;
Object.defineProperty(obj, 'intro', {
value: 'hello world',
});
console.log(obj); // {name: 'hunger', age: 3, intro: 'hello world'}
以上三种方法都可以用来定义 / 修改一个属性,Object.defineProperty 的方法貌似看起来有些小题大做。没关系,且往下看它更复杂的用法。
var obj = {};
Object.defineProperty(obj, 'intro', {
configurable: false,
value: 'hello world'
});
obj.intro = 'jirengu';
console.log(obj.intro); // "helloworld"
delete obj.intro; // false, 删除失败
console.log(obj.intro); // "helloworld"
var obj = {};
Object.defineProperty(obj, 'name', {
configurable: true,
value: 'hunger'
});
console.log(obj.name); // hunger
delete obj.name; // true , 成功删除
console.log(obj.name); // undefind
通过上面的例子可以看出,属性描述对象中 configurable 的值设置为 false 后(如果没设置,默认就是 false) ,以后就不能再次通过 Object.defineProperty修改属性,也无法删除该属性。
var obj = { name: 'hello' };
Object.defineProperty(obj, 'age', {
value: 3,
enumerable: false
});
for (var key in obj) {
console.log(key); // 只输出 'name', 不输出'age'
}
设置 enumerable 属性为 false 后,遍历对象的时候会忽略当前属性(如果未设置,默认就是 false不可遍历) 。
var obj = { name: 'hello' }
Object.defineProperty(obj, 'age', {
value: 3,
writable: false
});
obj.age = 4;
console.log(obj.age); // 3, writable为 false 时,修改对象的当前属性值无效
value 和 writable 叫数据描述符,具有以下可选键值:
var obj = {};
var age;
Object.defineProperty(obj, 'age', {
get: function () {
console.log('get age...')
return age;
},
set: function (val) {
console.log('set age...')
age = val;
}
});
obj.age = 100; // 'set age...'
console.log(obj.age); // 'get age...', 100
get 和 set 叫存取描述符,有以下可选键值:
var obj = {};
var age;
Object.defineProperty(obj, 'age', {
value: 100,
get: function () {
console.log('get age...')
return age;
},
set: function (val) {
console.log('set age...')
age = val;
}
});
因为有 value,又有 get,上述代码会报错。
浏览器报:Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, #
总结:
属性 | 默认值 | 说明 |
---|---|---|
configurable | false | 描述属性是否被删除,默认为false |
enumerable | false | 描述属性是否可以被for..in...或Objects.keys枚举,默认为false |
writable | false | 描述属性是否可以修改,默认为false |
get | undefind | 当访问属性时触发该方法,默认为undefined |
set | undefind | 当属性被修改时触发该方法,默认为undefined |
value | undefind | 属性值,默认为undefined |
现在我们利用 Object.defineProperty方法动态监听数据。
var data = {
name: 'hunger',
friends: [1, 2, 3]
};
observe(data);
console.log(data.name);
data.name = 'valley';
data.friends[0] = 4;
function observe(data) {
if (!data || typeof data !== 'object') {
return;
}
for (var key in data) {
let val = data[key];
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${val}`);
return val;
},
set: function(newVal) {
console.log(`changes happen: ${val} => ${newVal}`);
val = newVal;
}
});
if (typeof val === 'object'){
observe(val);
}
}
}
上面的 observe 函数实现了一个数据监听,当监听某个对象后,我们可以在用户读取或者设置属性值的时候做个拦截,做我们想做的事
发布 - 订阅模式(Publish-Subscribe Pattern, pub-sub)又叫观察者模式(Observer Pattern),它定义了一种一对多的关系,让多个订阅者对象同时监听某一个发布者,或者叫主题对象,这个主题对象的状态发生变化时就会通知所有订阅自己的订阅者对象,使得它们能够自动更新自己。
一个典型的观察者模式应用场景是用户在一个网站订阅主题
以下是代码实现:
Subject是构造函数,new Subject() 创建一个主题对象,该对象内部维护订阅当前主题的观察者数组。主题对象上有一些方法,如添加观察者(addObserver)、删除观察者(removeObserver)、通知观察者更新(notify)。 当notify 时实际上调用全部观察者 observer 自身的 update 方法。
Observer 是构造函数,new Observer() 创建一个观察者对象,该对象有一个 update 方法。
// ES5写法
function Subject() {
this.observers = []
}
Subject.prototype.addObserver = function(observer) {
this.observers.push(observer)
}
Subject.prototype.removeObserver = function(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
Subject.prototype.notify = function() {
this.observers.forEach(function(observer){
observer.update()
})
}
function Observer(name) {
this.name = name
this.update = function(){
console.log(name + ' update...')
}
}
// 创建主题
var subject = new Subject();
//创建观察者1
var observer1 = new Observer('hunger');
//主题添加观察者1
subject.addObserver(observer1);
//创建观察者2
var observer2 = new Observer('valley');
//主题添加观察者2
subject.addObserver(observer2);
//主题通知所有的观察者更新
subject.notify();
// ES6写法
class Subject {
constructor() {
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(observer=> {
observer.update()
})
}
}
class Observer{
constructor() {
this.update = function() {}
}
}
let subject = new Subject();
let observer1 = new Observer();
observer1.update = function() {
console.log('observer1 update')
}
subject.addObserver(observer1);
let observer2 = new Observer('valley');
observer2.update = function() {
console.log('observer2 update')
}
subject.addObserver(observer2);
subject.notify();
上面的代码中,主题被观察者订阅的写法是 subject.addObserver(observer), 不是很直观,给观察者增加订阅方法。
class Observer{
constructor() {
this.update = function() {}
}
subscribeTo(subject) {
subject.addObserver(this)
}
}
let subject = new Subject()
let observer = new Observer()
observer.update = function() {
console.log('observer update');
}
observer.subscribeTo(subject); //观察者订阅主题
subject.notify();
思考:假设有如下代码,data 里的name
会和视图中的{{name}}
一一映射,修改 data 里的值,会直接引起视图中对应数据的变化。
{{name}}
如何实现上述 mvvm 呢?
一起回想之前讲的观察者模式和数据监听:
上面的例子中,主题应该是data
的 name
属性,观察者是视图里的{{name}}
,当一开始执行mvvm初始化(根据 el 解析模板发现{{name}}
)的时候订阅主题,当data.name
发生改变的时候,通知观察者更新内容。 我们可以在一开始监控 data.name (Object.defineProperty(data, 'name', {...})),当用户修改 data.name 的时候调用主题的 subject.notify。
{{name}} 's age is {{age}}
function observe(data) {
if(!data || typeof data !== 'object') return
for(var key in data) {
let val = data[key]
let subject = new Subject()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${key}: ${val}`)
if(currentObserver){
console.log('has currentObserver')
currentObserver.subscribeTo(subject)
}
return val
},
set: function(newVal) {
val = newVal
console.log('start notify...')
subject.notify()
}
})
if(typeof val === 'object'){
observe(val)
}
}
}
let id = 0
let currentObserver = null
class Subject {
constructor() {
this.id = id++
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(observer=> {
observer.update()
})
}
}
class Observer{
constructor(vm, key, cb) {
this.subjects = {}
this.vm = vm
this.key = key
this.cb = cb
this.value = this.getValue()
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal) {
this.value = value
this.cb.bind(this.vm)(value, oldVal)
}
}
subscribeTo(subject) {
if(!this.subjects[subject.id]){
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
this.subjects[subject.id] = subject
}
}
getValue(){
currentObserver = this
let value = this.vm.$data[this.key]
currentObserver = null
return value
}
}
class mvvm {
constructor(opts) {
this.init(opts)
observe(this.$data)
this.compile()
}
init(opts){
this.$el = document.querySelector(opts.el)
this.$data = opts.data
this.observers = []
}
compile(){
this.traverse(this.$el)
}
traverse(node){
if(node.nodeType === 1){
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
}else if(node.nodeType === 3){ //文本
this.renderText(node)
}
}
renderText(node){
let reg = /{{(.+?)}}/g
let match
while(match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.$data[key])
new Observer(this, key, function(val, oldVal){
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
}
let vm = new mvvm({
el: '#app',
data: {
name: 'zhangfei',
age: 3
}
})
setInterval(function(){
vm.$data.age++
}, 1000)
{{name}} 's age is {{age}}
function observe(data) {
if(!data || typeof data !== 'object') return
for(var key in data) {
let val = data[key]
let subject = new Subject()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${key}: ${val}`)
if(currentObserver){
console.log('has currentObserver')
currentObserver.subscribeTo(subject)
}
return val
},
set: function(newVal) {
val = newVal
console.log('start notify...')
subject.notify()
}
})
if(typeof val === 'object'){
observe(val)
}
}
}
let id = 0
let currentObserver = null
class Subject {
constructor() {
this.id = id++
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(observer=> {
observer.update()
})
}
}
class Observer{
constructor(vm, key, cb) {
this.subjects = {}
this.vm = vm
this.key = key
this.cb = cb
this.value = this.getValue()
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal) {
this.value = value
this.cb.bind(this.vm)(value, oldVal)
}
}
subscribeTo(subject) {
if(!this.subjects[subject.id]){
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
this.subjects[subject.id] = subject
}
}
getValue(){
currentObserver = this
let value = this.vm.$data[this.key]
currentObserver = null
return value
}
}
class Compile {
constructor(vm){
this.vm = vm
this.node = vm.$el
this.compile()
}
compile(){
this.traverse(this.node)
}
traverse(node){
if(node.nodeType === 1){
this.compileNode(node) //解析节点上的v-bind 属性
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
}else if(node.nodeType === 3){ //处理文本
this.compileText(node)
}
}
compileText(node){
let reg = /{{(.+?)}}/g
let match
console.log(node)
while(match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.vm.$data[key])
new Observer(this.vm, key, function(val, oldVal){
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
//处理指令
compileNode(node){
let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
attrs.forEach(attr=>{
//attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
if(this.isDirective(attr.name)){
let key = attr.value //attr.value === 'name'
node.value = this.vm.$data[key]
new Observer(this.vm, key, function(newVal){
node.value = newVal
})
node.oninput = (e)=>{
this.vm.$data[key] = e.target.value //因为是箭头函数,所以这里的 this 是 compile 对象
}
}
})
}
//判断属性名是否是指令
isDirective(attrName){
return attrName === 'v-model'
}
}
class mvvm {
constructor(opts) {
this.init(opts)
observe(this.$data)
new Compile(this)
}
init(opts){
this.$el = document.querySelector(opts.el)
this.$data = opts.data
}
}
let vm = new mvvm({
el: '#app',
data: {
name: 'zhangfei',
age: 3
}
})
setInterval(function(){
vm.$data.age++
}, 1000)
{{name}} 's age is {{age}}
function observe(data) {
if(!data || typeof data !== 'object') return
for(var key in data) {
let val = data[key]
let subject = new Subject()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
console.log(`get ${key}: ${val}`)
if(currentObserver){
console.log('has currentObserver')
currentObserver.subscribeTo(subject)
}
return val
},
set: function(newVal) {
val = newVal
console.log('start notify...')
subject.notify()
}
})
if(typeof val === 'object'){
observe(val)
}
}
}
let id = 0
let currentObserver = null
class Subject {
constructor() {
this.id = id++
this.observers = []
}
addObserver(observer) {
this.observers.push(observer)
}
removeObserver(observer) {
var index = this.observers.indexOf(observer)
if(index > -1){
this.observers.splice(index, 1)
}
}
notify() {
this.observers.forEach(observer=> {
observer.update()
})
}
}
class Observer{
constructor(vm, key, cb) {
this.subjects = {}
this.vm = vm
this.key = key
this.cb = cb
this.value = this.getValue()
}
update(){
let oldVal = this.value
let value = this.getValue()
if(value !== oldVal) {
this.value = value
this.cb.bind(this.vm)(value, oldVal)
}
}
subscribeTo(subject) {
if(!this.subjects[subject.id]){
console.log('subscribeTo.. ', subject)
subject.addObserver(this)
this.subjects[subject.id] = subject
}
}
getValue(){
currentObserver = this
let value = this.vm[this.key] //等同于 this.vm.$data[this.key]
currentObserver = null
return value
}
}
class Compile {
constructor(vm){
this.vm = vm
this.node = vm.$el
this.compile()
}
compile(){
this.traverse(this.node)
}
traverse(node){
if(node.nodeType === 1){
this.compileNode(node) //解析节点上的v-bind 属性
node.childNodes.forEach(childNode=>{
this.traverse(childNode)
})
}else if(node.nodeType === 3){ //处理文本
this.compileText(node)
}
}
compileText(node){
let reg = /{{(.+?)}}/g
let match
console.log(node)
while(match = reg.exec(node.nodeValue)){
let raw = match[0]
let key = match[1].trim()
node.nodeValue = node.nodeValue.replace(raw, this.vm[key])
new Observer(this.vm, key, function(val, oldVal){
node.nodeValue = node.nodeValue.replace(oldVal, val)
})
}
}
//处理指令
compileNode(node){
let attrs = [...node.attributes] //类数组对象转换成数组,也可用其他方法
attrs.forEach(attr=>{
//attr 是个对象,attr.name 是属性的名字如 v-model, attr.value 是对应的值,如 name
if(this.isModelDirective(attr.name)){
this.bindModel(node, attr)
}else if(this.isEventDirective(attr.name)){
this.bindEventHander(node, attr)
}
})
}
bindModel(node, attr){
let key = attr.value //attr.value === 'name'
node.value = this.vm[key]
new Observer(this.vm, key, function(newVal){
node.value = newVal
})
node.oninput = (e)=>{
this.vm[key] = e.target.value //因为是箭头函数,所以这里的 this 是 compile 对象
}
}
bindEventHander(node, attr){ //attr.name === 'v-on:click', attr.value === 'sayHi'
let eventType = attr.name.substr(5) // click
let methodName = attr.value
node.addEventListener(eventType, this.vm.$methods[methodName])
}
//判断属性名是否是指令
isModelDirective(attrName){
return attrName === 'v-model'
}
isEventDirective(attrName){
return attrName.indexOf('v-on') === 0
}
}
class mvvm {
constructor(opts) {
this.init(opts)
observe(this.$data)
new Compile(this)
}
init(opts){
this.$el = document.querySelector(opts.el)
this.$data = opts.data || {}
this.$methods = opts.methods || {}
//把$data 中的数据直接代理到当前 vm 对象
for(let key in this.$data) {
Object.defineProperty(this, key, {
enumerable: true,
configurable: true,
get: ()=> { //这里用了箭头函数,所有里面的 this 就指代外面的 this 也就是 vm
return this.$data[key]
},
set: newVal=> {
this.$data[key] = newVal
}
})
}
//让 this.$methods 里面的函数中的 this,都指向当前的 this,也就是 vm
for(let key in this.$methods) {
this.$methods[key] = this.$methods[key].bind(this)
}
}
}
let vm = new mvvm({
el: '#app',
data: {
name: 'jirengu',
age: 3
},
methods: {
sayHi(){
alert(`hi ${this.name}` )
}
}
})
let clock = setInterval(function(){
vm.age++ //等同于 vm.$data.age, 见 mvvm init 方法内的数据劫持
if(vm.age === 10) clearInterval(clock)
}, 1000)
mdn:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
饥人谷:https://xiedaimala.com/
慕课网:https://www.imooc.com/