Regularjs是网易开源的 mvvm 框架,今天就以 Regularjs 为例阅读源码片段,便于更好的理解脏检测双向绑定的原理。
实例
1
| <div id="wrapper"></div>
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| var Button = Regular.extend({ name: "UIButton", template: "<button on-click={this.click()}>{ #if flag}<span>{label}</span>{ #else}no{/if}</button>", click: function() { debugger; this.data.flag = !this.data.flag; } }); var TestUI = Regular.extend({ template: '<div style="margin: 100px">{value}<UIButton label="button"></UIButton></div>' }); new TestUI({ data: { label: "button show" } }).$inject("#wrapper");
|
模板编译
模板编译应该是 Regular 中最复杂的代码了,但功能很单一,就是将一段 html 文本转换为相应的对象。
模板在编译时会生成以下几类元素对象:
- attribute (nodeType==2 类型)
- component (自定义组件类型)
- element (nodeType==1 类型)
- expression({label}类型)
- if ({ #if}{ #else}类型)
- list ({ #list}循环类型)
- text (nodeType==3 类型)
在本例中,一共定义了两个 Regular 类,TestUI 的模板中引用到了 Button 类。
在实例化 TestUI 的时候会先编译模板,将
1 2 3 4
| <div style="margin: 100px"> {value} <UIButton label="button"></UIButton> </div>
|
转化为对象构成数结构的对象:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| var vdom = [ { attrs: [{ name: "style", type: "attribute", value: "margin:100px" }], children: [ { body: "c._sg_('value', d, e)", constant: false, setbody: "c._ss_('value',p_,d, '=', 1)", type: "expression" }, { type: "element", tag: "UIButton", attrs: [{ type: "attribute", name: "label", value: "button" }], children: [] } ], tag: "div", type: "element" } ];
|
walkers.expression 方法用来处理模板中的{value}, 会生成一个 textNode,并\$watch,
这段代码就揭示了双向绑定的原理,当值变化时更新 dom。也可以看出来这种更新是很细粒度的。
1 2 3 4 5 6 7 8 9 10 11
| walkers.expression = function(ast, options) { var node = document.createTextNode(""); this.$watch( ast, function(newval) { dom.text(node, "" + (newval == null ? "" : "" + newval)); }, { init: true } ); return node; };
|
\$watch 方法会 push 一个 watch 对象到 this._watchers 数组中,用于日后统一更新。
然后执行_checkSingleWatch 方法,如果 now 与 last 值不等则 dirty=true 进行 watch 更新 dom。
walkers.element 方法用来处理模板中的标签元素和自定义组件,如本例中的 div、UIButton
当生成一个 Regular 类并带有 name 属性的时候会保存在 Regular._components 对象上。如果在模板中匹配到类的 name 则使用类进行标签初始化。
walkers.component 在 walkers.element 中进行调用,此方法中会进行自定义组件的 data 初始化:
1
| data["label"] = "button";
|
然后会 new 一个 UIButton 实例,并且会保存进$parent._children数组中,用于后续操作,本例中$parent 为 TestUI 的实例
walkers[‘if’]处理模板中的 if else.
walkers.text 方法用于处理正常的字符串
1 2 3 4
| walkers.text = function(ast, options) { var node = document.createTextNode(_.convertEntity(ast.text)); return node; };
|
\$update 方法
this.\$update 方法会找到最外层组件进行更新,所以不管是哪一层的子组件更新,都会“冒泡”到最上层组件进行更新检测。当然 Regular 提供了 data.isolate 用来阻止组件向上“冒泡”
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| $update: function(){ var rootParent = this; do{ if(rootParent.data.isolate || !rootParent.$parent) break; rootParent = rootParent.$parent; } while(rootParent)
var prephase =rootParent.$phase; rootParent.$phase = 'digest'
this.$set.apply(this, arguments);
rootParent.$phase = prephase
rootParent.$digest(); return this; }
|
\$digest 方法
当 dom 都渲染好了之后如何更新 dom 呢?this.$update()方法中会调用$digest 方法,\$digest 会进行脏检测。
$digest方法中会进行$phase 判断,如果正在脏检测则直接 return,所以在$update的回调函数(如regular提供的事件函数)中调用$update 方法是无效操作,这样有利于避免频繁的无用的更新操作。
$digest方法里有一个while循环,如果循环20次以上则认为出了问题并抛错。在进行$digest 时,只要有任何的组件 dirty=true 都会再进入循环,直到所有组件 dirty=false。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| $digest: function(){ if(this.$phase === 'digest' || this._mute) return; this.$phase = 'digest'; var dirty = false, n =0; while(dirty = this._digest()){
if((++n) > 20){ throw Error('there may a circular dependencies reaches') } } if( n > 0 && this.$emit) { this.$emit("$update"); } this.$phase = null; }
|
_digest 方法
一个组件是否 dirty 和两个因素有关:
- 组件所有监听的数据是否 dirty(组件本身的监听的变量是否 dirty)
- 组件所有的子组件是否 dirty(组件包含的子组件是否 dirty)
组件本身监听函数保存在 this._watchers 数组中,组件的子组件实例保存在 this.children 数组中。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| _digest: function(){
var watchers = this._watchers; var dirty = false, children, watcher, watcherDirty; var len = watchers && watchers.length; if(len){ for(var i =0; i < len; i++ ){ watcher = watchers[i]; if( !watcher || watcher.removed ){ watchers.splice( i--, 1 ); len--; }else{ watcherDirty = this._checkSingleWatch(watcher, i); if(watcherDirty) dirty = true; } } } children = this._children; if(children && children.length){ for(var m = 0, mlen = children.length; m < mlen; m++){ var child = children[m]; if(child && child._digest()) dirty = true; } } return dirty; },
|
处理 if else
Regular 会将 if else 模板的值保存在 group 数组中,并为 flag 变量注册一个 watcher,当 flag 变化的时候,先删除掉之前保存在 group 中的 dom,然后生成新的 dom 并保存在 goup 中。
处理 if else 的时候要解决 unwatch 的问题。在本例中,{ #if flag}值为 true 的时候会多注册一个{label}的 express 监听。当 flag==false 的时候,这个监听就没用了,所以这个时候要 unwatch 掉这个监听。
Regular 是如何做到的呢?Regualr 在注册对 flag 的监听函数的回调函数中(下边的 update)调用\$compile 方法,此方法会根据 if else 中夹杂的模板生成相应的 dom。
- flag = true —> <span>{label}</span>
- flag = false —> no
此方法的第二个参数{record:true}的意思是说:哦,我要收集一下这个模板里的所有 watcher,日后用于删除哦!
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| var update = function(nvalue, old) { var value = !!nvalue; if (value === preValue) return; preValue = value; if (group.children[1]) { group.children[1].destroy(true); group.children.pop(); } if (value) { if (ast.consequent && ast.consequent.length) { consequent = self.$compile(ast.consequent, { record: true, outer: options.outer, namespace: namespace, extra: extra }); group.push(consequent); if (placeholder.parentNode) { animate.inject(combine.node(consequent), placeholder, "before"); } } } else { if (ast.alternate && ast.alternate.length) { alternate = self.$compile(ast.alternate, { record: true, outer: options.outer, namespace: namespace, extra: extra }); group.push(alternate); if (placeholder.parentNode) { animate.inject(combine.node(alternate), placeholder, "before"); } } } };
|
\$compile 方法
\$compile 方法用于生成 dom 元素,第一个参数为编译好的 html 模板对象(本例中的 vdom)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| $compile: function(ast, options){ options = options || {}; if(typeof ast === 'string'){ ast = new Parser(ast).parse() } var preExt = this.__ext__, record = options.record, records;
if(options.extra) this.__ext__ = options.extra;
if(record) this._record(); var group = this._walk(ast, options); if(record){ records = this._release(); var self = this; if(records.length){ group.ondestroy = function(){ self.$unwatch(records); } } } if(options.extra) this.__ext__ = preExt; return group; }
|
_walk 方法
_walk 方法会遍历 html 树结构的对象,根据不同的元素类型生成对应的 dom 元素。
1 2 3 4 5 6 7 8 9 10 11 12 13
| _walk: function(ast, arg1){ if( _.typeOf(ast) === 'array' ){ var res = [];
for(var i = 0, len = ast.length; i < len; i++){ res.push( this._walk(ast[i], arg1) ); }
return new Group(res); } if(typeof ast === 'string') return doc.createTextNode(ast) return walkers[ast.type || "default"].call(this, ast, arg1); }
|
总结
以上为 Regular 中较为重要的一些方法,日后会再做补充。