阅完此文,Vue响应式不再话下
- 2019 年 12 月 10 日
- 筆記
这篇文章,将会特别轻松的换个角度让你明白整个过程。just do it !!! ???
Vue的响应式系统
我们第一次使用 Vue 的时候,会感觉有些神奇,举个例子:
<div id="app"> <div>价格:¥{{price}}</div> <div>总价:¥{{price*quantity}}</div> <div>折扣后:¥{{totlePriceWithTax}}</div> </div> <script> var vm=new Vue({ el:'#app', data:(){ price:5.00,//单价 quantity:2//数量 }, computed:{ totlePriceWithTax(){ return this.price*this.quantity*1.03 } } }) </script>
我们使用 vue 的时候,不知道它内部做了什么。它都能知道 price 这个字段的值是否发生过变化,如果发生了变化,他会做如下几件事:
- 更新页面显示的 price 的值
- 重新计算总价的乘法表达式并且更新显示结果
- 重新调用 totlePriceWithTax 函数,并且更新显示
这儿,咱们就有一个疑问,vue 怎么就知道 price 变化了之后,都要更新哪些值呢?为什么,每次一变化,就要更新呢?如何跟踪的呢?
JavaScript 正常的运行方式
我们把这个例子整理成我们正常的 JavaScript 程序来看看:
let price=5; let quantity=2; let total=price*quantity;//计算总价 pice=20;//price字段发生变更之后 console.log(`变化之后的总价:${total}`);
这个会输出打印多少呢?因为我这儿没有使用 Vue,很明显,这儿会输出 10:
>> 变化之后的总价:10
在咱们经常使用的 Vue 中,我们想要在 price 或者 quantity 这两个字段更新时,和它有关的表达式也会更新,和它有关的函数也会执行。
>> 变化之后的总价:40
但是,javascript 是过程性的,不是响应式的,所以这个代码在实际运行的时候是不行的。为了让 total 在 price 更新的时候,它也跟着更新,我们必须让 JavaScript 语言具备不同的运行方式。
问题
那么我们现在就遇到了一个问题,怎么样,才能在 price 字段或者 quantity 更新的时候,total 也重新更新显示呢?
尝试一下
首先,我们需要明白 price 和 totle 的关联是:
let total=price*quantity;
那么,在 price 更新之后,需要重新得到新的 total,就需要重新执行这个方法。那么就需要有一个地方把这个方法储存起来,在 price 变更的时候,重新运行储存起来的方法,这样 total 值就更新了。

那我们就来尝试一下,把函数记录下来,后面变更的时候,再次运行。
let price=5; let quantity=2; let total=0; let target=null; //记录函数 target=()=>{ total=price*quantity; } record();//后面讲解,记住这个我们后面想要运行的函数 target();//同时,我们执行一遍这个方法
record 记录函数的实现就很简单了:
let storage=[];//这是要记录函数的地方,就是上面图中椭圆的那个东西 //记录方法的实现,这个时候的target就是我们要记录的方法 function record(){ storage.push(target) }
这一步,将 target 储存了起来,这样我们后面就可以运行它。这个时候,我们就需要一个运行所有记录的内容的函数。那我们就来搞一哈:
function replay(){ storage.forEach((run)=>{ run(); }) }
这儿,我们遍历了所有记录的内容,并且每一个都执行。
这个时候,我们的代码就可以更改一下:
let price=5; let quantity=2; let total=0; let target=null; function record(){ storage.push(target) } function replay(){ storage.forEach((run)=>{ run(); }) } target=()=>{ total=price*quantity; } record(); target(); console.log(total)// 10 price=20; replay(); console.log(total)//40
这样我们就实现了,一个记录的过程,但是这样没有一个很好地管理,我们能不能把记录这块的内容,维护成一个类,让这个类维护一个 tagert 列表,每次需要重新运行的时候,这个类都会得到通知。
年轻人,火力旺,说干就干。维护一个单独的Dep类,代码如下:
class Dep{ constructor(){ this.subscribers=[];//维护所有target的列表,在得到通知的时候,全部都会运行 } depend(){ if(target&&!this.subscribers.includes(target)){ //只有target有方法,并且没有被记录过 this.subscribers.push(target); } } notify(){ this.subscribers.forEach((sub)=>{ sub(); }) } }
在这个类中,我们不再使用 storage,使用 subscribers 这个字符来记录 target 函数的内容,也不再使用 record,使用 depend,也用了 notify 替代了 replay,这个时候要运行,就只需要:
const dep=new Dep(); let price=5; let quantity=2; let total=0; let target=null; target=()=>{ total=price*quantity; } dep.depend();//记录到subscribers中 target(); console.log(total)// 10 price=20; dep.notify();//遍历执行所有target,分发内容 console.log(total)//40
这样,整体的过程就会好一点,但是还是会显得很冗余,如果能过把匿名函数创建,观察,更新的这些行为封装起来,那就更好了。
年轻人,总是冲动,咱们说干就干。把原来的创建和记录:
target=()=>{ total=price*quantity; } dep.depend();//记录到subscribers中 target();
这块内容封装起来,咱们给封装起来的函数起名叫做 watcher,封装起来之后,我们就只需要这样调用:
watcher(()=>{ total=price*quantity })
那我们在实现 watcher 的时候,这么做就好:
function watcher(myFunc){ target=myFunc;//传入的函数赋值 dep.depend();//收集 target();//执行一下 target=null;//重置 }
这儿,咱们看到 watcher 函数接受了一个变量 myFunc, 这个 myFunc 后面接收的是匿名函数,然后赋值给 target 属性,调用 dep.depend(),将以订阅者的形式添加 target 到记录的地方,然后调用 target,并且重置。
现在结合上面的代码咱们尝试一下这个代码:
price=20; console.log(total); dep.notify(); console.log(total);
这里面有一个问题,就是target为什么要设置成全局变量,而不是将其传递给需要的函数。咱们后面会细聊。
现在我们有一个Dep类了,但是我们整整想要实现的情况是,每一个变量都有响应的地方记录它关联的变更,每个变量都有自己的Dep。这个可咋整?
年轻人,不怕事,说干就干。咱们首先把所有的变量放到一起:
let data={ price:5, quantity:2 }
现在我们假设每一个属性(price和quantity)都有自己内部的Dep类。

当我们运行watcher这个函数的时候:
wacther(()=>{ total=data.price*data.quantity })
因为我们是使用到了data.price的值,那么我们希望price属性的Dep类可以将使用它的匿名函数(储存在target上)放在订阅数组中,记录下来(通过调用dep.depend())。同时data.quantity这个变量也被访问了,所以也希望能够被记录下来,放在对应的订阅数组中:

如果这个时候还有其他的地方也在使用data.price,我们也希望可以把对应的匿名函数放到Dep类中记录下来。

那么,什么时候会调用price对应的Dep中的notify呢?在price赋值,值发生改变的时候。我们最后希望发生的效果是:
>> total 10 >> price=20 >> total 40
我们希望,当数据被访问的时候,能够把对应的target匿名函数储存到订阅数组中,当属性变更的时候,能够运行对应的储存在订阅数组中的匿名函数。
解决方案
这个一眼看过去,访问时,改变时。脑海中直接就出来了Object.defineProperty,这个允许我们为属性定义getter和setter函数。在展示如何和Dep结合的之前,先看下用法:
let data={price:5,quantity:2}; Object.defineProperty(data,'price',{ get(){ console.log('被访问') }, set(newVal){ console.log('被修改') } }); data.price;//输出:被访问 data.price=20;//输出:被修改
这里,我们并没有实际的修改get和set的值,因为功能被覆盖了。现在,我们希望get的时候能够返回一个值,set的时候能够更新值。所以我们先添加一个变量internalValue来储存当前的price的值。
let data={price:5,quantity:2}; let internalValue=data.price;//初始值 Object.defineProperty(data,'price',{ get(){ console.log('被访问'); return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal } }); total=data.price*data.quantity;//调用get data.price=20;//调用set
这样我们就可以把所有我们想要的监听的数据,全部给处理一下:
let data={price:5,quantity:2}; Object.keys(data).forEach((key)=>{ let internalValue=data[key];//初始值 Object.defineProperty(data,key,{ get(){ console.log('被访问'); return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal } }); }) total=data.price*data.quantity;//调用get data.price=20;//调用set
这样所有的数据都变了可监听的了。
把他们结合起来
total=data.price*data.quantity
当这个代码运行的时候,会触发price属性对应的get方法,我们希望price的Dep可以记住这个对应的匿名函数(target)。通过这个方式,如果发生改变,触发了set,那么就能够调用这个属性对应的储存起来的匿名函数。
- Get—记住匿名函数,当值发生变化的时候重新运行。
- Set—运行保存的匿名函数,对应匿名函数绑定的值就会发生变化
切换到Dep class的模式:
- price被访问时—调用dep.depend保存当前target
- price被改变时—调用price的dep.notify,重新运行所有的target
最后,我们就把这个结合起来,年轻人,不要磨磨蹭蹭,突突两下就可以了:
let data={price:5,quantity:2}; let target=null; class Dep{ constructor(){ this.subscribers=[];//维护所有target的列表,在得到通知的时候,全部都会运行 } depend(){ if(target&&!this.subscribers.includes(target)){ //只有target有方法,并且没有被记录过 this.subscribers.push(target); } } notify(){ this.subscribers.forEach((sub)=>{ sub(); }) } } Object.keys(data).forEach((key)=>{ let internalValue=data[key];//初始值 Object.defineProperty(data,key,{ get(){ console.log('被访问'); dep.depend();//添加对应的匿名函数target return internalValue }, set(newVal){ console.log('被修改'); internalValue=newVal; dep.notify();//触发对应的储存的函数 } }); }) function watcher(myFunc){ target=myFunc;//传入的函数赋值 target();//执行一下 target=null;//重置 } watcher(()=>{ data.total=data.price*data.quantity; })
这就结合了这一块的东西,price和quantity两个属性变成了响应式的情况,可以下来试一下。
直接上架构图:

最后,Vue2中还有很多东西,Vue3也出来了,我们这块出了对应的课程。年轻人不要犹犹豫豫。机会和成长总在犹豫的时候就溜走了。
在这样一个信息爆炸、知识唾手可得的时代,年轻人一定要做个明白人,懂得筛选和判断优质内容。
你可能经常会领取到海量前端资料包,收藏起来就再也没看过。
但今天,我们想给你点真正有品质的内容——【你不知道的Vue.js 性能优化】
- 本次专题课深度讲解 Vue.js 性能优化,以及 Vue3.0 那些值得关注的新特性。在高级前端岗位面试中,性能优化是一个必问的知识点,本课程通过对 Vue 面试核心知识点的拆解,带你解锁你可能不知道的 Vue.js 性能优化,直达大厂offer!
它将带你学到什么?
1.Vue首屏优化实践
- 大厂面试问Vue项目优化时的各种讲解
- 核心工程化知识点讲解
- 不同的核心优化方案剖析
- 常考Vue知识点串讲
2.面试常问的Vue双向数据深度解析
- 修正对于Object.defineProperty的错误理解
- Vue2中双向数据绑定为什么性能不好?
- 数组的双向数据绑定怎么处理的
3.深度对比 Vue2 & 3,助你直达offer
- 浅尝Vue3的使用
- Vue3的新特性解析
- Vue3核心双向数据绑定的实现解析
- 深度对比Vue2,助你直达offer