Vue3.0性能提升以及响应式系统原理

1,478 阅读7分钟

大家好,我是王大傻。最近看了Vue3.0的源码,感触颇深,对比于Vue2.x版本,Vue3.0的响应式原理究竟做了哪些调整呢?让我们一起来看下吧

问题汇总

Vue3.0性能提升

1. 响应式系统升级
    1. 收集依赖
    2. reactive
    3. ref
    4. toRefs
    5. computed
2. 编译优化
3. 源码体积优化

image.png

响应式系统升级

Vue3.0相比于2.x版本做了响应式系统的升级,在Vue2.x版本中是通过defineProperty去设置响应式,但在3.0中采取了Proxy的方式,这样做有三个优点

  • 可以监听动态添加对象的属性
  • 可以监听删除的属性
  • 可以监听数组的索引以及数组的length属性
收集依赖

收集依赖的过程主要是通过三个函数,这三个函数分别是

  1. effect 处理副作用的函数 它会接受一个函数作为参数 并且执行这个函数 并把这个函数存储起来 在收集依赖完毕后 重置
  2. track 建立关系 首先声明一个weakMap 用来建立对象和值的关系 对象 target 值 map
    1. 这个值是一个map对象 用来建立对象中的属性和属性值的关系 对象 key 值 set
    2. map中的值是一个set的数据结构 用来描述属性值 通过key 获取值
  3. trigger 通过target key 去获取weakMap中储存的响应式对象 找到后获取到存取的值并返回 首先是Effect函数
let activeEffect = null // 定义一个变量用于储存我们的callback函数
 function effect (callback) {
  activeEffect = callback
  callback() // 访问响应式对象属性,去收集依赖
  activeEffect = null // 在收集完毕后对变量进行销毁
}

然后是track函数,先上张图

image.png 根据图里面内容我们可以看到track内容相互之间建立的映射,我们也来实现下track函数吧

let targetMap = new WeakMap()// 初始化一个weakMap数据解构

 function track (target, key) {// 接收两个参数 对象名 以及 对象的属性
  if (!activeEffect) return// 判断当前的weakMap是否存在 如果已经有值 就直接返回
  let depsMap = targetMap.get(target)// 获取我们存在weakMap中的对象 
  if (!depsMap) {// 如果不存在 说明我们还没有储存 就去储存一下 当然储存的值为Map数据解构 保证数据的唯一性
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)// 获取我们存储属性对应的值 
  if (!dep) {// 如果没有 创建一个set数据解构来储存我们的属性的值 这样做可以保证储存的函数唯一性
    depsMap.set(key, (dep = new Set()))
  }
  dep.add(activeEffect)// 先去添加上我们前面存储的回调函数 
}

最后是trigger函数

function trigger (target, key) {// 这里接收两个参数 属性名以及key
  const depsMap = targetMap.get(target)// 通过属性名获取存储的weakMap
  if (!depsMap) return// 如果不存在的话就直接返回
  const dep = depsMap.get(key)// 如果存在就获取我们存取的set结构
  if (dep) {// 如果存在的话 遍历去执行里面的回调函数
    dep.forEach(effect => {
      effect()// 执行回调函数
    })
  }
}

image.png 看到这里是不是一脸懵逼,不知道搞这么多到底为了干什么,带着疑问我们往下看,我们通过实现reactive 响应式对象来具体看下怎么实现

reactive

根据reactive的特性我们知道

  1. 传入的必须是一个对象,如果不是对象就直接返回
  2. 对象的属性添加删除都是响应式的 但是对对象进行重新赋值会丢失响应式
  3. 不可以解构 解构后的属性不是响应式
// 用到的工具函数
const isObject = val => val !== null && typeof val === 'object'
const convert = target => isObject(target) ? reactive(target) : target
const hasOwnProperty = Object.prototype.hasOwnProperty// 判断是否是自己的属性,这个方法会查找一个对象是否有某个属性,但是不会去查找它的原型链
const hasOwn = (target, key) => hasOwnProperty.call(target, key)
function reactive (target) {
  if (!isObject(target)) return target// 判断是否为对象

  const handler = {
    get (target, key, receiver) {// Proxy中第三个参数是receiver 默认是当前proxy对象 可以用来绑定this
      // 收集依赖
      track(target, key)// 调用我们的track来收集依赖
      const result = Reflect.get(target, key, receiver)// Reflect.get 方法是从一个对象中取值
      return convert(result)// 返回结果 
    },
    set (target, key, value, receiver) {
      const oldValue = Reflect.get(target, key, receiver)
      let result = true
      if (oldValue !== value) {
        result = Reflect.set(target, key, value, receiver)// Reflect.set 方法想对象里面的某个属性存入一个值
        // 触发更新
        trigger(target, key)
      }
      return result// 返回结果
    },
    deleteProperty (target, key) {// 删除属性的监听
      const hadKey = hasOwn(target, key)// 首先判断是否有这个属性
      const result = Reflect.deleteProperty(target, key)// 返回一个布尔值 表示是否已经成功定义过了这个属性
      if (hadKey && result) {// 两个条件都成立时候 才执行
        // 触发更新
        trigger(target, key)
      }
      return result// 返回结果
    }
  }

  return new Proxy(target, handler)// 返回一个有proxy包装的对象 需要代理的对象 代理函数
}

看到这相信 你肯定豁然明白了这些弯弯绕,那我们就趁热打铁,来进行下一步吧!

image.png

ref

ref的特点是

  1. 对传入的基本数据类型的数据进行响应式处理
  2. 将这个基本类型的数据包装为一个响应式对象
  3. 通过value属性来访问这个包装后对象的值
function ref (raw) {
  // 判断 raw 是否是ref 创建的对象,如果是的话直接返回
  if (isObject(raw) && raw.__v_isRef) {
    return
  }
  let value = convert(raw)
  const r = {// 包装对象
    __v_isRef: true,// 这个值标识是通过我们ref创建的响应式对象
    get value () {
      track(r, 'value')// 依赖收集
      return value
    },
    set value (newValue) {
      if (newValue !== value) {
        raw = newValue
        value = convert(raw)
        trigger(r, 'value')// 触发更新
      }
    }
  }
  return r
}

同理来实现下我们的toRefs

toRefs

首先toRefs是对我们reactive包装过的响应式对象进行处理,在经过处理后就可以进行解构赋值的操作

function toRefs (proxy) {// 传入的是一个proxy代理的对象
  const ret = proxy instanceof Array ? new Array(proxy.length) : {}// 初始化一个数据 通过判断传入的是数组还是对象来进行相应的变更

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)// 为响应式对象中的每一个属性都来添加上响应式处理 
  }

  return ret
}

function toProxyRef (proxy, key) {// 和我们的ref一样 
  const r = {
    __v_isRef: true,
    get value () {
      return proxy[key]
    },
    set value (newValue) {
      proxy[key] = newValue
    }
  }
  return r
}

最后呢就是我们的computed,有了以上的函数,那么我们的computed就更容易实现了

computed

computed 接收的是一个函数,在计算后返回得到一个值

function computed (getter) {// 接受一个函数
  const result = ref()// 初始化一个ref包装的空的响应式对象

  effect(() => (result.value = getter()))// 通过调用effect来通知我们的track去收集依赖

  return result// 返回最终的结果
}

到此,我们的Vue3.0响应式原理就阐述完毕了,之前也写过Vue2.x的响应式原理,相比之下。vue3的原理更清晰,而且还对2.x的一些不足之处进行了修补。大家感兴趣的话也可以看下大傻文章列表中的Vue2.x的原理。

最后呢,来简单概括下其余的两个优化具体有哪些变动

编译优化

编译优化主要体现在了Diff算法这块的优化,感兴趣可以看看大傻diff算法的讲解,vue3.0中是对静态节点进行标记提升,从而在对比时候就跳过了这些节点。

  1. Vue2.x中通过标记静态根节点 优化diff的过程
  2. Vue3.0中标记和提升所有静态的根节点,diff的时候只需要对比动态节点内容
    1. Fragments 升级vetur插件
    2. 静态提升
    3. Patch flag
    4. 缓存事件处理函数

源码体积优化

源码体积这块 Vue3.0通过将各个功能都解耦可以单独引入的方式,进行了多包管理,用户可以自己按需引入,因此在整体代码体积上有大幅度的缩减

  1. 优化打包体积
  2. Vue3.0中移除了一些不常用的API
    1. inline-template filter等
  3. Tree-shaking 感兴趣的也可以看看Vue3.0官方的API地址以及Composition API的介绍,相信会有一些更加独特的收获

image.png