1.Vue2源码学习笔记 - 7.响应式原理一基础
2.Vue3响应式系统原理:无限递归循环处理
3.vue2的响e响响应式原理介绍——Object.defineProperty
4.Vue—关于响应式(二、异步更新队列原理分析)
5.Vue 3源码解析--响应式原理
6.Vue3源码系列 (四) ref
Vue2源码学习笔记 - 7.响应式原理一基础
深入研究 Vue 的应式源码应式源码响应式核心,了解响应式机制在 Vue 中的网站核心地位。Vue 的解析响应式原理,让数据模型简单直接地管理状态,响e响无需侵入性操作。应式源码应式源码照片展示源码
当你将普通 JavaScript 对象作为 Vue 实例的网站 data 选项时,Vue 会遍历对象属性并使用 Object.defineProperty 转换为 getter 和 setter。解析此特性仅在 ES5 中可用,响e响不支持 IE8 及以下浏览器。应式源码应式源码
这些 getter 和 setter 在内部追踪依赖,网站当属性被访问或修改时,解析会通知 Vue。响e响类似于 PHP 的应式源码应式源码魔术方法或 Java 的 getXXX\setXXX,但实现上存在差异。网站Java 可能拥有更接近的实现,比如 CGLib。
每个 Vue 组件实例对应一个 watcher,记录接触过的数据属性为依赖。当依赖项的 setter 触发时,watcher 被通知,组件重新渲染。
简单 demo 通过 defineReactive 实现响应式设置,允许访问 data 中的属性,设值触发 setter,引用触发 getter。此方法依赖于 Object.defineProperty,是响应式原理的核心。
Proxy 是 ES 定义的类,用于创建对象代理,实现基本操作拦截和自定义。通过简单的 demo 可见,更新和引用数据时会调用 setter/getter 方法。Vue2 使用 Proxy,但用途不多。
总结,学习 Object.defineProperty 和 Proxy 实现响应式的底层方法。它们在数据更新和引用时触发特定方法,执行期望的操作实现响应式。下篇深入 Vue 响应式实现。
Vue3响应式系统原理:无限递归循环处理
在Vue3的响应式系统中,若副作用函数既包含对响应式对象的读操作,又包含写操作,会导致无限递归循环现象。具体问题如下的代码所示:
代码中,副作用函数同时执行读取与修改响应式对象操作,完整代码如下:
深入分析,报内存栈容量超出错误的原因在于此循环依赖。为解决此问题,gapso源码策略是在trigger函数中,当检测到即将执行的副作用函数与当前执行的函数相同,则选择不执行该函数。总结:在副作用函数处理响应式对象读写时,引起无限递归循环。通过在trigger函数中判定即将执行与当前执行的函数是否一致,如一致则跳过不执行,可有效解决此问题。
vue2的响应式原理介绍——Object.defineProperty
大家好,我是前端GGBond,今天我们来聊一下vue2的响应式原理。什么是响应式呢?
数据发生变化后,会重新对页面渲染,这就是Vue响应式,如下图
想完成这个过程,我们需要做些以下几点:数据劫持/数据代理,侦测数据的变化
依赖收集,收集视图依赖了哪些数据
发布订阅模式,数据变化时,自动“通知”需要更新的视图部分,并进行更新
今天我们重点聊一下数据劫持的实现,以及vue在响应式监听中存在的问题,以及对应的解决方法。
[]()一、针对对象的监听实现[]()前置知识、Object.defineProperty介绍(对象劫持)[]()定义:Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。
[]()基本使用[]()语法:Object.defineProperty(obj,prop,descriptor)
[]()参数:
[]()1.?obj,要添加属性的对象
[]()2.?prop,要定义或修改的属性的名称或[Symbol]
[]()3.?descriptor,要定义或修改的属性描述符
[]()前两个参数都很好理解,这里重点说一下第三个参数:属性描述符。
[]()对象里目前存在的属性描述符,包括configurable、enumerable、value、writable、get、set。
[]()而vue中针对对象的监听,主要是通过属性描述符的最后两个属性,get及set
[]()l?get
[]()属性的getter函数,当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的taotuzi源码返回值会被用作属性的值。
[]()l?set
[]()属性的setter函数,当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this对象。默认为undefined
[]()下面通过代码展示:
[]()定义一个响应式函数defineReactive?
constobj={ foo:''}functionupdate(){ console.log('obj.foo更新了',obj.foo)}functiondefineReactive(obj,key,val){ Object.defineProperty(obj,key,{ get(){ console.log(`get${ key}:${ val}`);returnval},set(newVal){ if(newVal!==val){ val=newValupdate()}}})}[]()调用defineReactive,数据发生变化触发update方法,实现数据响应式
defineReactive(obj,'foo','')setTimeout(()=>{ obj.foo=newDate().toLocaleTimeString()[]()},)[]()?上面的情况可以监听到对象一个属性的变化。
[]()但是,在对象存在多个属性的情况下,需要进行遍历。
functionobserve(obj){ if(typeofobj!=='object'||obj==null){ return}Object.keys(obj).forEach(key=>{ defineReactive(obj,key,obj[key])})}[]()如果存在嵌套对象的情况,还需要在defineReactive中进行递归
functiondefineReactive(obj,key,val){ observe(val)Object.defineProperty(obj,key,{ get(){ console.log(`get${ key}:${ val}`);returnval},set(newVal){ if(newVal!==val){ val=newValupdate()}}})}[]()当给key赋值为对象的时候,还需要在set属性中进行递归
set(newVal){ if(newVal!==val){ observe(newVal)//新值是对象的情况notifyUpdate()}}[]()上述例子能够实现对一个对象的基本响应式,但仍然存在诸多问题。
[]()比如现在对一个对象进行删除与添加属性操作,无法劫持到:
constobj={ foo:"foo",bar:"bar"}observe(obj)deleteobj.foo//nookobj.jar='xxx'//nook这些问题如何解决?我们后面再来讲。
[]()二、针对数组的监听[]()首先说一下,vue针对数组的响应式并没有用到Object.defineProperty,主要原因有以下两点:
[]()1.Object.defineProperty,可以监听到数组属性变化,但因为性能消耗严重,所以成为废案。
[]()其实Object.defineProperty,可以监听到数组属性变化,以下是测试代码:
lettestArray=[0];functiontest(data,key,val){ Object.defineProperty(data,key,{ get(){ console.log(val);},set(newV){ if(newV!==val){ val=newV;console.log('检测到变更');}},});}test(testArray,0,aa[0]);testArray[0]=1[]()l?直接复制代码控制台输出
[]()网上其实很多人都在说Object.defineProperty是不能通过下标来修改数组的数据。但是自己测试怎么是可以检测到修改的?难道是他们说的都是错误的?
[]()再来继续测试一下length的变化,
[]()testArray.length=5
[]()l?我们来看控制台输出,数据的长度是修改成功的,看来真的是他们的答案有误。
[]()l?于是我继续寻找,终于找到了,确实不是Object.defineProperty()的问题,是vue本身做了限制,当数据是数组时,会停止对数据属性的监测,
[]()还有一个问题则是,如果存在深层的嵌套对象关系,需要深层的进行监听,造成了性能的极大问题。
[]()所以,Object.defineProperty**是有监控数组下标变化的能力的,只是在Vue2的实现中,从性能/体验的性价比考虑,放弃了这个特性。
**[]()2.Object.defineProperty无法监听到数组api的变化[]()当我们对一个数组api进行监听的时候,发现Object.defineProperty并不那么好使了
constarrData=[1,docbar源码2,3,4,5];arrData.forEach((val,index)=>{ defineProperty(arrData,index,val)})arrData.push()//nookarrData.pop()//nook[]()arrDate[0]=//ok[]()可以看到数据的api无法劫持到,从而无法实现数据响应式,
[]()所以在Vue2中,针对数组的监听没有用到Object.defineProperty,而是增加了set、deleteAPI,并且对数组api方法进行一个重写。
与此同时,官网也提到,Vue不能检测以下数组的变动:
当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem]=newValue
当你修改数组的长度时,例如:vm.items.length=newLength
[]()小结[]()Vue不能检测以下对象的变动:[]()1.?检测不到对象属性的添加和删除
[]()Vue不能检测以下数组的变动:[]()1.?当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem]=newValue
[]()2.?当你修改数组的长度时,例如:vm.items.length=newLength
[]()针对这些不能监听变化的问题,vue肯定给出了解决方案,具体有哪些呢?
[]()?
[]()三、对象的手动更新1.更新对象的单个属性——vue.set()对于已经创建的实例,Vue不允许动态添加根级别的响应式property。但是,可以使用?Vue.set(object,propertyName,value)?方法向嵌套对象添加响应式property。vue.set()这个api,其实就是做一次Object.defineProperty。语法是:
Vue.set(vm.someObject,'b',2)
您还可以使用?vm.$set?实例方法,这也是全局?Vue.set?方法的别名:
this.$set(this.someObject,'b',2)
2.更新对象的多个属性有时你可能需要为已有对象赋值多个新property(属性),比如使用?Object.assign()?或?_.extend()。但是,这样添加到对象上的新property不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的property一起创建一个新的对象。
//代替Object.assign(this.someObject,{ a:1,b:2})this.someObject=Object.assign({ },this.someObject,{ a:1,b:2})四、数组的手动更新1.索引赋值更新无效的问题,例如vue.items[indexOfItem]=newValue为了解决.items[indexOfItem]=newValue?更新无效的问题,以下两种方式都可以实现和?vm.items[indexOfItem]=newValue?相同的效果,同时也将在响应式系统内触发状态更新:
//Vue.setVue.set(vm.items,indexOfItem,newValue)//Array.prototype.splicevm.items.splice(indexOfItem,1,newValue)你也可以使用?vm.$set?实例方法,该方法是全局方法?Vue.set?的一个别名:
vm.$set(vm.items,indexOfItem,newValue)
2.watch中的deep监听如果数组中带有对象,需要对每个属性进行遍历监听,如果嵌套对象,需要深层监听,造成性能问题。所以,vue默认是不会做深度监听的。
但如果就是要深度监听怎么办呢?watch的专门有个deep的api,解决这个问题。
deep:代表深度监听,它有两个值分别是是true或false,不仅能监听到数组中对象的变化,也监听到该对象的属性变化。
3.修改数组的长度无法更新的问题,例如:vm.items.length=newLength为了解决第二类问题,你可以使用?枪神源码splice:
vm.items.splice(newLength)
五、另一种解决方法——$forceUpdate()手动刷新domvm.$forceUpdate()
示例:迫使Vue实例重新渲染。注意它仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。(说人话就是只更新自己这个组件,不会更新子组件)
参考文章:一文搞懂Object.defineProperty和Proxy,Vue3.0为什么采用Proxy?
vue对通过下标修改数组监听不到,和Object.defineProperty无关(这个锅它不背)
原文;/post/Vue—关于响应式(二、异步更新队列原理分析)
本节学习要点:Event Loop、Promise
关于Event Loop的介绍,可以参考阮一峰老师的文章。
关于Promise,请访问:developer.mozilla.org/z...
上一节介绍了Vue通过Object.defineProperty拦截数据变化的响应式原理,数据变化后会触发notify方法来通知变更。这一节将继续分析,收到通知后Vue会开启一个异步更新队列。
以下是两个问题:
一、异步更新队列
首先看一段代码演示。
将上一节的代码拿过来,假设我们现在不仅依赖x,还有y、z,分别将x、y、z输出到页面上。我们现在依赖了x、y、z三个变量,那么我们应该把onXChange函数名改为watch,表示它可以监听变化,而不仅仅是监听一个x的变化。
可以看到这三个值都被打印在页面上。
现在我们对x、y、z的value进行修改。
查看页面,结果没有问题,每个数据的变化都被监听到并且进行了响应。
既然结果是对的,那我们的问题是什么?
这个问题是:每次数据变化都进行了响应,每次都渲染了模板,如果数据变化了一百次、一千次呢?难道要重复渲染一百遍、一千遍吗?
我们都知道频繁操作DOM会影响网页性能,涉及重排和重绘的知识感兴趣请阅读阮一峰老师的文章:ruanyifeng.com/blog/...
因此,既要保证所有的依赖都准确更新,又要保证不能频繁渲染成为了首要问题。现在我们修改x.value、y.value、z.value都是同步通知依赖进行更新的,有没有一种机制可以等到我修改这些值之后再执行更新任务呢?
这个答案是——异步。
异步任务会等到同步任务清空后执行,借助这个特点和我们前面的分析,我们需要:
按照步骤,我们创建如下代码:
接着我们需要修改一下notify的代码,监听到数据变化后不立即调用依赖进行更新,而是将依赖添加到队列中。
回到页面,我们发现页面上还是重复渲染了三次模板。
那么我们写的这段代码有什么用呢?异步又体现在哪里呢?接着往下看。
二、nextTick原理分析
上面的代码中,虽然我们开启了一个队列,并且成功将任务推入队列中进行执行,但本质上还是同步推入和执行的。我们要让它变成异步队列。
于是到了Promise发挥作用的时候了。关于宏任务和微任务的介绍请参考:zhuanlan.zhihu.com/p/...
我们创建nextTick函数,nextTick接收一个回调函数,返回一个状态为fulfilled的Promise,并将回调函数传给then方法。
然后只需要在添加任务时调用nextTick,将执行任务的flushJobs函数传给nextTick即可。
回到页面。
虽然修改了x、y、z三个变量的value,最后页面上只渲染了一次。
再来总结一下这段代码的执行过程:
这也正是Vue采用的解决方案——异步更新队列,官方文档描述得很清楚。
文档地址:cn.vuejs.org/v2/guide/r...
三、结合Vue源码来看nextTick
在Vue中,我们可以通过两种方式来调用nextTick:
(至于什么时候使用nextTick,如果你不偷懒看了官方文档的话,都能找到答案哈哈)
以下源码节选自vue2.6.版本,这两个API分别在initGlobalAPI函数和renderMixin函数中挂载,它们都引用了nextTick函数。
nextTick源码如下:
在内部,它访问了外部的callbacks,这个callbacks就是前面提到的队列,nextTick一调用就给队列push一个回调函数,然后判断pending(pending的作用就是控制同一时间内只执行一次timerFunc),调用timerFunc(),最后返回了一个Promise(使用过nextTick的应该都知道吧)。
我们来看一下callbacks、pending、timerFunc是如何定义的。
可以看到timerFunc函数只是调用了p.then方法并将flushCallbacks函数推入了微任务队列,而p是一个fulfilled状态的Promise,与我们自己的nextTick功能一致。
这个flushCallbacks函数又干了什么呢?
flushCallbacks中重新将pending置为初始值,复制callbacks队列中的任务后将队列清空,然后依次执行复制的任务,与我们自己的flushJobs函数功能一致。
看完上面的源码,可以总结出Vue是这么做的,又到了小学语文之——提炼中心思想的时候了。
对比一下我们自己写的代码,你学会了吗?
以上演示代码已上传github:github.com/Mr-Jemp/VueS...
后面要学习的内容在这里:
Vue—关于响应式(三、Diff Patch原理分析)
Vue—关于响应式(四、深入学习Vue响应式源码)
本文由博客一文多发平台OpenWrite发布!
Vue 3源码解析--响应式原理
Vue 3响应式核心原理解析
Vue 3相对于Vue 2改动较大的模块是响应式reactivity,性能提升显著。其核心改变是采用ES 6的Proxy API代替Vue2中Object.defineProperty方法,实现响应式。Proxy API定义为用于定义基本操作自定义行为的原生对象,如属性查找、赋值、枚举、函数调用等。Proxy对象作为目标对象的代理,拦截所有对外操作,允许对操作进行拦截、过滤或修改。通过Proxy,可以实现对象限制操作,如禁止删除和修改属性,以及监听数组变化。
Proxy API基本语法包括目标对象和handler对象,后者定义了执行各种操作时代理的行为。常见使用方法展示了如何生成代理对象及其撤销操作。Proxy共有接近个handler,分别对应不同操作,如禁止操作、属性修改校验等。结合这些handler,可以实现对象限制功能。
在Vue 3中,响应式对象通过ref/reactive方法实现,利用Proxy API简化响应式逻辑。ref方法的主要逻辑在源码中体现,通过Proxy的特性实现双向数据绑定能力,无需配置,利用原生特性轻松实现。具体运行原理涉及ref方法、toReactive方法、createReactiveObject方法等,最终创建响应式对象。
Vue 3响应式的核心在于Proxy API的利用,尤其是handler的set方法,实现双向数据绑定逻辑,这与Vue 2中的Object.defineProperty方法形成显著区别。Proxy的特性简化了复杂逻辑,使得响应式对象的创建和管理更加高效、直观。
ref()方法的运行原理涉及创建响应式对象的过程,从接收参数到创建Proxy对象,实现了对深层嵌套对象属性的监听和修改。在创建响应式对象的流程中,通过Base Handlers和Collection Handlers分别处理不同类型的对象,确保响应式对象的高效创建和管理。
在Vue 3源码中,所有响应式代码集中在vue-next/package/reactivity目录下。ref方法的实现主要在reactivity/src/ref.ts中,展示了如何利用Proxy API简化响应式逻辑。通过toReactive方法创建响应式对象,再通过createReactiveObject方法实现深层属性监听和修改。
createReactiveObject方法内部实现包括创建Proxy对象,分别处理基础对象和集合对象(如Map、Set等),避免重复创建响应式对象,同时利用Proxy handler实现属性监听和修改功能。Proxy handler包括get、set等方法,分别对应属性读取和修改逻辑,确保响应式流程的高效执行。
总结而言,Vue 3响应式核心原理解析展示了Proxy API的高效应用,简化了响应式逻辑,实现了复杂操作的轻松实现。通过深入理解Proxy API及其在Vue 3响应式实现中的应用,开发者可以更高效地构建响应式应用,提升用户体验和性能。
Vue3源码系列 (四) ref
一般而言,reactive用于定义响应式对象,而ref则用于定义响应式原始值。前文已介绍reactive,了解到通过Proxy对目标对象进行代理实现响应式,非对象原始值的响应式问题则由ref解决。
ref和shallowRef各有三种重载,参数不同,都返回Ref/ShallowRef类型的值。createRef函数用于创建响应式值,类似reactive,createRef也是通过createReactiveObject创建响应式对象。而createRef返回RefImpl实例。
RefImpl是ref的核心内容,构造函数接收两个参数,value是传入的原始值,__v_isShallow用于区分深层/浅层响应式,isShallow()函数利用这个属性做判断。在Ref中,_value属性存储实际值,dep属性存储依赖,在class的getter中通过trackRefValue(this)收集依赖,在setter中调用triggerRefValue(this, newVal)。
trackRefValue用于收集Ref依赖,接收RefBase类型值,在ref函数中接收RefImpl实例。shouldTrack用于暂停和恢复捕获依赖的标志,activeEffect标记当前活跃的effect。内部调用trackEffects函数收集依赖,该函数来自effect模块。
triggerRefValue函数用于触发Ref的响应式更新,triggerEffects函数来自effect模块。
Vue3还提供了自定义的Ref,可以传入getter和setter,自由选择track和trigger时机。
在setup函数中返回参数时,使用toRef创建ObjectRefImpl实例对响应式对象的某个属性进行解构。
ObjectRefImpl通过_object属性引用原始响应式对象,在getter中通过_object访问值,依赖收集由_object完成;在setter中,通过引用_object达到赋值操作,从而在_object中触发更新。toRef判断入参是否是Ref,是则直接返回,否则返回ObjectRefImpl。toRefs对传入的对象/数组进行遍历并执行toRef解构。
浅谈Vue3响应式原理与源码解读
Vue3响应式原理的核心在于数据劫持、依赖收集和依赖更新,主要通过Proxy与Reflect这两个ES6新特性实现。首先,理解响应式,它涉及数据变化触发函数自动更新的过程,如视图依赖数据,数据变动则自动刷新视图。副作用函数就是那些引用外部数据的函数,如Vue中的effect函数。
实现响应式的基本步骤是,当数据发生变化时,能够自动调用与之相关的副作用函数。Vue2通过Object.defineProperty进行数据劫持,而Vue3则利用Proxy的set和get拦截器,结合Reflect API,动态跟踪和更新依赖。reactive函数是Vue3响应式的核心,它会创建一个代理对象,通过baseHandlers中的get和set方法进行依赖收集与更新,其中依赖收集在effect.ts中的track()方法中处理。
ref则用于定义基本数据类型的响应式,其源码在packages/reactivity/src/ref.ts。总的来说,Vue3响应式原理的实现是借助Proxy的代理功能,配合Reflect进行数据操作的拦截和反射,从而实现实时响应数据变化的效果。
深入理解这些原理,可以参考Vue官方文档和JavaScript.info的相关内容。
Vue2源码学习笔记 - .响应式原理一computed与watch浅析
本文仅简要介绍Vue2源码中计算属性和侦听属性的初始化过程,深入研究响应式原理将在后续内容中进行。
计算属性初始化:在Vue实例化过程中,传入的计算属性配置被传递至initComputed函数。该函数生成每个计算属性的Watcher对象,且设置lazy选项为真。通过defineComputed函数定义计算属性为响应式变量,实现计算属性的初始化。在defineComputed中,使用Object.defineProperty将计算属性设置为响应式属性,通过生成getter函数(如computedGetter),在获取属性值时,计算并收集依赖。
侦听属性初始化:在initState函数中,侦听属性的初始化调用initWatch函数。此函数直接将侦听属性传递至Vue.prototype.$watch方法,配置侦听属性与回调函数,实现侦听属性的初始化。$watch方法实例化Watcher对象,监听属性变动,当检测到变动时执行回调函数。
总结:计算属性与侦听属性的初始化相对简化,主要依赖于Watcher类。计算属性通过生成Watcher对象与getter函数,实现响应式计算与依赖收集;侦听属性则通过配置Watcher对象与回调函数,实现属性变动时的自动响应。在后续内容中,将深入研究Watcher类及其与计算属性、侦听属性的关联与配合机制。
2024-11-18 13:07
2024-11-18 12:54
2024-11-18 12:53
2024-11-18 12:31
2024-11-18 12:15
2024-11-18 11:47
2024-11-18 11:45
2024-11-18 11:09