简介javascript
avalon是国内 司徒正美 写的MVVM框架,相比同类框架它的特色是:html
须要看基础介绍的话建议直接看司徒的博客。在网上搜了一圈,发现已经有了avalon很好的源码分析,这里也推荐一下:地址。 avalon在圈子里一直被诟病不够规范的问题,请各位没必要再留言在我这里,看源码无非是取其精华去其糟粕。能够点评,但老是讨论用哪一个框架好哪一个很差就没什么意义了,如果本身把握不住,用什么都很差。vue
今天的分析以 avalon.mobile 1.2.5 为准,avalon.mobile 是专门为高级浏览器准备的,不兼容IE8如下。java
入口node
仍是先看启动代码数组
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等方法。 仍是老规矩,咱们先看看总体机制图:ruby
双工引擎app
接下来就是直接一探 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 源码分析,敬请期待。