源码解析篇1——Vue响应式原型

796 阅读11分钟

前言

在学习VUE3源码时,看到了一个深入浅出的关于VUE3响应式原型的小册子,28|响应式:万能的面试题,怎么手写响应式系统 (geekbang.org),有关于vue的底层实现又更深一步了解了,特做如下的复盘。

复盘内容目录

  • vue3中响应式的重要性
  • jest(JavaScript 测试框架)的基本使用
  • 实现手写一个min版响应式原型

vue3中响应式的重要性

  • 什么是 Vue3 的响应式系统?

    Vue3 的响应式系统是一种数据绑定机制,它允许开发者在组件中使用响应式数据,以便在数据变化时更新视图。

  • Vue3 的响应式系统使用了什么技术?

    Vue3 的响应式系统使用了 ES6 中的 Proxy 对象,它可以拦截对对象属性的访问、赋值和删除操作,并且可以在这些操作发生时触发特定的行为。

  • Vue3 的响应式系统的优点是什么?

    Vue3 的响应式系统可以帮助开发者更加高效地编写代码,减少错误和 bug 的出现,并且可以帮助开发者构建更加灵活和可维护的代码。

  • Vue3 的响应式系统如何实现数据更新?

    当数据变化时,Vue3 的响应式系统会自动检测数据的变化,并且可以在需要时更新相关的视图。这样一来,开发者不需要手动编写大量的代码来完成数据的更新。

  • Vue3 的响应式系统如何帮助开发者构建可维护的代码?

    Vue3 的响应式系统可以让开发者轻松地组织和管理数据,并且可以让开发者在需要的时候快速地调整代码的结构和功能。这样一来,开发者可以更加方便地实现功能,并且可以更加容易地修改和扩展代码。

综上所述,Vue3 的响应式系统是该框架的一个非常重要的特性,它可以帮助开发者轻松地实现数据的管理和 UI 的更新,并且可以帮助开发者构建更加灵活和可维护的代码。

min版响应式原型

简单的小例子

Vue 的响应式是可以独立在其他平台使用的。比如你可以新建 test.js,使用下面的代码在 node 环境中使用 Vue 响应。以 reactive 为例,我们使用 reactive 包裹 JavaScript 对象之后,每一次对响应式对象 counter 的修改,都会执行 effect 内部注册的函数:

const {effect, reactive} = require('@vue/reactivity')

let dummy
const counter = reactive({ num1: 1, num2: 2 })
effect(() => {
  dummy = counter.num1 + counter.num2
  console.log(dummy)// 每次counter.num1修改都会打印日志
})
setInterval(()=>{
  counter.num1++
},1000)

执行 node test.js 之后,每次 count.value 修改之后都会执行effect 内部的函数。

流程图

我们先来看一下响应式整体的流程图,上面的代码中我们使用 reactive 把普通的 JavaScript 对象包裹成响应式数据了。所以,在 effect 中获取 counter.num1 和 counter.num2 的时候,就会触发 counter 的 get 拦截函数;get 函数,会把当前的 effect 函数注册到一个全局的依赖地图中去。这样 counter.num1 在修改的时候,就会触发 set 拦截函数,去依赖地图中找到注册的 effect 函数,然后执行。

image.png

测试文件目录

    ├──reactivity
        ├── __test___
            ├──reactive.spec.js
            ├──ref.spec.js
        ├──baseHandler.js
        ├──effect.js
        ├──reactive.js
        ├──ref.js
        ├──shared.js

用jest构建测试用例

reactive.spec.js

import { reactive } from '../reactive'
import { effect } from '../effect'
describe('reactive', () => {
    it('测试', () => {
        expect(1 + 2).toBe(3)
    })
    it('reactive 基本使用', () => {
        // expect(1 + 2).toBe(3)
        let obj = {num: 0, num1: 1}
        const ret = reactive(obj)
        const ret2 = reactive(obj)
        let val
        effect(() => {
            val = ret.num // 运行 依赖收集
        })
        expect(val).toBe(0)
        ret.num++
        expect(val).toBe(1)
    })
    test('一个reactive 对象的属性在多个effect中', () => {
        const ret = reactive({num: 0})
        let val, val2
        effect(() => {
            val = ret.num
        })
        effect(() => {
            val2 = ret.num
        })
        expect(val).toBe(0)
        expect(val2).toBe(0)
        ret.num++
        expect(val).toBe(1)
        expect(val2).toBe(1)
    })
    test('shallowReactive基本使用', () => {
        const ret = shallowReactive({num: 0})
        let val
        effect(() => {
            val = ret.num
        })
        expect(val).toBe(0)
        ret.num++
        expect(val).toBe(1)
    })
    test('shallowReactive浅层响应式', () => {
        const ret = shallowReactive({
            info: {
                price: 129,
                type: 'f2e'
            }
        })
        let price
        effect(() => {
            price = ret.info.price
        })
        expect(price).toBe(129)
        ret.info.price++
        expect(price).toBe(129)
    })
    it('reactive 嵌套', () => {
        const ret = reactive({
            info: {
                price: 129,
                type: 'f2e'
            }
        })
        let price
        effect(() => {
            price = ret.info.price
        })
        expect(price).toBe(129)
        ret.info.price++
        expect(price).toBe(130)
    })
})

ref.spec.js

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

describe('ref测试', () => {
    it('ref 基本使用', () => {
        const r = ref(0)
        let val
        effect(() => {
            val = r.value
        })
        expect(val).toBe(0)
        r.value++
        expect(val).toBe(1)
    })
    it('should make nested properties reactive', () => {
        const a = ref({
          count: 1
        })
        let dummy
        effect(() => {
          dummy = a.value.count
        })
        expect(dummy).toBe(1)
        a.value.count = 2
        expect(dummy).toBe(2)
      })
})

reactive

import { mutableHandlers,shallowReactiveHandlers } from './baseHandlers'
export const reactiveMap = new WeakMap()
export const shallowReactiveMap = new WeakMap()
export const reactiveMap = new WeakMap() // 定义一个reactive对象地图

export function reactive(target) {
    return createReactiveObject(target, reactiveMap, mutableHandlers)
}

function createReactiveObject(target, proxyMap, proxyHandlers) {
    if (typeof target !== 'object') {
        console.warn('reactive ${target} 必须是一个对象')
        return target
    }
    //在reactive对象地图中查找是否有target,防止重复注册同一个reactive对象
    const existingProxy = proxyMap.get(target)
    if (existingProxy) {
        return existingProxy
    }

    // 通过Proxy 创建代理,不同的Map存储不同类型的reactive依赖关系
    const proxy = new Proxy(target, proxyHandlers)
    proxyMap.set(target, proxy) // 把从未注册过的reactive对象放入reactive地图中
    return proxy // 返回的是一个一个Proxy实例对象
}
// 浅层的代理
export function shallowReactive(target) {
    return createReactiveObject(
        target,
        shallowReactiveMap,
        shallowReactiveHandlers
    )
}

梳理思路:

  1. 此时通过reactive包裹的obj对象,返回的对象是一个Proxy实例对象。
  2. 定义一个reactive地图,防止重复注册同一个reactive对象。

此时const ret = reactive(obj) 的任务基本完成了。因为返回的是一个Proxy实例对象,可以拦截属性的读取(get)和设置(set)行为,如果对Proxy不太了解,可以参考Proxy - ECMAScript 6入门 (ruanyifeng.com),所以我们在这个Proxy实例上重写其handler参数。

baseHandlers

    import {
        reactive, 
        reactiveMap, 
        shallowReactiveMap 
    } from './reactive'
    import { track, trigger } from './effect'
    import { isObject } from './shared'

    const get = createGetter() 
    const set = craeteSeter()
    const has = () => {}
    const deleteProperty = () => {}

    const shallowReactiveGet = createGetter(true)

    function createGetter(shallow = false) { //默认是深层代理
        return function get(target, key, receiver) {
            const res = Reflect.get(target, key, receiver)
            track(target, "get", key)  // 收集依赖
            if (isObject(res)) { // 处理嵌套的情况
                return shallow ? res : reactive(res)
            }
            return res
        }
    }

    function craeteSeter() {
        return function set(target, key, value, receiver) {
            const result = Reflect.set(target, key, value, receiver)
            trigger(target, "set", key)
            return result
        }
    }
    //深层代理
    export const mutableHandlers = {
        get,
        set,
        has,
        deleteProperty
    }
    // 可以选择浅层代理
    export const shallowReactiveHandlers = {
        get: shallowReactiveGet,
        set,
        has,
        deleteProperty
    }

当触发了get和set拦截操作,我们再看看effect里是怎么处理track和trigger的。

依赖地图的格式,用代码描述如下:

    targetMap = {
     target: {
       key1: [回调函数1,回调函数2],
       key2: [回调函数3,回调函数4],
     }  ,
      target1: {
       key3: [回调函数5]
     }  

    }

effect

    let activeEffect = null
    const targetMap = new WeakMap()
    export function effect(fn, options = {}) { 
        // effect嵌套,通过队列管理
        const effectFn = () => {
            try {
                activeEffect = effectFn
                //fn执行的时候,内部读取响应式数据的时候,就能在get配置里读取到activeEffect
                return fn()
            } finally {
                activeEffect = null 
            }
        }
        // 第二个参数options,传递lazy和scheduler来控制函数执行的时机
        if (!options.lazy) {
            /没有配置lazy 直接执行
            effectFn() // proxy实例对象发起拦截操作
        }
        effectFn.scheduler = options.scheduler // 调度时机 watchEffect会用到
        return effectFn
    }

    export function track(target, type, key) {
        let depsMap = targetMap.get(target) 
        if (!depsMap) { // 防止重复注册
            targetMap.set(target, (depsMap = new Map()))
        }
        let deps = depsMap.get(key)
        if (!deps) { // 防止重复注册
            deps = new Set() 
        }
        if (!deps.has(activeEffect) && activeEffect) { // 防止重复注册
            deps.add(activeEffect)
        }
        depsMap.set(key, deps)
    }

    export function trigger(target, type, key) {
        const depsMap =  targetMap.get(target)
        if (!depsMap) {
            return
        }
        const deps = depsMap.get(key)
        if (!deps) {
            return
        }
        deps.forEach((effectFn) => { // 挨个执行effect函数
            effectFn()
        })
    }

梳理思路:

1.为什么定义注册全局地图依赖是使用WeakMap数据类型呢?

因为,它的键名所引用的对象都是弱引用,即垃圾回收机制不将该引用考虑在内。因此,只要所引用的对象的其他引用都被清除,垃圾回收机制就会释放该对象所占用的内存。也就是说,一旦不再需要,WeakMap 里面的键名对象和所对应的键值对会自动消失,不用手动删除引用

2.effect 传递的函数,比如可以通过传递 lazy 和 scheduler 来控制函数执行的时机,默认是同步执行。scheduler 存在的意义就是我们可以手动控制函数执行的时机,方便应对一些性能优化的场景,比如数据在一次交互中可能会被修改很多次,我们不想每次修改都重新执行依次 effect 函数,而是合并最终的状态之后,最后统一修改一次。

3.track函数的作用就是把effect注册到依赖地图中,其中用Set数据类型存储effect,属于是一种性能优化,防止重复注册相同的effect。

4,trigger函数的作用就是把依赖地图中对应的effect函数数组全部执行一遍

ref

    export function ref(val) {
      if (isRef(val)) {
        return val
      }
      return new RefImpl(val)
    }
    export function isRef(val) {
      return !!(val && val.__isRef)
    }

    // ref就是利用面向对象的getter和setters进行track和trigget
    class RefImpl {
      constructor(val) {
        this.__isRef = true
        this._val = convert(val)
      }
      get value() {
        track(this, 'value')
        return this._val
      }

      set value(val) {
        if (val !== this._val) {
          this._val = convert(val)
          trigger(this, 'value')
        }
      }
    }

    // ref也可以支持复杂数据结构
    function convert(val) {
      return isObject(val) ? reactive(val) : val
    }

梳理思路:

1.ref 的执行逻辑要比 reactive 要简单一些,不需要使用 Proxy 代理语法,直接使用对象语法的 getter 和 setter 配置,监听 value 属性即可。

2.ref 函数实现的相对简单很多,只是利用面向对象的 getter 和 setter 拦截了 value 属性的读写,这也是为什么我们需要操作 ref 对象的 value 属性的原因。值得一提的是,ref 也可以包裹复杂的数据结构,内部会直接调用 reactive 来实现,但是需要去操作ref对象的value属性才能拿到复杂数据类型的值

3.此时我们对ref和reactive的理解又更加深刻了,当ref包裹的是一个原始类型的值时,例如:null、0等,并没有用到Proxy代理,直接用对象语法就可以完成监听。

总结

对vue响应式的理解

  1. 一句话来概括vue响应式就是,把JavaScript对象或者原始数据类型的值包裹成响应式对象,通过拦截获取和修改操作,相应触发track和trigger,实现依赖数据的自动化更新
  2. 因为在MVVM框架中,核心问题就是连接数据层和视图层,数据驱动来更新数据、视图,有了响应式,数据变化了,马上可以作出更新。vue响应式就显得极其重要。
  3. 开发人员只需要操作数据,关心业务,完全不用接触繁琐的DOM操作,从而大大提升开发效率,降低开发难度。

响应式模型实现的流程

  1. 讲述一下vue响应式模型关于reactive和ref
  2. 当要把一个js对象包裹成一个reactive对象,需要通过Proxy代理来实现,当读这个属性的值时,Proxy会拦截get操作,先执行track函数,把effect注册到依赖地图中。当修改这个属性的值时,拦截set操作,执行trigger函数,把关于改属性的effect函数挨个执行。
  3. 如果是要包裹成ref对象的话,对于原始数据类型的值来说,直接用对象语法的getter和setter配置,监听value属性,相应触发track和trigger函数,对于是引用数据类型来说,实际上就是用到了reactive的那一套流程,不过想要获取属性的值,需要.value才可以拿到值。

vue实现功能的同时所做的性能优化

  1. 看源码让我比较惊艳的地方就是它在实现功能的同时,去做的性能优化,可以收获很多。
  2. vue响应式原型模型来说,不是去直接存储effect函数,修改一次就马上执行,而是包装了一层对象对这个函数的执行实际进行管理,执行的方式是通过lazyscheduler 来控制函数执行的时机,
  3. 当使用lazy选项时,effect函数只有在被依赖项实际被访问时才会被计算,而不是在每次变化时都立即计算。
  4. 当使用scheduler,使用数组管理传递的执行任务,最后使用 Promise.resolve 只执行最后一次。
  5. 注册全局地图依赖是使用WeakMap数据类型Set数据类型存储effect函数,优化了性能。

更多细节

  • effect函数需要支持嵌套的形式,需要通过栈来进行管理,因为组件就是进行嵌套的,父组件进行数据的更新,会诱发只执行子组件的diff,因为此时的activeEffect指向子组件,通过栈来进行管理,避免丢失。