一、什么是响应式
在开始响应式原理与源码解析之前,需要先了解一下什么是响应式?首先明确一个概念:响应式是一个过程,它有两个参与方: 触发方:数据, 响应方:引用数据的函数;当数据发生改变时,引用数据的函数会自动重新执行,例如,视图渲染中使用了数据,数据改变后,视图也会自动更新,这就完成了一个响应的过程。
Vue2.x的响应式
- 实现原理:
- 对象类型:通过
``Object.defineProperty()``对属性的读取、修改进行拦截(数据劫持)。
- 数组类型:通过重写更新数组的一系列方法来实现拦截。(对数组的变更方法进行了包裹)。
- 存在问题:
- 新增属性、删除属性, 界面不会更新。
- 直接通过下标修改数组, 界面不会自动更新。
Vue3.0的响应式
- 实现原理:
- 通过Proxy(代理): 拦截对象中任意属性的变化, 包括:属性值的读写、属性的添加、属性的删除等。
- 通过Reflect(反射): 对源对象的属性进行操作。
- MDN文档中描述的Proxy与Reflect:
new Proxy(data, {
// 拦截读取属性值
get (target, prop) {
return Reflect.get(target, prop)
},
// 拦截设置属性值或添加新属性
set (target, prop, value) {
return Reflect.set(target, prop, value)
},
// 拦截删除属性
deleteProperty (target, prop) {
return Reflect.deleteProperty(target, prop)
}
})
proxy.name = 'tom'
二. Proxy 与 Reflect
Proxy
Proxy: 代理,顾名思义主要用于为对象创建一个代理,从而实现对对象基本操作的拦截和自定义。可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。基本语法:
let proxy = new Proxy(target, handler);
target: 需要拦截的目标对象handler: 也是一个对象,用来定制拦截行为 举个例子:
const obj = {
name: 'John',
age: 16
}
const objProxy = new Proxy(obj,{})
objProxy.age = 20
console.log('obj.age',obj.age);
console.log('objProxy.age',objProxy.age);
console.log('obj与objProxy是否相等',obj === objProxy);
这里objProxy的handler为空,则直接指向被代理对象,并且代理对象与数据源对象并不全等.如果需要更加灵活的拦截对象的操作,就需要在handler中添加对应的属性。例如:
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
return target[key]
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
target[key] = value
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
return delete target[key]
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
上面的例子,我们在捕获器中定义了set()、get()、deleteProperty()属性,通过对proxy的操作实现了对obj的操作拦截。这些属性的触发方法有如下参数: * target —— 是目标对象,该对象被作为第一个参数传递给new Proxy
key—— 目标属性名称value—— 目标属性的值receiver—— 指向的是当前操作 正确的上下文。如果目标属性是一个getter访问器属性,则receiver就是本次读取属性所指向的this对象。通常,receiver这就是proxy对象本身,但是如果我们从proxy继承,则receiver指的是从该proxy继承的对象
Reflect
Reflect: 反射,就是将代理的内容反射出去。Reflect与Proxy一样,也是 ES6 为了操作对象而提供的新 API。它提供拦截JavaScript操作的方法,这些方法与Proxy handlers 提供的的方法是一一对应的,只要是Proxy对象的方法,就能在Reflect对象上找到对应的方法。且 Reflect 不是一个函数对象,即不能进行实例化,其所有属性和方法都是静态的。还是上面的例子
const obj = {
name: 'John',
age: 16
}
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
return Reflect.get(target, key, receiver)
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
return Reflect.set(target, key, value, receiver)
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
return Reflect.deleteProperty(target, key)
},
}
const proxy = new Proxy(obj, handler)
console.log(proxy.age)
proxy.age = 20
console.log(delete proxy.age)
上面的例子中 Reflect.get()代替target[key]操作 Reflect.set()代替target[key] = value操作 * Reflect.deleteProperty()代替delete target[key]操作 。Reflect的作用在于提供了一种更加安全和易于管理的方式来执行底层操作,而不是直接操作对象本身。
三、简单实现reactive函数
// 判断是否为对象 ,注意 null 也是对象
const isObject = val => val !== null && typeof val === 'object'
// 判断key是否存在
const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
function reactive(target) {
// 首先先判断是否为对象
if (!isObject(target)) return target
const handler = {
get(target, key, receiver) {
console.log(`获取对象属性${key}值`)
// ... 这里还需要收集依赖,先空着
const result = Reflect.get(target, key, receiver)
// 递归判断的关键, 如果发现子元素存在引用类型,递归处理。
if (isObject(result)) {
return reactive(result)
}
return result
},
set(target, key, value, receiver) {
console.log(`设置对象属性${key}值`)
// 首先先获取旧值
const oldValue = Reflect.get(target, key, reactive)
// set 是需要返回 布尔值的
let result = true
// 判断新值和旧值是否一样来决定是否更新setter
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
// 更新操作 等下再补
}
return result
},
deleteProperty(target, key) {
console.log(`删除对象属性${key}值`)
// 先判断是否有key
const hadKey = hasOwn(target, key)
const result = Reflect.deleteProperty(target, key)
if (hadKey && result) {
// 更新操作 等下再补
}
return result
},
}
return new Proxy(target, handler)
}
const obj = {
name: '小浪',
age: 22,
test: {
test1: {
test2: 21,
},
},
}
const proxy = reactive(obj)
console.log(proxy.age)
proxy.test.test1.test2 = 22
console.log(delete proxy.age)
四、收集依赖/触发更新
在 Proxy 第二个参数 handler 中,拦截各种取值、赋值操作,依托 track 和 trigger 两个函数进行依赖收集和派发更新。
track 用来在读取时收集依赖。
trigger 用来在更新时触发依赖。
track
// targetMap 表里每个key都是一个普通对象 对应他们的 depsMap
let targetMap = new WeakMap()
function track(target, key) {
// 如果当前没有effect就不执行追踪
if (!activeEffect) return
// 获取当前对象的依赖图
let depsMap = targetMap.get(target)
// 不存在就新建
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
// 根据key 从 依赖图 里获取到到 effect 集合
let dep = depsMap.get(key)
// 不存在就新建
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
// 如果当前effectc 不存在,才注册到 dep里
if (!dep.has(activeEffect)) {
dep.add(activeEffect)
}
}
target 是原对象;
key 是指本次访问的是数据中的哪个 key,比如下文例子中收集依赖的 key 就是 count;
最后添加到hander 里 get 中
get(target, key, receiver) {
// ...
// 收集依赖
track(target, key)
// ...
},
effect函数:在Vue3中,副作用函数(Effect Function)通常指的是那些依赖于响应式数据并在数据变化时重新执行的函数。我们使用effect函数来创建副作用函数。effect函数接受一个函数作为参数,并在该函数内部访问响应式状态时收集依赖,从而创建一个响应式的副作用。
依赖地图:
首先全局会存在一个 targetMap,它用来建立 数据 -> 依赖 的映射,它是一个 WeakMap 数据结构。而 targetMap 通过数据 target,可以获取到 depsMap,它用来存放这个数据对应的所有响应式依赖。depsMap 的每一项则是一个 Set 数据结构,而这个 Set 就存放着对应 key 的更新函数。
例如对于下面这个例子的依赖关系:
let product=reactive({ price:5,quantity:2})
let total=0
let effect=()=>{
total =product.price *product.quantity
}
effect()
console.log(total)
product.quantity=3
console.log(total)
- 全局的
targetMap是:
targetMap: {
{ price:5,quantity:2}: dep
}
2. dep 则是
dep: {
price: Set { effection },
quantity: Set { effection }
}
这样一层层的下去,就可以通过 target 找到 count 对应的更新函数 effection 了。
trigger
// trigger 响应式触发
export function trigger(target, key) {
// 拿到依赖图
const depsMap = targetMap.get(target)
if (!depsMap) {
// 没有被追踪,直接 return
return
}
// 拿到了视图渲染effect 就可以进行排队更新 effect 了
const dep = depsMap.get(key)
// 遍历 dep 集合执行里面 effect 副作用方法
if (dep) {
dep.forEach(effect => {
effect()
})
}
}
最后添加到hander 的 set 和 deleteProperty 中
set(target, key, value, receiver) {
// ...
if (oldValue !== value) {
result = Reflect.set(target, key, value, receiver)
trigger(target, key)
}
// ...
},
deleteProperty(target, key) {
// ...
if (hadKey && result) {
// 更新操作
trigger(target, key)
}
// ...
}
五、简化版Vue3响应式实现
const targetMap = new WeakMap();
let total = 0;
const effect = () => { total = product.price * product.quantity };
const track = (target, key) => {
let depsMap = targetMap.get(target);
if(!depsMap){
targetMap.set(target, (depsMap = new Map()));
}
let dep = depsMap.get(key);
if(!dep) {
depsMap.set(key, (dep = new Set()));
}
dep.add(effect);
}
const trigger = (target, key) => {
const depsMap = targetMap.get(target);
if(!depsMap) return;
let dep = depsMap.get(key);
if(dep) {
dep.forEach( effect => effect() );
}
};
const reactive = (target) => {
const handler = {
get(target, key, receiver){
console.log('正在读取的数据:',key);
const result = Reflect.get(target, key, receiver);
track(target, key); // 自动调用 track 方法收集依赖
return result;
},
set(target, key, value, receiver){
console.log('正在修改的数据:', key, ',值为:', value);
const oldValue = target[key];
const result = Reflect.set(target, key, value, receiver);
if(oldValue != result){
trigger(target, key); // 自动调用 trigger 方法执行依赖
}
return result;
}
}
return new Proxy(target, handler);
}
let product = reactive({price: 10, quantity: 2});
effect();
console.log(total);
product.price = 20;
console.log(total);
六、补充:reactive对比ref
- 从定义数据角度对比:
- ref用来定义:基本类型数据。
- reactive用来定义:对象(或数组)类型数据。
- 备注:ref也可以用来定义对象(或数组)类型数据, 它内部会自动通过
``reactive``转为代理对象。
- 从原理角度对比:
- ref通过
`Object.defineProperty()`的``get``与``set``来实现响应式(数据劫持)。
- reactive通过使用Proxy来实现响应式(数据劫持), 并通过Reflect操作源对象内部的数据。
- 从使用角度对比:
- ref定义的数据:操作数据需要
``.value``,读取数据时模板中直接读取不需要``.value``。
- reactive定义的数据:操作数据与读取数据:均不需要
``.value``。