MVVM模型
M:模型(Model):对应data中的数据
V:视图(View):模板
VM:视图模型(ViewModel):Vue实例对象
分解任务
<div id="app">
<input type="text" v-model="text">
{{text}}
</div>
拟实现:
1、输入框以及文本节点与data中的数据绑定
2、输入框内容变化时,data中的数据同步变化。即view => model的变化。
3、data中的数据变化时,文本节点的内容同步变化。即model => view的变化。
要实现任务一,需要对DOM进行编译,这里有一个知识点:DocumentFragment。
1、DocumentFragment
DocumentFragment(文档片段)可以看作节点容器,它可以包含多个子节点,当我们将它插入到DOM中时,只有它的子节点会插入目标节点,所以把它看作一组节点的容器。使用DocumentFragment处理节点,速度和性能远远优于直接操作DOM。Vue进行编译时,就是将挂载目标的所有子节点劫持(真的是劫持)到DocumentFragment中,经过一番处理后,再将DocumentFragment整体返回插入挂载目标。
var dom=nodeToFragment(document.getElementById('app'))
console.log(dom)
function nodeToFragment(node){
var flag=document.createDocumentFragment();
var child;
while(child=node.firstChild){
flag.append(child) //劫持node中的所有子节点
}
return flag;
}
2、数据初始化绑定
function nodeToFragment(node,vm){
var flag=document.createDocumentFragment();
var child;
while(child=node.firstChild){
compile(child,vm)
flag.append(child) //劫持node中的所有子节点
}
return flag;
}
function compile(node,vm){
//节点类型为元素
if(node.nodeType===1){
var attr=node.attributes;
for(var i=0;i<attr.length;i++){
if(attr[i].nodeName=='v-model'){
var name=attr[i].nodeValue; //获取v-model绑定的属性名
node.value=vm.data[name]; //将data的值赋给该node
node.removeAttribute('v-model')
}
}
}
var reg = /\{\{(.*)\}\}/;
//节点类型为文本
if(node.nodeType===3){
if(reg.test(node.textContent)){
var name=RegExp.$1; //获取匹配到的字符串
name=name.trim();
node.nodeValue=vm.data[name];//将data的值赋给该node
}
}
}
function MVVM(options){
this.data=options.data;
var id=options.el;
var dom=nodeToFragment(document.getElementById(id),this)
//编译完成后,把dom返回到app中
console.log(dom)
document.getElementById(id).appendChild(dom)
}
var vm=new MVVM({
el:'app',
data:{
text:'hello world'
}
})
以上代码实现了任务一,我们可以看到,hello world已经呈现在输入框和文本节点中。
3、响应式的数据绑定
再来看任务二的实现思路:当我们在输入框输入数据的时候,首先触发input事件(或者keyup、change事件),在相应的事件处理程序中,我们获取输入框的value并赋值给vm实例的text属性。我们会利用defineProperty将data中的text劫持为vm的访问器属性,因此给vm.text赋值,就会触发set方法。在set方法中主要做两件事,第一是更新属性的值,第二留到任务三再说。
function defineReactive(obj,key,val) {
Object.defineProperty(obj,key,{
get:function(){
return val
},
set:function(newVal){
if(newVal === val) return
val=newVal
console.log('set被调用了',val)
}
})
}
function observe(obj,vm) {
Object.keys(obj).forEach(function(key){
defineReactive(vm,key,obj[key])
})
}
function MVVM(options){
this.data=options.data;
var data=this.data
observe(data,this)
var id=options.el;
var dom=nodeToFragment(document.getElementById(id),this)
//编译完成后,把dom返回到app中
document.getElementById(id).appendChild(dom)
}
function compile(node,vm){
//节点类型为元素
if(node.nodeType===1){
//...
//...{
//...{
var name=attr[i].nodeValue; //获取v-model绑定的属性名
node,addEventListener('input',function(e){
vm[name]=e.target.value //触发vm的访问器属性的set
})
node.value=vm[name]; //将data的值赋给该node
node.removeAttribute('v-model')
}
}
}
//...
if(node.nodeType===3){
if(reg.test(node.textContent)){
//..
node.nodeValue=vm[name];//将data的值赋给该node
}
}
}
任务二也就完成了,text属性值会与输入框的内容同步变化(实现view->model):
4双向绑定的实现
回顾一下,每当new一个Vue,主要做了两件事:第一个是监听数据:observe(data),第二个是编译HTML:nodeToFragement(id)。
在监听数据的过程中,会为data中的每一个属性生成一个主题对象dep。
在编译HTML的过程中,会为每个与数据绑定相关的节点生成一个订阅者watcher,watcher会将自己添加到相应属性的dep中。
我们已经实现:修改输入框内容 => 在事件回调函数中修改属性值 => 触发属性的set方法。
接下来我们要实现的是:发出通知dep.notify() => 触发订阅者的update方法 => 更新视图。
这里的关键逻辑是:如何将watcher添加到关联属性的dep中。
function Watcher(vm,node,name){
Dep.target=this
this.name=name
this.node=node
this.vm=vm
this.updater()
Dep.target=null
}
Watcher.prototype={
updater:function() {
this.get()
this.node.nodeValue=this.value
},
//获取data中的属性值
get:function(){
this.value=this.vm[this.name]
}
}
首先,Watcher将自己赋给了一个全局变量Dep.target;
其次,执行了update方法,进而执行了get方法,get的方法读取了vm的访问器属性,从而触发了访问器属性的get方法,get方法中将该watcher添加到了对应访问器属性的dep中;
再次,获取属性的值,然后更新视图。
最后,将Dep.target设为空。因为它是全局变量,也是watcher与dep关联的唯一桥梁,任何时刻都必须保证Dep.target只有一个值。
function Dep(){
this.subs=[]
}
Dep.prototype={
addSub:function(sub){
this.subs.push(sub)
},
notify:function(){
this.subs.forEach(function(sub){
sub.update()
})
},
}
至此实现了双向绑定
总结
整理了一下,要实现mvvm的双向绑定,就必须要实现以下几点:
1、实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者
2、实现一个指令解析器Compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数
3、实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定的相应回调函数,从而更新视图
4、mvvm入口函数,整合以上三者