Vue3响应式原理简单实现

背景

首先用一个例子来解释什么是响应式。

  1. 设置变量a1
  2. 设置变量ba2之和;
  3. 当修改a的值,b的值也会随之发生改变。

初步实现

let a = 1
let b = a + 2
a = 3 // b 为 3
a = 4 // b 为 3 

上述代码显然无法满足例子中的第三点,即ba的变化而变化。原因很简单,JS代码是从上往下执行的。a的赋值操作无法通知上一行的b = a + 2。 如果一定要希望b及时能够响应a的变化,可以在每次a的赋值操作之后紧跟b = a + 2

简单代码的改进

let a = 1
let b = undefined
b = a + 2
// ...
a = 3
b = a + 2
// ...
a = 4
b = a + 2 

方便起见,这里将b = a + 2包在一个函数里。

let a = 1
let b = undefined
function foo() {
   b = a + 2
}
foo()
// ...
a = 3
foo()
// ...
a = 4
foo() 

但这里有一个问题,就是a的赋值操作必须手工紧跟这个foo函数的执行。 那么,是否有一个自动的方式来处理这个依赖关系呢?

Proxy对象

ES6里有一个Proxy,可以对对象属性的get/set操作进行拦截处理。 如果要将Proxy的这个功能运用到这里,需要将a包装成一个对象。

let a = 1
let b = undefined
let obj_a = {a} // 将a包装成对象
let proxy_a = new Proxy(obj_a, {
    get(target, property) {
       return target[property]
    },
    set(target, property, value) {
        target[property] = value
        foo() // 拦截set操作,执行foo函数
        return true
    }
})
function foo() {
    b = proxy_a.a + 2
}

foo()
// ...
proxy_a.a = 3 // b 为 5
// ...
proxy_a.a = 4 // b 为 6 
  • 首先,将a封装成目标对象obj_a,用Proxyobj_a进行代理,返回的对象proxy_a为目标对象obj_a的代理对象;
  • 其次,因为对obj_aset操作进行拦截调用foo函数,因此proxy_a的属性赋值操作,会实时的运行foo函数,这样就实时更新b值。
  • 只有对proxy_a的属性赋值才会自动调用foo函数,如果是对obj_a的属性赋值,或者直接对a赋值,则不会自动调用foo函数。

为了简化后续分析,我们将上面的问题进行进一步抽象和简化:

  • 因为这里重点关注的是响应式机制的实现,而Proxy的输入又只能适用于对象,因此文章后面部分我们只考虑对象类型的参数(因为基本类型也可以通过简单封装,改造成对象)。
  • 另外,b存在的意义在于观察程序是否能够响应proxy_a.a的变化,因此用一个console.log(proxy_a.a)也能购代替b的作用,而且还少一个变量。

可以将代码改成如下:

let obj_a = {a}
let proxy_a = new Proxy(obj_a, {
    get(target, property) {
       return target[property]
    },
    set(target, property, value) {
        target[property] = value
        foo()
        return true
    }
})
functon foo() {
    console.log('foo ', proxy_a.a)
} 

只要在每次proxy_a.a赋值,看下console.log输出的值是否也有响应变化,就能判断响应式机制是否正确实现了。

多个类似的foo函数的情况

前面的例子中正好只有一个foo函数,且这个foo函数正好用到了proxy_a.a,因此我们在set操作的时候可以精准的知道需要调用的哪个函数和函数名。 但如果是如下情况呢?

let obj_a = {a:1}
let proxy_a = new Proxy(obj_a, {
    get(target, property) {
       return target[property]
    },
    set(target, property, value) {
        target[property] = value
        foo() // 这里调用的是foo,而不是foo1还foo2,原因是什么?
        return true
    }
})
function foo() {
    console.log('foo ', proxy_a.a)
}
function foo1() {
    console.log('foo1')
}
function foo2() {
    console.log('foo2', proxy_a.a)
}
..... 

这里有多个类似的foo函数,有些和proxy_a有关,有些又没有关系。如果我们想事前就实现一个通用的proxy_a,在定义proxy_aset操作的时候,如何知道该调用哪个函数呢?

一个有效的实现:

  • 首先运行每个foo函数,运行过程中检查是否有用到proxy_aget方法;
  • 如果有的话,就将当前foo函数和当前的代理对象proxy_a的关系记录下来;
  • 后续如果有用到proxy_aset方法时,再从这个对应关系获得相关联的foo函数,并依次执行。
let obj_a ={a: 1}
let set = new Set() // 保存和proxy_a有关联的函数变量
let activeFunc = undefined // 于记录当前运行的是哪个函数变量

let foo = function() {
    activeFunc = foo
    console.log('foo =', proxy_a.a)
    activeFunc = undefined
}
let foo1 = function() {
    activeFunc = foo1
    console.log('foo1')
    activeFunc = undefined
}
let foo2 = function() {
    activeFunc = foo2
    console.log('foo2 =', proxy_a.a)
    activeFunc = undefined
}

let proxy_a = new Proxy(obj_a, {
    get(target, property) {
        if(activeFunc) {
          set.add(activeFunc)
        }
        return target[property]
    },
    set(target, property, value) {
        for(let item of set) {
            item()
        }
        target[property] = value
        return true
    }
})

foo()
foo1()
foo2()

//....
proxy_a.a = 3
// ...
proxy_a.a = 4 
  • set保存和proxy_a有关联的函数变量;
  • activeFunc用于记录当前运行的是哪个函数;
  • 如果当前运行foo函数中,存在proxy_a.a,那么就会进入到get拦截器,将当前的函数变量添加到Set集合;
  • 运行完全部的foo函数,Set集合保存了所有和proxy_a.a有关系的函数变量;
  • 后续在给proxy_a.a赋值的时候,进入到set拦截器,将Set集合中保存的函数逐个执行。

总结

以上就是Vue3响应式实现的思路原型。我们总结下:

  • Vue3的响应式处理的是对象,我们称为目标对象;如果不是对象,可以通过处理,封装成一个对象;
  • 利用Proxy,从目标对象生成一个代理对象,代理对象的属性读写操作可以进行拦截预处理;
  • 将响应式关联逻辑封装到函数中,且响应式逻辑中的目标对象须替换为代理对象;
  • 程序运行的同时也同时运行上述封装的函数,并收集和代理对象get操作有关的函数变量,保存到一个Set集合中;
  • 执行完毕之后,如有对代理对象的属性赋值操作,则会触发Set集合中的函数变量依次执行,从而完成响应式流程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值