迷你MVVM框架 avalonjs 沉思录 第2节 DOM操作的三大问题

jQuery之所以击败Prototype.js,是因为它自一开始就了解这三大问题,并提出完善的解决方案。

第一个问题,DOM什么时候可用。JS不像C那样有一个main函数,里面的逻辑不分主次。但JS是这样玩,并不意味着DOM也是这样。被JS自由惯了的人,于是傻眼了。
这涉及一个时间的概念。牛顿与爱因斯坦的差别,也是在于这个时间的引入。我们的脚本并不是一下子就引入,页面也不是一下加载完毕。前者引发脚本加载管理问题,后者就是DOMReady这个概念的导入。页面是从上到下生成,除了样式或图片在浏览器的主导下会另开线程,不阻塞外,其他都是一步步往下走。每个标签最后都解析成DOM,放在DOM树上。只有待到这DOM树建成,我们做开发才算是最安全的。没有这概念的人,总是遇到“此方法不存在”的问题——因为你这时得到的元素其实是null。浏览器还是很nice的,统一提供了一个onload事件,高级一点还有DOMContentLoaded事件(俗称ready事件)。当jQuery为了让页面尽快进入可用状态而发掘出这宝贝时,世界进入了一场竞赛,各种hack层出不穷。最后各大类库框架都加上这东西。

下面是avalon的实现

    var ready = W3C ? "DOMContentLoaded" : "readystatechange"

function fireReady() {
if (DOC.body) { // 在IE8 iframe中doScrollCheck可能不正确
modules["ready!"].state = 2
innerRequire.checkDeps()
fireReady = noop //隋性函数,防止IE9二次调用_checkDeps
}
}

function doScrollCheck() {
try { //IE下通过doScrollCheck检测DOM树是否建完
root.doScroll("left")
fireReady()
} catch (e) {
setTimeout(doScrollCheck)
}
}

if (DOC.readyState === "complete") {
setTimeout(fireReady) //如果在domReady之外加载
} else if (W3C) {
DOC.addEventListener(ready, fireReady)
window.addEventListener("load", fireReady)
} else {
DOC.attachEvent("onreadystatechange", function() {
if (DOC.readyState === "complete") {
fireReady()
}
})
window.attachEvent("onload", fireReady)
if (root.doScroll) {
doScrollCheck()
}
}


用法是avalon.ready(fn),只传入一个回调就行了。

jQuery的用法,尤其是第二种,让人眼前一亮:

    $(document).ready(function(){

})

$(function(){

})


第二个问题,如何找到这个元素。这个Prototype很早就注意这问题了,提供了两个方法$与$$,但它只是getElementById,getElementsByTagName的简单封装。其实90%是在用getElementById,于是引发满屏ID,也引发了CSS文件里是否用ID还是类名的讨论。正如我在开篇里说的那样,远古巨神总是被遗忘的,早在Prototype.js诞生前,就有高人想到选择器引擎这东西。在《javascript框架设计》一书里,我考究出两个比较早期的引擎,2003年Simon
Willison的getElementsBySelector,2004年Dean Edwards的cssQuery。来自Ruby界的Sam
Stephenson醉心于把Ruby的方法移植到javascript中,全然不觉选择器引擎这神器的存在。与Dean Edwards关系非常亲近的John
Resig知道这东西的重要性,选择器的行数占全库的一半。待1.3之时,Sizzle放出,江山到手。选择器的发明与普及,改写了前端操作DOM的习惯,所有操作都是围绕CSS选择符展开,并且很多时候我们是一下子得到一大堆节点,因此批量化处理也是jQuery的一大特性。

第三个问题,如何操作元素。这个jQuery与Prototype也是天壤之别。jQuery是将找到的元素存放在类数组结构的jQuery实例上,Prototype则是设法在元素节点的原型上扩展,对于旧式IE,原型没有暴露出来,只能逐个元素上加这些方法了。于是有一问题了,如果这个元素中途被删掉或置换,我们再对它进行操作就出问题了。对于jQuery是做了许多代码防御的,每一步操作都会对它的存在进行判定。而Prototype.js就对不起了,这个只能让用户自己做。易用性立判高下。其次,所有DOM操作API,勿庸置疑,需要做一层厚厚的封装,搞定各种兼容性问题。而DOM的封装一直是jQuery的拿手好戏,现在其他库也一直以抄袭jQuery为荣。虽然比起抄袭工具函数来说,这个有点难度,并还是可以拆出来的。

这是围绕DOM产生的三个问题,其实DOM操作并不是我们想要的,后端只是想让我们把收集的为数据提交上来。后来为了用户体验,出现了弹出一个层,让用户专注在这个区域填写东西;为了防止用户误填需要多少重写,于是需要数据验证,出于tooltip什么东西。再后来,这些辅助性的交互越来越多了,我们真正要做的事情其实很少,并且深陷在复杂的DOM操作里面。

但显然我们也不能开倒车,于是问题的重点变成如何分离这两种逻辑——分层架构引进来了。最初是MVC,但经过jQuery洗礼的人受不了为一点点操作要写这么多代码。不过两层,另一个更NB的东西MVVM终于也登场了。其中angular与knockout功不可没。它们的思路都很相似,就是把一些待操作的DOM,就HTML里就把它们标出来,具体做法是定义一些冗余的绑定属性。然后在JS代码搞一个能偷偷操作它们的特殊对象,我们只要操作它,页面就会发生改变。在这里,你可不到选择元素,操作元素的任何逻辑,一切都这么魔术奇幻。

对于第一个问题,几乎所有MVVM框架的做法都一致,拥有这接口,但一般的示例是没有它的展示。

<!DOCTYPE html>
<html>
<head>
<title>ms-css</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="../avalon.js" ></script>
<script>
avalon.define("test", function(vm) {
vm.first = {
aaa: 111
}
})
</script>
</head>
<body>
<div ms-controller="test">
<p><input ms-duplex="first.aaa" /></p>
{{first.aaa}}
</div>
</body>
</html>

<!DOCTYPE html>
<html>
<head>
<title>ms-css</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js" ></script>
<script>
avalon.define("test", function(vm) {
vm.first = {
aaa: 111
}
})
</script>
</head>
<body>
<div ms-controller="test">
<p><input ms-duplex="first.aaa" /></p>
{{first.aaa}}
</div>
</body>
</html>

运行代码

你看不到window.onload
,ready这样的方法,但它的确是正确将数据填空到页面没有报错。缘由是它们在框架内部偷偷调用DOMReady的逻辑。

    avalon.ready = function(fn) {
innerRequire("ready!", fn)
}

avalon.ready(function() {
avalon.scan(DOC.body)
})


对于第二个问题,如何找到待操作的元素节点。avalon, angular, knockout都是通过扫描机制实现。在avalon是通过带有ms-*属性的元素节点或{{}}的文本节点,
angular是带有ng-*属性的元素节点,或属性里面有{{}}的元素节点,或存在{{}}的
文本节点,knockout是带有data-bind的元素节点。这些绑定属性与文本绑定一经扫描,就会移除掉(可能也有不移除的),转换为一个刷新函数,与VM的属性关联在一起,因此当VM中的属性发生改变时,那些属性的状态,样式什么都会同步。这是魔术的其中一个端倪。

这个魔法让我们省掉大量查找元素的代码,对于框架来说,扫描机制,就是一个递归遍历节点的过程,比编写一个千行以上的选择器引擎轻松多了。这个是双赢的局面,皆大欢喜。

对于第三个问题,如何操作DOM,上面说过就是将绑定属性转换为视图刷新函数,比如说ms-css-background="color",当VM中的color变成red,那么元素也就变红。底层的代码可以简单的理解为data.element.style[background]
=
data.value。事实上,这修改样式,还是需要像avalon.css这样高度封装的API。jQuery提出的一系列兼容性处理方案,始终发扬光大。而avalon的实现其实与jQuery的差不了多远。angular就内置了实现了jQuery一系列接口的jqLite对象,并且它与jQuery混用时,干脆用jQuery的。至于当用户修改了VM的某一属性,如何通知视图呢,这又是另一个黑魔法,并且是一个非常高级的魔法,一般的大魔导士是施展不出来了。因此MVVM框架的数量才这么少。只有护国法师
,创世魔法师才有这能耐。

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="avalon.js"></script>
<script>
var model = avalon.define("test", function(vm) {
vm.color = "green"
vm.clickfn = function(){
vm.color = ‘#‘+(Math.random()*0xffffff<<0).toString(16);
}
})
</script>
</head>
<body>
<div ms-controller="test">
<div style="width:200px;height:200px" ms-css-background="color"></div>
<button type="button" ms-click="clickfn">每点我一次都改变颜色</button>
</div>
</body>
</html>

<!DOCTYPE html>
<html>
<head>
<title>ms-css</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js" ></script>
<script>
var model = avalon.define("test", function(vm) {
vm.color = "green"
vm.clickfn = function(){
vm.color = ‘#‘+(Math.random()*0xffffff<<0).toString(16);
}
})
</script>
</head>
<body>
<div ms-controller="test">
<div style="width:200px;height:200px" ms-css-background="color"></div>
<button type="button" ms-click="clickfn">每点我一次都改变颜色</button>
</div>
</body>
</html>

运行代码

总体来说,这魔法有三种实现。第一种就是将VM的属性全部转换为函数,这个是knockout的实现,是最笨拙的做法。第二种是将定义VM的函数取toString进行重新编译,然后所有操作都受限于各种服务,通过服务对VM函数的属性进行改动。这是angular的做法,还美曰其名为注赖注入。这种魔法对创建魔法的人与施法者都是沉重的负担。当然angular官网上有一些小清新的例子,用法迷惑人展示它们是多少简单。当项目的用法是另一样。第三种是使用Object.defineProperty(ecma262
v5新增的API),当此方法在IE8下有问题,是针对元素节点,想应用于Object还需用IE9+浏览器。这是其他语言都有setter,
getter
魔法。我们可以把所有操作都列为五大操作,读值,赋值,遍历,删除,方法调用。setter,getter只是把普通的取值语句与赋值语句变成一个方法调用的形式,我们可以hack这个方法,从而实现跟视图的同步。对于IE6-8,就惨了。幸好avalon的作者还会一点VBScript,通过VBscript类的set,
get,
let语句也能实现类似的效果。这正是avalon的NB之处,通过VBScript与Object.defineProperty,就能用4000行的代码达到angular
16000行的效果,并且性能更好,也不用记这么多概念,这么多API。

            //Object.defineProperty带来的魔法
var obj = {
a: 1,
_b: 2
}
Object.defineProperty(obj, "b", {
set: function(_b) {//重写b的setter, getter
this._b = _b
},
get: function() {
return this._b + 10
}
})
console.log(obj.a)//1
console.log(obj._b)//2
console.log(obj.b)//12
obj.a = 10
obj.b = 20
console.log(obj.a)//10
console.log(obj._b)//20
console.log(obj.b)//30

正因为有了扫描机制,有了ecma262v5的setter,
getter,我们就不用管如何查找元素,如果操作元素。元素在第一次内部扫描时已经就绪,想什么操作,就在目标元素上定义不同的绑定属性。绑定属性与普通的style,class,id那样学习成本很低,它们还类似模板的用途。因此有关页面的构成完成交由专门的HTML制作人员就行了,我们只负责javascript里的VM定义。

重复一次,我们用avalon做前端开发,现在只有两步工作。在JS中定义VM,VM有一个名字,对应页面上某个元素的ms-controller的值,然后VM里面有许多方法与属性名,对应这个区域里面绑定属性的值。第二步,编写页面,通过绑定属性实现原来用jQuery搞的各种效果,如ms-attr对应jQuery的attr方法,
ms-css对应jQuery的css方法,ms-visible对应jQuery的show, hide方法, ms-if对应jQuery的append,
remove方法, ms-repeat相当于循环实现一整块HTML页面, {{prop}}就是在对应位置将值打印出来……

我们来一个hello world吧

jquery

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="jquery.js"></script>
<script>
$(function(){
$("h3").text("Hello World")
})
</script>
</head>
<body>
<div>
<h3></h3>
</div>
</body>
</html>

avalon

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="avalon.js"></script>
<script>
avalon.define("helloworld", function(vm) {
vm.text = "Hello World"
})
</script>
</head>
<body>
<div ms-controller="helloworld">
<h3>{{text}}</h3>
</div>
</body>
</html>

貌似jQuery更干净,来点交互怎么样!

jquery

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="jquery.js"></script>
<script>
$(function(){
$("h3").text("He say Hello World !!!").click(function(){
$(this).text("He say Good Bye !!!")
})
})
</script>
</head>
<body>
<div>
<h3></h3>
</div>
</body>
</html>

有点麻烦了吧,因为jQuery的选择器引擎是基于元素节点,因此我们无法把Hello World变成一个变量,重复使用。看avalon!

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<script src="avalon.js"></script>
<script>
avalon.define("helloworld", function(vm) {
vm.text = "Hello World"
vm.alert = function(){
vm.text = "Good Bye"
}
})
</script>
</head>
<body>
<div ms-controller="helloworld">
<h3 ms-click="alert">He say {{text}} !!!</h3>
</div>
</body>
</html>

再复杂一点

<!DOCTYPE html>
<html>
<head>
<title></title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script src="avalon.js"></script>
<script>
var model = avalon.define("test", function(vm) {
vm.w = 100;
vm.h = 100;
vm.click = function() {
vm.w = parseFloat(vm.w) + 10;
vm.h = parseFloat(vm.h) + 10;
}
vm.arr = ["aaa", ‘bbb‘, "ccc", "ddd"]
vm.selected = ["bbb", "ccc"]
vm.checkAllbool = vm.arr.length === vm.selected.length
vm.checkAll = function() {
if (this.checked) {
vm.selected = vm.arr
} else {
vm.selected.clear()
}
}
})
model.selected.$watch("length", function(n) {
model.checkAllbool = n === model.arr.size()
})

</script>
</head>
<body>
<div ms-controller="test">
<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-duplex="w" data-duplex-event="change"/></p>
<p>H: <input type="text" ms-duplex="h" /></p>
<ul>
<li><input type="checkbox" ms-click="checkAll" ms-checked="checkAllbool"/>全选</li>
<li ms-repeat="arr" ><input type="checkbox" ms-value="el" ms-duplex="selected"/>{{el}}</li>
</ul>
</div>
</body>
</html>


<!DOCTYPE html>
<html>
<head>
<title>ms-css</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<script src="http://files.cnblogs.com/rubylouvre/avalon20130929.js" ></script>
<script>
var model = avalon.define("test", function(vm) {
vm.w = 100;
vm.h = 100;
vm.click = function() {
vm.w = parseFloat(vm.w) + 10;
vm.h = parseFloat(vm.h) + 10;
}
vm.arr = ["aaa", ‘bbb‘, "ccc", "ddd"]
vm.selected = ["bbb", "ccc"]
vm.checkAllbool = vm.arr.length === vm.selected.length
vm.checkAll = function() {
if (this.checked) {
vm.selected = vm.arr
} else {
vm.selected.clear()
}
}
})
model.selected.$watch("length", function(n) {
model.checkAllbool = n === model.arr.size()
})

</script>
</head>
<body>
<div ms-controller="test">
<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-duplex="w" data-duplex-event="change"/></p>
<p>H: <input type="text" ms-duplex="h" /></p>
<ul>
<li><input type="checkbox" ms-click="checkAll" ms-checked="checkAllbool"/>全选</li>
<li ms-repeat="arr" ><input type="checkbox" ms-value="el" ms-duplex="selected"/>{{el}}</li>
</ul>
</div>
</body>
</html>

运行代码

光是那个全选非全选就够你受的,你们自己思惦一下如何用jQuery实现吧。反正在avalon里,没有了DOM操作,上述说的三大问题都没有了。你可以像后端的JAVAer那样转致于领域建模与业务逻辑的构思。一旦你用上它,你就进入一个更高的台阶。之前你一直疲于亡命、剪不断、理还乱的
DOM操作,交由avalon这个框架在水底下更精细准确地运作着,比你自己写更安全高效。

如果您觉得这文章对您有帮助,可以打赏点钱给我,鼓励我继续写一些高质量的博文

时间: 2024-11-05 11:55:26

迷你MVVM框架 avalonjs 沉思录 第2节 DOM操作的三大问题的相关文章

迷你MVVM框架 avalonjs 沉思录 第1节 土耳其开局

#cnblogs_post_body p{ text-indent:2em; margin-top: 1em; } 正如一切传说的开端那样,有一远古巨神开天辟地,然后就是其他半神喧宾夺主.我们对最巨贡献与创建力的远古巨神懵懂不知,却对巫师们的话语津津乐道.这同样也是我们前端的现实. MVVM是来自.NET,另一个遥远的界域.前端,相对于后端,怎么看都是蛮夷之地.JS这个肩负着前端一切交互工作的语言,竟然被视为恶魔,屡屡被屏蔽禁用.些微可用的脚本,变量与函数没有组织地野蛮生长着,直到JAVA的传教

迷你MVVM框架 avalonjs 沉思录 第3节 动态模板

模板的发明是编程史上的一大里程碑,让我们摆脱了烦锁且易出错的字符串拼接,维护性大大提高. 都在JSP,ASP时代,人们已经学会使用include等语句,将多个页面片断拼接成一个页面. 此外,为了将数据库中的数据或业务中用到的变量输出到页面,我们需要将页面某个地方标记一下,将变量塞到里面去. 最后,出于方便循环输出一组数据,就需要将each语句从HTML里撕开一道口子,加上其他什么if语句,页面上其实变撕裂成两部分 一种是与后端语言相近的逻辑部分,一个是够为纯净的HTML部分,到最后,模板引擎就发

迷你MVVM框架 avalonjs 入门教程(司徒正美)

迷你MVVM框架 avalonjs 入门教程 关于AvalonJs 开始的例子 扫描 视图模型 数据模型 绑定属性与动态模板 作用域绑定(ms-controller, ms-important) 模板绑定(ms-include) 数据填充(ms-text, ms-html) 类名切换(ms-class, ms-hover, ms-active) 事件绑定(ms-on,……) 显示绑定(ms-visible) 插入绑定(ms-if) 双工绑定(ms-duplex) 样式绑定(ms-css) 数据绑

迷你MVVM框架 avalonjs 1.3.1发布

avalon1.3.1发布. interpolate支持注释节点做定界符,avalon.config({interpolate:["<!--","-->"]}) 监控数组添加pushArray方法,类似于push方法,不过参数是一个数组 data-duplex-changed回调会在第一次赋值就触发 添加一配置项,调整ms-repeat的对象池的大小,avalon.config({maxRepeatSize:30}) 迷你MVVM框架在github的仓库

迷你MVVM框架 avalonjs 1.3.2发布

时隔一个月,avalon的新版本终于出来了,本次更新带来强大的模块间通信机制,其他就往常一样FIX BUG. 在文本绑定里,IE会对流离于DOM树外的文本节点的data属性赋值报错,需要添加一层判定 派发事件的逻辑,由DOC.createEvent("Event")必成DOC.createEvent("Events") 添加一个反XSS的过滤器sanitize 重构事件系统,暴露eventHooks对象 firefox添加对mousewheel的兼容支持 升级ms-

迷你MVVM框架 avalonjs 1.3.3发布

这应该是1.3X系列最后一个版本了,大家可以在仓库中看到,多出了一个叫avalon.observe的东西,它是基于Object.observe,dataset, Promise等新API实现.其中,它也使用全新的静态收集依赖的机制,这个机制也完成得差不多,因此avalon与avalon.mobile下一版将会应用这最新成果,进行大改. fix IE6-8下直接修改表单元素值不触发data-duplex-changed回调的BUG, 详见这里 chrome浏览器对文本域进行Ctrl+V操作,会触发

迷你MVVM框架 avalonjs 1.3.5发布

本版本主要是修复内存泄漏问题,让其在移动端更好的运作. 修正visible BUG 详见这里 修正$fire方法里的正则错误 详见这里 修正ms-attr BUG,在IE9-11,直接用element.setAttribute("value","xxx")还是不能同步到element.value 详见这里 修正ms-class BUG,现在ms-hover对应的属性发生变成,类名也能跟着变了详见这里 修正avalon.modern的AMD加载器的onerror触发逻

迷你MVVM框架 avalonjs 1.3.8发布

avalon1.3.8主要是在ms-repeat. ms-each. ms-with等循环绑定上做重大性能优化,其次是对一些绑定了事件的指令添加了roolback,让其CG回收更顺畅. 重构ms-repeat.ms-each.ms-with, 内部的代理对象全部使用普通的对象实现, 并且一个监控数组只对应一个代理VM数组,从而大量减少VM的数量. avalon不再使用scanCallback实现内部各种rendred回调,改成checkScan方法.详看这里的例子. fix parseHTML在

迷你MVVM框架 avalonjs 1.3.7发布

又到每个月的15号了,现在avalon已经固定在每个月的15号发布新版本.这次发布又带来许多新特性,让大家写码更加轻松,借助于"操作数据即操作DOM"的核心理念与双向绑定机制,现在越来越多人加入到avalon的阵营中来.此外,基于avalon的UI库OniUI也越来越强大,很快,树组件也开发完毕,下星期也与大家见面了.到时,OniUI也有两个Grid组件,一个树组件的完整UI库.与本次发布的UI还有验证组件,mask组件,百叶窗切换组件-- UI库的广告就到时为止,我们看一下新版本带来