vue3响应式原理解读

vue2 PK vue3

要说到vue2和vue3的响应式是怎么实现的,大家都能大概回答出来

  • vue2的响应式是基于Object.defineProperty来实现的
  • vue3的响应式是基于Proxy实现的

抽象的回答却也说出了两个版本的核心原理,就是提现在Object.defineProperty和Proxy上

vue2

先简单举个例子,来看下vue2的object.defineProperty

function 复制代码
    Object.defineProperty(obj, key, {
        get() {
            console.log(`访问了${value}属性`)
            return value
        },
        set(val) {
            console.log(`将${value}改成了${val}`)
            value = val;
        }
    })
}

const data = {
    name: 'gaoyan',
    age: 28
};
Object.keys(data).forEach(key => {
    reactive(data, key, data[key])
})

console.log(data.name);    
data.name='Mrs.gao'
console.log(data.name);
-----打印结果
访问了gaoyan属性
gaoyan
将gaoyan改成了Mrs.gao
访问了Mrs.gao属性
Mrs.gao

但是如果我新增一个属性呢

js 复制代码
data.hobby='coding';
console.log(data.hobby);

data.hobby='write';
console.log(data.hobby);
-----打印结果
coding
write

这时我们会发现我们新增了一个属性,并且对这个属性进行了读取和修改,但是没有触发get和set,也就是说Object.defineProperty不会对新增的对象进行监听,需要额外使用Vue.$set来把新增的属性设置为响应式对象

vue3响应式原理

js 复制代码
let name='高高',age=22,money=1;
let myself = `${name}今年${age}岁,存款${money}元`

console.log(myself);  // 高高今年22岁,存款1元

money=100;

console.log(myself);  // 高高今年22岁,存款1元

看完上面一段代码,我们发现,当变量money由1变成10以后,myself的值却没有发生变化,那如果想要按照我们的预期让myself跟着money的变化而变化,我们需要把myself再执行一次,如下图

let name='高高',age=22,money=1; let myself = ${name}今年${age}岁,存款${money}元

console.log(myself); // 高高今年22岁,存款1元

money=100;

myself = ${name}今年${age}岁,存款${money}元;

console.log(myself); // 高高今年22岁,存款100元

由此可见,每次money改变就要执行一次myself,才能是的myself更新,那我们来封装一个effect使得写法更加优雅

js 复制代码
let name = '高高', age = 22, money = 1;
let myself = '';

const effect = () => myself = `${name}今年${age}岁,存款${money}元`
effect()
console.log(myself);  // 高高今年22岁,存款1元

money = 100;
effect()
console.log(myself);  // 高高今年22岁,存款100元

但是如果我们再增加一个变量herSelf,就得再写一个effect,然后每次执行一次,一旦增加的变量变多,就得写很多很多的effect

track和trigger

针对上面很多effect的问题,我们用track函数把所有依赖moeny变量的effect函数都搜集起来,放在dep里,不要重复搜集依赖,所以dep用Set自动去重,搜集完以后,只要money发生变化,就执行trigger函数来通知所有依赖money的effect函数执行,实现更新

js 复制代码
let name = '高高', age = 22, money = 1;
let myself = '';
let herSelf = ''
const dep = new Set();

const effect = () => myself = `${name}今年${age}岁,存款${money}元`
const effect2 = () => herSelf = `${age}岁的${name}居然有${money}元`

const track = (fn) => {
    // 搜集依赖
    dep.add(fn)
}

const trigger = () => {
    // 执行依赖
    dep.forEach(fn => fn())
}
// effect()
// effect2()
track(effect)
track(effect2)
trigger()    
console.log(myself);  // 高高今年22岁,存款1元
console.log(herSelf);  //22岁的高高居然有1元
money = 100;
// effect()
// effect2()
trigger()
console.log(myself);  // 高高今年22岁,存款100元
console.log(herSelf);  //22岁的高高居然有100元

到目前为止,我们实现了基本数据类型的响应式,那如果是对象呢,我们可以暂时把person对象里的name和car看成两个变量,他们各自有各自依赖变量,关系如下图,那person有两个属性,所以他拥有两个dep,该怎么存储这两个呢,我们可以用Map来存储

js 复制代码
let str1 = '';
let str2 = '';
const depsMap = new Map()
const effect = () => str1 = `我的名字叫${person.name}`
const effect2 = () => str2 = `我的车是${person.car}`

function track(key) {
    let dep = depsMap.get(key);
    if (!dep) {
        depsMap.set(key, dep = new Set())
    }
    if (key === 'name') {
        dep.add(effect)
    } else {
        dep.add(effect2)
    }
}

function trigger(key) {
    let dep = depsMap.get(key);
    if (dep) {
        dep.forEach(fn => fn());
    }
}
track('name');
track('car');
trigger('name');
trigger('car')
console.log(str1, str2)   // 我的名字叫高高 我的车是宾利
person.name = '琳琳';
person.car = '兰博基尼'
trigger('name');
trigger('car')
console.log(str1, str2)    // 我的名字叫琳琳 我的车是兰博基尼
console.log(depsMap)
// Map(2) {
// 'name' => Set(1) { [Function: effect] },
//  'car' => Set(1) { [Function: effect2] }
}

如果是多个对象,那么我们就用weakMap来把多个对象进行封装,具体解释如下图

js 复制代码
const person = { name: '高高', car: '宾利' }
const tool = { dis: '小刀', usage: '裁纸' }

let str1 = '';
let str2 = '';
let str3 = '';
let str4 = '';
const targetMap = new WeakMap();
const effect = () => str1 = `我的名字叫${person.name}`
const effect2 = () => str2 = `我的车是${person.car}`

const effect3 = () => str3 = `工具的名字叫${tool.dis}`
const effect4 = () => str4 = `工具的作用是${tool.usage}`

function 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())
    }
    if (target === person) {
        if (key === 'name') {
            dep.add(effect)
        } else {
            dep.add(effect2)
        }
    } else {
        if (key === 'dis') {
            dep.add(effect3)
        } else {
            dep.add(effect4)
        }
    }
}

function trigger(target, key) {
    let depsMap = targetMap.get(target);
    if (depsMap) {
        let dep = depsMap.get(key);
        dep.forEach(fn => fn());
    }
}

track(person, 'name');
track(person, 'car');
track(tool, 'dis');
track(tool, 'usage');
trigger(person, 'name');
trigger(person, 'car');
trigger(tool, 'dis');
trigger(tool, 'usage');
console.log(str1, str2, str3, str4)   // 我的名字叫高高 我的车是宾利
person.name = '琳琳';
person.car = '兰博基尼'
tool.dis='锤子'
tool.usage='锤锤锤'
track(person, 'name');
track(person, 'car');
track(tool, 'dis');
track(tool, 'usage');
trigger(person, 'name');
trigger(person, 'car');
trigger(tool, 'dis');
trigger(tool, 'usage');
console.log(str1, str2, str3, str4)    // 我的名字叫琳琳 我的车是兰博基尼

console.log(targetMap)

Proxy

上面的例子虽然实现了依赖对象随着数据更新而改变,但是需要手动track搜集依赖,手动trigger通知更新,所以我们用proxy来实现自动搜集和更新

js 复制代码
// ----------使用proxy自动搜集和更新-------------
function reactive(target) {
    const handler = {
        get(target, key, receiver) {
            track(receiver, key)
            return Reflect.get(target, key, receiver)
        },
        set(target, key, value, receiver) {
            Reflect.set(target, key, value, receiver)
            trigger(receiver, key)
        }
    }
    return new Proxy(target,handler)
}
const person = reactive({ name: '高高', car: '宾利' })
const tool = reactive({ dis: '小刀', usage: '裁纸' })

解决写死的问题

在之前的track函数中,我们会用if/else去判断,那么每次多加对象就需要多加一些if/else ,所以我们定义了一个全局变量activeEffect,每次搜集依赖的时候就把依赖函数赋值给activeEffect,每次effect一执行,就把增加加入到dep中,那么就需要加一个函数来统一处理传入的函数执行,我们需要改一下effect函数和track

js 复制代码
let activeEffect = null;

function commonEffect(fn) {
    activeEffect = fn;
    activeEffect();
    activeEffect = null;
}

function track(target, key) {
    if(!activeEffect) return 
    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(activeEffect)
    // if (target === person) {
    //     if (key === 'name') {
    //         dep.add(effect)
    //     } else {
    //         dep.add(effect2)
    //     }
    // } else {
    //     if (key === 'dis') {
    //         dep.add(effect3)
    //     } else {
    //         dep.add(effect4)
    //     }
    // }
    
commonEffect(effect);
commonEffect(effect2);
commonEffect(effect3);
commonEffect(effect4)
}

到此,我们就实现了一个reactive响应式功能

实现ref

js 复制代码
let num = ref(5) console.log(num.value) // 5

num会成为一个响应式数据,但是在使用num时需要写num.value才可以用,所以我们把reactive封装下来实现ref

js 复制代码
function ref1(data) {
    return reactive({ value: data })
}
let number = ref1(5);
console.log(number.value)   // 5
number.value=100;
console.log(number.value)  //100

参考文章 juejin.cn/post/700199...

相关推荐
迷雾漫步者1 小时前
Flutter组件————FloatingActionButton
前端·flutter·dart
向前看-2 小时前
验证码机制
前端·后端
燃先生._.3 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖4 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235244 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240255 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar5 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人5 小时前
前端知识补充—CSS
前端·css
GISer_Jing6 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试