你为什么看不懂源码之Vue 3.0 结语

1,055 阅读4分钟

先唠会儿嗑

之前大概了解了 响应式原理、让普通属性变成响应式数据的 ref、effect 的实现以及 计算属性的包装。

本篇从逆向的角度做个简单的整理,草草的结束掉写的很烂的文章。 你可以看往期的写的很烂的 你为什么看不懂源码之Vue 3.0

假如你是作者

你的需求是开发 reactive 模块,它是干什么的呢?将变量生成响应式数据。

响应式数据被更改时,需要做一些反应。那怎么能检测响应式的变更呢?在谈这个问题前,得先罗列下,变量有哪些类型?

变量的类型

基本类型

String Number Symbol Boolean

形如:

let a = 2
let b = 'lalla'
let c = Symbol('cObj')

普通对象

形如:

let obj = { name: 'obj' }

集合对象

Map WeakMap Set WeakSet 形如

let map = new Map([{name: 'map'}])
let set = new Set([1,2,3])

基本上是这三类,但这三类可能交织纵横,互相“包庇”!我太难了。

拦截

既然分出了变量的类型,逐一拦截,然后再做“反应”就行了。先从普通变量谈起。

普通变量拦截

不管是 Object.defineProperty() 还是 Proxy(),拦截的都是对象,所以通过这两种方式来拦截普通变量肯定行不通,但我们可以包装它,从而实现拦截。最简单的方式便是对象的特性“存取器”。

let a = 3
const b = {
  get value() {
     return a
  },
  set value(val) {
     a = val
  }
}

这块儿的实现便是 Ref.ts

普通对象的拦截

这在 Vue2.0 中已经实现过了,用的 ES5 的 Object.defineProperty。直接拿来用就行了。

A: 等等,不能用!!
B: 为什么?
A: Object.defineProperty 在拦截对象时,需要遍历其 key 值。如果对象层级深,还要递归的遍历。

Object.keys(obj).forEach((key) => {
  Object.defineProperty(obj, key, handlers)
})

A: 它还拦截不了数组的方法,想拦截还得做 hack,包装 push\pop......操作。
B: 那咋整
A: Proxy 呀,以上问题它都能迎刃而解,具体我们不是在往期文章分析过了么。这里就不赘述了。

集合的拦截

Proxy 这么牛,集合应该也能拦截呀。不幸的是,集合的各种方法都不能被 Proxy 拦截。 拿 Set 类型举例子。

const sets = new Set([1,2,3])
let p2 = new Proxy(sets, {
  get(target, key ,receiver) {
    return Reflect.get(target, key, receiver)
  },
  add() {}
})
p2.add(6)

当你这样调用时,会发现控制台报错了

Uncaught TypeError: Method Set.prototype.add called on incompatible receiver [object Object]

导致这个报错的原因是,在调用 add 时,其内部的 this 本应该是 set 本身,但在此处变为了 p2

那咋整呢,要不,我们自己实现个 add 方法,在 p2.add 时,用 apply改变 指针。

const sets = new Set([1,2,3])
let p2 = new Proxy(sets, {
  get(target, key ,receiver) {
    const fn = Reflect.get(target, key, receiver)
    if(typeof fn === 'function') {
      return (...val) => {
        return target[key].apply(sets, val)
      }
    }
    return fn
  }
})
p2.add(6)

真实情况比这要复杂,你需要考虑各种 traps。

经过以上,目前我们也能拦截到 collections 了。

计算属性

响应式数据是不够的,如果一个数据的变更引起了一堆复杂的逻辑,我们处理起来就会特别麻烦,计算属性显然是个好选择。

const data = reactive({
  name: 'qqqdu'
})
computed((val) => {
  return data.name + ' hello'
})

以上方法有两个场景:

  • 1,初始化时,计算属性的函数参数会被执行一遍
  • 2,每次函数内计算属性改变时,函数也会执行一遍。

前者好说,要实现 2,就需要建立起 函数与计算属性的联系。这个联系在什么阶段建立比较好呢,当然是初始化的时候。

当初始化时,开始调用函数时加锁,运行到 data.name 会触发其 Proxy get,这说明该函数依赖这个计算属性,那他们之间的联系就建立起来了。当函数生命周期结束时,解锁就可以了。

那 2,什么时候执行呢,当然是计算属性 set 时了,如果值发生了变化,就要遍历执行与之相关的所有计算函数。

到这里就可以了,带着这个思路再回顾,会清晰不少。

结束

源码这种东西吧,越看越觉得自己卑微,向大佬们致敬! --xiaodu