深入理解vue3 响应式数据

之前研究了vue的reactive方法,简写了vue3的响应式的核心原理,然后萌生了编写简易版vue3实现的想法。查阅相关资料后,来使用原生js环境模vue3的响应式数据的核心api实现。

// 响应式代码
const reative = (target) => {
    return new Proxy(target, {
        get: function (obj, key, ctx) {
            console.log('我被获取了')
            // 收集依赖
            return obj[key];
        },
        set: function (obj, key, value) {
            console.log('我被设置了', key)
            // 执行回调
            obj[key] = value;
            return true;
        }
    })
}

const obj = reative({
    name: '张三'
})

console.log(obj.name, '----')
obj.name = '张三子'
console.log(obj.name, '------')

// 我被获取了
// 张三 ----
// 我被设置了 name
// 我被获取了
// 张三子 ------

这个例子很好理解,reative 这个方法创建对象后,能去监听数据的get/set。 不管是vue2还是vue3, 最核心的还是 依赖收集 + 执行回调 ,而effect函数可以说是它的核心实现了。

一、简易版的reactive、effect 实现

我们先看一段代码:

import {effect} from 'vue'
let a = ref(1)
effect(() => {
    console.log(a.value)
})
a.value = 10

// 1
// 10

那么这个effect函数是什么呢?简单来说vue中的响应式数据的回调方法。此方法内所用使用到的响应式数据只要发生变化,effect方法就会重新执行, computed、watch等api 就是基于effect的二次封装。接下来我们分析下effect在这个例子中做了什么事。

猜测effect做了三件事:
  1. 执行回调方法
  2. 监测回调中用到了哪些响应式数据,并把回调加入到相对应的响应式数据的副作用队列中;
  3. 触发a的set方法时执行相应的副作用方法;

动手实践:

const reative = (target) => {
    return new Proxy(target, {
        get: function (obj, key, ctx) {
            // 收集依赖
            track()
            return obj[key];
        },
        set: function (obj, key, value) {
            obj[key] = value;
            // 调用副作用方法
            trigger()
            return true;
        }
    })
}

// 定义一个副作用集合 set 数据集不会出现重复
let dep = new Set()

// 定义响应式对象
const p1 = reative({
    name: '张三',
    age: 5
})

// 定义获取/设置响应式数据的effect
let effect1 = () => {
    console.log(p1.age)
}

// 收集依赖
const track = () => {
    dep.add(effect1)
}

// 调用副作用方法
const trigger = () => {
    dep.forEach(effect => effect())
}

// effect 方法要先执行一次
effect1()
p1.age = 10

// 5
// 10

effect1 先执行一次,当age属性发生变化后,在次执行effect1方法。

这里我们实现了和vue3 一样的运行逻辑。和vue3区别在于关键的几个步骤我们都是通过手动去关联和触发对应的方法,接下来我们梳理下缺少的部分并逐个实现。

  1. track方法和effect关联,只收集当前数据相关的副作用;
  2. trigger方法和dep、数据源关联,执行的副作用队列应该都和数据源相关。
关联track方法和effect(重点)

关联他俩之前我们再梳理下整个流程的执行循序:

执行effect1、effect2、… => 收集依赖

我们再更细致的拆解下执行顺序

==执行effect1 && 收集effect1的依赖 => 执行effect2 && 收集effect2的依赖==

思路好像清晰了,effect执行的时候进行的依赖收集。那增加一个currentEffectfun(重点)数据,当响应式数据触发get方法,说明currentEffectfun 的effect 内使用了触发的这个响应式数据,那么直接就给当前响应式数据添加一个dep方法,然后就完成关联了。

1、改造响应式数据的get方法,

增加key参数,从而能在副作用池找到相对应的方法

const reative = (target) => {
    return new Proxy(target, {
        get: function (obj, key, ctx) {
            track(key)
            return obj[key];
        },
        set: function (obj, key, value) {
            obj[key] = value;
            trigger()
            return true;
        }
    })
}
2、记录当前执行的effect方法

使用一个currentEffect 字段来记录当前正在实行的effect 方法,他是一个时间段。而在这个时间段内执行的proxy set方法都需要添加到当前数据的dep 池中, 当当前数据改变,再去执行dep池内的所有方法。

let currentEffect = null

const effect = (fun) => {
    currentEffect = fun
    currentEffect()
    currentEffect = null
}
3、改造track方法

改造track 之前我们先改造下dep, 符合以下两点:

  1. 支持多个响应式数据;
  2. 每个响应式数据的副作用方法有多个;

根据需求设计这样一个数据结构

const deps = new Map({
    [key]: new Set([
        fun1, fun2, fun3...
    ]
})

改造track,用传入的key值去确定需要执行的effect方法的位置

let deps = new Map()

const track = (key) => {
    if (!deps[key]) {
        deps[key] = new Set()
    }
    deps[key].add(currentEffect)
}
4、改造下执行依赖的方法
const trigger = (key) => {
   deps[key].forEach(effect => effect && effect())
}

完整实现

const reative = (target) => {
    return new Proxy(target, {
        get: function (obj, key, ctx) {
            track(key)
            return obj[key];
        },
        set: function (obj, key, value) {
            obj[key] = value;
            trigger(key)
            return true;
        }
    })
}

let deps = new Map()
let currentEffect = null

const effect = (fun) => {
    currentEffect = fun
    currentEffect()
    currentEffect = null
}
//收集依赖
const track = (key) => {
    if (!deps[key]) {
        deps[key] = new Set()
    }
    deps[key].add(currentEffect)
}
// 执行依赖
const trigger = (key) => {
    deps[key].forEach(effect => effect && effect())
}

const p1 = reative({
    name: '张三',
    age: 5
})

effect(() => {
    console.log(p1.age)
})

p1.age = 10

执行结果

二、小结

根据effect 函数的种种表现,成功复刻了effect 方法执行的整个流程。但是vue3远不止如此, 虽然表面上看起来执行结果一致,但是其中很多地方经不起推敲,比如多层嵌套的对象如何监听数据变化,如何储存依赖项,还有著名的‘set陷阱’我们还没有推导到。再比如基础数据类型如何监听数据变化,作者为什么要使用Reflect,等等诸多问题我们先梳理,接下来再去逐个解决。

梳理的问题点:

  1. 在reactive函数的set方法中,官方使用的是Reflect,而我们使用的obj[key];
  2. 复杂/深层次的响应式数据如何监听和存储依赖项;
  3. 官方的依赖项是存储在各自的proxy对象的dep 属性上,而我们是全部放在一个单独的deps对象内;

ps: 一定要学习ts的语法,因为vue源码还有各种相关文章的示例代码全都是ts, 不懂ts语法看起来那是相当的头疼。之前维护过ts的项目,使用了一段时间ts,觉得那个写起来麻烦,想着以后项目断然也不会去使用它,所以就一直拖着没学,但是现在想法转变了: 你可以不用 但是不能不会。(前端码农看不懂ts 说出去也被人笑话!!!)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值