初探vue3 - 响应式实现原理,手写响应式代码

143 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情

前言

学习vue3有一段时间了,也用vue3写了几个项目,今天抽空整理一下关于vue3的响应式原理知识,写文章的初衷是,加深自己的记忆、理解,方便以后复习、查阅。

关于Proxy和Reflect

Proxyes6为操作对象提供的API,可以理解为“代理”,它给目标对象架设一层“拦截”,可以设置外界对目标对象访问的过滤和改写。
Reflect也是es6为操作对象提供的API,它和Object相似,只不过更加的优雅而已;比较重要的一点是,Reflect对象的方法和Proxy对象的方法一一对应,只要是Proxy对象上的方法,在Reflect上都能找到对应的方法,所以Reflect简直就是Proxy的最佳搭档。
具体的介绍,可以看“阮一峰”大佬的《ECMAScript 6 入门》,顺便也可以了解下weakMap、Map、Set这几种es6新增的数据类型,在vue3响应式实现中,都有用到。

vue3为什么要用Proxy

Object.defineProperty的局限性

  • 只对初始化对象的属性进行拦截,新增的属性不会触发get和set。
  • 无法监听数组基于下标的修改(这里其实有误区)
    Object.defineProperty其实也是可以监听数组下标的变化的,本质上数组也是键值对集合,只不过key是数字, 那么Object.defineProperty自然是可以监听到的。
    但是为啥vue2要改写数组的七个原生方法push、pop、shift、unshift、splice、sort、reverse,而不是用Object.defineProperty来实现数组全方位监听呢?
    原因是出于性能方面的取舍,数组的操作一般用上面那7种方法就够了,如果要用Object.defineProperty全方位监控的话,每次改变都要重新将整个数组的所有key通过递归加上setter和getter,而且数组的keyvalue变动都是很频繁的,当数据量大的时候,难免会带来性能开销问题。
  • 因为Object.defineProperty只能对初始化对象的属性进行拦截,所以vue2的响应式数据需要写到data对象里面,在组件初始化的时候为整个data对象的属性加上gettersetter,而且如果对象嵌套多层的话,需要去递归遍历给每个属性加上gettersetter,在data对象比较庞大的时候,会影响到组件的初始化速度。

Proxy的优势

  • 可以拦截对象新增的属性。
  • 只要是对象都能被代理。
  • 实现惰性监听
    vue3通过提供reactivecomputedeffect方法,用户可以自己选择是否需要使用响应式数据,组件实例化的时候是不需要去遍历对象的,而且对于多次嵌套的对象,只会在访问的时候为去设置Proxy代理,这样做就可以加快组件初始化时间,减少依赖项的保存,降低运行内存。

手写vue3 响应式

vue 响应式数据图.png

  • 在Proxy的Handler中拦截对象的各种取值、赋值操作,依托track和trigger两个函数进行依赖收集和派发更新。
  • track用来在读取时收集依赖
  • trigger用来在更新时触发依赖
  • 创建一个effect函数
  • 立即执行effect,然后将当前渲染的effect赋值给activeEffect
// 定义reactive方法
function reactive(target) {
  const handler = {
    // target:目标对象
    // key: 要访问的属性名
    // receiver: proxy实例本身(严格地说,是操作行为所针对的对象)
    get(target, key, receiver) {
      track(receover,key) // 访问时收集依赖
      return Reflect.get(target, key, receiver)
    },
    
    // value:属性值
    set(target, key, value, receiver) {
      Reflect.set(target, key, value, receiver)
      trigger(receiver, key) // 值变动时自动派发更新
    }
  }
  return new Proxy(target, handler)
}

// 实现effect函数,接收一个回调函数,并赋值给activeEffect,并立即执行
let activeEffect = null // 用来存放当前执行的副作用函数
function effect(cb) {
  activeEffect = cb
  activeEffect() // 立即执行,访问响应式对象,触发响应式数据getter
  activeEffect = null // 执行完之后,需要清空,因为如果有多层嵌套的对象,这是一个递归的过程
}

// 实现track方法
const targetMap = new WeakMap() // 用来存放收集的所有Reactive Object对象集合
function track(target, key) {
  if(!activeEffect) return 
  let depsMap = targetMap.get(target)
  // 如果当前依赖里面没有,那么创建一个
  if (!depsMap) {
    // depsMap = new Map() 用来存放响应式对象中的属性
    targetMap.set(target, depsMap = new Map())
  }
  
  let dep = depsMap.get(key)
  // 如果当前对象里面没有这个属性,那么创建一个
  if (!dep) {
    // dep = new Set() 用来存放收集的副作用函数,Set可以自动去重
    depsMap.set(key, dep = new Set())
  }
  dep.add(activeEffect) // 把此时的activeEffect添加进去
}

// 实现 trigger函数,派发更新(执行收集到的依赖)
function trigger(target, key) {
  let depsMap = targetMap.get(target)
  // 如果有依赖,那么取出其中的副作用函数,依次执行
  if (depsMap) {
    const dep = depsMap.get(key)
    if (dep) {
      dep.forEach(effect => effect())
    }
  }
}