当前位置: 首页 > 科技观察

深入理解vue响应式原理

时间:2023-03-18 02:42:03 科技观察

【.com原稿】前言Vue最独特的特性之一就是它的非侵入式响应系统。数据模型只是普通的JavaScript对象。当您修改它们时,视图将更新。这使得状态管理非常简单,但了解它的工作原理同样重要,这样可以避免一些常见问题。----官方文档本文将详细介绍响应式原理,带你实现一个基础版的响应式系统。本文代码请戳Github博客。什么叫反应灵敏?我们先来看一个例子:

Price:¥{{price}}
Total:¥{{price*quantity}}
Taxes:¥{{totalPriceWithTax}}
更改价格
varapp=newVue({el:'#app',data(){return{price:5.0,quantity:2};},computed:{totalPriceWithTax(){returnthis.price*this.quantity*1.03;}},methods:{changePrice(){this.price=10;}}})在上面的例子中,当价格发生变化时,Vue知道它需要做三件事:更新页面上price的值来计算表达式price*quantity的值,更新页面调用totalPriceWithTax函数,更新页面变化后,它将重新呈现页面。这就是Vue响应式,那么它是如何工作的呢?要完成这个过程,我们需要:检测数据变化以及集合视图依赖于哪些数据?当数据发生变化时,自动“通知”需要更新的部分视图,更新对应的专业成语是:数据劫持/数据代理依赖于集合发布订阅模式如何检测数据变化首先有一个问题,如何在Javascript中检测一个对象的变化?检测变化其实有两种方式:使用Object.defineProperty和ES6Proxy,也就是数据劫持或者数据代理。这部分代码主要参考了Everest架构类。方法一、Object.defineProperty实现Vue通过设置对象属性的setter/getter方法来监听数据变化,通过getter收集依赖,每个setter方法都是一个观察者,当数据变化时通知订阅者更新视图。functionrender(){console.log('模拟视图渲染')}letdata={name:'乘风破浪',location:{x:100,y:100}}observe(data)functionobj){//判断类型if(!obj||typeofobj!=='object'){return}Object.keys(obj).forEach(key=>{defineReactive(obj,key,obj[key])})functiondefineReactive(obj,key,value){//递归子属性observe(value)Object.defineProperty(obj,key,{enumerable:true,//enumerable(可以遍历)configurable:true,//可配置(比如可以删除)get:functionreactiveGetter(){console.log('get',value)//监听返回值},set:functionreactiveSetter(newVal){observe(newVal)//如果赋值的是对象,也递归子属性if(newVal!==value){console.log('set',newVal)//监控渲染()value=newVal}}})}}data.location={x:1000,y:1000}//设置{x:1000,y:1000}模拟视图渲染data.name//get乘风破浪几个注意点补充说明:该方法无法检测对象属性(如data.location.a=1).这是因为Vue使用Object.defineProperty将对象的key转换成getter/setter的形式来跟踪变化,但是getter/setter只能跟踪一条数据是否被修改,无法跟踪新增和删除的属性。如果是删除一个属性,我们可以使用vm.$delete来实现,那么如果是一个新的属性怎么办呢?1)可以使用Vue.set(location,a,1)方法给嵌套对象添加响应式属性;2)也可以重新赋值这个对象,比如data.location={...data.location,a:1}Object.defineProperty无法监听数组的变化,需要重写数组方法functionrender(){console.log('模拟视图渲染')}letobj=[1,2,3]letmethods=['pop','shift','unshift','sort','reverse','splice','push']//首先获取原始原型上的方法letarrayProto=Array.prototype//创建自己的原型并重写这些方法letproto=Object.create(arrayProto)methods.forEach(method=>{proto[method]=function(){//AOParrayProto[method].call(this,...arguments)render()}})functionobserver(obj){//定义所有属性的方式为set/getif(Array.isArray(obj)){obj.__proto__=protoreturn}if(typeofobj=='object'){for(letkeyinobj){defineReactive(obj,key,obj[key])}}}functiondefineReactive(data,key,value){observer(value)Object.defineProperty(data,key,{get(){returnvalue},set(newValue){observer(newValue)if(newValue!==value){render()value=newValue}}})}observer(obj)func$set(data,key,value){defineReactive(data,key,value)}obj.push(123,55)console.log(obj)//[1,2,3,123,55]这个方法重写了数组的常用方法,然后覆盖了原来的数组方法。重写的数组方法需要可以拦截但是有些数组在运行Vue时无法拦截,当然没办法响应,比如:obj.length--//不支持数组obj[0]的长度变化=1//修改数组***ES6提供了元编程的能力,所以具备了拦截的能力。Vue3.0可能会使用ES6中的Proxy作为实现数据代理的主要方式。方法二、Proxy实现Proxy是JavaScript2015的新特性,Proxy的代理是针对整个对象,而不是对象的某个属性。因此,不同于Object.defineProperty必须遍历对象的每一个属性,Proxy只需要做一个代理,监听同层结构下的所有属性变化。当然对于深层结构,递归还是要做的。另外,**Proxy支持改变代理数组。**functionrender(){console.log('模拟视图的更新')}letobj={name:'前端工匠',age:{age:100},arr:[1,2,3]}lethandler={get(target,key){//如果值为对象,则对该对象进行数据劫持if(typeoftarget[key]=='object'&&target[key]!==null){returnnewProxy(target[key],handler)}returnReflect.get(target,key)},set(target,key,value){if(key==='length')returntruerender()returnReflect.set(target,key,value)}}letproxy=newProxy(obj,handler)proxy.age.name='浪中行舟'//支持新属性console.log(proxy.age.name)//模拟视图更新proxy.arr[0]='行船在theWaves'//支持数组的内容发生变化console.log(proxy.arr)//模拟视图的更新['BoatintheWaves',2,3]proxy.arr.length--//无效上面的代码不仅简化了,而且实现了一套代码,既适用于物体检测,也适用于数组检测。但是Proxy的兼容性不是很好!我们之所以要观察数据,是因为当数据的属性发生变化时,我们可以通知那些已经使用过数据的地方。比如***例子中,模板中使用了价格数据,当它发生变化时,应该向使用它的地方发送通知。那么如何收集依赖呢?收集依赖以及发布-订阅模式下如何收集依赖。总结一句话,在getter中收集依赖,在setter中触发依赖。让我们首先实现一个Dep类来解耦属性依赖收集和调度更新操作。//通过Dep解耦属性依赖和更新操作classDep{constructor(){this.subs=[]}//添加依赖addSub(sub){this.subs.push(sub)}//更新notify(){this.subs.forEach(sub=>{sub.update()})}}//全局属性,通过该属性配置WatcherDep.target=null,需要依赖收集时调用addSub,需要派发update时调用notify。具体怎么称呼呢?letdp=newDep()dp.addSub(()=>{console.log('emithere')})dp.notify()这是“事件发布订阅模式”的简单实现,当然代码只是励志思路,实际应用还是比较“粗糙”,没有事件名称设置,API也不丰富,但已经能充分说明问题了。下面简单了解下Vue组件挂载时添加响应式的过程。组件挂载时,会先为所有需要的属性调用Object.defineProperty(),然后实例化Watcher,传入组件更新回调。在实例化期间,评估模板中的属性,触发依赖项收集。我们可以把Watcher理解为一个中介角色,当数据发生变化时通知它,然后再通知其他地方。***需要修改defineReactive函数,在自定义函数中添加依赖收集和分发更新相关的代码。functionrender(){console.log('模拟视图渲染')}letdata={name:'浪中之舟',location:{x:100,y:100}}observe(data)letdp=newDep()functionobserve(obj){//判断类型if(!obj||typeofobj!=='object'){return}Object.keys(obj).forEach(key=>{defineReactive(obj,key,obj[key])})functiondefineReactive(obj,key,value){//递归子属性observe(value)Object.defineProperty(obj,key,{enumerable:true,//可枚举(可遍历)configurable:true,//可配置(如可以删除)get:functionreactiveGetter(){console.log('get',value)//监控//添加Watcher到订阅中if(Dep.target){dp.addSub(Dep.target)}returnvalue},set:functionreactiveSetter(newVal){observe(newVal)//如果赋值的是对象,还递归子属性if(newVal!==value){console.log('set',newVal)//监听render()value=newVal//执行watcher的update方法dp.notify()}}})}}以上代码都实现了一个简单的数据响应样式,核心思想是手动触发一个propertygetter实现依赖收集。综上所述,我们再回顾一下整个过程:在Vue中,模板编译过程中的一条指令或数据绑定会实例化一个Watcher实例,在实例化过程中会触发get()将自身指向Dep.target;data执行getter时Observer会触发dep.depend()收集依赖;依赖收集的结果:当数据在Observer中时,关闭的dep实例的subs添加观察它的Watcher实例;将观察对象Observer添加到Watcher的deps时的闭包dep;当data中Observer中的一个对象的值发生改变后,subs中观察它的watcher会被触发执行update()方法,实际上是调用watcher的回调函数cb来更新视图。参考文章和书籍珠峰架构课程(强烈推荐)Vue.js内部运行机制浅析Vue.jsVue官方文档前端面试之路前端开发核心知识进阶Javascript响应式最通俗易懂的讲解(翻译)IntroducingSailingintheWaves:研究生,主攻前端。个人公众号:《前端工匠》,致力于打造一系列适合初中级工程师快速吸收的优质文章!【原创稿件,合作网站转载请注明原作者及出处为.com】