资讯专栏INFORMATION COLUMN

用原生 JS 实现 MVVM 框架1——观察者模式和数据监控

TANKING / 1015人阅读

摘要:在前端页面中,把用纯对象表示,负责显示,两者做到了最大化的分离把和关联起来的就是。了解了思想后,自己用原生实现一个框架。注意数据描述符和存储描述符不能同时存在,否则会报错报错数据拦截使用来实现数据拦截,从而实现数据监听。

在前端页面中,把 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

writableconfigurable的区别是前者是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——单向绑定

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/97271.html

相关文章

  • 原生 JS 实现 MVVM 框架2——单向绑定

    摘要:上一篇写了实现框架的一些基本概念本篇用代码来实现一个完整的框架思考假设有如下代码,里面的会和试图中的一一映射,修改的值,会直接引起试图中对应数据的变化如何实现上述呢回想下这篇讲的观察者模式和数据监听主题是什么观察者是什么观察者何时订阅主题主 上一篇写了实现 MVVM 框架的一些基本概念 本篇用代码来实现一个完整的 MVVM 框架 思考 假设有如下代码,data里面的name会和试图中的...

    Zoom 评论0 收藏0
  • WeX5的正确打开方式(3)——绑定机制

    摘要:今天整理一下的绑定机制。声明式绑定使用简单易读的语法方便地将模型数据与元素绑定在一起。同理,也可以通过妹子妹子妹子这样的方式重置可观察对象数组,也能立即刷新。 今天整理一下WeX5的绑定机制。原生的问题 假设我们做一个订单系统,需要显示商品单价,然后可以根据输入数量计算出总价并显示出来。使用原生代码也很容易实现,效果:         showImg(https://segmentf...

    dantezhao 评论0 收藏0
  • MVC MVP MVVM

    摘要:,的事件回调函数中调用的操作方法。以为例调用关系模式实际就是将中的改名为,调用过程基本一致,最大的改良是间的双向绑定。和间,有一个对象,可以操作修改,使用。 参考:MVC,MVP 和 MVVM 的图示 - 阮一峰http://www.ruanyifeng.com/blo...Web开发的MVVM模式http://www.cnblogs.com/dxy198...界面之下:还原真实的MV...

    wushuiyong 评论0 收藏0
  • MVC MVP MVVM

    摘要:,的事件回调函数中调用的操作方法。以为例调用关系模式实际就是将中的改名为,调用过程基本一致,最大的改良是间的双向绑定。和间,有一个对象,可以操作修改,使用。 参考:MVC,MVP 和 MVVM 的图示 - 阮一峰http://www.ruanyifeng.com/blo...Web开发的MVVM模式http://www.cnblogs.com/dxy198...界面之下:还原真实的MV...

    Tangpj 评论0 收藏0
  • JS中的双向数据绑定及Object.defineProperty方法

    摘要:,而且每种框架双向数据绑定的实现方式都不太一致,比如内部使用的是脏检查,而内部实现方式的本质是设置属性访问器。在中也有类似的概念,不过不叫魔术方法,而是叫做访问器。 缘起前几天在看一些流行的迷你mvvm框架(比如avalon.js、 vue.js 这种较轻的框架,而非Angularjs、Emberjs这种较重的框架)的实现。现代流行的mvvm框架一般都会将数据双向绑定(two-ways...

    szysky 评论0 收藏0

发表评论

0条评论

TANKING

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<