🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
### 几种实现双向绑定的做法 主流的mvc(vm)框架,都实现了单向数据绑定,而双向数据绑定无非就是在单向绑定的基础上给可输入元素(input,textarea)等添加了change(input)事件,来动态修改model和view,并没有多高深。 实现数据绑定的做法 1. 发布者-订阅者(backbone.js) 2. 脏值检查(angular.js) 3. 数据劫持(vue.js) #### 发布者-订阅者 一般通过sub,pub的方式实现数据和视图的绑定监听,更新数据的方式通常做法 vm.set('property',value),这种方式毕竟太low,我们更希望**vm.property=value这种方式更新数据,同时自动更新视图**。 以下两种方式: #### 脏值检查 angular.js是通过脏值检测的方式对比数据是否有变更,来决定是否更新视图,最简单的方式就是 **通过setInterval()定时轮询检测数据变动**,当然google不会这么low,**angular只有在指定的事件触发时进入脏值检测**,大致如下: * DOM事件,譬如用户输入文本,点击按钮等(ng-click) * XHR响应事件($http) * 浏览器Location变更事件($location) * Timer事件($timeout,$interval) * 执行$digest()或$apply() ### 数据劫持 vue.js采用**数据劫持结合发布者-订阅者模式**的方式,通过**Object.defineProperty()来劫持各个属性的setter和getter,在数据变动时发布消息给订阅者,触发相应的监听回调**。 ### 整理思路 vue是通过数据劫持的方式来做数据绑定的,其中最核心的方法便是通过Object.defineProperty()来实现对属性的劫持,达到监听数据变动的目的。 要实现mvvm的双向绑定 1. 实现一个数据监听器Observer,能够对数据对象的所有属性进行监听,如有变动可拿到最新值并通知订阅者 2. 实现一个指令解析器compile,对每个元素节点的指令进行扫描和解析,根据指令模板替换数据,以及绑定相应的更新函数 3. 实现一个Watcher,作为连接Observer和Compile的桥梁,能够订阅并收到每个属性变动的通知,执行指令绑定 ### 1. 实现Observer 可以利用Object.defineProperty()来监听属性变动 那么将**需要observe的数据对象进行递归遍历,包括自属性对象的属性,都加上setter和getter** 这样的话,给这个对象的某个值赋值,就会触发setter,那么就能坚挺到了数据变化。 ``` var data={name:'hhh'} observe(data); function observe(data){ if(!data || typeof data !=='object'){ return; } //去除所有属性遍历 Object.keys(data).forEach(function(key){ defineReactive(data,key,data[key]); }); }; function defineReactive(data,key,val){ observe(val);//监听子属性 Object.defineProperty(data,key,{ enumerable:true,//可枚举 configurable:false,//不能在define get:function(){ return val; }, set:function(newVal){ console.log('监听到值的变化了',val,newVal); val=newVal; } }) } ``` 这样我们已经可以监听每个数据的变化了,那么监听到变化怎么通知订阅者呢?接下来需要实现一个消息订阅器,很简单,维护一个数组,用来收集订阅者,数据变动触发notify,在调用订阅者的update方法 ``` function defineReactive(data, key, val){ var dep = new Dep(); observe(val); Object.defineProperty(data,key,{ //省略 set:function(newVal){ if(val === newVal) return; console.log('监听到值的变化了',val,newVal); val=newVal; dep.notify();//通知所有订阅者 } }) } function Dep(){ this.subs=[]; } Dep.prototype={ addSub: function(sub){ this.subs.push(sub); }, notify:function(){ this.subs.forEach(function(sub){ sub.update(); }) } } ``` 那么谁是订阅者?怎么网订阅器里添加订阅者? 上面的思路整理中,我们已经明确订阅者应该是Watcher,而且 var dep = new Dep();是在defineReactive方法内部定义的,所以想通过dep添加订阅者,就必须要在闭包内操作,所以我们在getter里面动手脚 ``` // Observer.js var dep = new Dep(); observe(val); // 监听子属性 Object.defineProperty(data, key, { get: function(){ //由于需要在闭包内添加watcher,所以通过Dep定义一个全局target属性,暂存watcher,添加完移除 Dep.target && addDep(Dep.target); return val; } }) function Dep() { this.subs = []; } Dep.prototype = { addSub: function(sub) { this.subs.push(sub); }, notify: function() { this.subs.forEach(function(sub) { sub.update(); }); } }; //Watcher.js Watcher.prototype = { get:function(key){ Dep.target = this; this.value = data[key];//这里会触发属性的getter,从而添加订阅者 Dep.target = null } } ``` 这里已经实现了一个Observer了,已经具备了监听数据和数据变化通知订阅者的功能。 ### 2. 实现Compile compile:解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。 因为遍历接卸的过程有多次操作dom,为了提高效率,将根节点el转换成碎片fragment进行接卸编译操作,解析完成,在将fragment添加回原来的真是dom节点中 #### Object.defineProperty()方法 会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象 `Object.defineProperty(obj, prop, descriptor)` descriptor:将被定义或修改的属性描述符 返回值:传递给函数的对象。 描述:该方法允许精确添加或修改对象的属性。通过赋值操作添加的普通属性是可枚举的(for in,Object.keys),这些属性的值可以被改变,也可以被删除,这个**方法允许修改默认的额外选项(配置)**。默认情况下,使用Object.defineProperty添加的属性是不可修改的。 #### 属性描述符 **数据描述符和存取描述符** 数据描述符:是一个具有值的属性,该值可能是可写的,也可能不是可写的。 存取描述符:有getter-setter函数对描述的属性。 描述符必须是这两种形式之一。 数据描述符和存取描述符均具有以下可选键值: configurable:true,**true时,该属性描述符才能够被改变**,默认false。 enumerable:**true时,该属性才能够出现对象的枚举属性中**。默认false。 **数据描述符**同时具有以下可选键值: value:属性值,默认undefined writable:true时,value才能被赋值运算符改变,默认false。 **存取描述符**同时具有以下可选键值: get:一个给属性提供getter的方法**,如果没有getter则为undefined**,当访问该属性时,改方法会被执行,方法执行时没有参数传入,但是会传入this对象(this并以一定是定义改属性的对象)。 set:一个给属性提供setter的方法,如果没有setter则为undefined,当属性值修改时,触发改方法。 描述符可同时具有的键值 如果一个描述符**不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符**。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生**一个异常**。 记住:这些选项不一定是自身属性,如果是继承来的也要考虑。 为了确认保留这些默认值,你可能要在这之前**冻结 Object.prototype**,明确指定所有的选项,或者通过 **Object.create(null)将__proto__属性指向null**。