MVVM试图更加清晰的讲用户界面(UI)开发从应用程序的业务逻辑与行为中心分离,因为,很多这样的模式的实现都需要利用声明式数据绑定来实现讲View(视图)工作从其他层分离

所以出现了一大堆自定义的声明式的语法:

如:Avalon

  1. 作用域绑定(ms-controller, ms-important)
  2. 模板绑定(ms-include)
  3. 数据填充(ms-text, ms-html)
  4. 类名切换(ms-class, ms-hover, ms-active)
  5. 事件绑定(ms-on,……)
  6. 显示绑定(ms-visible)
  7. 插入绑定(ms-if)
  8. 双工绑定(ms-duplex,原来的ms-model)
  9. 样式绑定(ms-css)
  10. 数据绑定(ms-data)
  11. 布尔属性绑定(ms-checked, ms-selected, ms-readonly, ms-disabled, ms-enabled)
  12. 字符串属性绑定(ms-title, ms-src, ms-href……)
  13. 万能属性绑定(ms-attr)
  14. 万能绑定(ms-bind)
  15. 数组循环绑定(ms-each)
  16. 对象循环绑定(ms-with)
    等等………

顾名思义,自定义声明语法,那么游览器本身是不能识别的,那么如何游览器能过识别自定义的HTML语法,它能让你讲行为关系到HTML元素或者属性上,甚至能让你创造具有自定义行为的新元素呢,我们暂且讲这个过程称之为“HTML编译”吧。

我们先看一段HTML代码

<div id='box' ms-controller="box">

    <div style=" background: #a9ea00;" ms-css-width="w" ms-css-height="h"  ms-click="click"></div>

    <p>{{ w }} x {{ h }}</p>
<p>W: <input type="text" ms-model="w" data-event="change"/></p>
<p>H: <input type="text" ms-model="h" /></p> </div>
    avalon.define("box", function(vm) {
vm.w = 100;
vm.h = 100;
vm.click = function() {
vm.w = parseFloat(vm.w) + 10;
vm.h = parseFloat(vm.h) + 10;
}
})
avalon.scan(document.getElementById('box'));

HTML结构中充斥了大量的ms开头的自定义标签,还有{}插值表达式。。等等

声明1:

ms-controller="box"

avalon提供ms-controller, ms-important来指定VM在视图的作用范围。比如有两个VM,它们都有一个firstName属性,在DIV中,如果我们用 ms-controller="VM1", 那么对于DIV里面的{{firstName}}就会解析成VM1的firstName中的值。

声明2:

ms-css-width="w" ms-css-height="h"

用来处理样式

声明3:

ms-click="click"

avalon通过ms-on-click或ms-click进行事件绑定,并在IE对事件对象进行修复,并统一了所有浏览器对return false的处理

其实就是把部分的行为操作提升到了dom上了,然后有框架在后台给你处理好,通过加入各种自定义的属性可以让任何的HTML元素都实现这样的行为

具体看源码的执行流程:

总的来说就是匹配每一给节点上的属性,通过匹配分配到指定的bindingHandlers处理函数上,之后的处理本章不暂时不涉及

//扫描入口
avalon.scan = function(elem, vmodel) //扫描子节点
function scanNodes(parent, vmodels, callback) //开始扫描
function scanTag(elem, vmodels) //扫描文本
function scanText(textNode, vmodels) //扫描表达式
function scanExpr(str) //扫描属性节点
function scanAttr(el, vmodels) //抽取绑定
function executeBindings(bindings, vmodels) //抽取文本绑定
function extractTextBindings(textNode)

看看命名就大概能猜出函数的作用了

1.入口函数  avalon.scan

avalon.scanavalon.scan = function(elem, vmodel) {
elem = elem || root
var vmodels = vmodel ? [].concat(vmodel) : []
scanTag(elem, vmodels)
}

默认从文本的根documentElement开始,如果传递了第一个elem,那么就是指定了扫描的作用域了,类似 jQuery( selector, [ context ] )

2. 执行扫描 scanTag

avalon.scan    vmodels = vmodels || []
var a = elem.getAttribute(prefix + "skip")
var b = elem.getAttribute(prefix + "important")
var c = elem.getAttribute(prefix + "controller") //这三个绑定优先处理,其中a > b > c
if (typeof a === "string") {
return
} else if (b) {
if (!VMODELS[b]) {
return
} else {
vmodels = [VMODELS[b]]
elem.removeAttribute(prefix + "important")
}
} else if (c) {
var newVmodel = VMODELS[c]
if (!newVmodel) {
return
}
vmodels = [newVmodel].concat(vmodels)
elem.removeAttribute(prefix + "controller")
}
scanAttr(elem, vmodels) //扫描特性节点 if (!stopScan[elem.tagName.toLowerCase()] && rbind.test(elem.innerHTML)) {
scanNodes(elem, vmodels)
}
  • 依次要检测是当前元素上是否有ms-skip,ms-important,ms-controller属性,用于最开始的处理
  • 如果ms-controller存在就取出vm视图模型对象
  • 清除这个自定义属性
  • 执行sacnAttr 属性扫描

3. 扫描属性节点 scanAttr

avalon.scanfunction scanAttr(el, vmodels) {
var bindings = []
for (var i = 0, attr; attr = el.attributes[i++]; ) { 1
if (attr.specified) { 2
var isBinding = false
if (attr.name.indexOf(prefix) !== -1) { 3
//如果是以指定前缀命名的
var type = attr.name.replace(prefix, "")
if (type.indexOf("-") > 0) { 4
var args = type.split("-")
type = args.shift()
}
isBinding = typeof bindingHandlers[type] === "function" 5
}
if (isBinding) {
bindings.push({ 6
type: type,
args: args || [],
element: el,
remove: true,
node: attr,
value: attr.nodeValue
})
}
}
}
executeBindings(bindings, vmodels)
}

attributes 属性返回包含被选节点属性的 NamedNodeMap。

如果在文档中设置了属性值,则 specified 属性返回 true.

是否是avalon的HTML指示 "ms-"开头

如果还指定了参数

能找到对应的处理函数

bindings 保存参数

4. 执行绑定 executeBindings

avalon.scanfunction executeBindings(bindings, vmodels) {
bindings.forEach(function(data) {
var flag = bindingHandlers[data.type](data, vmodels)
if (flag !== false && data.remove) { //移除数据绑定,防止被二次解析
data.element.removeAttribute(data.node.name)
}
})
}

找到对应的类型的bindingHandlers方法,传入数据与vm对象,实现处理

移除数据绑定,防止被二次解析

5. 扫描子节点 scanNodes

avalon.scanfunction scanNodes(parent, vmodels, callback) {
var nodes = aslice.call(parent.childNodes);
callback && callback();
for (var i = 0, node; node = nodes[i++]; ) {
if (node.nodeType === 1) {
scanTag(node, vmodels) //扫描元素节点
} else if (node.nodeType === 3) {
scanText(node, vmodels) //扫描文本节点
}
}
}

其实就循环处理子节点列表了,注意要过滤空文本类型

如果是元素节点就递归循环scanTag方法

如果是文本节点就scanText

6. 扫描文本 scanText

avalon.scanfunction scanText(textNode, vmodels) {
var bindings = extractTextBindings(textNode)
if (bindings.length) {
executeBindings(bindings, vmodels)
}
}

7.抽出文本绑定 extractTextBindings

avalon.scanfunction extractTextBindings(textNode) {
var bindings = [],
tokens = scanExpr(textNode.nodeValue)//分解表达式 if (tokens.length) {
while (tokens.length) { //将文本转换为文本节点,并替换原来的文本节点
var token = tokens.shift()
var node = DOC.createTextNode(token.value)
if (token.expr) { //如果分解的是表达式
var filters = token.filters
var binding = {
type: "text",
node: node,
args: [],
element: textNode.parentNode,
value: token.value,
filters: filters
}
if (filters && filters.indexOf("html") !== -1) {
avalon.Array.remove(filters, "html")
binding.type = "html"
binding.replaceNodes = [node]
}
bindings.push(binding) //收集带有插值表达式的文本
}
documentFragment.appendChild(node)
}
textNode.parentNode.replaceChild(documentFragment, textNode)
}
return bindings
}

文本解析是个比较复杂的东西,可以匹配很多种情况,所以需要加入很多解析的规则

scanExpr 就是扫描的表达式的匹配

documentFragment 先把这个结构让到文档碎片中,性能处理

8. 表达式匹配scanExpr

avalon.scanfunction scanExpr(str) {
var tokens = [],
value, start = 0,
stop
if (rexpr.test(str)) {
do {
var stop = str.indexOf(openTag, start)
if (stop === -1) {
break
}
value = str.slice(start, stop)
if (value) { // {{ 左边的文本
tokens.push({
value: value,
expr: false
})
}
start = stop + openTag.length
stop = str.indexOf(closeTag, start)
if (stop === -1) {
break
}
value = str.slice(start, stop)
if (value) { //{{ }} 之间的表达式
var leach = []
if (value.indexOf("|") > 0) { // 注意排除短路与
value = value.replace(rfilters, function(c, d, e) {
leach.push(d + (e || ""))
return c.charAt(0)
})
}
tokens.push({
value: value,
expr: true,
filters: leach.length ? leach : void 0
})
}
start = stop + closeTag.length;
} while (1);
value = str.slice(start);
if (value) { //}} 右边的文本
tokens.push({
value: value,
expr: false
})
}
}
return tokens
}

代码很长,但是处理的东西确很简单的

比如:

"{{ w }} x {{ h }}" 一个插值表达式,那么应该如何解析

分析这个表达式,解析可以分三块
1.  {{ w }}

2    x

3   {{ h }}

左右两边都是vm视图所有关联的属性,中间x就是求值

那么解析的规则,分解3个部分,组成处理数据

tokens 就有3个组成对象

  1. expr: true
  2. filters: undefined
  3. value: " w "
  1. expr: false
  2. value: " x "
  1. expr: true
  2. filters: undefined
  3. value: " h "

解析后分解成绑定的数据

然后就是一次循环了, 遇到条件stopScan就终止了

所以总结scan无非就干了一件事,扫描到指定的行为,发送数据给处理函数

前端MVVM框架avalon揭秘 - HTML编译器的更多相关文章

  1. 前端MVVM框架avalon揭秘 - 双向绑定原理

    avalon大家可能不熟悉,但是Knockout估计或多或少听过用过,那么说说KO的几个概念 监控属性(Observables)和依赖跟踪(Dependency tracking) 声明式绑定(Dec ...

  2. 前端MVVM框架avalon - 模型转换1

    轻量级前端MVVM框架avalon - 模型转换(一) 接上一章 ViewModel modelFactory工厂是如何加工用户定义的VM? 附源码 洋洋洒洒100多行内部是魔幻般的实现 1: fun ...

  3. 轻量级前端MVVM框架avalon - 初步接触

    迷你简单易用的MVVM框架 avalon的介绍http://rubylouvre.github.io/mvvm/ 按照作者的介绍,在HTML中添加绑定,在JS中用avalon.define定义View ...

  4. 轻量级前端MVVM框架avalon - 执行流程2

    接上一章 执行流程1 在这一大堆扫描绑定方法中应该会哪些实现? 首先我们看avalon能帮你做什么? 数据填充,比如表单的一些初始值,切换卡的各个面板的内容({{xxx}},{{xxx|html}}, ...

  5. 轻量级前端MVVM框架avalon源码分析-总结

    距avalon0.7版本发布有一段时间,由于之前的稳定性,就停止一段时间更新,期间研究了下Knockout源码,也尝试写了一个小型的mvvm的实现模型,仅仅只是仿造ko的核心实现,把无关的东西给剥离掉 ...

  6. 轻量级前端MVVM框架avalon - 执行流程1

    基本上确定了avalon的几个重要元素的关系: M,即model,一个普通的JS对象,可能是后台传过来的,也可能是直接从VM中拿到,即VM.$json.有关的这个$json的名字还在商讨 V,即Vie ...

  7. 轻量级前端MVVM框架avalon - 整体架构

    官网提供架构图 单看这个图呢,还木有说明,感觉有点蛋疼,作者的抽象度太高了,还好在前面已经大概分析过了执行流程 如图 左边是View视图,我们就理解html结构,换句话就是说用户能看到的界面,渲染页面 ...

  8. 轻量级前端MVVM框架avalon - ViewModel

    废话说了大几篇,我们开始来点干货了~ ViewModel的内部机制 在MVVM中,数据是核心.而jQuery则以DOM为核心. 而DOM只是HTML在JS的世界的抽象,是一个很易变的东西.因此如果业务 ...

  9. 轻量级前端MVVM框架avalon - 控制器

    引子: 最近工作挺忙,avalon只能断断续续的写下去了,大概看了下angular的源码,看到小一半就比较难坚持了,是块硬骨头,慢慢啃吧 不过angular的的文档中用词还是很优雅: HTML编译器 ...

随机推荐

  1. JavaScript使用封装

    基本封装方法 请看下面的例子: var Person = function(name,age){ this.name = name; this.age = age || "未填写" ...

  2. Oracle Flashback 闪回

    Oracle 的闪回技术是一种数据恢复技术,仅能对用户逻辑错误进行恢复, 闪回针对的是提交commit的事务,没有提交的事务,使用rollback 1.闪回版本查询 Flashback Version ...

  3. tornado学习笔记18 _RequestDispatcher 请求分发器

    根据Application的配置,主要负责将客户端的请求分发到具体的RequestHandler.这个类实现了HTTPMessageDelegate接口. 18.1 构造函数 定义: def __in ...

  4. 如何查看bash shell 帮助信息?

    man bash 查看bash的命令帮助 info bash 查看bash的文档 help 命令显示bash支持的命令: 如果想看某个命令的帮助可以 help 命令.如 help cd 对bash的命 ...

  5. Qt里怎么处理二进制数据

    Qt里有个专门的类QDataStream就是专门读写二进制数据的, 它与QByteArray搭配在网络编程中有奇效. 来个栗子: // write data QByteArray data; QDat ...

  6. JavaScript 中string方法

    注意:JavaScript的字符串是不可变的(immutable),String类定义的方法不能改变原来字符串内容,例如String.toUpperCase()这样的方法,返回的是全新的字符串,而不是 ...

  7. 让/etc/profile文件修改后立即生效

    方法1: 让/etc/profile文件修改后立即生效 ,可以使用如下命令: # .  /etc/profile 注意: . 和 /etc/profile 有空格 方法2: 让/etc/profile ...

  8. druid sql黑名单 报异常 sql injection violation, part alway true condition not allow

    最近使用druid,发现阿里这个连接池 真的很好用,可以监控到连接池活跃连接数 开辟到多少个连接数 关闭了多少个,对于我在项目中查看错误 问题,很有帮助, 但是最近发现里面 有条sql语句 被拦截了, ...

  9. initWithCoder与initWithFrame的区别

    1. initWithFrame方法是什么?   initWithFrame方法用来初始化并返回一个新的视图对象,根据指定的CGRect(尺寸). 当然,其他UI对象,也有initWithFrame方 ...

  10. 使用DataList实现数据分页的技术

    今天做网站的时候,用到了分页技术,我把使用方法记录下来,以便日后查阅以及帮助新手朋友们. DataList控件可以按照列表的形式显示数据表中的多行记录,但是被显示的多行记录没有分页功能,使用起来不太方 ...