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

从设计模式理解Vue响应式(多图预警)

时间:2023-03-31 18:32:45 vue.js

从设计模式理解Vue响应式前言最近公司开发一个拖拽表单项目,用到了Vue,部门老大开始研究Vue源码,传给我们,boss说看源码不仅能看懂源码,还能理解他的设计思路。他为什么要这样设计,把自己当成设计师来读,才能真正明白。在这篇文章中,我将按照老大的方向,根据自己的理解来谈谈Vue的响应式原理及其设计思想。Vue响应式的官方解释是什么:Vue最独特的特性之一就是它的非侵入式响应系统。数据模型只是普通的JavaScript对象。当您修改它们时,视图将被更新。简单地说,如果数据发生变化,视图将相应更新。如果视图发生变化,比如输入,数据也会随之变化。响应式原理及其设计模式Vue响应式使用的设计模式是观察者模式,通俗地说就是,比如:郭先生每天上班带一个橙子,他吃的时候我想咬一口橘子,但是我不想一直盯着郭老师看他什么时候吃,就跟郭老师说,你吃橘子的时候告诉我,郭老师吃橘子的时候,我就收到了信息,立即让我的Whatyouwanttodo(takeasip)这样做可以节省重复搜索的资源消耗,获得更高的反馈速度。观察对象(Subject):它有两个必要的身份,通知当前实例拥有的观察者的方法。一种将观察者添加到当前实例的方法。观察者(Observer):具有必要的标识,通知更新实例状态的方法。联系方式:观察对象(Subject)通过自身内部的通知函数调用所有观察者列表中所有观察者对应的回调函数,达到通知观察者的目的。观察者(Observer)通过调用观察者对象(Subject)中的add方法,将自己的回调函数传递到自己的观察者列表中。响应式和观察者模式的整合我们先来看看实现一个基本的Vue.js需要哪些文件。我们以友达的miniVue作为demo进行讨论。我们先来一一介绍这几个文件的作用;vue是一个事件总线文件,会把对象描述文本中的data数据取出来,然后通过_proxyData函数劫持data的所有数据。劫持后Data会有getter和setter两个方法。getter在读取时触发并用于添加依赖项。setter在分配时触发以通知更新。观察者文件,这里的walk方法递归遍历data中的每一个属性。然后在defineReactive中为每个属性新建一个Dep来存放自己的依赖(观察者)。Object.defineProperty是响应性的根源。如果是triggeredget,将其Dep.target添加到Deplist中,这一步是收集依赖。你接受了我的陈述,你对我感兴趣,所以我把你加到我的观察员表里了。如果触发了set,??说明数据发生变化,触发notifyindep,通知大家。观察员们,有数据更新,赶紧行动吧。dep文件是观察者模式下的一个观察对象(Subject),它有一个存放观察的容器,以及添加观察者和通知观察者的方法。编译器文件是一个处理文件,专门用来处理解析指令、差异表达式等的集合。watcher文件是观察者模式下的观察者,里面有更新视图的方法,调用dep中添加观察者的方法。总结从上面文件的基本描述,我们可以看出Vue的响应式使用的是观察者模式。它有一个观察对象dep.js和一个观察者watcher.js。在watcher.js中,观察者会被添加到dep的观察者列表中,当dep发送通知时,会收到消息,然后触发自身的updateview方法。这样就采用了观察者模式来实现响应式。Vue实例初始化下面我们一一看一下这三个步骤的源码。首先是入口文件vue.js。Data中所有数据的代理都是通过defineProperty完成的。classVue{constructor(options){//1.通过数据存储数据this.$options=options||{}//保存选项this.$el=typeofoptions.el==='string'?document.querySelector(options.el):options.el//getdomthis.$data=options.data//获取数据this.$methods=options.methods//2.设置属性的setter和getter方法this._proxyData(this.$data)//3.调用observe对象监听数据变化newObserver(this.$data)//4.调用compiler对象,解析指令和差异表达式newCompiler(this)}_proxyData(data){//遍历所有数据Object.keys(data).forEach(key=>{//将数据属性注入vueObject.defineProperty(this,key,{enumerable:true,configurable:true,get(){returndata[key]},set(newValue){if(data[key]===newValue){return}data[key]=newValue}})})}}然后进入Observe.js,遍历所有数据,收集dependencies如果是get,如果是set,会发送通知classObserver{constructor(data){this.walk(data)}walk(data){//循环执行数据if(!data||typeofdata!=='object'){return}Object.keys(data).forEach(key=>{this.defineReactive(数据,键,数据[键])})}defineReactive(obj,key,val){letthat=thisthis.walk(val)//如果val是一个对象,给他绑定get和set时触发的方法letdep=newDep()//负责用于收集依赖项和发送通知[key],会变成死循环},set(newValue){if(newValue===val){return}val=newValuethat.walk(newValue)//修改后可能是一个对象,set内部调用函数,修改为指向dep.notify()//发送通知}})}}总结:这个文件是一个响应式实现,使用walk方法,递归遍历data中的每个属性,然后放入defineReactive为每个对象新建一个Dep来存放自己的依赖(观察者),然后开启对象的可读可写属性,定义一个set和get触发方法。如果是triggeredget,就会将他的Dep.target添加到Dep列表中,这一步是收集依赖,你拿了我的声明说你对我感兴趣,所以我把你添加到我的观察者列表中。如果触发了set,??说明数据发生变化,触发notifyindep,通知所有观察者数据已经更新,所以要迅速行动,以达到响应式。然后输入watcher.js。这里使用了一个Dep类的静态属性target来记录当前的watcher对象。通过使用其中的属性,触发get将自己的依赖添加到观察者列表中,形成观察者模式,然后恢复Dep.target,方便下次使用Watcher。收到被观察对象的通知后,调用自身的update方法完成视图更新。classWatcher{constructor(vm,key,cb){this.vm=vm//数据中的属性名this.key=key//回调函数负责更新视图this.cb=cb//记录watcher对象到Dep类的静态属性targetDep.target=this//触发get方法,addSub会在get方法中被调用this.oldValue=vm[key]Dep.target=null}//数据更新时更新视图changesupdate(){letnewValue=this.vm[this.key]if(this.oldValue===newValue){return}this.cb(newValue)}}最后我们看一下初始化流程图,如下箭头要知道:首先,完成初始化阶段三步:调用vue.js中的_proxyData进行数据劫持。进入Observe.js,创建一个Dep(observationtarget),他都有一个容器来存放观察者列表,以及添加观察者和通知观察者的方法。创建一个Watcher(观察者),它是观察者模式中的观察者,所以他有通知更新的响应方法。总结:这三个步骤是Vue响应式和观察者模式实现的核心。Watcher(观察者)通过触发get将自己添加到观察目标的观察者列表中。Dep通过遍历自己的观察者列表来通知所有的观察者,从而实现响应式。视图渲染和视图更新先看一下compiler.js文件的主要内容){node.value=valuenewWatcher(this.vm,key,(newValue)=>{//创建一个观察者对象,当数据变化时更新视图node.value=newValue})//双向绑定节点.addEventListener('input',()=>{this.vm[key]=node.value})}//编译模板compile(el){letchildNodes=el.childNodesArray.from(childNodes).forEach(node=>{if(this.isTextNode(node)){//处理文本节点this.compileText(node)}elseif(this.isElementNode(node)){//处理元素节点this.compileElement(node)}//如果有子节点,递归调用if(node.childNodes&&node.childNodes.length>0){this.compile(node)}})}//编译元素节点,处理指令compileElement(node){//控制台.log(node.attributes)if(node.attributes.length){Array.from(node.attributes).forEach(attr=>{//遍历所有元素节点letattrName=attr.nameif(this.isDirective(attrName)){//判断是不是指令attrName=attrName.indexOf(':')>-1?在trName.substr(5):attrName.substr(2)//获取v-后面的值letkey=attr.value//获取数据名this.update(node,key,attrName)}})}}//编译文本节点,处理差异表达式compileText(node){//获取{{}}中的值//console.dir(node)//console.dir=>转换为对象形式letreg=/{{(.+?)}}/letvalue=node.textContentif(reg.test(value)){letkey=RegExp.$1.trim()//返回第一个匹配的字符串,去掉空格node.textContent=value.replace(reg,this.vm[key])newWatcher(this.vm,key,(newValue)=>{//创建一个watcher对象,当数据变化时更新viewnode.textContent=newValue})}}我们再看看路线图总结:compile将元素转化为vnode对象,然后遍历vnode对象,根据标识分为元素节点、文本节点、数据三类,分别进入不同的处理函数,创建Watcher对象,以及然后在Trigger中进入Watcher对象来实现响应能力。同步会执行updata更新数据,转换成真正的dom,完成页面渲染。以这种方式重复更新。整体响应式操作流程图最后,我们根据这张流程图来复习一下知识。首先是初始化分三步:通过_proxyData进行数据劫持,代理Data,创建Dep(观察目标)对象,然后创建Watcher(观察者)对象,然后开始渲染阶段vm.render()触发模板编译complie(template)将element编译成vnode对象,遍历对象,创建Watcher,添加依赖,完成响应。updata更新数据,然后转换成真正的dom完成渲染。到这里,Vue响应式的原理和它的设计模式应该已经很清楚了。如果您有任何问题,请留言。