小白也能看懂的vue3源码之vue2响应式系统实现

前言

最近在学习vue源码中的响应式系统,说实话确实花了点时间去研究它是如何实现的,研究过程也遇到了一些问题,我相信也是大家可能会感到疑惑的问题,经过一番折腾,终于是明白了它的操作,只能说一个字,绝! 尤大yyds(吹捧一下)
现在我们就从零开始实现下吧

One Dep类

先明白它的作用是啥

  • 储存依赖项 subscribes
  • 收集依赖项 depend
  • 通知更新依赖项 notify
    首先我们要知道啥是依赖项呢?它的作用是啥?
    依赖项即我们需要更新该数据所依赖的一些方法和引用 不同的地方调用同一个数据,或者对数据做了转化,本质上还是用到了该数据 都成为该数据的依赖项 比如vue2 中data定义的数据 computed计算属性里用到data的数据 一旦该数据改变 响应式的方式应该就是 页面中直接引用到该数据及计算属性对其转化后的结果都要进行一次更新 也就是说页面中引用到该数据(不管是直接还是间接转化后的数据)都应该加入依赖项,这样才能准确的进行更新。
    用set集合储存唯一不重复的依赖项
    depend 这里的activeEffect我们稍后再来解释它的作用,为什么要这样来写 先简单记住是添加依赖项
    notify 直接对集合做一次遍历挨个执行依赖项,及更新dom等想要做的操作
class Dep {
    constructor() {
        this.subscribes = new Set()
    }
    depend() {
        if (activeEffect) {
            this.subscribes.add(activeEffect)
        }
    }
    notify() {
        this.subscribes.forEach(effect => {
            effect()
        })
    }
}

Two watchEffect 添加依赖处理事项

watchEffect的作用是将我们想要对依赖的数据进行一些操作时能加入到subscribes,以便后面更新也能一起执行该事项
举个简单的例子:我需要每次对我修改的一个数据进行一次翻倍处理,即当我们修改了该数据时我们需要执行该对应依赖处理事项,在这个事项中将该数据*2
很多同学不明白为什么activeEffect要放在最外层,这样的意义何在?
这里的用意很巧妙,这是为了后面你收集依赖执行depend()不需要传递函数进去,这样实现起来就很方便,我们来看看reactive就知道了,最后会完整解答一下

let activeEffect = null
function watchEffect(effect) {
    activeEffect = effect
    effect()//这里需要执行一次是为了收集依赖,以便能触发到 get()函数将该依赖函数加入到subscribes
    activeEffect = null
}

Three getDep 生成依赖数据结构

我们的响应式数据不可能只是一个对象,所以我们需要制定特殊的数据结构来更好的访问存储依赖

  • Map({key: value}): key是一个字符串
  • WeakMap({key(对象): value}): key是一个对象, 弱引用
我们需要的数据结构[{key(对象):value(对象)}]  
 const info = reactive({ name: 'kzj', age: 18 })  
 const obj = reactive({hobby:'play'})  
最后变成[{info:[{name:[依赖项(方法)]},{age:[依赖项1(方法1),依赖项2(方法2)]}]},{obj:[{hobby:[依赖项(方法)]}]}]  

targetMap中的value不是存值的,是存依赖关系的
流程就是

  • 生成targetMap存储依赖
  • 调用getDep()读取对应依赖
  • 查找是否存在该依赖名 targetMap.get(target)即在targetMap中寻找是否有用这个对象作为key
  • 没有找到该对象存储在targetMap中,将该对象作为targetMap的key,自己添加一个空数组,依赖为空 targetMap.set(target, targetObj)
  • 获取该对象中对应传进来的Key依赖
  • 值为null则调用new Dep()实例化构建依赖对象,并添加到对应该对象key的value中
  • 返回所对应的对象中的属性 依赖对象
const targetMap = new WeakMap()
function getDep(target, key) {
    // 1.根据对象(target)取出对应的Map对象
    //没有取到值则自己存一个
    let targetObj = targetMap.get(target)
    if (!targetObj) {
        targetObj = new Map()
        targetMap.set(target, targetObj)
    }

    let targetValue = targetObj.get(key)
    if (!targetValue) {
        targetValue = new Dep()
        targetObj.set(key, targetValue)
    }
    return targetValue
}

Four reactive响应式实现

  • vue2 Object.defineProperty方式实现
    遍历传入的对象,存储在特定的数据结构targetMap中,通过Object.defineProperty进行数据劫持,读取该属性时get中先收集依赖,以便修改该值时触发set中的dep.notify()来达到响应式效果
// vue2实现响应式
function reactive(raw) {
    Object.keys(raw).forEach(key => {
        const dep = getDep(raw, key)
        let value = raw[key]
        Object.defineProperty(raw, key, {
            get() {
                dep.depend() //收集依赖
                return value
            },
            set(newValue) {
                if (value != newValue) {
                    value = newValue
                    dep.notify()
                }
            }
        })
    })
    return raw
}

测试

//测试代码
//1.reactive
const info = reactive({ name: 'kzj', age: 18 })
const obj = reactive({hobby:'play'})
console.log(info);
//2.watchEffect
// 依赖事项 及监听到数据改变是需要做的操作 如更新dom
watchEffect(function () {
    console.log('info两者都依赖了');
    console.log(info.name + '----' + info.age);
})
watchEffect(function () {
    console.log('只依赖了info.age');
    console.log(info.age * 2);
})
watchEffect(function () {
    console.log('只依赖了obj.hobby');
    console.log(obj.hobby);
})

//3.改值
// info.name = 'tbb' //依赖且触发一次 1
// info.age = 15    //依赖且触发两次 1 2
obj.hobby = 'game'  //依赖且触发一次 3

结果

image.png
前三个都执行是因为effect执行了一次收集依赖,我们发现成功了,只有hobby做出了响应

我们用修改obj.hobby来走一遍流程

  • 还未修改(执行流程1,流程2),初始化执行watchEffect函数给变量activeEffect赋值对应方法后执行该方法,触发get()后触发depend刚好将变量activeEffect添加到各对应依赖(因为里面读取了对象属性后触发了对应对象属性的get()方法,而get()方法中的depend()来进行添加依赖。这也就是为什么effect要在赋值后再执行一次的原因)依赖关系添加完成,步骤如下
  • 1.reactive 执行流程
传入需要响应式对象
遍历对象中的属性将该对象及属性传入getDep中进行依赖数据结构存储
通过Object.defineProperty进行数据劫持
若监测到读取该属性则执行get并调用depend收集依赖
若监测到该属性被修改则执行set修改值后执行notify通知依赖项更新
  • 2.watchEffect执行流程
执行watchEffectef函数传入依赖事项函数
activeEffect=effect赋值
执行effect
触发对象对应属性中的get方法
执行dep.depend收集依赖
depend拿到activeEffect中的依赖事项并添加到subscribes中
清空activeEffect

-3.改值 执行流程 这里以obj.hobby模拟

obj.hobby='game'改值
监测到obj.hobby修改触发set方法
set方法中确定值发生变化修改局部变量value后执行notify通知相关依赖项执行
notify方法中遍历subscribers执行对应依赖事项
执行该依赖事项读取到了obj.hobby执行对应get方法
执行get方法中的depend发现此时activeEffect为空则不需要添加依赖
读取obj.hobby时返回最新局部变量value值,修改成功

其他问题

1.为什么activeEffect要放在最外层?
看了完整的过程相信大家心里已经能感觉到了,初始化使用watchEffect时充分利用执行effect()时触发属性get()方法执行收集依赖depend()时,activeEffect刚好是已经有值的情况下了进行添加依赖,执行effect()后又置空activeEffect,这就意味着只有初始化使用watchEffect时才能真正的添加依赖,而后面我们改值,读取值时触发getz中的depend时由于activeEffect为null而不能进行添加依赖。另外如果改用传参的方式,在get中不好确定什么时候该传递依赖事项,什么时候又不需传递依赖事项,且也不好取到依赖事项的值,采用这样的方式能更好的到达我们的需求,大家慢慢体会吧。

2.reactive中的raw[key]为什么要用局部变量value来保存,而不是直接用呢?
若采用raw[key] = newValue方式直接赋值,这样会触发递归,因为你又对该属性做了修改操作,又会触发set方法,一直递归下去。而用局部变量value是完全不影响的。

3.targetMap的数据结构
看图

const info = reactive({ name: 'kzj', age: 18 })
const obj = reactive({hobby:'play'})
// 依赖事项 及监听到数据改变是需要做的操作 如更新dom
watchEffect(function () {
    console.log('info两者都依赖了');
    console.log(info.name + '----' + info.age);
})
watchEffect(function () {
    console.log('只依赖了info.age');
    console.log(info.age * 2);
})
watchEffect(function () {
    console.log('只依赖了obj.hobby');
    console.log(obj.hobby);
})

image.png

End

若有未说明白的地方或者错误不当,欢迎留言~ 点赞加关注不迷路
下一章分享vue3响应式系统如何实现 下期见

QQ图片20200210181218.jpg

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值