简介

avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特点是:

  • 使用 observe 模式,性能高。
  • 将原始对象用object.defineProperty重写,不需要用户像用knockout时那样显示定义各种属性。
  • 对低版本的IE使用了VBScript来兼容,一直兼容到IE6。

需要看基础介绍的话建议直接看司徒的博客。在网上搜了一圈,发现已经有了avalon很好的源码分析,这里也推荐一下:地址。 avalon在圈子里一直被诟病不够规范的问题,请各位不必再留言在我这里,看源码无非是取其精华去其糟粕。可以点评,但总是讨论用哪个框架好哪个不好就没什么意义了,若是自己把握不住,用什么都不好。

今天的分析以 avalon.mobile 1.2.5 为准,avalon.mobile 是专门为高级浏览器准备的,不兼容IE8以下。

入口

还是先看启动代码

avalon.ready(function() {
avalon.define("box", function(vm) {
vm.w = 100;
vm.h = 100;
vm.area = function(){
get : function(){ return this.w * this.h }
}
vm.logW =function(){ console.log(vm.w)} })
avalon.scan()
})

  

还是两件事:定义viewModel 和 执行扫描。 翻到define 定义:

avalon.define = function(id, factory) {
if (VMODELS[id]) {
log("warning: " + id + " 已经存在于avalon.vmodels中")
}
var scope = {
$watch: noop
}
factory(scope) //得到所有定义
var model = modelFactory(scope) //偷天换日,将scope换为model
stopRepeatAssign = true
factory(model)
stopRepeatAssign = false
model.$id = id
return VMODELS[id] = model
}

  

其实已经可以一眼看明白了。这里只提一点,为什么要执行两次factory?建议读者先自己想一下。我这里直接说出来了: 因为modelFactory中,如果属性是函数,就会被直接复制到新的model上,但函数内的vm却仍然指向的原来的定义函数的中的vm,因此发生错误。所以通过二次执行factory来修正引用错误。
那为什么不在modelFactory中直接就把通过Function.bind或其他方法来把引用给指定好呢?而且可以在通过scope获得定以后就直接把 scope 对象修改成viewModel就好了啊?
这里的代码写法其实是直接从avalon兼容IE的完整版中搬出来的,因为对老浏览器要创造VBScript对象,所以只能先传个scope进去获取定义,在根据定义去创造。并且老的浏览器也不支持bind等方法。 还是老规矩,我们先看看整体机制图:

双工引擎

接下来就是直接一探 modelFactory 内部了。翻到代码 324 行。

function modelFactory(scope, model) {
if (Array.isArray(scope)) {
var arr = scope.concat()//原数组的作为新生成的监控数组的$model而存在
scope.length = 0
var collection = Collection(scope)
collection.push.apply(collection, arr)
return collection
}
if (typeof scope.nodeType === "number") {
return scope
}
var vmodel = {} //要返回的对象
model = model || {} //放置$model上的属性
var accessingProperties = {} //监控属性
var normalProperties = {} //普通属性
var computedProperties = [] //计算属性
var watchProperties = arguments[2] || {} //强制要监听的属性
var skipArray = scope.$skipArray //要忽略监控的属性
for (var i = 0, name; name = skipProperties[i++]; ) {
delete scope[name]
normalProperties[name] = true
}
if (Array.isArray(skipArray)) {
for (var i = 0, name; name = skipArray[i++]; ) {
normalProperties[name] = true
}
}
for (var i in scope) {
loopModel(i, scope[i], model, normalProperties, accessingProperties, computedProperties, watchProperties)
}
vmodel = Object.defineProperties(vmodel, descriptorFactory(accessingProperties)) //生成一个空的ViewModel
for (var name in normalProperties) {
vmodel[name] = normalProperties[name]
}
watchProperties.vmodel = vmodel
vmodel.$model = model
vmodel.$events = {}
vmodel.$id = generateID()
vmodel.$accessors = accessingProperties
vmodel[subscribers] = []
for (var i in Observable) {
vmodel[i] = Observable[i]
}
Object.defineProperty(vmodel, "hasOwnProperty", {
value: function(name) {
return name in vmodel.$model
},
writable: false,
enumerable: false,
configurable: true
})
for (var i = 0, fn; fn = computedProperties[i++]; ) { //最后强逼计算属性 计算自己的值
Registry[expose] = fn
fn()
collectSubscribers(fn)
delete Registry[expose]
}
return vmodel
}

  

前面声明了一对变量作为容器,用来保存转换过的 控制属性(相当于ko中的observable) 和 计算属性(相当于ko中的computed) 等等。往下翻到最关键的352行,这个 loopModel 函数就是用来生成好各个属性的入口了。继续深入:

function loopModel(name, val, model, normalProperties, accessingProperties, computedProperties, watchProperties) {
model[name] = val
if (normalProperties[name] || (val && val.nodeType)) { //如果是元素节点或在全局的skipProperties里或在当前的$skipArray里
return normalProperties[name] = val
}
if (name[0] === "$" && !watchProperties[name]) { //如果是$开头,并且不在watchProperties里
return normalProperties[name] = val
}
var valueType = getType(val)
if (valueType === "function") { //如果是函数,也不用监控
return normalProperties[name] = val
}
var accessor, oldArgs
if (valueType === "object" && typeof val.get === "function" && Object.keys(val).length <= 2) {
var setter = val.set,
getter = val.get
accessor = function(newValue) { //创建计算属性,因变量,基本上由其他监控属性触发其改变
var vmodel = watchProperties.vmodel
var value = model[name],
preValue = value if (arguments.length) {
if (stopRepeatAssign) {
return
} if (typeof setter === "function") {
var backup = vmodel.$events[name]
vmodel.$events[name] = [] //清空回调,防止内部冒泡而触发多次$fire
setter.call(vmodel, newValue)
vmodel.$events[name] = backup
}
if (!isEqual(oldArgs, newValue)) {
oldArgs = newValue
newValue = model[name] = getter.call(vmodel)//同步$model
withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循环绑定中的代理VM notifySubscribers(accessor) //通知顶层改变
safeFire(vmodel, name, newValue, preValue)//触发$watch回调
}
} else {
if (avalon.openComputedCollect) { // 收集视图刷新函数
collectSubscribers(accessor)
}
newValue = model[name] = getter.call(vmodel)
if (!isEqual(value, newValue)) {
oldArgs = void 0
safeFire(vmodel, name, newValue, preValue)
}
return newValue
}
}
computedProperties.push(accessor)
} else if (rchecktype.test(valueType)) {
accessor = function(newValue) { //子ViewModel或监控数组
var realAccessor = accessor.$vmodel, preValue = realAccessor.$model
if (arguments.length) {
if (stopRepeatAssign) {
return
} if (!isEqual(preValue, newValue)) { newValue = accessor.$vmodel = updateVModel(realAccessor, newValue, valueType)
var fn = rebindings[newValue.$id]
fn && fn()//更新视图
var parent = watchProperties.vmodel
withProxyCount && updateWithProxy(parent.$id, name, newValue)//同步循环绑定中的代理VM
model[name] = newValue.$model//同步$model
notifySubscribers(realAccessor) //通知顶层改变
safeFire(parent, name, model[name], preValue) //触发$watch回调
}
} else {
collectSubscribers(realAccessor) //收集视图函数
return realAccessor
}
}
accessor.$vmodel = val.$model ? val : modelFactory(val, val)
model[name] = accessor.$vmodel.$model
} else {
accessor = function(newValue) { //简单的数据类型
var preValue = model[name]
if (arguments.length) {
if (!isEqual(preValue, newValue)) {
model[name] = newValue //同步$model
var vmodel = watchProperties.vmodel
withProxyCount && updateWithProxy(vmodel.$id, name, newValue)//同步循环绑定中的代理VM
notifySubscribers(accessor) //通知顶层改变
safeFire(vmodel, name, newValue, preValue)//触发$watch回调
}
} else {
collectSubscribers(accessor) //收集视图函数
return preValue
}
}
model[name] = val
}
accessor[subscribers] = [] //订阅者数组
accessingProperties[name] = accessor
}

  

源码的注释其实已经写得非常清楚了,如果你看过我上一篇对knockout源码的解读,你会发现avalon这里面的机制和knockout几乎是一样的。函数无非就是根据定义函数中各个属性的类型来生成读写器(accessor),这个读写器会用在后面的 defineProperty 中。这里唯一值得提一下的就是那个 updateWithProxy 函数。只有一种情况需要用到它,就是当页面上使用了 ms-repeat 或者其他循环绑定来处理 数组或对象 时,会生为循环中的对象生成一个代理对象,这个代理对象记录除数据本身外和作用于相关的一些变量,和knockout的bindingContext有些像。 好了,到这里源码基本上没什么难度,我们来做一点有意思的事情。还记得之前我们提出的关于 执行两次 factory的 疑问吗?第二次执行主要是为了修正函数属性中的引用,我们看上面这代码中,但属性的类型是function时,就直接复制,如果我们对这个函数执行一下bind的方法呢,是不是就不用使用factory修正引用了?来试一下,先将 318 行的二次执行factory注释掉。再loopModel函数中 424 行改成

return normalProperties[name] = val.bind(model)

  

我们写个页面载入改过的avalon,然后跑一下这段测试:

var vma = avalon.define('a',function(vm){

    vm.a = "a"

    vm.b = "b"

    vm.c = {

        get : function(){return this.a+this.b}

    }

    vm.c2 = {

        get : function(){return vm.a+vm.b}

    }

    vm.d = function(){

        return this.a+this.b //注意这里用的是 this

    }

})

vma.a = "c"

console.log(vma.c == vma.a+vma.b)

console.log(vma.d() == vma.a+vma.b)

  

有没有验证,结果大家最好自己试验一下。 这里可以看到,如果只是针对现代浏览器,avalon的内核还是有很多可以重构的地方的。

viewModel的内部实现已经搞清,接下来就只剩看看如何处理和页面元素的绑定了。翻到 1214 行scan函数的定义,主要是执行了 scanTag 。再看,主要是执行了 scanAttr。再看,终于找到了和 knockout 看起来一样的 bindingHandlers 了,再往下翻翻就会发现和 knockout 是一样的绑定机制了。读者可以自己看,看不懂的地方翻翻我上一篇中ko的同样部分看看就知道了。

其他

最后还是讲讲对数组的处理。之前在ko中我们看到ko为对象专门准备了一个observableArray,里面重写pop等方法,以保证在处理函数时能只通知改动元素相关的绑定,而不用修改整个数组绑定的视图。在avalon中,我们看到在 loopModel 467行的 rchecktype.test(valueType) 这个语句。rchecktype 是个正则 /^(?:object|array)$/ ,也就是判断该属性是不是对象或数组。如果是,在 491 行 的

accessor.$vmodel = val.$model ? val : modelFactory(val, val)

又生成一个modelFactory,这时传入modelFactory的第一个参数就可能是数组了,再看modelFacotry 定义,当第一个函数为数组时,将其变成了一个Collection对象,而Collection也是重写了各种数组方法。果然,机制大家都差不多。不过司徒在博客中强调了它的数组处理效率更高,大家可以自己看看。

最后推荐两篇作者的博客文章,看看他在写MVVM中更多技术细节

迷你MVVM框架 avalonjs 实现上的几个难点
迷你MVVM框架avalon在兼容旧式IE做的努力

还是那句话,取其精华。明天将带来MVVM新贵 vue.js 源码分析,敬请期待。

MVVM大比拼之avalon.js源码精析的更多相关文章

  1. MVVM大比拼之knockout.js源码精析

    简介 本文主要对源码和内部机制做较深如的分析,基础部分请参阅官网文档. knockout.js (以下简称 ko )是最早将 MVVM 引入到前端的重要功臣之一.目前版本已更新到 3 .相比同类主要有 ...

  2. MVVM大比拼之vue.js源码精析

    VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多来自knockout.angularj ...

  3. vue.js源码精析

    MVVM大比拼之vue.js源码精析 VUE 源码分析 简介 Vue 是 MVVM 框架中的新贵,如果我没记错的话作者应该毕业不久,现在在google.vue 如作者自己所说,在api设计上受到了很多 ...

  4. MVVM大比拼之AngularJS源码精析

    MVVM大比拼之AngularJS源码精析 简介 AngularJS的学习资源已经非常非常多了,AngularJS基础请直接看官网文档.这里推荐几个深度学习的资料: AngularJS学习笔记 作者: ...

  5. MVVM架构~knockoutjs系列之从Knockout.Validation.js源码中学习它的用法

    返回目录 说在前 有时,我们在使用一个插件时,在网上即找不到它的相关API,这时,我们会很抓狂的,与其抓狂,还不如踏下心来,分析一下它的源码,事实上,对于JS这种开发语言来说,它开发的插件的使用方法都 ...

  6. Vue.js 源码分析(一) 代码结构

    关于Vue vue是一个兴起的前端js库,是一个精简的MVVM.MVVM模式是由经典的软件架构MVC衍生来的,当View(视图层)变化时,会自动更新到ViewModel(视图模型),反之亦然,View ...

  7. 深入理解unslider.js源码

    最近用到了一个挺好用的幻灯片插件,叫做unslider.js,就想看看怎么实现幻灯片功能,就看看源码,顺便自己也学习学习.看完之后收获很多,这里和大家分享一下. unslider.js 源码和使用教程 ...

  8. Jquery.cookie.js 源码和使用方法

    jquery.cookie.js源码和使用方法 jQuery操作cookie的插件,大概的使用方法如下 $.cookie(‘the_cookie’); //读取Cookie值$.cookie(’the ...

  9. basket.js 源码分析

    basket.js 源码分析 一.前言 basket.js 可以用来加载js脚本并且保存到 LocalStorage 上,使我们可以更加精准地控制缓存,即使是在 http 缓存过期之后也可以使用.因此 ...

随机推荐

  1. CI Weekly #10 | 2017 DevOps 趋势预测

    2016 年的最后几个工作日,我们对 flow.ci Android & iOS 项目做了一些优化与修复: iOS 镜像 cocoapods 版本更新: fir iOS上传插件时间问题修复: ...

  2. Dapper where Id in的解决方案

    简单记一下,一会出去有点事情~ 我们一般写sql都是==>update NoteInfo set NDataStatus=@NDataStatus where NId in (@NIds) Da ...

  3. junit4进行单元测试

    一.前言 提供服务的时候,为了保证服务的正确性,有时候需要编写测试类验证其正确性和可用性.以前的做法都是自己简单写一个控制层,然后在控制层里调用服务并测试,这样做虽然能够达到测试的目的,但是太不专业了 ...

  4. [原] KVM 虚拟化原理探究(2)— QEMU启动过程

    KVM 虚拟化原理探究- QEMU启动过程 标签(空格分隔): KVM [TOC] 虚拟机启动过程 第一步,获取到kvm句柄 kvmfd = open("/dev/kvm", O_ ...

  5. Performance Monitor4:监控SQL Server的IO性能

    SQL Server的IO性能受到物理Disk的IO延迟和SQL Server内部执行的IO操作的影响.在监控Disk性能时,最主要的度量值(metric)是IO延迟,IO延迟是指从Applicati ...

  6. 游戏AI系列内容 咋样才能做个有意思的AI呢

    游戏AI系列内容 咋样才能做个有意思的AI呢 写在前面的话 怪物AI怎么才能做的比较有意思.其实这个命题有点大,我作为一个仅仅进入游戏行业两年接触怪物AI还不到一年的程序员来说,来谈这个话题,我想我是 ...

  7. ES6(块级作用域)

    我们都知道在javascript里是没有块级作用域的,而ES6添加了块级作用域,块级作用域能带来什么好处呢?为什么会添加这个功能呢?那就得了解ES5没有块级作用域时出现了哪些问题. ES5在没有块级作 ...

  8. 信息安全-2:python之hill密码算法[原创]

    转发注明出处:http://www.cnblogs.com/0zcl/p/6106513.html 前言: hill密码算法我打算简要介绍就好,加密矩阵我用教材上的3*3矩阵,只做了加密,解密没有做, ...

  9. Greenplum 的分布式框架结构

    Greenplum 的分布式框架结构 1.基本架构 Greenplum(以下简称 GPDB)是一款典型的 Shared-Nothing 分布式数据库系统.GPDB 拥有一个中控节点( Master ) ...

  10. Hadoop伪分布式集群环境搭建

    本教程讲述在单机环境下搭建Hadoop伪分布式集群环境,帮助初学者方便学习Hadoop相关知识. 首先安装Hadoop之前需要准备安装环境. 安装Centos6.5(64位).(操作系统再次不做过多描 ...