Vue.js3 原理剖析:数据响应系统

2019-10-0907:03:23WEB前端开发Comments2,198 views字数 2937阅读模式

Vue 3 目前的状态其实很适合阅读,因为代码量不多,并且核心功能是不会有什么大的变动的。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

因此笔者 fork 了目前的源码,并且加以注释。同时为了照顾不怎么熟悉 TS 的人群,笔者也对一些核心的 TS 语法做了解释。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

这份注释不是干巴巴的只对一行代码说明是干什么的,而是结合了上下文来讲解它的用处。如果你想读源码但是又怕看不懂的话,可以通过我这个 仓库 来学习。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

先导知识

Vue 3 代码的写法有了很大的变化,如果你还不清楚这方面的内容,推荐先阅读 Vue Function-based API RFC文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

数据响应机制

众所周知,在 Vue 3 中使用了 Proxy 替换了原先的 Object.defineproperty 来实现数据响应。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

另外如果你不熟悉 Proxy 的用法,推荐先阅读 文档文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

我们先来学习下如何使用这个 API 吧。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

const value = reactive({ num: 0 })
// 需要注意的一点,这个回调中用到了 value.num
// 那么只有当外部给 value.num 赋值才会触发回调
effect(() => {
  console.log(value.num)
})
value.num = 7
复制代码

很简单,上述代码就实现了数据的响应式,并且能在数据改变以后执行相应的回调。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

reactive 内部的核心代码简化如下:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

function reactive(target) {
    if (!isObject(target)) {
        return target
    }
    if (!canObserve(target)) {
        return target
    }
    const handlers = collectionTypes.has(target.constructor)
        ? collectionHandlers
        : baseHandlers
    observed = new Proxy(target, handlers)
    return observed
}
复制代码

首先判断传入的参数类型是否可以用于观察,目前支持的类型为 Object|Array|Map|Set|WeakMap|WeakSet文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

接下来判断参数的构造函数,根据类型获得不同的 handlers。这里我们就统一使用 baseHandlers,因为这个已经覆盖 99% 的情况了。只有 Set, Map, WeakMap, WeakSet 才会使用到 collectionHandlers文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

对于 baseHandlers 来说,最主要的是劫持了 getset 行为,这两个行为同时也能原生劫持数组下标修改值及对象新增属性的行为,这两个行为相关的内容会在下文中说到。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

最后就是构造一个 Proxy 对象完成数据的响应式。相比 Object.defineproperty 一开始就要递归遍历整个对象的做法来说,使用 Proxy 性能会好得多。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

接下来当我们去使用 value 这个对象的时候,就能劫持到内部的行为。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

比如说 console.log(value.num) 就会触发 get 函数;value.num = 2 就会触发 set 函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

以下是这两个函数的核心剖析:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

function get(target: any, key: string | symbol, receiver: any) {
  // 获得结果
  const res = Reflect.get(target, key, receiver)
  track(target, OperationTypes.GET, key)
  // 判断是否为对象,是的话将对象包装成 proxy
  return isObject(res) ? reactive(res) : res
}
复制代码

对于 get 函数来说,获取值肯定是最核心的一步骤了。接下来是调用 track,这个和 effect 有关,下文再说。最后是判断值的类型,如果是对象的话就继续包装成 Proxy文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

function set(
  target: any,
  key: string | symbol,
  value: any,
  receiver: any
): boolean {
  const result = Reflect.set(target, key, value, receiver)
  if (是否新增 key) {
    trigger(target, OperationTypes.ADD, key)
  } else if (value !== oldValue) {
    trigger(target, OperationTypes.SET, key)
  }  
  return result
}
复制代码

对于 set 函数来说,设置值是第一步骤,然后调用 trigger,这也是 effect 中的内容。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

简单来说,如果某个 effct 回调中有使用到 value.num,那么这个回调会被收集起来,并在调用 value.num = 2 时触发。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

那么怎么收集这些内容呢?这就要说说 targetMap 这个对象了。它用于存储依赖关系,类似以下结构,这个结构会在 effect 文件中被用到文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

{
  target: {
    key: Dep
  }
}
复制代码

先来解释下三者到底是什么,这个很重要文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

  • target 就是被 proxy 的对象
  • key 是对象触发 get 行为以后的属性。比如 counter.num 触发了 get 行为,num 就是 key
  • dep 是回调函数,也就是 effect 中调用了 counter.num 的话,这个回调就是 dep,需要收集起来下次使用

这里笔者把这些内容脱离源码串起来讲一下流程。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

const counter = reactive({ num: 0 })
effect(() => {
  console.log(counter.num)
})
counter.num = 7
复制代码

首先创建一个 Proxy 对象,targetMap 会把这个对象收集起来当做 key。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

接下来调用 effect 回调的时候会把这个回调保存起来,用于下面的依赖收集。在调用的过程中会触发 counterget 函数,内部调用了 track 函数,这个函数会使用到 targetMap文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

这里首先通过 targettargetMap 中取到一个对象,这个对象也就是 target 所有的依赖关系。那么对于 counter.num 来说,num 就是这个对象的 key(这里如果有点模糊的话可以先看下上面的数据结构),值是一个依赖回调的集合,因为 counter.num 可能会被多个地方依赖到。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

回调执行完毕以后会把保存的回调销毁掉。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

当我们调用 counter.num = 7 时,触发 set 函数,内部调用 trigger 函数,同样会使用到 targetMap文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

同样通过 target 取到一个对象,然后通过 key 也就是 num 去取出依赖集合,最后遍历这个集合执行里面所有的回调函数。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

另外对于 computed 来说,内部也是使用到了 effect,无非它的回调不会在调用 effect 后立即执行,只有当触发 get 行为以后才会执行回调并进行依赖收集,举个例子:文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

const value = reactive({ num: 0 })
const cValue = computed(() => value.num)
value.num = 1
复制代码

对于以上代码来说,computed 的回调永远不会执行,只有当使用到了 cValue.value 时才会执行回调,然后接下来的操作就和上面的没区别了。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

作者:yck
链接:https://juejin.im/post/5d996e3e6fb9a04e3043cc5b
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html

文章源自菜鸟学院-https://www.cainiaoxueyuan.com/gcs/16809.html
  • 本站内容整理自互联网,仅提供信息存储空间服务,以方便学习之用。如对文章、图片、字体等版权有疑问,请在下方留言,管理员看到后,将第一时间进行处理。
  • 转载请务必保留本文链接:https://www.cainiaoxueyuan.com/gcs/16809.html

Comment

匿名网友 填写信息

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定