vue3源码阅读与实现: 响应式系统的基本概念
本文为阅读本专栏响应式系统源码部分时的前置知识和个人的一些浅薄见解
响应式系统
什么是响应式
在vue
中响应式是指: 当数据发生变化,使用数据的视图自动变化.
为什么要响应式
要想弄懂响应式,要先知道为什么需要响应式,只有放在具体场景中,响应式的概念才能更加立体
上个时代前端开发方式
在现代前端框架出现之前,前端的业务逻辑基本都是使用原生JS
直接操作DOM
来实现的,比如:
实现一个价格修改器,页面上展示一个商品的价格为1
,当点击按钮的时候修改价格为2
,大概会这样实现:
<div class="prices"></div>
<button class="btn">点击修改价格</button>
...
const pricesEl = document.querySelector(".prices");
const btnEl = document.querySelector(".btn");
let prices = 1;
pricesEl.innerText = prices;
btnEl.addEventListener("click", () => {
prices = 2;
pricesEl.innerText = prices;
});
来分析一下完成这个微小的需求,我们经历的几步:
- 初始化,监听按钮点击事件
- 修改
prices
为2
- 修改对应的视图
pricesEl
内容为2
一共需要三步,其实仔细思考这三步真正和业务逻辑相关的只有第二步:修改价格.数据的变化正是业务的体现,所以实际开发的时候,完成需求实际上就是完成以下两方面:
- 数据如何变化
- 数据变化后视图如何修改
当业务复杂起来的时候,比如:有一百个价格需要修改,那么在处理数据的变化和视图的修改时就变的非常繁琐,于是我们想着能不能简化实现方式:
-
数据变化这个步骤可不可以不手动做?
数据的变化承载着业务,而业务随着应用场景不同而不同,那么数据的变化是不可预测的,所以这个步骤一定要手动根据业务来做
-
那视图的修改呢?
视图的修改只是与浏览器交互,方式是固定的,既然是固定的那能不能在数据变化的时候,对应的视图自动变化,自动的执行innerText,这样开发时只用关注数据的变化就好了,将减少大量繁琐的手动渲染工作
-
那么如何让视图自己变化呢?
要想视图自动的变化,关键点在于:
- 需要一个能监听数据变化的东西,在数据变化的时候自动做一些事情比如:修改视图
那么有这么个东西,来帮助我们监听数据变化吗?
当然有,那就是Object.defineProperty
Object.defineProperty
vue2
中实现响应式的核心就是依赖JS提供的API: Object.defineProperty(obj, prop, descriptor)
,MDN,通过他可以向对象的属性添加描述符,比如get
和set
get
:一个函数,用作属性getter
的函数,当访问某个属性的时候,会自动触发set(newVal)
: 一个函数,用作属性setter
的函数,当设置某个属性的时候会自动触发
那么想实现修改数据自动修改视图,就可以利用set
,监听到对象属性的修改,我们就去修改视图:
<div class="prices"></div>
<button class="btn">点击修改价格</button>
...
const pricesEl = document.querySelector(".prices");
const btnEl = document.querySelector(".btn");
let prices = 1;
const dataObj = {};
Object.defineProperty(dataObj, "prices", {
get() {
return prices;
},
set(newVal) {
prices = newVal;
pricesEl.innerText = newVal;
},
});
pricesEl.innerText = dataObj.prices; // 访问dataObj.prices触发get,实际返回的是prices
btnEl.addEventListener("click", () => {
dataObj.prices = 2; // 访问dataObj.prices触发set,在set函数中修改对应的视图
});
这个dataObj.prices
现在变成了响应式数据,怎么修改dataObj.prices
视图都会自动变化.再也不用每次修改完dataObj.prices
还要手动更新视图了.
这就是响应式数据的实现原理,在vue
中响应式系统设计的更全面,封装的更好,让我们使用起来更加方便.
Object.defineProperty缺陷
虽然Object.defineProperty
可以实现响应式数据,但其有一个很大的缺陷:
- 无法检测到对象/数组中新增的属性/元素的修改
这个原因其实很好理解,从Object.defineProperty
的参数就能看出来,他是根据props
来确定要监听哪个属性的,所以只能对某个对象的指定属性进行监听,
这个特点放在vue
中就导致一个现象:,vue无法将对象/数组新增的属性/元素处理为响应式.因为:初始化时vue
将对象/数组中所有属性进行响应式处理,在这之后如果我们对这个对象新增了一个属性,vue
是无法知道新增了哪个属性,也就没办法将这个属性处理成响应式数据了
由于这个缺陷,在vue3
中使用proxy
代替Object.defineProperty
来实现数据的响应式
proxy
Proxy
可以根据一个对象创建另一个对象,叫做代理对象,对代理对象的基本操作会被拦截如:set,get,包括属性的新增
new Proxy(target, handler)
proxy
为什么能弥补Object.defineProperty
的缺陷呢?
原因就在于: Object.defineProperty
的痛点在于无法检测到对象属性的新增,而proxy
返回的是一个代理对象,对这个代理对象添加属性也会被拦截到,从而进行一些自定义操作,比如将这个新增属性处理为响应式数据,于是就完美解决了使用Object.defineProperty
时带来的问题
Reflect
反射(Reflect
),proxy
的伴生对象,提供拦截 JavaScript
操作的方法.这个有什么用呢?通过例子了解起来更直观:
当我们构建响应式数据的时候,所希望的是所有响应式数据的变化都会触发set,如果只用proxy
来处理的话,是无法达到这个效果的:
const obj = {
lastName: "张",
firstName: "三",
// 通过 get 标识符标记,可以让方法的调用像属性的调用一样
get fullName() {
return this.lastName + this.firstName;
},
};
const proxyObj = new Proxy(obj, {
get(target, key, receiver) { // receiver => 代理对象proxyObj
console.log("getter触发了");
return target[key]; //这里不能是receiver(代理对象)会无限循环
},
});
console.log(proxyObj.fullName);
上述代码,使用proxy
创建了obj
的代理对象,拦截了代理对象中属性的getter
,然后访问了代理对象的fullName
,这是我们希望的是对每个属性的访问都会触发getter
,一共触发三次get
操作分别是:
- 访问
proxyObj.fullName
触发一次 - 访问
this.lastName
触发一次 - 访问
this.firstName
触发一次
但实际情况只会打印一次:
getter触发了
张三
这是为什么呢?问题就出在this
,proxy
的拦截只有操作代理对象proxyObj
时才会生效,而访问this.lastName,this.firstName
时,this
指向的是obj
因此后两次的访问都没有生效,如果把target[key]
换成receiver[key]
又会触发无限循环,那该怎么做才能达到目的呢?
答案就是使用Reflect
来访问,Reflect.get(target,key,[,receiver])
他的意思是返回target[key]
的值,第三个参数是可选参数,当指定了第三个参数的时候,访问key
时会将this
指向第三个参数,把上面的代码修改一下:
const proxyObj = new Proxy(obj, {
get(target, key, receiver) {
console.log("getter触发了");
return Reflect.get(target,key,receiver)
},
});
打印:
getter触发了
getter触发了
getter触发了
张三
这时,就达到了我们想要的效果,这就是Reflect
的作用.
转载自:https://juejin.cn/post/7395523104741720103