响应式实现原理
❄响应式系统的设计原则
01:JS的程序性
ini
let obj = {
num:5,
value:2
}
let total = obj.num*obj.value
console.log(total) //10
obj.value = 4
console.log(total) //10
此时当我们第二次打印total时,想要的打印结果是20,但是并没有得到想要的结果
因为JS是程序性的,所以想要得到20必须要做一些额外的处理
ini
let obj = {
num:5,
value:2
}
let total
const effect = ()=>{
total = obj.num*obj.value
}
console.log(total) //10
obj.value = 4
effect()
console.log(total) //20
这样就可以得到想要的结果了,但是每次都需要重新调用一下effect函数才行。
02:Vue2的响应式原理
Vue2通过Object.defineProperty AP来实现响应性的:
javascript
let value = 2
let obj = {
num:5,
value:value
}
let total
const effect = ()=>{
total = obj.num*obj.value
}
Object.defineProperty(obj,"value",{
get(){
return value
},
set(newVal){
value = newVal
effect()
}
})
console.log(total) //10
obj.value = 4
console.log(total) //20
这样就不需要每次手动调用effectt函数了,这样每次修改value的值,都会触发set修改value的值并且拿到最新的total的值。
由于javascript的限制,导致vue2中响应性的限制:
- 当data中没有对应的属性时,向data中新增的属性都是不具备响应性的。
- 通过数组下标的形式新增元素时,此时也不具备响应性
因为Object.defineProperty 只能监听指定对象的指定属性,所以再vue中data中没有预先定义的属性是没有响应性的
03:Vue3的响应性Proxy
由于Object.defineProperty 的缺陷,因此Vue3使用Proxy来实现响应性
javascript
let obj = {
num:5,
value:2
}
let total
const effect = ()=>{
total = p1.num*p1.value
}
const p1 = new Proxy(obj,{
get(target, key, receiver){
return target[key]
},
set(target, key, newVal,receiver){
target[key] = newVal
effect()
return true
}
})
console.log(total) //10
p1.value = 4
console.log(total) //20
如上例子,我们可以总结vue2和vue3响应式的区别
Proxy
- proxy将代理一个被代理对象obj,返回 一个代理对象,他代理的是整个对象而不是指定对象的指定属性
- 当需要修改属性时,我们可以通过代理对象来修改
Object.defineProperty
- 只可以代理指定对象的指定属性,
- 当想要修改属性时,通过原对象进行修改
❄ 源码实现------reactive
创建如下测试实例:
xml
<body>
<div id="app"></div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: 'zhangsan'
})
effect(() => {
document.querySelector('#app').innerText = obj.name
})
setTimeout(() => {
obj.name = 'lisi'
}, 2000)
console.log(obj)
</script>
在源码中首先会执行reactive 方法,reactive 方法中会返回createObjectReactive方法的调用,主要逻辑会这个方法中进行,这个方法接收三个参数:
- target对象即调用reactive传的对象,
- baseHandlers 函数封装好的get 和set函数,
- proxyMap 用过缓存处理的Weakmap对象。
接下来我们进行实现:
javascript
/**
*
* @param target 代理对象
* @param mutableHandlers get set函数封装
* @param reactiveMap 弱引用 map 用过缓存处理
*
*/
export function reactive(target: Object) {
return createObjectReactive(target, mutableHandlers, reactiveMap)
}
function createObjectReactive(target, baseHandlers, proxyMap) {
// 从缓存中读取
const existingProxy = proxyMap.get(target)
// 如果已经存在直接返回 无需再创建
if (existingProxy) {
return existingProxy
}
// 通过Proxy代理传过来的target对象
const proxy = new Proxy(target, baseHandlers)
// 标志为一个reactive
proxy[ReactiveFlag.IS_REACTIVE] = true
// 缓存处理
proxyMap.set(target, proxy)
return proxy
}
baseHandlers函数:
typescript
export const createGetter = () => {
return function get(target: Object, key: any, receiver: Object) {
//读取代理对象值触发
const res = Reflect.get(target, key, receiver)
return res
}
}
export const createSetter = () => {
return function set(target: Object, key: any, value: unknown, receiver: Object) {
//修改代理对象值触发
const result = Reflect.set(target, key, value, receiver)
return result
}
}
const get = createGetter()
const set = createSetter()
export const mutableHandlers: ProxyHandler<object> = {
get, set
}
reactive 函数执行完毕,接下来会执行到我们测试实例中的effect方法:
源码中会调用effect 方法,创建ReactiveEffect 实例,ReactiveEffect 是一个类,该类中有一个run和stop方法,该类接收一个fn 函数,即调用effect 传的匿名函数。调用ReactiveEffect的run方法就会调用fn函数.
代理如下:
typescript
let activeEffect = nul
export class ReactiveEffect<T = any> {
constructor(public fn: () => T) {
}
run() {
activeEffect = this
return this.fn()
}
stop() { }
}
// effect fn函数即 () => {document.querySelector('#app').innerText = obj.name}
export function effect<T = any>(fn: () => T, options?: ReactiveEffectOptions) {
const _effect = new ReactiveEffect(fn)
if (!options ) {
//执行fn函数
_effect.run()
}
}
调用fn 函数以后,因为document.querySelector('#app').innerText = obj.name
会读取obj.name 即会调用代理对象的get 函数,这次触发get 函数,和以往不一样,会通过track函数进行依赖收集
代码如下:
typescript
export let activeEffect: ReactiveEffect | undefined
/**
* 收集所有依赖的 WeakMap 实例:
* 1. `key`:响应性对象
* 2. `value`:`Map` 对象
* 1. `key`:响应性对象的指定属性
* 2. `value`:指定对象的指定属性的 执行函数
*/
export let targetMap = new WeakMap<any, KeyToDepMap>()
//get函数
export const createGetter = () => {
return function get(target: Object, key: any, receiver: Object) {
const res = Reflect.get(target, key, receiver)
// 依赖收集
track(target, key)
return res
}
}
// 收集依赖
export function track(target: object, key: unknown) {
//当前不存在执行函数 直接return
if (!activeEffect) {
return
}
//通过target对象获取map对象
let depsMap = targetMap.get(target)
if (!depsMap) {
//获取不到进行初始化
targetMap.set(target, (depsMap = new Map()))
}
//给map对象的指定属性 即target对象的执行属性设置对应的回调函数 建立联系
depsMap.set(key,activeEffect )
}
二秒之后执行setTimeout(() => {obj.name = 'lisi'}, 2000)
,即会触发代理对象的set 函数,这时触发会进行依赖触发即trigger函数
csharp
// 触发依赖
export function trigger(target: Object, key: unknown) {
//获取get时存储的依赖map对象
const depsMap = targetMap.get(target)
//没获取到直接return
if (!depsMap) {
return
}
//通过指定属性名获取对象的activeEffect
const effect = depsMap.get(key) as ReactiveEffect
if (!effect) {
return
}
//触发run方法即触发fn函数 获取到最新的值 并修改视图
//document.querySelector('#app').innerText = obj.name fn函数
effect.run()
}
测试实例执行完成,基本的reactive函数已经构建完成
但是此时存在一个问题,每个响应式数据只能处理一个effect的回调,如下:
xml
<body>
<div id="app">
<p id="p1"></p>
<p id="p2"></p>
</div>
</body>
<script>
const { reactive, effect } = Vue
const obj = reactive({
name: '张三'
})
// 调用 effect 方法
effect(() => {
document.querySelector('#p1').innerText = obj.name
})
effect(() => {
document.querySelector('#p2').innerText = obj.name
})
setTimeout(() => {
obj.name = '李四'
}, 2000);
</script>
要想处理一个响应式数据可以对应多个effect模块,那必须要处理一对多的现象,也就是当我们处理上述desmap的时候存储的activeEffect要变成一个数组。
要想实现,源码中时通过增加一个dep set对象实现的
targetMap的结构
- key(taget) 响应性对象
- value map对象
- key 响应性对象的指定属性
- value Set对象 存储唯一不会重复
- reactiveEffect 响应性对象指定属性对应的effect回调函数fn
如下图所示结构:
✑ 代码实现:
typescript
//dep 创建set对象模块
export type Dep = Set<ReactiveEffect>
export const createDep = (effects?: ReactiveEffect[]): Dep => {
const dep = new Set<ReactiveEffect>(effects) as Dep
return dep
}
javascript
// 收集依赖
export function track(target: object, key: unknown) {
if (!activeEffect) {
return
}
//通过target获取对应的map对象
let depsMap = targetMap.get(target)
//获取不到则进行存储
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
//再通过属性名 获取存储多个fn函数的set对象
let dep = depsMap.get(key)
//获取不到则进行初始化
if (!dep) {
depsMap.set(key, (dep = createDep()))
}
//存储当前activeEffect 即fn函数
trackEffects(dep)
}
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
// 触发依赖
export function trigger(target: Object, key: unknown) {
//通过target获取对应的map对象
const depsMap = targetMap.get(target)
if (!depsMap) {
return
}
//再通过属性名 获取存储多个fn函数的set对象
const dep: Dep | undefined = depsMap.get(key)
if (!dep) {
return
}
triggerEffects(dep)
}
export const triggerEffects = (dep: Dep) => {
//因为是一对多 则进行遍历触发fn函数
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
triggerEffect(effect)
}
}
export const triggerEffect = (effect: ReactiveEffect) => {
effect.run()
}
✑此时,一个响应性对象属性就可以对应多个effect函数了
✑reactive 函数的局限性,对于 reactive
函数而言,它会把传入的 object 作为 proxy
的 target
参数,而对于 proxy
而言,他只能代理 对象 ,而不能代理简单数据类型,所以说:我们不可以使用 reactive
函数,构建简单数据类型的响应性。
为了构建简单数据类型的响应式Vue
是通过ref来实现的
❄ 源码实现------ref
✑ref处理复杂类型
创建如下测试实例:
xml
<body>
<div id="app"></div>
<script>
const { ref, effect } = Vue
const obj = ref({
name: '张三'
})
effect(() => {
document.querySelector('#app').innerText = obj.value.name
})
setTimeout(() => {
obj.value.name = '李四'
}, 2000)
</script>
</body>
首先会进入ref 函数,ref 函数会返回
createRef
函数的调用,这个方法中会通过一个isRef
方法判断当前传入的值是否已经是一个ref
的数据,如果是就直接返回,如果不是会通过一个RefImpl
类创建实例,该类主要提供了get value
和set value
来监听,这也是为什么ref
为什么要.value
的原因。
✑代码实现:
typescript
export function ref(value: unknown) {
return createRef(value, false)
}
export function isRef(r) {
return !!(r && r.__v_isRef === true)
}
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined //用来进行依赖收集的set对象
private _rawValue: T
public readonly __v_isRef = true //用来判断当前是否为ref数据
constructor(value: T, shallow: boolean) {
this._rawValue = value
//判断当前传过来的数据是否是对象,是对象则用reactive来处理,不是则原值返回
this._value = toReactive(value)
}
// .value 会触发
get value() {
return this._value
}
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
this._rawValue = newValue
this._value = toReactive(newValue)
}
}
}
/**
* 创建 RefImpl 实例
* @param rawValue 原始数据
* @param shallow boolean 形数据,表示《浅层的响应性(即:只有 .value 是响应性的)》
* @returns
*/
export function createRef(rawValue: unknown, shallow: boolean) {
if (isRef(rawValue)) {
return rawValue
}
return new RefImpl(rawValue, shallow)
}
toReactive
方法
scss
//判断当前传过来的数据是否是对象,是对象则用reactive来处理
export function toReactive(value) {
return isObject(value) ? reactive(value) : value
}
ref函数执行完毕,接下来执行
effect
函数,执行逻辑和之前并无差别,但是obj.value.name
会触发RefImpl
类的get value
方法,这个方法首先会进行依赖收集,将当前的ReactiveEffect
放入dep Set对象当中,因为他是复杂类型数据,响应性是通过reactive
实现的,因此会触发reactive
函数的getter进行依赖收集,逻辑与之前一样。
csharp
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
private _rawValue: T
public readonly __v_isRef = true
constructor(value: T, shallow: boolean) {
this._rawValue = value
this._value = toReactive(value)
}
get value() {
//依赖收集
trackRefValue(this)
return this._value
}
set value(newValue) {
if (hasChanged(newValue, this._rawValue)) {
this._rawValue = newValue
this._value = toReactive(newValue)
triggerRefValue(this)
}
}
}
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
export function trackRefValue(ref) {
//如果当前activeEffect存在才去收集依赖
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
//依赖收集
export function trackEffects(dep: Dep) {
dep.add(activeEffect!)
}
effect
函数执行完毕,两秒以后执行obj.value.name = '李四'
这个代码可以分解为:
iniconst value = obj.value value.name = "李四"
由上可知,首先会触发
RefImpl
的get value
,再次收集依赖,但是此时的activeEffect
是之前触发effect
函数的fn函数
,因为Set对象存储的值唯一不会重复,所以此时不会被收集到dep当中。此时会触发reactive
函数的setter
触发之前收集的依赖,也就是触发effect
函数中的fn函数,视图发生改变变成李四。
✑ref处理简单数据类型
创建如下测试实例:
xml
<body>
<div id="app"></div>
<script>
const { ref, effect } = Vue
const obj = ref('张三')
effect(() => {
document.querySelector('#app').innerText = obj.value
})
setTimeout(() => {
obj.value = '李四'
}, 2000)
</script>
</body>
首先执行
ref
函数,创建一个RefImpl
实例接着执行effect
函数,执行document.querySelector('#app').innerText = obj.value
,obj.value
会触发RefImpl
的get value进行依赖收集,两秒之后执行obj.value = '李四'
,触发set value进行依赖触发。
代码实现:
typescript
export function ref(value: unknown) {
return createRef(value, false)
}
export function isRef(r) {
return !!(r && r.__v_isRef === true)
}
class RefImpl<T> {
private _value: T
public dep?: Dep = undefined
private _rawValue: T //保存当前值 触发set的时候用来比较新值和旧值是否相同
public readonly __v_isRef = true
constructor(value: T, shallow: boolean) {
this._rawValue = value
this._value = toReactive(value)
}
get value() {
//依赖收集
trackRefValue(this)
return this._value
}
set value(newValue) {
//判断新值和旧值是否相同
if (hasChanged(newValue, this._rawValue)) {
//将新值赋值给旧值
this._rawValue = newValue
this._value = toReactive(newValue)
triggerRefValue(this)
}
}
}
export function triggerRefValue(ref) {
if (ref.dep) {
triggerEffects(ref.dep)
}
}
//依赖收集
export function trackRefValue(ref) {
if (activeEffect) {
trackEffects(ref.dep || (ref.dep = createDep()))
}
}
//循环触发依赖
export const triggerEffects = (dep: Dep) => {
const effects = isArray(dep) ? dep : [...dep]
for (const effect of effects) {
triggerEffect(effect)
}
}
//依赖触发
export const triggerEffect = (effect: ReactiveEffect) => {
effect.run()
}
dart
//hasChanged方法 判断两个值是否相同
export const hasChanged = (newValue, oldValue) => {
return !Object.is(newValue, oldValue)
}
简单数据类型,不具备数据件监听的概念,即本身并不是响应性的。
只是因为
vue
通过了set value()
的语法,把 函数调用变成了属性调用的形式,让我们通过主动调用该函数,来完成了一个 "类似于" 响应性的结果。