深入理解Vue二

哪家白癜风医院较好 http://yyk.39.net/bj/zhuanke/89ac7.html

实现过程

我们已经知道如何实现数据的双向绑定了, 那么首先要对数据进行劫持监听,所以我们首先要设置一个监听器Observer,用来监听所有的属性,当属性变化时,就需要通知订阅者Watcher,看是否需要更新.因为属性可能是多个,所以会有多个订阅者,故我们需要一个消息订阅器Dep来专门收集这些订阅者,并在监听器Observer和订阅者Watcher之间进行统一的管理.以为在节点元素上可能存在一些指令,所以我们还需要有一个指令解析器Compile,对每个节点元素进行扫描和解析,将相关指令初始化成一个订阅者Watcher,并替换模板数据并绑定相应的函数,这时候当订阅者Watcher接受到相应属性的变化,就会执行相对应的更新函数,从而更新视图.

整理上面的思路,我们需要实现三个步骤,来完成双向绑定:

1.实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。2.实现一个订阅者Watcher,可以收到属性的变化通知并执行相应的函数,从而更新视图。3.实现一个解析器Compile,可以扫描和解析每个节点的相关指令,并根据初始化模板数据以及初始化相应的订阅器。

流程图如下:

1.实现一个监听器Observer

数据监听器的核心方法就是Object.defineProperty(),通过遍历循环对所有属性值进行监听,并对其进行Object.defineProperty()处理,那么代码可以这样写:

//对所有属性都要监听,递归遍历所有属性functiondefineReactive(data,key,val){observe(val);//递归遍历所有的属性Object.defineProperty(data,key,{enumerable:true,//当且仅当该属性的configurable为true时,该属性描述符才能够被改变,同时该属性也能从对应的对象上被删除。configurable:true,//当且仅当该属性的enumerable为true时,该属性才能够出现在对象的枚举属性中get:function(){returnval;},set:function(newVal){val=newVal;console.log(属性+key+已经被监听,现在值为:"+newVal.toString()+");}})}functionobserve(data){if(!data

typeofdata!==object){return;}Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);});}varlibrary={book1:{name:},book2:};observe(library);library.book1.name=vue权威指南;//属性name已经被监听了,现在值为:“vue权威指南”library.book2=没有此书籍;//属性book2已经被监听了,现在值为:“没有此书籍”

通过observe()方法进行遍历向下找到所有的属性,并通过defineReactive()方法进行数据劫持监听.

在上面的思路中,我们需要一个可以容纳消息订阅者的消息订阅器Dep,订阅器主要收集消息订阅者,然后在属性变化时执行相应订阅者的更新函数,那么消息订阅器Dep需要有一个容器,用来存放消息订阅者.我们将上面的监听器Observer稍微修改一下:

functiondefineReactive(data,key,val){observe(val);vardep=newDep();Object.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){if(是否需要添加订阅者){//Watcher初始化触发dep.addSub(watcher);//在这里添加一个订阅者}returnval;},set:function(newVal){if(val===newVal){return;}val=newVal;console.log(属性+key+已经被监听了,现在值为:“+newVal.toString()+”);dep.notify();//如果数据变化,通知所有订阅者}});}functionobserve(data){if(!data

typeofdata!==object){return;}Object.keys(data).forEach(function(key){defineReactive(data,key,data[key]);});}functionDep(){this.subs=[];}//prototype属性使您有能力向对象添加属性和方法//prototype这个属性只有函数对象才有,具体的说就是构造函数具有.只要你声明定义了一个函数对象,这个prototype就会存在//对象实例是没有这个属性Dep.prototype={addSub:function(sub){this.subs.push(sub);},notify:function(){this.subs.forEach(function(sub){sub.update();//通知每个订阅者检查更新})}}Dep.target=null;

在代码中,我们将订阅器Dep添加一个订阅者设计在get里面,这是为了让Watcher在初始化时触发,因此判断是否需要需要添加订阅者,至于具体实现的方法,我们在下文中深究.在set方法中,如果函数变化,就会通知所有的订阅者,订阅者们将会执行相对应的更新函数,到目前为止,一个比较完善的Observer已经成型了,下面我们要写订阅者Watcher.

2.实现订阅者Watcher

根据我们的思路,订阅者Wahcher在初始化时要将自己添加到订阅器Dep中,那么如何进行添加呢?

我们已经知道监听器Observer是在get函数中执行了添加订阅者的操作的,所以我们只需要在订阅者Watcher在初始化时触发相对应的get函数来执行添加订阅者的操作即可.那么怎么触发对应的get函数呢?我们只需要获取对应的属性值,就可以通过Object.defineProperty()触发对应的get了.

在这里需要注意一个细节,我们只需要在订阅者初始化时才执行添加订阅者,所以我们需要一个判断,在Dep.target上缓存一下订阅者,添加成功后去除就行了,代码如下:

functionWatcher(vm,exp,cb){this.vm=vm;//指向SelfVue的作用域this.exp=exp;//绑定属性的key值this.cb=cb;//闭包this.value=this.get();}Watcher.prototype={update:function(){this.run();},run:function(){varvalue=this.vm.data[this.exp];varoldVal=this.value;if(value!==oldVal){this.value=value;this.cb.call(this.vm,value,oldVal);}},get:function(){Dep.target=this;//缓存自己varvalue=this.vm.data[this.exp];//强制执行监听器里的get函数Dep.target=null;//释放自己returnvalue;}}

这个时候我们需要对监听器Observer中的defineReactive()做稍微的调整:

functiondefineReactive(data,key,val){observe(val);vardep=newDep();Object.defineProperty(data,key,{enumerable:true,configurable:true,get:function(){if(Dep.target){//判断是否需要添加订阅者dep.addSub(Dep.target);}returnval;},set:function(newVal){if(val===newVal){return;}val=newVal;console.log(属性+key+已经被监听了,现在值为:“+newVal.toString()+”);dep.notify();//如果数据变化,通知所有订阅者}});}

到目前为止,一个简易版的Watcher已经成型了,我们只需要将订阅者Watcher和监听器Observer关联起来,就可以实现一个简单的双向绑定.因为这里还没有设计指令解析器,所以对于模板数据我们都进行写死处理,假设模板上有一个节点元素,且id为'name',并且双向绑定的绑定变量也是name,且是通过两个大双括号包起来(暂时没有什么用处),模板代码如下:

bodyh1id="name"{{name}}/h1/body

我们需要定义一个SelfVue类,来实现observer和watcher的关联,代码如下:

//将Observer和Watcher关联起来functionSelfVue(data,el,exp){this.data=data;observe(data);el.innerHTML=this.data[exp];newWatcher(this,exp,function(value){el.innerHTML=value;});returnthis;}

然后在页面上new一个SelfVue,就可以实现双向绑定了:

bodyh1id="name"{{name}}/h1/bodyscriptsrc="../js/observer.js"/scriptscriptsrc="../js/Watcher.js"/scriptscriptsrc="../js/SelfVue.js"/scriptscriptvarele=document.querySelector(#name);varselfVue=newSelfVue({name:helloworld},ele,name);window.setTimeout(function(){console.log(name值改变了);selfVue.name=byebyeworld;},);/script

这时我们打开页面,显示的是helloworld,2s后变成了byebyeworld,一个简单的双向绑定实现了.

对比vue,我们发现了有一个问题,我们在为属性赋值的时候形式是:selfVue.data.name=byebyeworld,而我们理想的形式是:selfVue.name=byebyeworld,那么怎么实现这种形式呢,只需要在newSelfVue时做一个代理处理,让访问SelfVue的属性代理为访问selfVue.data的属性,原理还是使用Object.defineProperty()对属性在包装一层.代码如下:

functionSelfVue(data,el,exp){varself=this;this.data=data;//Object.keys()方法会返回一个由一个给定对象的自身可枚举属性组成的数组Object.keys(data).forEach(function(key){self.proxyKeys(key);//绑定代理属性});observe(data);el.innerHTML=this.data[exp];//初始化模板数据的值newWatcher(this,exp,function(value){el.innerHTML=value;});returnthis;}SelfVue.prototype={proxyKeys:function(key){varself=this;Object.defineProperty(this,key,{enumerable:false,configurable:true,get:functionproxyGetter(){returnself.data[key];},set:functionproxySetter(newVal){self.data[key]=newVal;}});}}

这样我们就可以用理想的形式改变模板数据了.

3.实现指令解析器Compile

再上面的双向绑定demo中,我们发现整个过程都没有解析dom节点,而是固定某个节点进行替换数据,所以接下来我们要实现一个解析器Compile来解析和绑定工作,分析解析器的作用,实现步骤如下:

1.解析模板指令,并替换模板数据,初始化视图2.将模板指令对应的节点绑定对应的更新函数,初始化相应的订阅器

为了解析模板,首先要获得dom元素,然后对含有dom元素上含有指令的节点进行处理,这个过程对dom元素的操作比较繁琐,所以我们可以先建一个fragment片段,将需要解析的dom元素存到fragment片段中在做处理:

nodeToFragment:function(el){varfragment=document.createDocumentFragment();//createdocumentfragment()方法创建了一虚拟的节点对象,节点对象包含所有属性和方法。varchild=el.firstChild;while(child){//将Dom元素移入fragment中fragment.appendChild(child);child=el.firstChild;}returnfragment;}

接下来需要遍历所有节点,对含有指令的节点进行特殊的处理,这里我们先处理最简单的情况,只对带有{{变量}}这种形式的指令进行处理,

代码如下:

//遍历各个节点,对含有相关指定的节点进行特殊处理



转载请注明地址:http://www.sanbaicaoasb.com/scgj/8127.html
  • 上一篇文章:
  • 下一篇文章:
  • 热点文章

    • 没有热点文章

    推荐文章

    • 没有推荐文章