当前位置: 首页 > Web前端 > vue.js

模拟Vue.js响应式原理

时间:2023-03-31 19:46:47 vue.js

数据驱动开发只需要关注数据,而不是关系数据如何渲染到视图数据响应式:当数据被修改时,视图会相应更新,避免了两次-DOM操作的方式绑定:数据变化,视图变化;视图变化,数据变化发布订阅模式和观察者模式定义了对象之间一对多的依赖关系。当目标对象的状态发生变化时,所有依赖于它的对象都会得到通知。观察者模式publisher:有一个notify方法,当发生变化时调用观察者对象的update方法Observer:有一个update方法.push(sub)}notify(){this.subs.forEach(handler=>{handler.update()})}}classObserver{constructor(name){this.name=name;}update(){console.log('收到通知',this.name);}}constsubscriber=newSubscriber();constjack=newObserver('杰克');consttom=newObserver('tom');subscriber.add(jack)subscriber.add(tom)subscriber.notify()发布-订阅模式发布-订阅模式和观察者模式的区别在于在发布者和观察者之间引入了一个事件中心。目标对象不直接通知观察者,而是通过事件中心派发通知。类EventController{constructor(){this.subs={}}subscribe(key,fn){this.subs[key]=this.subs[key]||[]this.subs[key].push(fn)}publish(key,...args){if(this.subs[key]){这个。子[键]。forEach(handler=>{handler(...args)});}}}constevent=newEventController()event.subscribe('onWork',time=>{console.log('offwork',time)});event.subscribe('offWork',time=>{console.log('offwork',time);});event.subscribe('onLaunch',time=>{console.log('eating',time);});event.publish('offWork','18:00:00');event.publish('onLaunch','20:00:00');总结观察者模式是由一个特定的目标来调度的。当事件触发时,发布者会主动调用观察者的方法,所以发布者和观察者之间存在依赖关系发布-订阅模式由事件中心统一调度,因为发布者之间没有强依赖和订户。数据响应的核心原则。Vue2当你将一个普通的JavaScript对象作为数据选项传递给Vue实例时,Vue会遍历这个对象的所有属性,并使用Object.defineProperty将这些属性全部转换为getter/setter。Object.defineProperty是ES5中无法shimmed的特性,这也是Vue不支持IE8及以下浏览器的原因。functionproxyData(data){//遍历数据对象的所有属性Object.keys(data).forEach(key=>{//将data中的属性转换成vm的setter/setterObject.defineProperty(vm,key,{可枚举:true,可配置:true,get(){console.log('get:',key,data[key])returndata[key]},set(newValue){console.log('set:',key,newValue)if(newValue===data[key]){return}data[key]=newValue//数据变化,更新DOM值document.querySelector('#app').textContent=data[key]}})})}由于JavaScript的限制,Vue无法检测到以下数组变化:当使用索引直接设置数组项时,例如:vm.items[indexOfItem]=newValue当修改数组长度时,例如:vm.items.length=newLengthObject.defineProperty在数组中的表现和在对象中的表现是一样的,数组的索引可以看作是对象中的key。当通过索引访问或设置相应元素的值时,可以通过push或unshift触发getter和setter方法增加索引。对于新添加的属性,需要手动初始化后才能被观察到。通过pop或shift删除元素会删除和更新索引,同时也会触发setter和getter方法。这两点在官方文档中简单概括为“由于JavaScript的限制”无法实现,其实原因并不是Object.defineProperty的漏洞,而是性能方面的考虑。Vue3Proxy是ES6中的新语法,IE不支持,浏览器对其性能进行了优化。Proxy直接监控对象,不监控属性,defineProperty监控对象中的一个属性。letvm=newProxy(data,{//执行代理行为的函数//当访问vm的成员时将执行get(target,key){console.log('get,key:',key,target[key])returntarget[key]},//设置vm成员时会执行set(target,key,newValue){console.log('set,key:',key,newValue)if(target[key]===newValue){return}target[key]=newValuedocument.querySelector('#app').textContent=target[key]}})实现过程Vue接收初始化参数,注入Vuereal列的$options属性并将转换后的数据属性注入到Vue实例的$data属性中,生成getter/setter调用观察者监听数据中属性的变化并调用编译器解析指令/插值表达式classVue{constructor(options){//1.通过属性保存option的数据this.$options=options||{};this.$data=options.data||{};this.$el=typeofoptions.el==='string'?document.querySelector(options.el):选项.el;//2.将data中的成员转换成getter和setter,注入到vue实例中this._proxyData(this.$data);//3.调用观察者对象监听数据变化newObserver(this.$data);//4.调用编译器对象,解析指令和差分表达式newCo编译器(这个);}_proxyData(data){Object.keys(data).forEach(key=>{//将data的属性注入到vue实例中Object.defineProperty(this,key,{configurable:true,enumerable:true,get(){returndata[key];},set(newValue){if(newValue===data[key]){return;}data[key]=newValue;}})})}}Observerputdata中的属性被转换响应式数据数据变化通知类Observer{constructor(data){this.walk(data);}walk(data){//1.判断data是否为对象if(!data||typeofdata!=='object'){return}//2.遍历data对象的所有属性Object.keys(data).forEach(key=>{this.defineReactive(data,key,data[key])})}defineReactive(data,key,val){//如果val是一个对象,将val内部的属性转换为响应式数据this.walk(val)constthat=this;//收集依赖并发送通知constdep=newDep();Object.defineProperty(data,key,{configurable:true,enumerable:true,get(){//收集依赖Dep.target&&dep.addSubs(Dep.target);返回值;},set(newVal){如果(newVal===val){返回;}val=newVal;//如果newValue是一个对象,将newValue的内部属性转换为响应式数据that.walk(newVal);//发送通知dep.notify();}})}}Compiler编译模板,解析指令页面渲染,数据变化时重新渲染视图classCompiler{constructor(vm){this.el=vm.$el;这个.vm=虚拟机;这个。编译(this.el);}//编译模板,处理文本节点和祖先节点compile(el){Array.from(el.childNodes).forEach(node=>{//处理文本节点if(this.isTextNode(node)){this.compileText(node)}elseif(this.isElementNode(node)){//处理元素节点this.compileElement(node)}//判断node节点是否有子节点,如果有子节点,递归调用compileif(node.childNodes&&node.childNodes.length){this.compile(node)}})}//编译文本节点并处理差异表达式compileText(node){constreg=/\{\{(.+?)\}\}/如果(reg.test(node.textContent)){constkey=RegExp.$1.trim();node.textContent=node.textContent.replace(reg,this.vm[key]);//创建一个watcher对象并在数据变化时更新视图newWatcher(this.vm,key,(newValue)=>{node.textContent=newValue});}}//编译元素节点,处理指令compileElement(node){Array.from(node.attributes).forEach(attr=>{if(this.isDirective(attr.name)){}constattrName=attr.name.substr(2);constkey=attr.value;constupdateFn=this[attrName+'Updater']updateFn&&updateFn.call(this,node,this.vm[key],key)})}//v-texttextUpdater(node,value,key){node.textContent=value;//创建一个watcher对象并在数据变化时更新视图newWatcher(this.vm,key,(newValue)=>{node.textContent=newValue});}//v-modelmodelUpdater(node,value,key){node.value=value;//创建一个watcher对象并在数据变化时更新视图newWatcher(this.vm,key,(newValue)=>{node.value=newValue});//双向绑定node.addEventListener('输入',()=>{this.vm[key]=node.value});}//判断元素属性是否为指令isDirective(attrName){returnattrName.startsWith('v-');}//判断节点是否为文本节点isTextNode(node){returnnode.nodeType===3;}//判断节点是否为元素节点isElementNode(node){returnnode.nodeType===1;}}Deppublisheringetter在setter中添加观察者并发送通知classDep{constructor(){//存储所有观察者this.subs=[];}//添加观察者addSubs(sub){if(sub&&sub.update){this.subs.push(sub);}}//发送通知notify(){this.subs.forEach(sub=>{sub.update()});}}watcheraddedtodep实例化时数据发生变化,dep通知所有watcher更新视图classWatcher{constructor(vm,key,cb){//vueinstancethis.vm=vm;//数据中的属性名this.key=key;//回调函数负责更新视图this.cb=cb;//记录watcher对象到Dep类target的静态属性Dep.target=this;//触发get方法,addSub会在get方法中被调用this.oldValue=vm[key];//留空,防止影响其他属性Dep.target=null;}//当数据改变时更新视图update(){constnewValue=this.vm[this.key]if(this.oldValue===newValue){return}this.cb(newValue);}}双向绑定监听文本框的输入事件并更新节点值//双向绑定node.addEventListener('input',()=>{this.vm[key]=node.value});