VUE响应式原理实例代码分析(vue,编程语言)

时间:2024-05-06 13:37:20 作者 : 石家庄SEO 分类 : 编程语言
  • TAG :

这篇“VUE响应式原理实例代码分析”文章的知识点大部分人都不太理解,所以小编给大家总结了以下内容,内容详细,步骤清晰,具有一定的借鉴价值,希望大家阅读完这篇文章能有所收获,下面我们一起来看看这篇“VUE响应式原理实例代码分析”文章吧。

Vue2.X响应式原理

1.defineProperty 的应用

Vue2.X 响应式中使用到了 defineProperty 进行数据劫持,所以我们对它必须有一定的了解,那么我们先来了解它的使用方法把, 这里我们来使用 defineProperty来模拟 Vue 中的 data

<body><divid="app"></div><script>//模拟Vue的dataletdata={msg:'',}//模拟Vue实例letvm={}//对vm的msg进行数据劫持Object.defineProperty(vm,'msg',{//获取数据get(){returndata.msg},//设置msgset(newValue){//如果传入的值相等就不用修改if(newValue===data.msg)return//修改数据data.msg=newValuedocument.querySelector('#app').textContent=data.msg},})//这样子就调用了definePropertyvm.msg的setvm.msg='1234'</script></body>

2.defineProperty修改多个参数为响应式

修改多个参数

看了上面的方法只能修改一个属性,实际上我们 data 中数据不可能只有一个,我们何不定义一个方法把data中的数据进行遍历都修改成响应式呢

<body><divid="app"></div> <script>//模拟Vue的dataletdata={msg:'哈哈',age:'18',}//模拟Vue实例letvm={}//把多个属性转化响应式functionproxyData(){//把data中每一项都[msg,age]拿出来操作Object.keys(data).forEach((key)=>{//对vm的属性进行数据劫持Object.defineProperty(vm,key,{//可枚举enumerable:true,//可配置configurable:true,//获取数据get(){returndata[key]},//设置属性值set(newValue){//如果传入的值相等就不用修改if(newValue===data[key])return//修改数据data[key]=newValuedocument.querySelector('#app').textContent=data[key]},})})}//调用方法proxyData(data) </script></body>

3.Proxy

Vue3 中使用 Proxy 来设置响应式的属性

先来了解下 Proxy 的两个参数

new Proxy(target,handler)

  • target :要使用 Proxy 包装的目标对象(可以是任何类型的对象,包括原生数组,函数,甚至另一个代理)

  • handler:一个通常以函数作为属性的对象,各属性中的函数分别定义了在执行各种操作时代理 p 的行为

其实 和 Vue2.X实现的逻辑差不多,不过实现的方法不一样

那么就放上代码了

<body><divid="app"></div><script>//模拟Vuedataletdata={msg:'',age:'',}//模拟Vue的一个实例//Proxy第一个letvm=newProxy(data,{//get()获取值//target表示需要代理的对象这里指的就是data//key就是对象的键get(target,key){returntarget[key]},//设置值//newValue是设置的值set(target,key,newValue){//也先判断下是否和之前的值一样节省性能if(target[key]===newValue)return//进行设置值target[key]=newValuedocument.querySelector('#app').textContent=target[key]},})</script></body>

触发setget 的方法

//触发了set方法vm.msg='haha'//触发了get方法console.log(vm.msg)

4.发布订阅模式

在Vue 响应式中应用到了 发布订阅模式 我们先来了解下

首先来说简单介绍下 一共有三个角色

发布者订阅者信号中心 举个现实中例子 作者(发布者)写一篇文章 发到了掘金(信号中心) ,掘金可以处理文章然后推送到了首页,然后各自大佬(订阅者)就可以订阅文章

在Vue 中的例子 就是EventBus $on $emit

那么我们就简单模仿一下 Vue 的事件总线吧

之前代码缩进4个单位有点宽,这里改成2个

<body><divid="app"></div><script>classVue{constructor(){//用来存储事件//存储的例子this.subs={'myclick':[fn1,fn2,fn3],'inputchange':[fn1,fn2]}this.subs={}}//实现$on方法type是任务队列的类型,fn是方法$on(type,fn){//判断在subs是否有当前类型的方法队列存在if(!this.subs[type]){//没有就新增一个默认为空数组this.subs[type]=[]}//把方法加到该类型中this.subs[type].push(fn)}//实现$emit方法$emit(type){//首先得判断该方法是否存在if(this.subs[type]){//获取到参数constargs=Array.prototype.slice.call(arguments,1)//循环队列调用fnthis.subs[type].forEach((fn)=>fn(...args))}}}//使用consteventHub=newVue()//使用$on添加一个sum类型的方法到subs['sum']中eventHub.$on('sum',function(){letcount=[...arguments].reduce((x,y)=>x+y)console.log(count)})//触发sum方法eventHub.$emit('sum',1,2,4,5,6,7,8,9,10)</script></body>

5.观察者模式

与 发布订阅 的差异

与发布订阅者不同 观察者中 发布者和订阅者(观察者)是相互依赖的 必须要求观察者订阅内容改变事件 ,而发布订阅者是由调度中心进行调度,那么看看观察者模式 是如何相互依赖,下面就举个简单例子

<body><divid="app"></div><script>//目标classSubject{constructor(){this.observerLists=[]}//添加观察者addObs(obs){//判断观察者是否有和存在更新订阅的方法if(obs&&obs.update){//添加到观察者列表中this.observerLists.push(obs)}}//通知观察者notify(){this.observerLists.forEach((obs)=>{//每个观察者收到通知后会更新事件obs.update()})}//清空观察者empty(){this.subs=[]}}classObserver{//定义观察者内容更新事件update(){//在更新事件要处理的逻辑console.log('目标更新了')}}//使用//创建目标letsub=newSubject()//创建观察者letobs1=newObserver()letobs2=newObserver()//把观察者添加到列表中sub.addObs(obs1)sub.addObs(obs2)//目标开启了通知每个观察者者都会自己触发update更新事件sub.notify()</script></body>

6.模拟Vue的响应式原理

这里来实现一个小型简单的 Vue 主要实现以下的功能

  • 接收初始化的参数,这里只举几个简单的例子 el data options

  • 通过私有方法 _proxyDatadata 注册到 Vue 中 转成getter setter

  • 使用 observerdata 中的属性转为 响应式 添加到 自身身上

  • 使用 observer 方法监听 data 的所有属性变化来 通过观察者模式 更新视图

  • 使用 compiler 编译元素节点上面指令 和 文本节点差值表达式

1.vue.js

在这里获取到 el data

通过 _proxyDatadata的属性 注册到Vue 并转成 getter setter

/*vue.js*/classVue{constructor(options){//获取到传入的对象没有默认为空对象this.$options=options||{}//获取elthis.$el=typeofoptions.el==='string'?document.querySelector(options.el):options.el//获取datathis.$data=options.data||{}//调用_proxyData处理data中的属性this._proxyData(this.$data)}//把data中的属性注册到Vue_proxyData(data){Object.keys(data).forEach((key)=>{//进行数据劫持//把每个data的属性到添加到Vue转化为gettersetter方法Object.defineProperty(this,key,{//设置可以枚举enumerable:true,//设置可以配置configurable:true,//获取数据get(){returndata[key]},//设置数据set(newValue){//判断新值和旧值是否相等if(newValue===data[key])return//设置新值data[key]=newValue},})})}}

2.observer.js

在这里把 data 中的 属性变为响应式加在自身的身上,还有一个主要功能就是 观察者模式在 第 4.dep.js 会有详细的使用

/*observer.js*/classObserver{constructor(data){//用来遍历datathis.walk(data)}//遍历data转为响应式walk(data){//判断data是否为空和对象if(!data||typeofdata!=='object')return//遍历dataObject.keys(data).forEach((key)=>{//转为响应式this.defineReactive(data,key,data[key])})}//转为响应式//要注意的和vue.js写的不同的是//vue.js中是将属性给了Vue转为gettersetter//这里是将data中的属性转为gettersetterdefineReactive(obj,key,value){//如果是对象类型的也调用walk变成响应式,不是对象类型的直接在walk会被returnthis.walk(value)//保存一下thisconstself=thisObject.defineProperty(obj,key,{//设置可枚举enumerable:true,//设置可配置configurable:true,//获取值get(){returnvalue},//设置值set(newValue){//判断旧值和新值是否相等if(newValue===value)return//设置新值value=newValue//赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的self.walk(newValue)},})}}

在html中引入的话注意顺序

<scriptsrc="./js/observer.js"></script><scriptsrc="./js/vue.js"></script>

然后在vue.js 中使用 Observer

/*vue.js*/classVue{constructor(options){...//使用Obsever把data中的数据转为响应式newObserver(this.$data)}//把data中的属性注册到Vue_proxyData(data){...}}

看到这里为什么做了两个重复性的操作呢?重复性两次把 data的属性转为响应式

obsever.js 中是把 data 的所有属性 加到 data 自身 变为响应式 转成 getter setter方式

vue.js 中 也把 data的 的所有属性 加到 Vue 上,是为了以后方面操作可以用 Vue 的实例直接访问到 或者在 Vue 中使用 this 访问

使用例子:

<body><divid="app"></div><scriptsrc="./js/observer.js"></script><scriptsrc="./js/vue.js"></script><script>letvm=newVue({el:'#app',data:{msg:'123',age:21,},})</script></body>

VUE响应式原理实例代码分析

这样在Vue$data 中都存在了 所有的data 属性了 并且是响应式的

3.compiler.js

comilper.js在这个文件里实现对文本节点 和 元素节点指令编译 主要是为了举例子 当然这个写的很简单 指令主要实现 v-text v-model

/*compiler.js*/classCompiler{//vm指Vue实例constructor(vm){//拿到vmthis.vm=vm//拿到elthis.el=vm.$el//编译模板this.compile(this.el)}//编译模板compile(el){//获取子节点如果使用forEach遍历就把伪数组转为真的数组letchildNodes=[...el.childNodes]childNodes.forEach((node)=>{//根据不同的节点类型进行编译//文本类型的节点if(this.isTextNode(node)){//编译文本节点this.compileText(node)}elseif(this.isElementNode(node)){//元素节点this.compileElement(node)}//判断是否还存在子节点考虑递归if(node.childNodes&&node.childNodes.length){//继续递归编译模板this.compile(node)}})}//编译文本节点(简单的实现)compileText(node){//核心思想利用把正则表达式把{{}}去掉找到里面的变量//再去Vue找这个变量赋值给node.textContentletreg=/\{\{(.+?)\}\}///获取节点的文本内容letval=node.textContent//判断是否有{{}}if(reg.test(val)){//获取分组一也就是{{}}里面的内容去除前后空格letkey=RegExp.$1.trim()//进行替换再赋值给nodenode.textContent=val.replace(reg,this.vm[key])}}//编译元素节点这里只处理指令compileElement(node){//获取到元素节点上面的所有属性进行遍历![...node.attributes].forEach((attr)=>{//获取属性名letattrName=attr.name//判断是否是v-开头的指令if(this.isDirective(attrName)){//除去v-方便操作attrName=attrName.substr(2)//获取指令的值就是v-text="msg"中msg//msg作为key去Vue找这个变量letkey=attr.value//指令操作执行指令方法//vue指令很多为了避免大量个if判断这里就写个uapdate方法this.update(node,key,attrName)}})}//添加指令方法并且执行update(node,key,attrName){//比如添加textUpdater就是用来处理v-text方法//我们应该就内置一个textUpdater方法进行调用//加个后缀加什么无所谓但是要定义相应的方法letupdateFn=this[attrName+'Updater']//如果存在这个内置方法就可以调用了updateFn&&updateFn(node,key,this.vm[key])}//提前写好相应的指定方法比如这个v-text//使用的时候和Vue的一样textUpdater(node,key,value){node.textContent=value}//v-modelmodelUpdater(node,key,value){node.value=value}//判断元素的属性是否是vue指令isDirective(attr){returnattr.startsWith('v-')}//判断是否是元素节点isElementNode(node){returnnode.nodeType===1}//判断是否是文本节点isTextNode(node){returnnode.nodeType===3}}

4.dep.js

写一个Dep类 它相当于 观察者中的发布者 每个响应式属性都会创建这么一个 Dep 对象 ,负责收集该依赖属性的Watcher对象 (是在使用响应式数据的时候做的操作)

当我们对响应式属性在 setter 中进行更新的时候,会调用 Depnotify 方法发送更新通知

然后去调用 Watcher 中的 update 实现视图的更新操作(是当数据发生变化的时候去通知观察者调用观察者的update更新视图)

总的来说 在Dep(这里指发布者) 中负责收集依赖 添加观察者(这里指Wathcer),然后在 setter 数据更新的时候通知观察者

说的这么多重复的话,大家应该知道是在哪个阶段 收集依赖 哪个阶段 通知观察者了吧,下面就来实现一下吧

先写Dep

/*dep.js*/classDep{constructor(){//存储观察者this.subs=[]}//添加观察者addSub(sub){//判断观察者是否存在和是否拥有update方法if(sub&&sub.update){this.subs.push(sub)}}//通知方法notify(){//触发每个观察者的更新方法this.subs.forEach((sub)=>{sub.update()})}}

obsever.js 中使用Dep

get 中添加 Dep.target (观察者)

set 中 触发 notify (通知)

/*observer.js*/classObserver{...}//遍历data转为响应式walk(data){...}//这里是将data中的属性转为gettersetterdefineReactive(obj,key,value){ ...//创建Dep对象letdep=newDep()Object.defineProperty(obj,key,{ ...//获取值get(){//在这里添加观察者对象Dep.target表示观察者Dep.target&&dep.addSub(Dep.target)returnvalue},//设置值set(newValue){if(newValue===value)returnvalue=newValueself.walk(newValue)//触发通知更新视图dep.notify()},})}}

5.watcher.js

**watcher **的作用 数据更新后 收到通知之后 调用 update 进行更新

/*watcher.js*/classWatcher{constructor(vm,key,cb){//vm是Vue实例this.vm=vm//key是data中的属性this.key=key//cb回调函数更新视图的具体方法this.cb=cb//把观察者的存放在Dep.targetDep.target=this//旧数据更新视图的时候要进行比较//还有一点就是vm[key]这个时候就触发了get方法//之前在get把观察者通过dep.addSub(Dep.target)添加到了dep.subs中this.oldValue=vm[key]//Dep.target就不用存在了因为上面的操作已经存好了Dep.target=null}//观察者中的必备方法用来更新视图update(){//获取新值letnewValue=this.vm[this.key]//比较旧值和新值if(newValue===this.oldValue)return//调用具体的更新方法this.cb(newValue)}}

那么去哪里创建 Watcher 呢?还记得在 compiler.js中 对文本节点的编译操作吗

在编译完文本节点后 在这里添加一个 Watcher

还有 v-text v-model 指令 当编译的是元素节点 就添加一个 Watcher

/*compiler.js*/classCompiler{//vm指Vue实例constructor(vm){//拿到vmthis.vm=vm//拿到elthis.el=vm.$el//编译模板this.compile(this.el)}//编译模板compile(el){letchildNodes=[...el.childNodes]childNodes.forEach((node)=>{if(this.isTextNode(node)){//编译文本节点this.compileText(node)}...}//编译文本节点(简单的实现)compileText(node){letreg=/\{\{(.+)\}\}/letval=node.textContentif(reg.test(val)){letkey=RegExp.$1.trim()node.textContent=val.replace(reg,this.vm[key])//创建观察者newWatcher(this.vm,key,newValue=>{node.textContent=newValue})}}...//v-texttextUpdater(node,key,value){node.textContent=value//创建观察者2newWatcher(this.vm,key,(newValue)=>{node.textContent=newValue})}//v-modelmodelUpdater(node,key,value){node.value=value//创建观察者newWatcher(this.vm,key,(newValue)=>{node.value=newValue})//这里实现双向绑定监听input事件修改data中的属性node.addEventListener('input',()=>{this.vm[key]=node.value})}}

当 我们改变 响应式属性的时候 触发了 set() 方法 ,然后里面 发布者 dep.notify 方法启动了,拿到了 所有的 观察者 watcher 实例去执行 update 方法调用了回调函数 cb(newValue) 方法并把 新值传递到了 cb() 当中 cb方法是的具体更新视图的方法 去更新视图

比如上面的例子里的第三个参数 cb方法

newWatcher(this.vm,key,newValue=>{node.textContent=newValue})

还有一点要实现v-model的双向绑定

不仅要通过修改数据来触发更新视图,还得为node添加 input 事件 改变 data数据中的属性

来达到双向绑定的效果

7.测试下自己写的

到了目前为止 响应式 和 双向绑定 都基本实现了 那么来写个例子测试下

<body><divid="app">{{msg}}<br/>{{age}}<br/><divv-text="msg"></div><inputv-model="msg"type="text"/></div><scriptsrc="./js/dep.js"></script><scriptsrc="./js/watcher.js"></script><scriptsrc="./js/compiler.js"></script><scriptsrc="./js/observer.js"></script><scriptsrc="./js/vue.js"></script><script>letvm=newVue({el:'#app',data:{msg:'123',age:21,},})</script></body>

8.五个文件代码

这里直接把5个文件个代码贴出来 上面有的地方省略了,下面是完整的方便大家阅读

vue.js

/*vue.js*/classVue{constructor(options){//获取到传入的对象没有默认为空对象this.$options=options||{}//获取elthis.$el=typeofoptions.el==='string'?document.querySelector(options.el):options.el//获取datathis.$data=options.data||{}//调用_proxyData处理data中的属性this._proxyData(this.$data)//使用Obsever把data中的数据转为响应式newObserver(this.$data)//编译模板newCompiler(this)}//把data中的属性注册到Vue_proxyData(data){Object.keys(data).forEach((key)=>{//进行数据劫持//把每个data的属性到添加到Vue转化为gettersetter方法Object.defineProperty(this,key,{//设置可以枚举enumerable:true,//设置可以配置configurable:true,//获取数据get(){returndata[key]},//设置数据set(newValue){//判断新值和旧值是否相等if(newValue===data[key])return//设置新值data[key]=newValue},})})}}

obsever.js

/*observer.js*/classObserver{constructor(data){//用来遍历datathis.walk(data)}//遍历data转为响应式walk(data){//判断data是否为空和对象if(!data||typeofdata!=='object')return//遍历dataObject.keys(data).forEach((key)=>{//转为响应式this.defineReactive(data,key,data[key])})}//转为响应式//要注意的和vue.js写的不同的是//vue.js中是将属性给了Vue转为gettersetter//这里是将data中的属性转为gettersetterdefineReactive(obj,key,value){//如果是对象类型的也调用walk变成响应式,不是对象类型的直接在walk会被returnthis.walk(value)//保存一下thisconstself=this//创建Dep对象letdep=newDep()Object.defineProperty(obj,key,{//设置可枚举enumerable:true,//设置可配置configurable:true,//获取值get(){//在这里添加观察者对象Dep.target表示观察者Dep.target&&dep.addSub(Dep.target)returnvalue},//设置值set(newValue){//判断旧值和新值是否相等if(newValue===value)return//设置新值value=newValue//赋值的话如果是newValue是对象,对象里面的属性也应该设置为响应式的self.walk(newValue)//触发通知更新视图dep.notify()},})}}

compiler.js

/*compiler.js*/classCompiler{//vm指Vue实例constructor(vm){//拿到vmthis.vm=vm//拿到elthis.el=vm.$el//编译模板this.compile(this.el)}//编译模板compile(el){//获取子节点如果使用forEach遍历就把伪数组转为真的数组letchildNodes=[...el.childNodes]childNodes.forEach((node)=>{//根据不同的节点类型进行编译//文本类型的节点if(this.isTextNode(node)){//编译文本节点this.compileText(node)}elseif(this.isElementNode(node)){//元素节点this.compileElement(node)}//判断是否还存在子节点考虑递归if(node.childNodes&&node.childNodes.length){//继续递归编译模板this.compile(node)}})}//编译文本节点(简单的实现)compileText(node){//核心思想利用把正则表达式把{{}}去掉找到里面的变量//再去Vue找这个变量赋值给node.textContentletreg=/\{\{(.+?)\}\}///获取节点的文本内容letval=node.textContent//判断是否有{{}}if(reg.test(val)){//获取分组一也就是{{}}里面的内容去除前后空格letkey=RegExp.$1.trim()//进行替换再赋值给nodenode.textContent=val.replace(reg,this.vm[key])//创建观察者newWatcher(this.vm,key,(newValue)=>{node.textContent=newValue})}}//编译元素节点这里只处理指令compileElement(node){//获取到元素节点上面的所有属性进行遍历![...node.attributes].forEach((attr)=>{//获取属性名letattrName=attr.name//判断是否是v-开头的指令if(this.isDirective(attrName)){//除去v-方便操作attrName=attrName.substr(2)//获取指令的值就是v-text="msg"中msg//msg作为key去Vue找这个变量letkey=attr.value//指令操作执行指令方法//vue指令很多为了避免大量个if判断这里就写个uapdate方法this.update(node,key,attrName)}})}//添加指令方法并且执行update(node,key,attrName){//比如添加textUpdater就是用来处理v-text方法//我们应该就内置一个textUpdater方法进行调用//加个后缀加什么无所谓但是要定义相应的方法letupdateFn=this[attrName+'Updater']//如果存在这个内置方法就可以调用了updateFn&&updateFn.call(this,node,key,this.vm[key])}//提前写好相应的指定方法比如这个v-text//使用的时候和Vue的一样textUpdater(node,key,value){node.textContent=value//创建观察者newWatcher(this.vm,key,(newValue)=>{node.textContent=newValue})}//v-modelmodelUpdater(node,key,value){node.value=value//创建观察者newWatcher(this.vm,key,(newValue)=>{node.value=newValue})//这里实现双向绑定node.addEventListener('input',()=>{this.vm[key]=node.value})}//判断元素的属性是否是vue指令isDirective(attr){returnattr.startsWith('v-')}//判断是否是元素节点isElementNode(node){returnnode.nodeType===1}//判断是否是文本节点isTextNode(node){returnnode.nodeType===3}}

dep.js

/*dep.js*/classDep{constructor(){//存储观察者this.subs=[]}//添加观察者addSub(sub){//判断观察者是否存在和是否拥有update方法if(sub&&sub.update){this.subs.push(sub)}}//通知方法notify(){//触发每个观察者的更新方法this.subs.forEach((sub)=>{sub.update()})}}

watcher.js

/*watcher.js*/classWatcher{constructor(vm,key,cb){//vm是Vue实例this.vm=vm//key是data中的属性this.key=key//cb回调函数更新视图的具体方法this.cb=cb//把观察者的存放在Dep.targetDep.target=this//旧数据更新视图的时候要进行比较//还有一点就是vm[key]这个时候就触发了get方法//之前在get把观察者通过dep.addSub(Dep.target)添加到了dep.subs中this.oldValue=vm[key]//Dep.target就不用存在了因为上面的操作已经存好了Dep.target=null}//观察者中的必备方法用来更新视图update(){//获取新值letnewValue=this.vm[this.key]//比较旧值和新值if(newValue===this.oldValue)return//调用具体的更新方法this.cb(newValue)}}

以上就是关于“VUE响应式原理实例代码分析”这篇文章的内容,相信大家都有了一定的了解,希望小编分享的内容对大家有帮助,若想了解更多相关的知识内容,请关注亿速云行业资讯频道。

本文:VUE响应式原理实例代码分析的详细内容,希望对您有所帮助,信息来源于网络。
上一篇:怎么用node抓取宝可梦图鉴并生成Excel文件下一篇:

6 人围观 / 0 条评论 ↓快速评论↓

(必须)

(必须,保密)

阿狸1 阿狸2 阿狸3 阿狸4 阿狸5 阿狸6 阿狸7 阿狸8 阿狸9 阿狸10 阿狸11 阿狸12 阿狸13 阿狸14 阿狸15 阿狸16 阿狸17 阿狸18