js设计模式-观察者模式来模拟vue的双向数据绑定

JavaScript015

js设计模式-观察者模式来模拟vue的双向数据绑定,第1张

vue的双向数据绑定大家应该很熟悉了,当一方的值发生改变时,另一方绑定的值也会随之变化,用起来是挺嗨的。

但是在原生中我们怎么使用这种机制呢?

最近有个需求是通过对接websocket获取后台服务器实时变化的值,推送给web端使用。

基于这个需求,我使用到了js中的设计模式-观察者模式。

那么,让我们来一起了解一下吧。

先来看看具体机制:

这里对象定义了四个属性,分别绑定四个函数。

1、订阅:订阅方通过传递回调函数,观察者模式把这个回调函数push到自身的订阅功能里,以此来得知谁订阅了,然后判断是否要推送。

2、退订:找到对应的回调函数,然后在自身的订阅功能里把当前函数删除掉

3、发布:循环所有的订阅方,当发布方进行发送的时候,把对应的数据推送给订阅方

4、发布订阅:定义一个对象,使其具备订阅并且发布的功能

流程是这样,说起来头头是道的,问题是怎么使用?

举个栗子:

我想定义一个对象,使其具备发布订阅功能,发布方数值改变的时候,订阅方得到平方值得变化

这里通过input框的change事件,模拟了数据的实时变更,然后把当前值进行发布,这边一发布,订阅方就能通过回调函数得到实时变化的值,然后得到值进行相应的操作。

效果:

这样就能简单实现数据变更推送功能了。

注:文件中引入的observer的js是最上面提到的观察者模式的那一套流程,tools的js大家可以不必在意,是我自己原生封装的$函数,用来获取dom元素的。

具体需求,大家还需要变通,稍作修改。

好了,以上就是js的观察者模式实现的双向数据绑定。

如有问题,请指出,接受批评。

vue.js 采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() 来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

首先我们为每个vue属性用Object.defineProperty()实现数据劫持,在监听数据的过程中,为每个属性分配一个订阅者集合的管理数组dep;然后在编译的时候在该属性的数组dep中添加订阅者 watcher,v-model会添加一个订阅者,{{}}也会,v-bind也会,只要用到该属性的指令理论上都会,接着为input会添加监听事件,修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。

实现步骤:修改输入框内容 =>在事件回调函数中修改属性值 =>触发属性的 set 方法=>发出通知 dep.notify() =>触发订阅者的 update 方法 =>更新视图。

流程图

在实例化一个Vue对象的时候,会传进去一个data对象,之后分成两个进程,一个进程是对挂载目标元素模板里的v-model和{{ }};两个指令进行编译。另一个进程是对传进去的data对象里面的数据进行监听。

上图中,observe是利用Object.defineProperty()对传入的data对象进行数据监听,在数据改变的时候触发该属性的set方法,更新该属性的值,并发布消息,我(该属性)的值变了。

compile是编译器,找到vue的指令v-model所在的元素,将data中该属性的值赋给元素的value,并给这个元素添加二级监听器,在元素的值改变的时候,将新值赋给data里面同名属性,这个时候就完成了单向数据绑定,视图 >>模型。

那么最终的由模型到视图的更新,依赖于dep和watcher,dep会收集订阅者,就是绑定了data里面属性的元素,在数据更新的时候,会触发该属性的set方法,在set里触发该属性的消息发布通知函数。而Watcher根据收到的数据变化通知,更新相应的数据。

dep这个东东给大家解释一下,就是data里的每个属性都有一个dep对象,dep对象里可以有很多订阅者(watcher),但是只有一个添加订阅者的方法和一个发布变化通知的方法,就是模板上可以有多处元素绑定data里的同一个属性值,所以dep是依赖于data里面的属性的。

而Watcher是每个{{ }}有一个,初次编译的时候,会在new的时候自动更新一下模板的数据,等到下次数据改变的时候,由dep通知数据更新,直接调用watcher的update方法,更新模板的绑定数据。

observer 模块共分为这几个部分:

示意图如下:

Observer的构造函数

value是需要被观察的数据对象,在构造函数中,会给value增加 ob 属性,作为数据已经被Observer观察的标志。如果value是数组,就使用observeArray遍历value,对value中每一个元素调用observe分别进行观察。如果value是对象,则使用walk遍历value上每个key,对每个key调用defineReactive来获得该key的set/get控制权。

Dep是Observer与Watcher之间的纽带,也可以认为Dep是服务于Observer的订阅系统。Watcher订阅某个Observer的Dep,当Observer观察的数据发生变化时,通过Dep通知各个已经订阅的Watcher。

Watcher是用来订阅数据的变化的并执行相应操作(例如更新视图)的。Watcher的构造器函数定义如下:

参数中,vm表示组件实例,expOrFn表示要订阅的数据字段(字符串表示,例如a.b.c)或是一个要执行的函数,cb表示watcher运行后的回调函数,options是选项对象,包含deep、user、lazy等配置。

Object.defineProperty(obj, prop, descriptor) ,这个语法内有三个参数,分别为 obj (要定义属性的对象) prop (要定义或修改的属性的名称或 Symbol ) descriptor (要定义或修改的属性描述符=>具体的改变方法)

简单地说,就是用这个方法来定义一个值。当调用时我们使用了它里面的get方法,当我们给这个属性赋值时,又用到了它里面的set方法;

主要解释第三个参数{

value: 设置属性的值

writable: 值是否可以重写。true | false

enumerable: 目标属性是否可以被枚举。true | false(就是能不能被遍历出来)

configurable: 目标属性是否可以被删除或是否可以再次修改特性 true | false

set: 目标属性设置值的方法

get:目标属性获取值的方法

}

set是一个函数,接收一个新值,会在值被重写或修改的时候触发这个函数

get是一个函数,返回一个值,会在属性被调用的时候触发。

Object.defineProperty()详解

Object.defineProperty()官方文档

已经了解到vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,那么在设置或者获取的时候我们就可以在get或者set方法里假如其他的触发函数,达到监听数据变动的目的。

我们知道通过Object.defineProperty()可以实现数据劫持,它的属性在赋值的时候触发set方法,

当然要是这么粗暴,肯定不行,性能会出很多的问题。

observer用来实现对每个vue中的data中定义的属性循环用Object.defineProperty()实现数据劫持,以便利用其中的setter和getter,然后通知订阅者,订阅者会触发它的update方法,对视图进行更新。

为什么要订阅者 :在vue中v-model,v-name,{{}}等都可以对数据进行显示,也就是说假如一个属性都通过这三个指令了,那么每当这个属性改变的时候,相应的这个三个指令的html视图也必须改变,于是vue中就是每当有这样的可能用到双向绑定的指令,就在一个Dep中增加一个订阅者,其订阅者只是更新自己的指令对应的数据,也就是v-model='name'和{{name}}有两个对应的订阅者,各自管理自己的地方。每当属性的set方法触发,就循环更新Dep中的订阅者。

订阅发布模式(又称观察者模式)定义了一种一对多的关系,让多个观察者同时监听某一个主题对象,这个主题对象的状态发生改变时就会通知所有观察者对象。

发布者发出通知 =>主题对象收到通知并推送给订阅者 =>订阅者执行相应操作

举个例子:

2.实现compile: compile的目的就是解析各种指令称真正的html。

这样一来就实现了vue的数据双向绑定。

参考链接:

理解VUE双向数据绑定原理和实现---赵佳乐

Vue的双向数据绑定原理

vue双向绑定原理分析

Vue原理解析之observer模块

深入响应式原理

在javascript中并没有原生的创建或者实现接口的方式,或者判定一个类型是否实现了某个接口,我们只能利用js的灵活性的特点,模拟接口。

在javascript中实现接口有三种方式:注释描述、属性验证、鸭子模型。

note:因为我看的是英文书,翻译水平有限,不知道有些词汇如何翻译,大家只能领会精神了。

1. 注释描述 (Describing Interfaces with Comments)

例子:

复制代码 代码如下:

/*

interface Composite {

function add(child)

function remove(child)

function getChild(index)

}

interface FormItem {

function save()

}

*/

var CompositeForm = function(id, method, action) { // implements Composite, FormItem

...

}

//Implement the Composite interface.

CompositeForm.prototype.add = function(child) {

...

}

CompositeForm.prototype.remove = function(child) {

...

}

CompositeForm.prototype.getChild = function(index) {

...

}

// Implement the FormItem interface.

CompositeForm.prototype.save = function() {

...

}

模拟其他面向对象语言,使用interface 和 implements关键字,但是需要将他们注释起来,这样就不会有语法错误。

这样做的目的,只是为了告诉其他编程人员,这些类需要实现什么方法,需要在编程的时候加以注意。但是没有提供一种验证方式,这些类是否正确实现了这些接口中的方法,这种方式就是一种文档化的作法。

2. 属性验证(Emulating Interfaces with Attribute Checking)

例子:

复制代码 代码如下:

/* interface

Composite {

function add(child)

function remove(child)

function getChild(index)

}

interface FormItem {

function save()

}

*/

var CompositeForm = function(id, method, action) {

this.implementsInterfaces = ['Composite', 'FormItem']

...

}

...

function addForm(formInstance) {

if(!implements(formInstance, 'Composite', 'FormItem')) {

throw new Error("Object does not implement a required interface.")

}

...

}

// The implements function, which checks to see if an object declares that it

// implements the required interfaces.

function implements(object) {

for(var i = 1i <arguments.lengthi++) {

// Looping through all arguments

// after the first one.

var interfaceName = arguments[i]

var interfaceFound = false

for(var j = 0j <object.implementsInterfaces.lengthj++) {

if(object.implementsInterfaces[j] == interfaceName) {

interfaceFound = true

break

}

}

if(!interfaceFound) {

return false

// An interface was not found.

 }

}

return true

// All interfaces were found.

}

这种方式比第一种方式有所改进,接口的定义仍然以注释的方式实现,但是添加了验证方法,判断一个类型是否实现了某个接口。

3.鸭子类型(Emulating Interfaces with Duck Typing)

复制代码 代码如下:

// Interfaces.

var Composite = new Interface('Composite', ['add', 'remove', 'getChild'])

var FormItem = new Interface('FormItem', ['save'])

// CompositeForm class

var CompositeForm = function(id, method, action) {

...

}

...

function addForm(formInstance) {

ensureImplements(formInstance, Composite, FormItem)

// This function will throw an error if a required method is not implemented.

...

}

// Constructor.

var Interface = function(name, methods) {

if(arguments.length != 2) {

throw new Error("Interface constructor called with "

 + arguments.length + "arguments, but expected exactly 2.")

}

this.name = name

this.methods = []

for(var i = 0, len = methods.lengthi <leni++) {

if(typeof methods[i] !== 'string') {

throw new Error("Interface constructor expects method names to be "

+ "passed in as a string.")

}

this.methods.push(methods[i])

}

}

// Static class method.

Interface.ensureImplements = function(object) {

if(arguments.length <2) {

throw new Error("Function Interface.ensureImplements called with "

+arguments.length + "arguments, but expected at least 2.")

}

for(var i = 1, len = arguments.lengthi <leni++) {

var interface = arguments[i]

if(interface.constructor !== Interface) {

throw new Error("Function Interface.ensureImplements expects arguments"

+ "two and above to be instances of Interface.")

}

for(var j = 0, methodsLen = interface.methods.lengthj <methodsLenj++) {

var method = interface.methods[j]

if(!object[method] || typeof object[method] !== 'function') {

throw new Error("Function Interface.ensureImplements: object "

+ "does not implement the " + interface.name + " interface. Method " + method + " was not found.")

}

}

}

}

何时使用接口?

一直使用严格的类型验证并不适合,因为大多数javascript程序员已经在没有接口和接口验证的情况下编程多年。当你用设计模式开始设计一个很复杂的系统的时候,使用接口更有益处。看起来使用接口好像限制了javascript的灵活性,但实际上他让你的代码变得更加的松耦合。他使你的代码变得更加灵活,你可以传送任何类型的变量,并且保证他有你想要的方法。有很多场景接口非常适合使用。

在一个大型系统里,很多程序员一起参与开发项目,接口就变得非常必要了。程序员经常要访问一个还没有实现的api,或者为其他程序员提供别人依赖的一个方法存根,在这种情况下,接口变得相当的有价值。他们可以文档化api,并作为编程的契约。当存根被实现的api替换的时候你能立即知道,如果在开发过程中api有所变动,他能被另一个实现该接口的方法无缝替换。

如何使用接口?

首先要解决的问题是,在你的代码中是否适合使用接口。如果是小项目,使用接口会增加代码的复杂度。所以你要确定使用接口的情况下,是否是益处大于弊端。如果要使用接口,下面有几条建议:

1.引用Interface 类到你的页面文件。interface的源文件你可以再如下站点找到: http://jsdesignpatterns.com/.

2.检查你的代码,确定哪些方法需要抽象到接口里面。

3.创建接口对象,没个接口对象里面包含一组相关的方法。

4.移除所有构造器验证,我们将使用第三种接口实现方式,也就是鸭子类型。

5.用Interface.ensureImplements替代构造器验证。

您可能感兴趣的文章:

小议javascript 设计模式 推荐

JavaScript 设计模式之组合模式解析

javascript 设计模式之单体模式 面向对象学习基础

JavaScript 设计模式 安全沙箱模式

JavaScript设计模式之观察者模式(发布者-订阅者模式)

JavaScript设计模式之原型模式(Object.create与prototype)介绍

JavaScript设计模式之工厂方法模式介绍

javascript设计模式之中介者模式Mediator

学习JavaScript设计模式之责任链模式