vue双向绑定的简单实现(2)

3、在每次劫持对应dom节点的过程中,我们也会相对应的实现对该dom元素的数据绑定,以求在最后直接添加到为根节点的子元素即可,这个过程我们就在nodeToFragment函数中插入了compile函数来初始化绑定,并且添加递归函数实现所有子元素的初始绑定;

4、在compile函数中我们添加的数据又从何而来呢?对,正是因为这点,所以我们建立MVVM的构造函数Vue来实现数据支持,并实现在实例化时就执行nodeToFragment同时重构dom和实现初始化绑定compile;

5、好了,单向绑定就是这么简单,4个函数即可Vue => nodeToFragment => compile => isChild。

完成图如下

vue双向绑定的简单实现

好了,再回过来看看整体的流程图,我们已经实现了这一块了

vue双向绑定的简单实现

接下来,休息下,大家准备开始流程图后面的双向绑定,ok,还是按照单向绑定的顺序,先跟大家讲明实现逻辑;

1、创建数据监听者observer去监听view层数据的变化;(利用Object.defineProperty劫持所有要用到的数据)

2、当view层数据变化后,通过通知者Dep通知订阅者去实现数据的更新;(通知后,遍历所有用到数据的订阅者更新数据)

3、订阅者watcher接收到view层数据变更后,重新对变化的数据进行赋值,改变model层,从而改变所有view层用到过该数据的地方。(更新数据,并改变view层所有用到该数据的节点值)

上面是实现逻辑,下面将通过具体代码告诉大家每一步的做法,由于双向绑定中订阅者会涉及初始化绑定的过程,所以代码量较多,我会在大更改处用——为大家框出来

//判断每个dom节点是否拥有子节点,若有则返回该节点 function isChild(node){ if(node.childNodes.length ===0){ return false; } else{ return node; } } //利用文档碎片劫持dom结构及数据,进而进行dom的重构 function nodeToFragment(node,vm){ var frag = document.createDocumentFragment(); var child; while(child = node.firstChild){ //一级dom节点数据绑定 compile(child,vm); //判断每个一级dom节点是否有二级节点,若有则递归处理文档碎片 if(isChild(child)){ nodeToFragment(isChild(child),vm); } frag.appendChild(child); } node.appendChild(frag); } //初始化绑定数据 function compile(node,vm){ //node节点为元素节点时 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; //特殊处理input标签 //------------------------ if(node.nodeName === 'INPUT'){ node.addEventListener('keyup',function(e){ vm[name] = e.target.value; }) } //由于数据已经由data劫持至vm下,所以直接赋值vm[name]即可触发getter访问器 node.value = vm[name]; //------------------------- node.removeAttribute(attr[i].nodeName); } } } //node节点为text文本节点时 if(node.nodeType === 3){ var reg = /\{\{(.*)\}\}/; if(reg.test(node.nodeValue.trim())){ var name = RegExp.$1; //node.nodeValue = vm[name]; //---------------------- //为每个节点建立订阅者,通过订阅者watcher初始化及更新视图数据 new watcher(vm,node,name); //----------------------- } } } //---------------------------------------------------------------- //订阅者(为每个节点的数据建立watcher队列,每次接受更改数据需求后,利用劫持数据执行对应节点的数据更新) function watcher(vm,node,name){ //将每个挂载了数据的dom节点添加到通知者列表,要保证每次创建watcher时只有一个添加目标,否则后续会因为watcher是全局而被覆盖,所以每次要清空目标 Dep.target = this; this.vm = vm; this.node = node; this.name = name; //执行update的时候会调用监听者劫持的getter事件,从而添加到watcher队列,因为update中有访问this.vm[this.name] this.update(); //为保证只有一个全局watcher,添加到队列后,清空全局watcher Dep.target = null; } watcher.prototype = { update(){ this.get(); //input标签特殊处理化 if(this.node.nodeName === 'INPUT'){ this.node.value = this.value; } else{ this.node.nodeValue = this.value; } }, get(){ //这里调用了数据劫持的getter this.value = this.vm[this.name]; } }; //通知者(将监听者的更改信息需求发送给订阅者,告诉订阅者哪些数据需要更改) function Dep(){ this.subs = []; } Dep.prototype = { addSub(watcher){ //添加用到数据的节点进入watcher队列 this.subs.push(watcher); }, notify(){ //遍历watcher队列,令相应数据节点重新更新view层数据,model => view this.subs.forEach(function(watcher){ watcher.update(); }) } }; //监听者(利用setter监听view => model的数据变化,发出通知更改model数据后再从model => view更新视图所有用到该数据的地方) function observer(data,vm){ //遍历劫持data下所有属性 Object.keys(data).forEach(function(key){ defineReactive(vm,key,data[key]); }) } function defineReactive(vm,key,val){ //新建通知者 var dep = new Dep(); //灵活利用setter与getter访问器 Object.defineProperty(vm,key,{ get(){ //初始化数据更新时将每个数据的watcher添加至队列栈中 if(Dep.target) dep.addSub(Dep.target); return val; }, set(newVal){ if(val === newVal) return ; //初始化后,文档碎片中的虚拟dom已与model层数据绑定起来了 val = newVal; //同步更新model中data属性下的数据 vm.data[key] = val; //数据有改动时向通知者发送通知 dep.notify(); } }) } //--------------------------------------------------------------- function Vue(options){ this.id = options.el; this.data = options.data; observer(this.data,this); nodeToFragment(document.getElementById(this.id),this); } var vm = new Vue({ el:'app', data:{ msg:'hello,two-ways-binding', test:'test key' } })

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wwjpgd.html