Vue3.0 Composition API响应式原理的简单模型

1.前言

之前总结了一篇博客——《Vue响应式的简单模型》,里面介绍了观察者模式和发布订阅模式,并通过自己实现一个轻量级Vue框架的方式介绍了Vue2.0和3.0响应式的简单原理。
但在自己实现的轻量级Vue框架中,仍然采用“传统”的Options API的方式。我们知道Vue3.0的一个亮点就是引入了Composition API,这篇博客就简单介绍一下Composition API响应式的原理。
和上一篇博客一样,我们先介绍Composition API的核心原理,然后再通过自己实现一个轻量级的Composition API来了解其中的细节。如果你没有看过上一篇博客,推荐先去看一下。此外本篇文章所用到的代码都放到了GitHub上。

2.核心原理

2.1 基本思路

Composition API响应式的实现依靠一个保存有依赖与更新对应关系的WeakMap数据结构。这里的WeakMap数据结构指的就是ES6提供的WeakMap数据结构依赖就是reactive/ref返回的对象。而computed/watch等都通过一个函数参数消费依赖,这些函数参数就是所谓的更新。Composition API把依赖作为key,更新函数作为value来构建WeakMap,在依赖有变更时在WeakMap中找到对应的更新函数来执行,以此实现响应式。

2.2 依赖和更新对应关系的WeakMap

我们通过一个例子来看这个WeakMap究竟长什么样

const obj = reactive({
      name: '特朗普',
      info: {
        message: '没有人比他更懂',
      },
})
const news = computed(() => `${obj.name}${obj.info.message}`)
// ...其他computed

最后构建出来的WeakMap就长这样:
在这里插入图片描述
从上图可以看出,构建WeakMap的过程中创建了如下三个依次嵌套的集合:

  1. targetMap: WeakMap 类型,用来记录目标对象和depsMap的关系
  2. depsMap: Map 类型,用来记录目标对象属性和dep的关系
  3. dep: Set 类型,用来记录属性对应的更新函数

当目标对象的某个属性被修改时,就去targetMap中找到目标对象的despMap,再从despMap中找到对应属性的dep,最后执行dep中所有的更新函数。
如果仔细观察上图,就会发现obj.info的值是对象,这个对象也被作为targetMap的key保存,这和reactive的实现逻辑有关,后面会详细介绍。
想要实现上述的逻辑,关键问题有两个:

  • 如何构建targetMap:
  • 如何监听属性变更

2.3 如何构建targetMap

  • reactive/ref返回的可响应对象,其get方法通过Proxy API 进行拦截。当get方法被触发时,可以获取到触发get的目标对象和属性。
  • computed/watch等API都通过回调函数的形式消费可响应对象的属性。使用computed/watch等API创建对象时,Composition API会先调用其回调函数,触发所用到属性的get方法。
  • 同时,Composition API维护一个单例 activeEffect,Composition API在调用回调函数前将activeEffect赋值为该回调函数,回调函数执行结束后将activeEffect置为null。
  • 由于可响应对象的get方法被劫持,在get方法被触发时,调用track函数构建targetMap,将activeEffect所指向的回调函数添加到targetMap对应对象、对应属性的dep中。

2.4 如何监听属性变更

这个问题很好解决,reactive/ref返回的可响应对象,其set方法也通过Proxy API进行拦截。当修改属性时,set方法就会被触发。在set方法中调用trigger函数,从targetMap中找到对应对象、对应属性的dep,执行dep内的回调函数。

3.实现一个轻量级的Composition API

我们还是通过实现一个轻量级的Composition API来了解其中的细节,首先创建一个html文件和js文件

<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>轻量级composition-api实现</title>
</head>
<body>
  <div id="app"></div>
  
  <script type="module" src="./index.js"></script>
</body>
</html>
// ./index.js
import Vue from './src/vue.js'

import {
  reactive,
  ref,
  computed,
} from './reactivity/index.js'

const App = new Vue({
  el: '#app',
  setup() {
    const obj = reactive({
      name: '特朗普',
      info: {
        message: '没有人比他更懂',
      },
    })

    const age = ref(10)

    const news = computed(() => `${age.value} 高龄的 ${obj.name}${obj.info.message}`)

    return {
      obj,
      age,
      news,
    }
  },
  render(createElement) {
    return createElement(
      'div',
      [
        createElement('span', `${this.$data.news.value}`),
      ]
    )
  },
})

setTimeout(() => {
  App.$data.obj.name = '懂王'
  App.$data.age.value = 80
  console.log('news', App.$data.news.value)
}, 2000)

setTimeout(() => {
  App.$data.obj.info.message = 'MAGA!!!'
  console.log('news', App.$data.news.value)
}, 4000)

setTimeout(() => {
  console.log('name', App.$data.obj.name)
  console.log('message', App.$data.obj.info.message)
}, 5000)

在index.js中我们创建了一个自己实现的Vue实例,将其挂在到id为app的节点上,接下来实现 ./src/vue.js中的代码

// ./src/vue.js
// effect等同于watch和watchEffect
import { effect } from '../reactivity/effect.js'
 
class Vue {
  constructor(options) {
    this.$options = options
    this.render = options.render
    this.$el = typeof options.el === 'string' ? document.querySelector(options.el) : options.el

	// 执行setup函数,并保存其返回的对象
    this.$data = options.setup()

    // 如果数据有变更,刷新视图
    effect(() => this.$mount())
  }

  createElement(tagName, children) {
    let element = document.createElement(tagName)

    if (Object.prototype.toString.call(children) === '[object Array]') {
      children.forEach((child) => {
        element.appendChild(child)
      })
    } else {
      element.textContent = children
    }

    return element
  }

  $mount() {
    const elements = this.render(this.createElement)
    this.$el.innerHTML = ''
    this.$el.appendChild(elements)
  }
}

export default Vue

接下来先处理较为关键的effect.js,因为它涉及到targetMap的构建

// effect.js
// 当前活动的 effect 函数
let activeEffect = null

export function effect(callback) {
  activeEffect = callback

  // 执行回调函数,会触发所使用数据的get函数
  // get函数中又执行了track函数
  callback()

  // 重置
  activeEffect = null
}

// 依赖列表,key是对象,value是map
// map的key的属性,value是set,里面是各个地方收集到的回调
let targetMap = new WeakMap()

// 收集依赖
export function track(target, key) {
  if (!activeEffect) return

  let depsMap = targetMap.get(target)

  // 如果没有,创建 depsMap 并添加到字典中
  if (!depsMap) {
    depsMap = new Map()
    targetMap.set(target, depsMap)
  }

  let dep = depsMap.get(key)

  // 如果没有,创建 dep 并添加到字典中
  if (!dep) {
    dep = new Set()
    depsMap.set(key, dep)
  }

  // 添加 effect 回调函数
  dep.add(activeEffect)

  // console.log('targetMap', targetMap)
}

// 触发更新
export function trigger(target, key) {
  const depsMap = targetMap.get(target)

  if (!depsMap) return

  const dep = depsMap.get(key)

  if (!dep) return

  // 遍历 dep 集合,执行 effect 回调函数
  dep.forEach((callback) => {
    callback()
  })
}

当定义好 effect/track/trigger之后,就可以编写reactive和ref的逻辑了

// reactive.js
import { track, trigger } from './effect.js'

// 判断val是否是对象
export const isObject = (val) => val !== null && typeof val === 'object'

// 递归处理
export const convert = (target) => (isObject(target) ? reactive(target) : target)
 
// 判断对象是否存在key属性
export const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key)
 
export function reactive(target) {
  // 不是对象,直接返回
  if (!isObject(target)) return target

  const handle = {
    get(target, key, receiver) {
      // 收集依赖
      track(target, key)

      // console.log('get', target, key)

      // 如果key对应的值也是对象,需要再将其转换为响应式对象,用于递归收集下一级的依赖
      // 这就是obj.info的值是对象,这个对象也被作为targetMap的key保存的原因
      // 如果递归处理对象,则修改obj.info.name无法实现响应
      return convert(Reflect.get(target, key, receiver))
    },
    set(target, key, value, receiver) {
      const oldVal = Reflect.get(target, key, receiver)

      let result = true

      if (oldVal !== value) {
        // console.log('set', target, key, value)

        result = Reflect.set(target, key, value, receiver)

        // 触发更新
        trigger(target, key)
      }

      return result
    },
    deleteProperty(target, key) {
      // 判断 target 中是否有自己的 key 属性
      const hadKey = hasOwn(target, key)

      // 判断是否删除成功(如果不存在 key 属性,也会返回成功)
      const result = Reflect.deleteProperty(target, key)

      if (hadKey && result) {
        console.log('delete', key)

        // 触发更新
        trigger(target, key)
      }

      return result
    },
  }

  return new Proxy(target, handle)
} 

ref的逻辑就比较简单了,利用了reactive的已有逻辑

import { convert, isObject } from './reactive.js'
import { track, trigger } from './effect.js'

// 将原始类型转换为响应式对象
export function ref(raw) {
  // 判断 raw 是否是 ref 创建的对象,如果是,直接返回
  if (isObject(raw) && raw.__v_isRef) return raw

  // convert 判断是否是对象,是就调用reactive,不是则直接返回
  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
}

computed的逻辑也比较简单,利用了ref和effect的实现

import { ref } from './ref.js'
import { effect } from './effect.js'

export function computed(callback) {
  const result = ref()

  // 通过 effect 监听响应式数据的变化
  // 内部调用 callback 并将结果赋值给 result.value
  effect(() => {
    result.value = callback()
  })

  return result
}

Composition API还提供了toRefs方法,这里也一并实现

// 将代理对象转换为ref
const toProxyRef = (proxy, key) => {
  return {
    get value() {
      // proxy 是响应式对象,所以这里不需要收集依赖
      return proxy[key]
    },
    set value(newValue) {
      proxy[key] = newValue
    },
  }
}

export function toRefs(proxy) {
  const ret = {}

  for (const key in proxy) {
    ret[key] = toProxyRef(proxy, key)
  }

  return ret
}

现在,这个简易版的Composition API就可以在浏览器里看到效果了
在这里插入图片描述

4.结束

这里只展示了Composition API响应式最简单的模型,肯定在细节和功能上与源码有很大的差异。但通过自己实现一个简单的Composition API,我们了解了它最基本的原理,下次面试官再问的时候心里就不慌了。

5.参考

Vue.js 3.x 响应式系统原理
Vue Composition API 响应式包装对象原理

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值