1添加订阅,2触发订阅(循环数组中的每一项)
构造函数中挂载了一个更新数据方法回调函数cb,(在实例构造函数时需要传递进来),最新的数据对象vm,需要更新的属性key
并定义了一个更新函数,调用上面的回调函数cb
在编译器中当数据变化进行更新的时候,需要创建一个watchers实例
在watcher类的构造函数中添加如下三行代码:1.订阅器的target属性等于当前的watcher
2.对当前的值key通过第二行代码进行获取(此时会调用get函数)
3.另dep.target=null
4.在get函数中判断dep.target如果存在,则调用添加订阅者函数
获取vue实例中el的位置,将data中的数据填充进el中,然后将结果渲染出来
vue双向数据绑定原理
根据最新的数据渲染自身的结构
complie编译器:1.将dom中的节点放入,文档碎片中
2.2.对文本子节点进行正则匹配与提取,替换
3.将文档碎片重新移入dom节点,进行渲染
频繁对dom节点进行操作会不停的触发重绘和重排,所以需要使用文档碎片(将节点元素存入内存中,这样页面上就没有dom元素了,随意修改它也不会触发重绘和重排,
对文档碎片进行编译,再将结果重新渲染到页面上),
正则表达式中:空白字符用/s*,*表示0个或多个
目前几种主流的mvc(vm)框架都实现了单向数据绑定,而我所理解的双向数据绑定无非就是在单向绑定的基础上给可输入元素(input、textare等)添加了change(input)事件,来动态修改model和view,并没有多高深。所以无需太过介怀是实现的单向或双向绑定。
实现数据绑定的做法有大致如下几种:
脏值检查(angular.js)
数据劫持(vue.js)
这种方式现在毕竟太low了,我们更希望通过vm.property=value这种方式更新数据,同时自动更新视图,于是有了下面两种方式
脏值检查:angular.js是通过脏值检测的方式比对数据是否有变更,来决定是否更新视图,最简单的方式就是通过setInterval()定时轮询检测数据变动,当然Google不会这么low,angular只有在指定的事件触发时进入脏值检测,大致如下:
数据劫持:vue.js则是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。
vardata={name:'kindeng'};observe(data);data.name='dmq';//哈哈哈,监听到值变化了kindeng-->dmqfunctionobserve(data){if(!data||typeofdata!=='object'){return;}//取出所有属性遍历,object.keys(data)得到对象的属性名数组,通过forEach()遍历属性名,属性名为参数key,data还是原来的对象Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);});};functiondefineReactive(data,key,val){observe(val);//监听子属性Object.defineProperty(data,key,{enumerable:true,//可枚举configurable:false,//不能再defineget:function(){returnval;},//newVal为改变后的值,set方法中默认传递的参数set:function(newVal){console.log('哈哈哈,监听到值变化了',val,'-->',newVal);val=newVal;}});}这样我们已经可以监听每个数据的变化了,那么监听到变化之后就是怎么通知订阅者了,所以接下来我们需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,再调用订阅者的update方法,代码改善之后是这样:
//...val为data[key]对应的值functiondefineReactive(data,key,val){vardep=newDep();observe(val);//监听子属性Object.defineProperty(data,key,{//...省略set:function(newVal){if(val===newVal)return;console.log('哈哈哈,监听到值变化了',val,'-->',newVal);val=newVal;dep.notify();//通知所有订阅者}});}functionDep(){this.subs=[];}Dep.prototype={addSub:function(sub){this.subs.push(sub);},notify:function(){this.subs.forEach(function(sub){sub.update();});}};那么问题来了,谁是订阅者?怎么往订阅器添加订阅者?没错,上面的思路整理中我们已经明确订阅者应该是Watcher,而且vardep=newDep();是在defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们可以在getter里面动手脚:
因为遍历解析的过程有多次操作dom节点,为提高性能和效率,会先将跟节点el转换成文档碎片fragment进行解析编译操作,解析完成,再将fragment添加回原来的真实dom节点中
functionCompile(el){this.$el=this.isElementNode(el)el:document.querySelector(el);if(this.$el){this.$fragment=this.node2Fragment(this.$el);this.init();this.$el.appendChild(this.$fragment);}}Compile.prototype={init:function(){this.compileElement(this.$fragment);},node2Fragment:function(el){varfragment=document.createDocumentFragment(),child;//将原生节点拷贝到fragmentwhile(child=el.firstChild){fragment.appendChild(child);}returnfragment;}};compileElement方法将遍历所有节点及其子节点,进行扫描解析编译,调用对应的指令渲染函数进行数据渲染,并调用对应的指令更新函数进行绑定,详看代码及注释说明:
Watcher订阅者作为Observer和Compile之间通信的桥梁,主要做的事情是:1、在自身实例化时往属性订阅器(dep)里面添加自己2、自身必须有一个update()方法3、待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。如果有点乱,可以回顾下前面的思路整理
MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化->视图更新;视图交互变化(input)->数据model变更的双向绑定效果。
一个简单的MVVM构造器是这样子:
functionMVVM(options){this.$options=options;vardata=this._data=this.$options.data;observe(data,this);this.$compile=newCompile(options.el||document.body,this)}但是这里有个问题,从代码中可看出监听的数据对象是options.data,每次需要更新视图,则必须通过varvm=newMVVM({data:{name:'kindeng'}});vm._data.name='dmq';这样的方式来改变数据。
显然不符合我们一开始的期望,我们所期望的调用方式应该是这样的:varvm=newMVVM({data:{name:'kindeng'}});vm.name='dmq';
所以这里需要给MVVM实例添加一个属性代理的方法,使访问vm的属性代理为访问vm._data的属性,改造后的代码如下:
functionMVVM(options){this.$options=options;vardata=this._data=this.$options.data,me=this;//属性代理,实现vm.xxx->vm._data.xxxObject.keys(data).forEach(function(key){me._proxy(key);});observe(data,this);this.$compile=newCompile(options.el||document.body,this)}MVVM.prototype={_proxy:function(key){varme=this;Object.defineProperty(me,key,{configurable:false,enumerable:true,get:functionproxyGetter(){returnme._data[key];},set:functionproxySetter(newVal){me._data[key]=newVal;}});}};这里主要还是利用了Object.defineProperty()这个方法来劫持了vm实例对象的属性的读写权,使读写vm实例的属性转成读写了vm._data的属性值,达到鱼目混珠的效果,哈哈
本文主要围绕“几种实现双向绑定的做法”、“实现Observer”、“实现Compile”、“实现Watcher”、“实现MVVM”这几个模块来阐述了双向绑定的原理和实现。并根据思路流程渐进梳理讲解了一些细节思路和比较关键的内容点,以及通过展示部分关键代码讲述了怎样一步步实现一个双向绑定MVVM。文中肯定会有一些不够严谨的思考和错误,欢迎大家指正,有兴趣欢迎一起探讨和改进~