第1页
如何构建⼀一个MVVM框架
第2页
⾃自我介绍
⺫⽬目前是饿了么的前端⼯工程师。 5年UI组件开发经验,在前司从事开发⼀一个类似 ExtJS的框架。
第3页
SIMPLE MVVM
https://github.com/furybean/simple-mvvm
第5页
AGENDA
• 介绍MVVM • MVVM的组成 • 实现Compiler • 实现ViewModel • 实现Directive
第6页
MVVM
• https://en.wikipedia.org/wiki/Model_View_ViewModel
View
ViewModel
DataBinding
Presentation and Presentation Logic
Model
BusinessLogic Object
第7页
WELL KNOWN MVVM
• AngularJs: Controller + HTML • Ember.js: Controller + Handlebars + Model • KnockoutJS: Observable + HTML • Vue.js:ViewModel + HTML
第8页
MVVM FOR WEB
HTML / DOM
ViewModel
DataBinding
Presentation and Presentation Logic
POJO
BusinessLogic Object
第9页
MVVM组成
第10页
PROBLEM
HTML / DOM
ViewModel
DataBinding
第11页
DOM Template
POJO
I/O
Compiler
DOM
ViewModel Directives
第12页
实现COMPILER
第13页
MAIN
$compile(element, context)
第14页
TODOS
第15页
CONTEXT
var context = { newTodo: "", todos: [ { title: 'first', done: true }, { title: 'second', done: false } ], add: function() { this.todos.push({ title: this.newTodo, done: false }); this.newTodo = ''; }, remove: function(item) { this.todos.splice(this.todos.indexOf(item), 1); }
};
第16页
HTML
<div id="demo"> <input d-model="newTodo" placeholder="Input your new todo"/> <button d-click="add()">Add</button> <ul> <li d-repeat="item in todos"> <input type="checkbox" d-model=“item.done"/> <span d-class="done:item.done">{{item.title}}</span> <a href="Javascript:" d-click="remove(item)">Remove</a> </li> </ul>
</div>
第17页
DOM WALK
function walk(node, callback) { if (node.nodeType === 1 || node.nodeType === 3) { var returnValue = callback(node); if (returnValue === false) { return; } }
if (node.nodeType === 1) { var current = node.firstChild; while (current) { walk(current, callback); current = current.nextSibling; } }
}
第18页
SUB CONTEXT
<div id="demo">
<input d-model="newTodo" placeholder="Input your new todo"/>
<button d-click="add()">Add</button>
<ul> context
<li d-repeat="item in todos">
<input type="checkbox" checked="{{item.done}}"/>
<span d-class="done:item.done">{{item.title}}</span>
<a href="Javascript:" d-click="remove(item)">Remove</a>
</li>
</ul> </div>
sub context
第19页
CONTEXT的继承
function newContext(context) { var empty = function() {}; empty.prototype = context;
return new empty();
}
Object.create(context);
第20页
WALK NODE
1. 节点不存在d-repeat属性,把表达式的值映射到 DOM,继续WALK⼦子节点。
2. 节点存在d-repeat属性,结束WALK⼦子节点:
1. Clone当前节点后把d-repeat属性删除,作为数组 元素使⽤用的模板childTemplate。
2. 对数组中每个元素创建⼀一个Sub Context,执⾏行 $compile(childTemplate, subContext)
第21页
从CONTEXT取值
item.title
(function() { return this.item.title;
}).bind(context);
第22页
CODE TO FUNCTION
var compileFn = function(body, context) { var fn; if (body) { fn = new Function('return ' + body + ';'); } if (fn && context) { return fn.bind(context); } return fn;
};
第23页
PARSE TYPES
• Repeat Expression(d-repeat): item in todos • Expression(attribute value): newTodo • Text(text node): {{item.title}} • Pair(d-class): done: item.done
第24页
EXPRESSION
• newTodo • item.title • add() • remove(item)
第25页
EXPRESSION OUTPUT
• newTodo => this.newTodo • item.title => this.item.title • add() => this.add() • remove(item) => this.remove(this.item)
第26页
OPEN SOURCE PARSER
• esprima:http://esprima.org/ Esprima is a high performance, standard-compliant ECMAScript parser written in ECMAScript
• jsep: https://github.com/soney/jsep JavaScript Expression Parser
第27页
JSEP PARSE RESULT
type === 'premium' ? 5 : 0
{ type: "ConditionalExpression", test: { type: "BinaryExpression", operator: "===", left: { type: “Identifier", name: "type" }, right: { type: “Literal", value: "premium" } }, consequent: { type: “Literal", value: 5 }, alternate: { type: “Literal", value: 0 }
}
第28页
type === 'premium' ? 5 : 0
consequent
Literal 5
Conditional Expression
test
Binary Expression
alternate
Literal 0
right
left
Identifier type
operator
===
Literal 'premium'
第29页
AST TO CODE
function astToCode(ast) { if (ast.type === 'Literal') { return typeof ast.value === 'string' ? '"' + ast.value + '"' : ''
+ ast.value; } else if (ast.type === 'ThisExpression') { return 'this'; } else if (ast.type === 'UnaryExpression') { return ast.operator + astToCode(ast.argument); } else if (ast.type === 'BinaryExpression' || ast.type ===
'LogicalExpression') { return astToCode(ast.left) + ' ' + ast.operator + ' ' +
astToCode(ast.right); } else if (ast.type === 'ConditionalExpression') { return '(' + astToCode(ast.test) + ' ? (' +
astToCode(ast.consequent) + ') : (' + astToCode(ast.alternate) + '))'; } else if (ast.type === 'Identifier') { return 'this.' + ast.name; } //...
}
第30页
PARSE TEXT(INTERPOLATE)
text{{expression}}text{{expression}} 使⽤用正则会遇到的边界情况:
• {{、}}在字符串或者在表达式{}()[]⾥里 遍历字符串,定义两个变量:inString和level。
第31页
PARSE PAIR
key: expression, key: expression
第32页
实现VIEWMODEL
第33页
WATCH
• Dirty Check: Angular.js 1.x • defineProperty: Ember.js / Vue.js • Object.observe
第34页
OBJECT.OBSERVE
var obj = { foo: 0
}; Object.observe(obj, function(changes) {
console.log(changes); }); obj.foo = 2; //[{type: 'update', object: { foo: 2 }, name: 'foo', oldValue: 0 }]
第35页
⽅方法
• $watch: Object.observe • $unwatch • $extend: Object.create • $destroy
第36页
实现DIRECTIVE
第37页
DIRECTIVE TYPE
• attr => setAttribute • event => addEventListener • text => innerText / nodeValue • class => className • model => value / addEventListener • repeat => [ Element ]
第38页
DIRECTIVE 属性
• element • context • expression • attr/className/eventName
第39页
DIRECTIVE ⽅方法
• bind • update • unbind • destroy
第40页
DIRECTIVE BIND
var directive = this; if (directive.element && directive.expression && directive.context){
directive.valueFn = compileExpr(directive.expression, directive.context);
var depends = getDepends(directive.expression); var context = directive.context; depends.forEach(function(depend) {
context.$watch(depend, directive); }); directive.update(); }
第41页
EXPRESSION DEPENDS
• 从AST提取,只关注两种类型: • Identifier: newTodo • MemberExpression: item.done
第42页
REPEAT DIRECTIVE
item in todos track by id
• track by的作⽤用是对Array中的元素进⾏行hash • Array的diff就简化成了有序集合的diff
第43页
12 3 4
821 4 5
• 遍历集合A,如果元素在集合A中存在,集合B中不存在,则该元素被删除
• 遍历集合B,如果元素在集合B中存在,集合A中不存在,则该元素为新增
如果元素在集合B中存在,集合A中存在,但是元素的前⼀一个 元素不同,则该元素被移动
第44页
REVIEW
• DOM TREE WALK • AST WALK • ViewModel => EVENT EMITTER • ARRAY DIFF => SET DIFF
第45页
THANKS