若是你对于MVVM的造成不是特别清晰,则能够先阅读如下部分。javascript
本文能够帮助你了解什么?html
咱们先来花点时间想一想,若是你是一个前端框架(Vue、React或者Angular)的开发者,你是有多么频繁的听到“MVVM”这个词,但你真正明白它的含义吗?前端
起初计算机科学家(如今的咱们是小菜鸡)在设计GUI(图形用户界面)应用程序的时候,代码是杂乱无章的,一般难以管理和维护。GUI的设计结构通常包括视图(View)、模型(Model)、逻辑(Application Logic、Business Logic以及Sync Logic),例如:vue
能够发如今GUI中视图和模型是自然能够进行分层的,杂乱无章的部分主要是逻辑。因而咱们的程序员们不断的绞尽脑汁在想办法优化GUI设计的逻辑,而后就出现了MVC、MVP以及MVVM等设计模式。java
在B/S架构的应用开发中,MV*设计模式概述并封装了应用程序及其环境中须要关注的地方,尽管JavaScript已经变成一门同构语言,可是在浏览器和服务器之间这些关注点可能不同:node
早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,而且开始用它编写GUI。而在Smalltalk-80版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC的架构模式,极大地下降了GUI的管理难度。git
如图所示,MVC把GUI分红View(视图)、Model(模型)、Controller(控制 器)(可热插拔,主要进行Model和View之间的协做,包括路由、输入预处理等业务逻辑)三个模块:程序员
Model的更新经过观察者模式,能够实现多视图共享同一个Model。github
传统的MVC设计对于Web前端开发而言是一种十分有利的模式,由于View是持续性的,而且View能够对应不一样的Model。Backbone.js就是一种稍微变种的MVC模式实现(和经典MVC较大的区别在于View能够直接操做Model,所以这个模式不能同构)。这里总结一下MVC设计模式可能带来的好处以及不够完美的地方:算法
优势:
缺点:
####服务端MVC
经典MVC只用于解决GUI问题,可是随着B/S架构的不断发展,Web服务端也衍生出了MVC设计模式。
JSP Model1是早期的Java动态Web应用技术,它的结构以下所示:
在Model1中,JSP同时包含了Controller和View,而JavaBean包含了Controller和Model,模块的职责相对混乱。在JSP Model1的基础上,Govind Seshadri借鉴了MVC设计模式提出了JSP Model2模式(具体可查看文章Understanding JavaServer Pages Model 2 architecture),它的结构以下所示:
在JSP Model2中,Controller、View和Model分工明确,Model的数据变动,一般经过JavaBean修改View而后进行前端实时渲染,这样从Web前端发起请求到数据回显路线很是明确。不过这里专门询问了相应的后端开发人员,也可能经过JavaBean到Controller(Controller主要识别当前数据对应的JSP)再到JSP,所以在服务端MVC中,也可能产生这样的流程View -> Controller -> Model -> Controller -> View。
在JSP Model2模式中,没有作到先后端分离,前端的开发大大受到了限制。
对于Web前端开发而言,最直观的感觉就是在Node服务中衍生Model2模式(例如结合Express以及EJS模板引擎等)。
在服务端的MVC模式设计中采用了HTTP协议通讯(HTTP是单工无状态协议),所以View在不一样的请求中都不保持状态(状态的保持须要额外经过Cookie存储),而且经典MVC中Model经过观察者模式告知View的环节被破坏(例如难以实现服务端推送)。固然在经典MVC中,Controller须要监听View并对输入作出反应,逻辑会变得很繁重,而在Model2中, Controller只关注路由处理等,而Model则更多的处理业务逻辑。
在上个世纪90年代,IBM旗下的子公司Taligent在用C/C++开发一个叫CommonPoint的图形界面应用系统的时候提出了MVP的概念。
如上图所示,MVP是MVC的模式的一种改良,打破了View对于Model的依赖,其他的依赖关系和MVC保持不变。
MVP模式可能产生的优缺点以下:
如上图所示:MVVM模式是在MVP模式的基础上进行了改良,将Presenter改良成ViewModel(抽象视图):
能够发现,MVVM在MVP的基础上带来了大量的好处,例如:
固然也带来了一些额外的问题:
对前端开发而言MVVM是很是好的一种设计模式。在浏览器中,路由层能够将控制权交由适当的ViewModel,后者又能够更新并响应持续的View,而且经过一些小修改MVVM模式能够很好的运行在服务器端,其中的缘由就在于Model与View已经彻底没有了依赖关系(经过View与Model的去耦合,能够容许短暂View与持续View的并存),这容许View经由给定的ViewModel进行渲染。
目前流行的框架Vue、React以及Angular都是MVVM设计模式的一种实现,而且均可以实现服务端渲染。须要注意目前的Web前端开发和传统Model2须要模板引擎渲染的方式不一样,经过Node启动服务进行页面渲染,而且经过代理的方式转发请求后端数据,彻底能够从后端的苦海中脱离,这样一来也能够大大的解放Web前端的生产力。
观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变动自动通知给这一系列观察者对象。当subject目标对象须要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。
如上图所示:一个或多个观察者对目标对象的状态感兴趣时,能够将本身依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每一个观察者的更新方法。若是观察者对目标对象的状态不感兴趣,也能够将本身从中分离。
发布/订阅模式使用一个事件通道,这个通道介于订阅者和发布者之间,该设计模式容许代码定义应用程序的特定事件,这些事件能够传递自定义参数,自定义参数包含订阅者须要的信息,采用事件通道能够避免发布者和订阅者之间产生依赖关系。
学生时期很长一段时间内用过Redis的发布/订阅机制,具体可查看zigbee-door/zigbee-tcp,可是惭愧的是没有好好阅读过这一块的源码。
观察者模式:容许观察者实例对象(订阅者)执行适当的事件处理程序来注册和接收目标实例对象(发布者)发出的通知(即在观察者实例对象上注册update
方法),使订阅者和发布者之间产生了依赖关系,且没有事件通道。不存在封装约束的单一对象,目标对象和观察者对象必须合做才能维持约束。 观察者对象向订阅它们的对象发布其感兴趣的事件。通讯只能是单向的。
发布/订阅模式:单一目标一般有不少观察者,有时一个目标的观察者是另外一个观察者的目标。通讯能够实现双向。该模式存在不稳定性,发布者没法感知订阅者的状态。
这里简单的描述一下Vue的运行机制(须要注意分析的是 Runtime + Compiler 的 Vue.js)。
init
过程会初始化生命周期,初始化事件中心,初始化渲染、执行beforeCreate
周期函数、初始化 data
、props
、computed
、watcher
、执行created
周期函数等。$mount
方法对Vue实例进行挂载(挂载的核心过程包括模板编译、渲染以及更新三个过程)。render
方法而是定义了template
,那么须要经历编译阶段。须要先将template
字符串编译成 render function
,template
字符串编译步骤以下 :
parse
正则解析template
字符串造成AST(抽象语法树,是源代码的抽象语法结构的树状表现形式)optimize
标记静态节点跳过diff算法(diff算法是逐层进行比对,只有同层级的节点进行比对,所以时间的复杂度只有O(n)。若是对于时间复杂度不是很清晰的,能够查看我写的文章ziyi2/algorithms-javascript/渐进记号)generate
将AST转化成render function
字符串render function
后,调用$mount
的mountComponent
方法,先执行beforeMount
钩子函数,而后核心是实例化一个渲染Watcher
,在它的回调函数(初始化的时候执行,以及组件实例中监测到数据发生变化时执行)中调用updateComponent
方法(此方法调用render
方法生成虚拟Node,最终调用update
方法更新DOM)。render
方法将render function
渲染成虚拟的Node(真正的 DOM 元素是很是庞大的,由于浏览器的标准就把 DOM 设计的很是复杂。若是频繁的去作 DOM 更新,会产生必定的性能问题,而 Virtual DOM 就是用一个原生的 JavaScript 对象去描述一个 DOM 节点,因此它比建立一个 DOM 的代价要小不少,并且修改属性也很轻松,还能够作到跨平台兼容),render
方法的第一个参数是createElement
(或者说是h
函数),这个在官方文档也有说明。update
方法,update
方法又会调用pacth
方法把虚拟DOM转换成真正的DOM节点。须要注意在图中忽略了新建真实DOM的状况(若是没有旧的虚拟Node,那么能够直接经过createElm
建立真实DOM节点),这里重点分析在已有虚拟Node的状况下,会经过sameVnode
判断当前须要更新的Node节点是否和旧的Node节点相同(例如咱们设置的key
属性发生了变化,那么节点显然不一样),若是节点不一样那么将旧节点采用新节点替换便可,若是相同且存在子节点,须要调用patchVNode
方法执行diff算法更新DOM,从而提高DOM操做的性能。须要注意在初始化阶段,没有详细描述数据的响应式过程,这个在响应式流程里作说明。
init
的时候会利用Object.defineProperty
方法(不兼容IE8)监听Vue实例的响应式数据的变化从而实现数据劫持能力(利用了JavaScript对象的访问器属性get
和set
,在将来的Vue3中会使用ES6的Proxy
来优化响应式原理)。在初始化流程中的编译阶段,当render function
被渲染的时候,会读取Vue实例中和视图相关的响应式数据,此时会触发getter
函数进行依赖收集(将观察者Watcher
对象存放到当前闭包的订阅者Dep
的subs
中),此时的数据劫持功能和观察者模式就实现了一个MVVM模式中的Binder,以后就是正常的渲染和更新流程。setter
函数,setter
会通知初始化依赖收集中的Dep
中的和视图相应的Watcher
,告知须要从新渲染视图,Wather
就会再次经过update
方法来更新视图。能够发现只要视图中添加监听事件,自动变动对应的数据变化时,就能够实现数据和视图的双向绑定了。
了解了MV*设计模式、观察者模式以及Vue运行机制以后,可能对于整个MVVM模式有了一个感性的认知,所以能够来手动实现一下,这里实现过程包括以下几个步骤:
MVVM示例的使用以下所示,包括browser.js
(View视图的更新)、mediator.js
(中介者)、binder.js
(MVVM的数据绑定引擎)、view.js
(视图)、hijack.js
(数据劫持)以及mvvm.js
(MVVM实例)。本示例相关的代码可查看github的ziyi2/mvvm:
<div id="app">
<input type="text" b-value="input.message" b-on-input="handlerInput">
<div>{{ input.message }}</div>
<div b-text="text"></div>
<div>{{ text }}</div>
<div b-html="htmlMessage"></div>
</div>
<script src="./browser.js"></script>
<script src="./mediator.js"></script>
<script src="./binder.js"></script>
<script src="./view.js"></script>
<script src="./hijack.js"></script>
<script src="./mvvm.js"></script>
<script>
let vm = new Mvvm({
el: '#app',
data: {
input: {
message: 'Hello Input!'
},
text: 'ziyi2',
htmlMessage: `<button>提交</button>`
},
methods: {
handlerInput(e) {
this.text = e.target.value
}
}
})
</script>
复制代码
这里简单的描述一下MVVM实现的运行机制。
options
参数proxyData
将MVVM实例对象的data
数据代理到MVVM实例对象上Hijack
类实现数据劫持功能(对MVVM实例跟视图对应的响应式数据进行监听,这里和Vue运行机制不一样,干掉了getter
依赖搜集功能)b-value
、b-on-input
、b-html
等,实际上是Vue编译的超级简化版),update
方法对View解析绑定指令后的文档碎片进行更新视图处理Browser
采用了外观模式对浏览器进行了简单的兼容性处理setter
方法最简单的中介者模式只须要实现发布、订阅和取消订阅的功能。发布和订阅之间经过事件通道(channels)进行信息传递,能够避免观察者模式中产生依赖的状况。中介者模式的代码以下:
class Mediator {
constructor() {
this.channels = {}
this.uid = 0
}
/** * @Desc: 订阅频道 * @Parm: {String} channel 频道 * {Function} cb 回调函数 */
sub(channel, cb) {
let { channels } = this
if(!channels[channel]) channels[channel] = []
this.uid ++
channels[channel].push({
context: this,
uid: this.uid,
cb
})
console.info('[mediator][sub] -> this.channels: ', this.channels)
return this.uid
}
/** * @Desc: 发布频道 * @Parm: {String} channel 频道 * {Any} data 数据 */
pub(channel, data) {
console.info('[mediator][pub] -> chanel: ', channel)
let ch = this.channels[channel]
if(!ch) return false
let len = ch.length
// 后订阅先触发
while(len --) {
ch[len].cb.call(ch[len].context, data)
}
return this
}
/** * @Desc: 取消订阅 * @Parm: {String} uid 订阅标识 */
cancel(uid) {
let { channels } = this
for(let channel of Object.keys(channels)) {
let ch = channels[channel]
if(ch.length === 1 && ch[0].uid === uid) {
delete channels[channel]
console.info('[mediator][cancel][delete] -> chanel: ', channel)
console.info('[mediator][cancel] -> chanels: ', channels)
return
}
for(let i=0,len=ch.length; i<len; i++) {
if(ch[i].uid === uid) {
ch.splice(i,1)
console.info('[mediator][cancel][splice] -> chanel: ', channel)
console.info('[mediator][cancel] -> chanels: ', channels)
return
}
}
}
}
}
复制代码
在每个MVVM实例中,都须要实例化一个中介者实例对象,中介者实例对象的使用方法以下:
let mediator = new Mediator()
// 订阅channel1
let channel1First = mediator.sub('channel1', (data) => {
console.info('[mediator][channel1First][callback] -> data', data)
})
// 再次订阅channel1
let channel1Second = mediator.sub('channel1', (data) => {
console.info('[mediator][channel1Second][callback] -> data', data)
})
// 订阅channel2
let channel2 = mediator.sub('channel2', (data) => {
console.info('[mediator][channel2][callback] -> data', data)
})
// 发布(广播)channel1,此时订阅channel1的两个回调函数会连续执行
mediator.pub('channel1', { name: 'ziyi1' })
// 发布(广播)channel2,此时订阅channel2的回调函数执行
mediator.pub('channel2', { name: 'ziyi2' })
// 取消channel1标识为channel1Second的订阅
mediator.cancel(channel1Second)
// 此时只会执行channel1中标识为channel1First的回调函数
mediator.pub('channel1', { name: 'ziyi1' })
复制代码
对象的属性可分为数据属性(特性包括[[Value]]
、[[Writable]]
、[[Enumerable]]
、[[Configurable]]
)和存储器/访问器属性(特性包括[[ Get ]]
、[[ Set ]]
、[[Enumerable]]
、[[Configurable]]
),对象的属性只能是数据属性或访问器属性的其中一种,这些属性的含义:
[[Configurable]]
: 表示可否经过 delete
删除属性从而从新定义属性,可否修改属性的特性,或者可否把属性修改成访问器属性。[[Enumerable]]
: 对象属性的可枚举性。[[Value]]
: 属性的值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为 undefined
。[[Writable]]
: 表示可否修改属性的值。[[ Get ]]
: 在读取属性时调用的函数。默认值为 undefined
。[[ Set ]]
: 在写入属性时调用的函数。默认值为 undefined
。数据劫持就是使用了
[[ Get ]]
和[[ Set ]]
的特性,在访问对象的属性和写入对象的属性时可以自动触发属性特性的调用函数,从而作到监听数据变化的目的。
对象的属性能够经过ES5的设置特性方法Object.defineProperty(data, key, descriptor)
改变属性的特性,其中descriptor
传入的就是以上所描述的特性集合。
let hijack = (data) => {
if(typeof data !== 'object') return
for(let key of Object.keys(data)) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.info('[hijack][get] -> val: ', val)
// 和执行 return data[key] 有什么区别 ?
return val
},
set(newVal) {
if(newVal === val) return
console.info('[hijack][set] -> newVal: ', newVal)
val = newVal
// 若是新值是object, 则对其属性劫持
hijack(newVal)
}
})
}
}
let person = { name: 'ziyi2', age: 1 }
hijack(person)
// [hijack][get] -> val: ziyi2
person.name
// [hijack][get] -> val: 1
person.age
// [hijack][set] -> newVal: ziyi
person.name = 'ziyi'
// 属性类型变化劫持
// [hijack][get] -> val: { familyName:"ziyi2", givenName:"xiankang" }
person.name = { familyName: 'zhu', givenName: 'xiankang' }
// [hijack][get] -> val: ziyi2
person.name.familyName = 'ziyi2'
// 数据属性
let job = { type: 'javascript' }
console.info(Object.getOwnPropertyDescriptor(job, "type"))
// 访问器属性
console.info(Object.getOwnPropertyDescriptor(person, "name"))
复制代码
注意Vue3.0将不产用Object.defineProperty
方式进行数据监听,缘由在于
hack
,因此若是要使数组响应化,须要注意使用Vue官方推荐的一些数组方法)在Vue3.0中将产用Proxy
解决以上痛点问题,固然会产生浏览器兼容性问题(例如万恶的IE,具体可查看Can I use proxy)。
须要注意是的在
hijack
中只进行了一层属性的遍历,若是要作到对象深层次属性的监听,须要继续对data[key]
进行hijack
操做,从而能够达到属性的深层次遍历监听,具体可查看mvvm/mvvm/hijack.js,
如上图所示,数据双向绑定主要包括数据的变化引发视图的变化(Model -> 监听数据变化 -> View)、视图的变化又改变数据(View -> 用户输入监听事件 -> Model),从而实现数据和视图之间的强联系。
在实现了数据监听的基础上,加上用户输入事件以及视图更新,就能够简单实现数据的双向绑定(其实就是一个最简单的Binder,只是这里的代码耦合严重):
<input id="input" type="text"> <div id="div"></div> 复制代码
// 监听数据变化
function hijack(data) {
if(typeof data !== 'object') return
for(let key of Object.keys(data)) {
let val = data[key]
Object.defineProperty(data, key, {
enumerable: true,
configurable: false,
get() {
console.log('[hijack][get] -> val: ', val)
// 和执行 return data[key] 有什么区别 ?
return val
},
set(newVal) {
if(newVal === val) return
console.log('[hijack][set] -> newVal: ', newVal)
val = newVal
// 更新全部和data.input数据相关联的视图
input.value = newVal
div.innerHTML = newVal
// 若是新值是object, 则对其属性劫持
hijack(newVal)
}
})
}
}
let input = document.getElementById('input')
let div = document.getElementById('div')
// model
let data = { input: '' }
// 数据劫持
hijack(data)
// model -> view
data.input = '11111112221'
// view -> model
input.oninput = function(e) {
// model -> view
data.input = e.target.value
}
复制代码
在MVVM的实现演示中,能够发现使用了b-value
、b-text
、b-on-input
、b-html
等绑定属性(这些属性在该MVVM示例中自行定义的,并非html标签原生的属性,相似于vue的v-html
、v-model
、v-text
指令等),这些指令只是方便用户进行Model和View的同步绑定操做而建立的,须要MVVM实例对象去识别这些指令并从新渲染出最终须要的DOM元素,例如
<div id="app">
<input type="text" b-value="message"> </div>
复制代码
最终须要转化成真实的DOM
<div id="app">
<input type="text" value='Hello World' /> </div>
复制代码
那么实现以上指令解析的步骤主要以下:
#app
元素#app
下的全部子元素)#app
元素HTML代码以下:
<div id="app"> <input type="text" b-value="message" /> <input type="text" b-value="message" /> <input type="text" b-value="message" /> </div> <script src="./browser.js"></script> <script src="./binder.js"></script> <script src="./view.js"></script> 复制代码
首先来看示例的使用
// 模型
let model = {
message: 'Hello World',
getData(key) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
val = val[keys[i]]
if(!val && i !== len - 1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`) }
}
return val
}
}
// 抽象视图(实现功能将b-value中对应的model.message转换成最终的value="Hello World")
new View('#app', model)
复制代码
在view.js
中实现了#app
下的元素转化成文档碎片以及对全部子元素进行属性遍历操做(用于binder.js
的绑定属性解析)
class View {
constructor(el, model) {
this.model = model
// 获取须要处理的node节点
this.el = el.nodeType === Node.ELEMENT_NODE ? el : document.querySelector(el)
if(!this.el) return
// 将已有的el元素的全部子元素转成文档碎片
this.fragment = this.node2Fragment(this.el)
// 解析和处理绑定指令并修改文档碎片
this.parseFragment(this.fragment)
// 将文档碎片从新添加到dom树
this.el.appendChild(this.fragment)
}
/** * @Desc: 将node节点转为文档碎片 * @Parm: {Object} node Node节点 */
node2Fragment(node) {
let fragment = document.createDocumentFragment(),
child;
while(child = node.firstChild) {
// 给文档碎片添加节点时,该节点会自动从dom中删除
fragment.appendChild(child)
}
return fragment
}
/** * @Desc: 解析文档碎片(在parseFragment中遍历的属性,须要在binder.parse中处理绑定指令的解析处理) * @Parm: {Object} fragment 文档碎片 */
parseFragment(fragment) {
// 类数组转化成数组进行遍历
for(let node of [].slice.call(fragment.childNodes)) {
if(node.nodeType !== Node.ELEMENT_NODE) continue
// 绑定视图指令解析
for(let attr of [].slice.call(node.attributes)) {
binder.parse(node, attr, this.model)
// 移除绑定属性
node.removeAttribute(attr.name)
}
// 遍历node节点树
if(node.childNodes && node.childNodes.length) this.parseFragment(node)
}
}
}
复制代码
接下来查看binder.js
如何处理绑定指令,这里以b-value
的解析为示例
(function(window, browser){
window.binder = {
/** * @Desc: 判断是不是绑定属性 * @Parm: {String} attr Node节点的属性 */
is(attr) {
return attr.includes('b-')
},
/** * @Desc: 解析绑定指令 * @Parm: {Object} attr html属性对象 * {Object} node Node节点 * {Object} model 数据 */
parse(node, attr, model) {
// 判断是不是绑定指令,不是则不对该属性进行处理
if(!this.is(attr.name)) return
// 获取model数据
this.model = model
// b-value = 'message', 所以attr.value = 'message'
let bindValue = attr.value,
// 'b-value'.substring(2) = value
bindType = attr.name.substring(2)
// 绑定视图指令b-value处理
// 这里采用了命令模式
this[bindType](node, bindValue.trim())
},
/** * @Desc: 值绑定处理(b-value) * @Parm: {Object} node Node节点 * {String} key model的属性 */
value(node, key) {
this.update(node, key)
},
/** * @Desc: 值绑定更新(b-value) * @Parm: {Object} node Node节点 * {String} key model的属性 */
update(node, key) {
// this.model.getData是用于获取model对象的属性值
// 例如 model = { a : { b : 111 } }
// <input type="text" b-value="a.b" />
// this.model.getData('a.b') = 111
// 从而能够将input元素更新为<input type="text" value="111" />
browser.val(node, this.model.getData(key))
}
}
})(window, browser)
复制代码
在browser.js
中使用外观模式对浏览器原生的事件以及DOM操做进行了再封装,从而能够作到浏览器的兼容处理等,这里只对b-value
须要的DOM操做进行了封装处理,方便阅读
let browser = {
/** * @Desc: Node节点的value处理 * @Parm: {Object} node Node节点 * {String} val 节点的值 */
val(node, val) {
// 将b-value转化成value,须要注意的是解析完后在view.js中会将b-value属性移除
node.value = val || ''
console.info(`[browser][val] -> node: `, node)
console.info(`[browser][val] -> val: `, val)
}
}
复制代码
至此MVVM示例中简化的Model -> ViewModel (未实现数据监听功能)-> View路走通,能够查看视图绑定指令的解析的demo。
ViewModel(内部绑定器Binder)的做用不只仅是实现了Model到View的自动同步(Sync Logic)逻辑(以上视图绑定指令的解析的实现只是实现了一个视图的绑定指令初始化,一旦Model变化,视图要更新的功能并无实现),还实现了View到Model的自动同步逻辑,从而最终实现了数据的双向绑定。
所以只要在视图绑定指令的解析的基础上增长Model的数据监听功能(数据变化更新视图)和View视图的input
事件监听功能(监听视图从而更新相应的Model数据,注意Model的变化又会由于数据监遵从而更新和Model相关的视图)就能够实现View和Model的双向绑定。同时须要注意的是,数据变化更新视图的过程须要使用发布/订阅模式,若是对流程不清晰,能够继续回看MVVM的结构设计。
在简易视图指令的编译过程实现的基础上进行修改,首先是HTML代码
<div id="app"> <input type="text" id="input1" b-value="message"> <input type="text" id="input2" b-value="message"> <input type="text" id="input3" b-value="message"> </div> <!-- 新增中介者 --> <script src="./mediator.js"></script> <!-- 新增数据劫持 --> <script src="./hijack.js"></script> <script src="./view.js"></script> <script src="./browser.js"></script> <script src="./binder.js"></script> 复制代码
mediator.js
再也不叙述,具体回看中介者模式的实现,view.js
和browser.js
也再也不叙述,具体回看简易视图指令的编译过程实现。
示例的使用:
// 模型
let model = {
message: 'Hello World',
setData(key, newVal) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
if(i < len - 1) {
val = val[keys[i]]
} else {
val[keys[i]] = newVal
}
}
// console.log('[mvvm][setData] -> val: ', val)
},
getData(key) {
let val = this
let keys = key.split('.')
for(let i=0, len=keys.length; i<len; i++) {
val = val[keys[i]]
if(!val && i !== len - 1) { throw new Error(`Cannot read property ${keys[i]} of undefined'`) }
}
return val
}
}
// 发布/订阅对象
let mediator = new Mediator()
// 数据劫持(监听model的变化,并发布model数据变化消息)
hijack(model, mediator)
// 抽象视图(实现绑定指令的解析,并订阅model数据的变化从而更新视图)
new View('#app', model, mediator)
// model -> view (会触发数据劫持的set函数,从而发布model变化,在binder中订阅model数据变化后会更新视图)
model.message = 'Hello Ziyi233333222'
复制代码
首先看下数据劫持,在** 数据劫持的实现的基础上,增长了中介者对象的发布数据变化功能(在抽象视图的Binder**中会订阅这个数据变化)
var hijack = (function() {
class Hijack {
/** * @Desc: 数据劫持构造函数 * @Parm: {Object} model 数据 * {Object} mediator 发布订阅对象 */
constructor(model, mediator) {
this.model = model
this.mediator = mediator
}
/** * @Desc: model数据劫持 * @Parm: * */
hijackData() {
let { model, mediator } = this
for(let key of Object.keys(model)) {
let val = model[key]
Object.defineProperty(model, key, {
enumerable: true,
configurable: false,
get() {
return val
},
set(newVal) {
if(newVal === val) return
val = newVal
// 发布数据劫持的数据变化信息
console.log('[mediator][pub] -> key: ', key)
// 重点注意这里的通道,在最后的MVVM示例中和这里的实现不同
mediator.pub(key)
}
})
}
}
}
return (model, mediator) => {
if(!model || typeof model !== 'object') return
new Hijack(model, mediator).hijackData()
}
})()
复制代码
接着重点来看binder.js
中的实现
(function(window, browser){
window.binder = {
/** * @Desc: 判断是不是绑定属性 * @Parm: {String} attr Node节点的属性 */
is(attr) {
return attr.includes('b-')
},
/** * @Desc: 解析绑定指令 * @Parm: {Object} attr html属性对象 * {Object} node Node节点 * {Object} model 数据 * {Object} mediator 中介者 */
parse(node, attr, model, mediator) {
if(!this.is(attr.name)) return
this.model = model
this.mediator = mediator
let bindValue = attr.value,
bindType = attr.name.substring(2)
// 绑定视图指令处理
this[bindType](node, bindValue.trim())
},
/** * @Desc: 值绑定处理(b-value) * @Parm: {Object} node Node节点 * {String} key model的属性 */
value(node, key) {
this.update(node, key)
// View -> ViewModel -> Model
// 监听用户的输入事件
browser.event.add(node, 'input', (e) => {
// 更新model
let newVal = browser.event.target(e).value
// 设置对应的model数据(由于进行了hijack(model))
// 由于进行了hijack(model),对model进行了变化监听,所以会触发hijack中的set,从而触发set中的mediator.pub
this.model.setData(key, newVal)
})
// 一旦model变化,数据劫持会mediator.pub变化的数据
// 订阅数据变化更新视图(闭包)
this.mediator.sub(key, () => {
console.log('[mediator][sub] -> key: ', key)
console.log('[mediator][sub] -> node: ', node)
this.update(node, key)
})
},
/** * @Desc: 值绑定更新(b-value) * @Parm: {Object} node Node节点 * {String} key model的属性 */
update(node, key) {
browser.val(node, this.model.getData(key))
}
}
})(window, browser)
复制代码
最终实现了具备viewModel的MVVM简单实例,具体查看ViewModel的实现的demo。
在ViewModel的实现的基础上:
b-text
、b-html
、b-on-*
(事件监听)指令的解析hijack.js
实现了对Model数据的深层次监听hijack.js
中的发布和订阅的channel
采用HTML属性中绑定的指令对应的值进行处理(例如b-value="a.b.c.d"
,那么channel
就是'a.b.c.d'
,这里是将Vue的观察者模式改为中介者模式后的一种尝试,只是一种实现方式,固然采用观察者模式关联性更强,而采用中介者模式会更解耦)。browser.js
中新增了事件监听的兼容处理、b-html
和b-text
等指令的DOM操做api等因为篇幅太长了,这里就不过多作说明了,感兴趣的童鞋能够直接查看ziyi2/mvvm,须要注意该示例中还存在必定的缺陷,例如Model的属性是一个对象,且该对象被重写时,发布和订阅维护的channels
中未将旧的属性监听的channel
移除处理。