vuejs设计与实现 4-6 响应式
### Vue
1. 响应式系统的作用与实现
2. 非原始值的响应式方案
3. 原始值的响应式方案
1. 响应式系统的作用与实现
- 响应式数据与副作用函数
- 副作用函数
- “副作用函数”通常指的是除了返回值之外,还会对函数外部的状态产生影响的函数。如,一个函数修改了全局变量、修改了传入的引用类型参数(而不是返回新的对象或数组)、进行了输入输出操作(如写入文件、发送网络请求等),这些都被认为是产生了副作用。
- 产生副作用可能会使代码的可预测性降低,增加调试和理解代码的难度。
function effect() {
document.body.innerText = 'hello';
}
// 当effect函数执行时,它会设置body的文本内容,但除了effect函数之外的其他函数都可以读取或设置body的文本内容,即effect函数的执行会直接或间接影响其他函数的执行,这时就说effect函数产生了副作用。副作用很容易产生,如一个函数修改了全局变量;
- 响应式数据
const obj = {a: '11'}
function effect(){
document.body.innerText = obj.a;
}
// 定义一个obj对象,副作用函数会读取obj.a属性值,当obj.a变化时,希望副作用函数会重新执行,如果能实现这个目标,那么obj即是响应式数据
// 1. 当副作用函数effect执行时,会触发字段obj.text的读取操作;
// 2. 当修改obj.text的值时,会触发字段obj.text的设置操作;
// 考虑拦截一个对象的读取和设置操作
// 1. 创建一个用于存储副作用函数的桶bucket,Set类型;
// 2. 定义原始数据data,obj是原始数据的代理对象;分别设置get和set拦截函数,用于拦截读取和设置操作;
// 3. 当读取属性时将副作用函数effect添加到桶里,即bucket.add(effect),然后返回属性值;当设置属性值时先更新原始数据,再将副作用函数从桶里取出并重新执行,即可实现响应式数据
// 响应式数据的基本实现
// 存储副作用函数的桶
const bucket = new Set();
// 原始数据
const data = { text: "hello world"};
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数effect添加到存储副作用函数的桶中(这里的副作用函数名称暂时被定死了,后期可以通过设置一个全局变量来存储被注册的副作用函数)
bucket.add(effect)
// 返回属性值
return target[key];
},
// 拦截设置操作
set(target, key, newVal){
// 设置属性值
target[key] = newVal;
// 把副作用函数从桶里取出并执行
bucket.forEach(fn => fn())
// 返回true代表设置操作成功
return true;
}
})
- 响应式系统
- 一个响应式系统的工作流程:1. 当读取操作发生时,将副作用函数收集到桶中;2. 当设置操作发生时,从桶中取出副作用函数并执行;
// 用一个全局变了存储被注册的副作用函数
let activeEffect;
// effect函数用于注册副作用函数
function effect(fn){
// 当调用effect注册副作用函数时,将副作用函数fn赋值给activeEffect
activeEffect = fn;
// 执行副作用函数
fn();
}
effect(
// 使用一个匿名的副作用函数
() => {
document.body.innerText = obj.text;
}
)
const obj = new Proxy(data, {
get(target, key) {
// 将activeEffect中存储的副作用函数收集到桶中
if (activeEffect){
bucket.add(activeEffect)
}
return target[key]
},
set(tarhet, key, newVal) {
target[key] = newVal;
bucket.forEach(fn => fn())
return true;
}
})
// 没有在副作用函数与被操作的目标字段之间建立明确的联系
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
// 将副作用函数activeEffect添加到存储副作用函数的桶中
track(target, key);
return target[key];
},
set(target, key, newVal) {
target[key] = newVal;
// 把副作用函数从桶里取出并执行
trigger(target, key);
}
})
// 在get拦截函数内调用track 函数追踪变化
function track(target, key){
if(!activeEffect) return;
let depsMap = bucket.get(target);
if (!depsMap){
bucket.set(target, (depsMap=new Map()))
}
let deps = depsMap.get(key);
if(!deps){
depsMap.set(key, (deps=new Set()))
}
depsMap.add(activeEffect)
}
// 在set拦截函数内调用trigger 副作用函数触发变化
function trigger(target, key) {
const depsMap = bucket.get(target);
if(!depsMap) return;
const effects = depsMap.get(key);
effects && effects.forEach(fn => fn())
}
- track函数:用来追踪和收集依赖的;
- trigger函数:用来触发副作用函数重新执行;
- computed
- 本质上是一个懒执行的副作用函数,通过lazy选项使得副作用函数可以懒执行;被标记为懒执行的副作用函数可以通过手动方式让其执行;利用这个特定,设计了计算属性,当读取计算属性的值时,只需要手动执行副作用函数即可;当计算属性依赖的响应式数据发生变化时,会通过scheduler将dirty标记设置为true,代表脏,这样,下次读取计算属性的值时,会重新计算真正的值。
- watch
- 本质就是观测一个响应式数据,当数据发生变化时通知并执行相应的回调函数;利用了effect及options.scheduler选项
2. 非原始值的响应式方案
- Vue.js 3的响应式数据是基于Proxy实现的,Proxy可以为其他对象创建一个代理对象;所谓代理,指的是对一个对象基本语义的代理。允许拦截并重新定义对一个对象的基本操作。在实现代理的过程中,遇到了访问器属性的this指向问题,需要使用Reflect.*方法并指定正确的reveiver来解决
- JavaScript中有两种对象,常规对象、异质对象;
- 深响应、浅响应,深只读、浅只读;这里的深浅指的是对象的层级,浅响应或浅只读代表仅代理对象的第一层属性,即只有对象的第一层属性值是响应的或只读的;深响应或深只读则恰恰相反,为了实现深响应或深只读,需要在返回属性值之前,对值做一层包装,将其包装为响应式或只读式数据后再返回。
3. 原始值的响应式方案
- ref本质上是一个包裹对象,因为JavaScript的Proxy无法提供对原始值的代理,所以需要使用一层对象作为包裹,间接实现原始值的响应式方案。由于包裹对象本质上与普通对象没有区别,因此为了区分ref与普通响应式对象,还为包裹对象定义了一个值为true的属性,即__v_isRef,用它来作为ref的标识。
- ref除了能够用于原始值的响应式方案外,还能用来解决响应式丢失的问题,为了解决该问题,实现了toRef及toRefs,本质上是对响应式数据做了一层包装或者叫访问代理。
- 自动脱ref的能力,自动对暴露到模板中的响应式数据进行脱ref处理,这样,用户在模板中使用响应式数据时,就无须关系一个值是不是ref了。
参考&感谢各路大神
1. vue.js设计与实现-霍春阳
宝剑锋从磨砺出,梅花香自苦寒来。