You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
编译成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函数),这个在官方文档也有说明。
<divid="app"><inputtype="text"b-value="input.message"b-on-input="handlerInput"><div>{{input.message }}</div><divb-text="text"></div><div>{{ text }}</div><divb-html="htmlMessage"></div></div><scriptsrc="./browser.js"></script><scriptsrc="./mediator.js"></script><scriptsrc="./binder.js"></script><scriptsrc="./view.js"></script><scriptsrc="./hijack.js"></script><scriptsrc="./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>
对象的属性可分为数据属性(特性包括[[Value]]、[[Writable]]、[[Enumerable]]、[[Configurable]])和存储器/访问器属性(特性包括[[ Get ]]、[[ Set ]]、[[Enumerable]]、[[Configurable]]),对象的属性只能是数据属性或访问器属性的其中一种,这些属性的含义:
基于Vue的简易MVVM实现
本文可以帮助你了解什么?
MV*设计模式的演变历史
我们先来花点时间想想,如果你是一个前端三贱客(Vue、React或者Angular)的开发者,你是有多么频繁的听到“MVVM”这个词,但你真正明白它的含义吗?
Web前端的演变历史
从单纯的HTML静态页面到MVVM模式的成熟应用,自我能感受的Web前端模式粗略的发展如下所示(可能顺序不是很严谨):
MV*设计模式的起源
起初计算机科学家(现在的我们是小菜鸡)在设计GUI(图形用户界面)应用程序的时候,代码是杂乱无章的,通常难以管理和维护。GUI的设计结构一般包括视图(View)、模型(Model)、逻辑(Application Logic、Business Logic以及Sync Logic),例如:
可以发现在GUI中视图和模型是天然可以进行分层的,杂乱无章的部分主要是逻辑。于是我们的程序员们不断的绞尽脑汁在想办法优化GUI设计的逻辑,然后就出现了MVC、MVP以及MVVM等设计模式。
MV*设计模式在B/S架构中的思考
在B/S架构的应用开发中,MV*设计模式概述并封装了应用程序及其环境中需要关注的地方,尽管JavaScript已经变成一门同构语言,但是在浏览器和服务器之间这些关注点可能不一样:
MVC(Model-View-Controller)
早在上个世纪70年代,美国的施乐公司(Xerox)的工程师研发了Smalltalk编程语言,并且开始用它编写GUI。而在Smalltalk-80版本的时候,一位叫Trygve Reenskaug的工程师设计了MVC的架构模式,极大地降低了GUI的管理难度。
如图所示,MVC把GUI分成View(视图)、Model(模型)、Controller(控制
器)(可热插拔,主要进行Model和View之间的协作,包括路由、输入预处理等业务逻辑)三个模块:
传统的MVC设计对于Web前端开发而言是一种十分有利的模式,因为View是持续性的,并且View可以对应不同的Model。Backbone.js就是一种稍微变种的MVC模式实现(和经典MVC较大的区别在于View可以直接操作Model,因此这个模式不能同构)。这里总结一下MVC设计模式可能带来的好处以及不够完美的地方:
优点:
缺点:
####服务端MVC
经典MVC只用于解决GUI问题,但是随着B/S架构的不断发展,Web服务端也衍生出了MVC设计模式。
JSP Model1和JSP Model2的演变过程
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。
Model2的衍生
对于Web前端开发而言,最直观的感受就是在Node服务中衍生Model2模式(例如结合Express以及EJS模板引擎等)。
服务端MVC和经典MVC的区别
在服务端的MVC模式设计中采用了HTTP协议通信(HTTP是单工无状态协议),因此View在不同的请求中都不保持状态(状态的保持需要额外通过Cookie存储),并且经典MVC中Model通过观察者模式告知View的环节被破坏(例如难以实现服务端推送)。当然在经典MVC中,Controller需要监听View并对输入做出反应,逻辑会变得很繁重,而在Model2中, Controller只关注路由处理等,而Model则更多的处理业务逻辑。
MVP(Model-View-Presenter)
在上个世纪90年代,IBM旗下的子公司Taligent在用C/C++开发一个叫CommonPoint的图形界面应用系统的时候提出了MVP的概念。
如上图所示,MVP是MVC的模式的一种改良,打破了View对于Model的依赖,其余的依赖关系和MVC保持不变。
MVP模式可能产生的优缺点如下:
MVVM(Model-View-ViewModel)
如上图所示:MVVM模式是在MVP模式的基础上进行了改良,将Presenter改良成ViewModel(抽象视图):
可以发现,MVVM在MVP的基础上带来了大量的好处,例如:
当然也带来了一些额外的问题:
对前端开发而言MVVM是非常好的一种设计模式。在浏览器中,路由层可以将控制权交由适当的ViewModel,后者又可以更新并响应持续的View,并且通过一些小修改MVVM模式可以很好的运行在服务器端,其中的原因就在于Model与View已经完全没有了依赖关系(通过View与Model的去耦合,可以允许短暂View与持续View的并存),这允许View经由给定的ViewModel进行渲染。
观察者模式和发布/订阅模式
观察者模式
观察者模式是使用一个subject目标对象维持一系列依赖于它的observer观察者对象,将有关状态的任何变更自动通知给这一系列观察者对象。当subject目标对象需要告诉观察者发生了什么事情时,它会向观察者对象们广播一个通知。
如上图所示:一个或多个观察者对目标对象的状态感兴趣时,可以将自己依附在目标对象上以便注册感兴趣的目标对象的状态变化,目标对象的状态发生改变就会发送一个通知消息,调用每个观察者的更新方法。如果观察者对目标对象的状态不感兴趣,也可以将自己从中分离。
发布/订阅模式
发布/订阅模式使用一个事件通道,这个通道介于订阅者和发布者之间,该设计模式允许代码定义应用程序的特定事件,这些事件可以传递自定义参数,自定义参数包含订阅者需要的信息,采用事件通道可以避免发布者和订阅者之间产生依赖关系。
两者的区别
观察者模式:允许观察者实例对象(订阅者)执行适当的事件处理程序来注册和接收目标实例对象(发布者)发出的通知(即在观察者实例对象上注册
update
方法),使订阅者和发布者之间产生了依赖关系,且没有事件通道。不存在封装约束的单一对象,目标对象和观察者对象必须合作才能维持约束。 观察者对象向订阅它们的对象发布其感兴趣的事件。通信只能是单向的。发布/订阅模式:单一目标通常有很多观察者,有时一个目标的观察者是另一个观察者的目标。通信可以实现双向。该模式存在不稳定性,发布者无法感知订阅者的状态。
Vue的运行机制简述
这里简单的描述一下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
方法来更新视图。基于Vue机制的简易MVVM实现
了解了MV*设计模式、观察者模式以及Vue运行机制之后,可能对于整个MVVM模式有了一个感性的认知,因此可以来手动实现一下,这里实现过程包括如下几个步骤:
MVVM的实现演示
MVVM示例的使用如下所示,包括
browser.js
(View视图的更新)、mediator.js
(中介者)、binder.js
(MVVM的数据绑定引擎)、view.js
(视图)、hijack.js
(数据劫持)以及mvvm.js
(MVVM实例)。本示例相关的代码可查看github的ziyi2/mvvm:MVVM的流程设计
这里简单的描述一下MVVM实现的运行机制。
初始化流程
options
参数proxyData
将MVVM实例对象的data
数据代理到MVVM实例对象上Hijack
类实现数据劫持功能(对MVVM实例跟视图对应的响应式数据进行监听,这里和Vue运行机制不同,干掉了getter
依赖搜集功能)b-value
、b-on-input
、b-html
等,其实是Vue编译的超级简化版),update
方法对View解析绑定指令后的文档碎片进行更新视图处理Browser
采用了外观模式对浏览器进行了简单的兼容性处理响应式流程
setter
方法中介者模式的实现
最简单的中介者模式只需要实现发布、订阅和取消订阅的功能。发布和订阅之间通过事件通道(channels)进行信息传递,可以避免观察者模式中产生依赖的情况。中介者模式的代码如下:
在每一个MVVM实例中,都需要实例化一个中介者实例对象,中介者实例对象的使用方法如下:
数据劫持的实现
对象的属性
对象的属性可分为数据属性(特性包括
[[Value]]
、[[Writable]]
、[[Enumerable]]
、[[Configurable]]
)和存储器/访问器属性(特性包括[[ Get ]]
、[[ Set ]]
、[[Enumerable]]
、[[Configurable]]
),对象的属性只能是数据属性或访问器属性的其中一种,这些属性的含义:[[Configurable]]
: 表示能否通过delete
删除属性从而重新定义属性,能否修改属性的特性,或者能否把属性修改为访问器属性。[[Enumerable]]
: 对象属性的可枚举性。[[Value]]
: 属性的值,读取属性值的时候,从这个位置读;写入属性值的时候,把新值保存在这个位置。这个特性的默认值为undefined
。[[Writable]]
: 表示能否修改属性的值。[[ Get ]]
: 在读取属性时调用的函数。默认值为undefined
。[[ Set ]]
: 在写入属性时调用的函数。默认值为undefined
。对象的属性可以通过ES5的设置特性方法
Object.defineProperty(data, key, descriptor)
改变属性的特性,其中descriptor
传入的就是以上所描述的特性集合。数据劫持
注意Vue3.0将不产用
Object.defineProperty
方式进行数据监听,原因在于hack
,所以如果要使数组响应化,需要注意使用Vue官方推荐的一些数组方法)在Vue3.0中将产用
Proxy
解决以上痛点问题,当然会产生浏览器兼容性问题(例如万恶的IE,具体可查看Can I use proxy)。数据双向绑定的实现
如上图所示,数据双向绑定主要包括数据的变化引起视图的变化(Model -> 监听数据变化 -> View)、视图的变化又改变数据(View -> 用户输入监听事件 -> Model),从而实现数据和视图之间的强联系。
在实现了数据监听的基础上,加上用户输入事件以及视图更新,就可以简单实现数据的双向绑定(其实就是一个最简单的Binder,只是这里的代码耦合严重):
简易视图指令的编译过程实现
在MVVM的实现演示中,可以发现使用了
b-value
、b-text
、b-on-input
、b-html
等绑定属性(这些属性在该MVVM示例中自行定义的,并不是html标签原生的属性,类似于vue的v-html
、v-model
、v-text
指令等),这些指令只是方便用户进行Model和View的同步绑定操作而创建的,需要MVVM实例对象去识别这些指令并重新渲染出最终需要的DOM元素,例如最终需要转化成真实的DOM
那么实现以上指令解析的步骤主要如下:
#app
元素#app
下的所有子元素)#app
元素HTML代码如下:
首先来看示例的使用
在
view.js
中实现了#app
下的元素转化成文档碎片以及对所有子元素进行属性遍历操作(用于binder.js
的绑定属性解析)接下来查看
binder.js
如何处理绑定指令,这里以b-value
的解析为示例在
browser.js
中使用外观模式对浏览器原生的事件以及DOM操作进行了再封装,从而可以做到浏览器的兼容处理等,这里只对b-value
需要的DOM操作进行了封装处理,方便阅读ViewModel的实现
ViewModel(内部绑定器Binder)的作用不仅仅是实现了Model到View的自动同步(Sync Logic)逻辑(以上视图绑定指令的解析的实现只是实现了一个视图的绑定指令初始化,一旦Model变化,视图要更新的功能并没有实现),还实现了View到Model的自动同步逻辑,从而最终实现了数据的双向绑定。
因此只要在视图绑定指令的解析的基础上增加Model的数据监听功能(数据变化更新视图)和View视图的
input
事件监听功能(监听视图从而更新相应的Model数据,注意Model的变化又会因为数据监听从而更新和Model相关的视图)就可以实现View和Model的双向绑定。同时需要注意的是,数据变化更新视图的过程需要使用发布/订阅模式,如果对流程不清晰,可以继续回看MVVM的结构设计。在简易视图指令的编译过程实现的基础上进行修改,首先是HTML代码
示例的使用:
首先看下数据劫持,在** 数据劫持的实现的基础上,增加了中介者对象的发布数据变化功能(在抽象视图的Binder**中会订阅这个数据变化)
接着重点来看
binder.js
中的实现MVVM的实现
在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
移除处理。设计模式
在以上MVVM示例的实现中,我也是抱着学习的心态用到了以下设计模式,如果对这些设计模式不了解,则可以前往查看示例代码。
参考资源
The text was updated successfully, but these errors were encountered: