Regular(MVVM)源码分析

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){ // max loop
throw Error('there may a circular dependencies reaches')
}
}
if( n > 0 && this.$emit) {
this.$emit("$update");
}
this.$phase = null;
}

_digest 方法

一个组件是否 dirty 和两个因素有关:

  1. 组件所有监听的数据是否 dirty(组件本身的监听的变量是否 dirty)
  2. 组件所有的子组件是否 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;
}
}
}
// check children's dirty.
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
//{ #if flag}监听函数的回调函数中, flag变化时触发
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) {
//true
if (ast.consequent && ast.consequent.length) {
consequent = self.$compile(ast.consequent, {
record: true,
outer: options.outer,
namespace: namespace,
extra: extra
});
// placeholder.parentNode && placeholder.parentNode.insertBefore( node, placeholder );
group.push(consequent);
if (placeholder.parentNode) {
animate.inject(combine.node(consequent), placeholder, "before");
}
}
} else {
//false
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;

//用于记录是否需要记录watcher,用于if else的watcher销毁
if(record) this._record();
//遍历整个vdom数用于生成真实的dom树
var group = this._walk(ast, options);
if(record){
records = this._release();
var self = this;
if(records.length){
// auto destroy all wather;
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 中较为重要的一些方法,日后会再做补充。