浅谈vue3响应式原理

关于vue3的响应式原理相关的知识,在我们前端的面试中是会被经常问到的问题,因此特意了解了一下它的实现思路到底是什么。vue3它的响应式原理底层是依赖了ES6提供的Proxy和Reflect对象,此外还有WeakMap对象,所以在学习原理之前,我们需要先知道它们到底是什么。

一、Proxy对象

基本用法

Proxy对象可以理解为是替其他对象进行操作的代理对象,在操作目标对象之前进行拦截。外界对某个对象进行操作之前,都要经过Proxy对象这层拦截。创建Proxy对象的基本语法:

js 复制代码
const objProxy = new Proxy(obj, handler)
// obj是被代理对象,handler是拦截器,对目标对象的操作可以在handler中处理

来段代码理解一下,首先创建一个userInfo对象:

js 复制代码
const userInfo = {
  username: 'abc',
  age: 18
}

我们要访问这个对象属性值,直接userInfo.username来访问,如果给这个对象设置一个代理对象,通过这个代理对象也获取到userInfo属性:

js 复制代码
const userInfo = {
  username: 'abc',
  age: 18
}

const userProxy = new Proxy(userInfo, {
  get: function(target, key, receiver) {
    console.log('get')
    return target[key]
  },
  set: function(target, key, value, receiver) {
    console.log('set')
    target[key] = value
  }
})

console.log(userProxy.username) // 打印结果:abc
userProxy.username = '123' 
console.log(userProxy.username) // 打印结果:123

也就是说,当获取对象属性值时会调用proxy对象的getter,设置属性值时会调用setter。通过设置这样一层拦截,可以实现在对属性值进行存取时进行额外的操作。

关于getter中的参数:

  • target:实际操作的目标对象,在我们的示例中是userInfo
  • key:操作的属性
  • receiver:当前的代理对象,在我们的示例中是userProxy,这个参数的作用在下面讲到Reflect时才能直接应用到

关于setter中的参数:

  • value:要设置的新值
  • 其余参数跟getter中的一样
Proxy捕获器

Proxy一共有13个捕获器(上面的get和set就是其中两个),所有的捕获器都是可选的,如果没有定义某个捕获器,会保留对象的默认行为。13个捕获器分别为:

  • handler.get(target, key, receiver):拦截对象属性的读取
  • handler.set(target, key, value, receiver):拦截对象属性设置
  • handler.has(target, key):拦截key in proxy的操作,返回一个布尔值
  • handler.deleteProperty(target, key):拦截delete proxy[key]的操作,返回一个布尔值。
  • handler.ownKeys(target) :拦截Object.getOwnPropertyNames(proxy)Object.getOwnPropertySymbols(proxy),返回目标对象所有自身的属性的属性名
  • handler.getOwnPropertyDescriptor(target, key) :拦截Object.getOwnPropertyDescriptor(proxy, key)
  • handler.defineProperty(target, key, propDesc):拦截Object.defineProperty(proxy, key, propDesc)
  • handler.preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值
  • handler.getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象
  • handler.isExtensible(target) :拦截Object.isExtensible(proxy),返回一个布尔值。
  • handler.setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
  • handler.apply(target, object, args) :拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)proxy.call(object, ...args)proxy.apply(...)
  • handler.construct(target, args) :拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)

二、Reflect对象

Reflect主要作用
  • 提供了很多操作JavaScript对象的方法,有点像Object中操作对象的方法。比如,Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf

为什么已经有Objec可以做这些操作了还要Reflect呢?

  • 因为早期ECMA规范中没有考虑到对象本身的操作如何设计更加规范,就把这些API都放到了Object身上
  • 但是Object作为一个构造函数,这些操作放到它身上不合适
  • 新增Reflect对象可以让操作都集中到Reflect对象上 Reflect中常见的方法和Object是一一对应的,因此我们的示例可以改为用Reflect来获取对象的值:
js 复制代码
const userInfo = {
  username: 'abc',
  age: 18
}

const userProxy = new Proxy(userInfo, {
  get: function(target, key, receiver) {
    console.log('get')
    return Reflect.get(target, key, receiver)
  },
  set: function(target, key, value, receiver) {
    console.log('set')
    Reflect.set(target, key, value, receiver)
  }
})

与大多数全局对象不同 Reflect 并非一个构造函数,所以不能通过new 运算符对其进行调用,或者将 Reflect 对象作为一个函数来调用。Reflect 的所有属性和方法都是静态的(就像Math对象)。

关于Receiver参数

上面说到Proxy对象时已经有谈到拦截器里的receiver参数,它本身是一个Proxy对象,其主要作用是作为Reflect对象方法的传参,目的是通过receiver来改变里面的this。来看下面的例子:

js 复制代码
const userInfo = {
  _name: 'abc',
  get Name() {
    return this._name
  },
  set Name(value) {
    this._name = value
  }
}

在上面的代码中,获取name属性时,内部还是通过this来获取_name,但是Reflect的存在就是为了避免直接操作Object上的属性,所以还是用Proxy对象来访问属性比较合理,就有了receiver,在内部访问私有属性时还是用的代理。比如内部访问this._name时其实是通过receiver的getter去获取的。

vue3响应式设计

响应式就是当一个变量发生变化的时候,使用到这个变量的地方能够重新执行获取新的值,这就需要监听对象属性的变化。 大概步骤如下:

  • 响应式函数的封装
js 复制代码
let activeReactiveFn = null

function watchFn(fn) {
    // 用全局变量暂存依赖执行函数
    activeReactiveFn = fn
    fn() // 函数在执行的过程过用到响应式对象时,会收集依赖,函数执行完,依赖收集完成,将全局暂存activeReactiveFn置为null
    activeReactiveFn = null
}
  • 封装Depend类,实现依赖收集和触发通知
js 复制代码
class Depend {
    constructor() {
        // 用Set收集依赖防止重复收集
        this.reactiveFns = new Set()
    }
    // 依赖收集
    depend() {
        if (activeReactiveFn) {
            this.reactiveFns.add(activeReactiveFn)
        }
    }
    // 通知变化
    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}
  • 创建监听对象变化的方法,并返回代理对象
js 复制代码
function reactive(obj) {
    return new Proxy(obj, {
        get: function(target, key, receiver) {
            const depend = getDepend(target, key)
            // 添加依赖
            depend.depend()
            return Reflect.get(target, key, receiver)
        },
        set: function(target, key, value, receiver) {
            Reflect.set(target, key, value, receiver)
            // 触发依赖
            const dep = getDepend(target,key)
            dep.notify()
        }
    })
}
  • 依赖收集的方法
js 复制代码
// 寻找依赖
const targetMap = new WeakMap()
function getDepend(obj, key) {
    // 根据对象获取对应的对象
    let objMap = targetMap.get(obj)
    if (!objMap) {
        objMap = new Map()
        targetMap.set(obj, objMap)
    }

    // 根据key获取对应的Depend对象
    let depend = objMap.get(key)
    if (!depend) {
        depend = new Depend()
        objMap.set(key, depend)
    }

    return depend
}

测试例子:

js 复制代码
let userinfo = reactive({
    username: '1111',
    age: 19
})

watchFn(function() {
    console.log('username', userinfo.username)
    console.log('age', userinfo.age)
})

watchFn(() => {
    console.log('username222', userinfo.username)
})

结果输出;

js 复制代码
// username 1111
// age 19
// username222 1111

watchFn中传的回调函数模拟量对象属性的依赖,当回调函数执行时,代理对象会捕获到并将依赖函数收集起来 当改变对象属性值时,会响应触发依赖函数重新执行:

js 复制代码
let userinfo = reactive({
    username: '1111',
    age: 19
})

watchFn(function() {
    console.log('username', userinfo.username)
    console.log('age', userinfo.age)
})

watchFn(() => {
    console.log('username222', userinfo.username)
})

userinfo.age = 90 // 改变属性值

// 输出
// username 1111
// age 19
// username222 1111
// username 1111
// age 90

以上就是大概模拟了vue3响应式实现原理的思路。

相关推荐
qfZYG13 分钟前
Trae 编辑器在 Python 环境缺少 Pylance,怎么解决
前端·vue.js·编辑器
bug爱好者14 分钟前
Vue3 基于Element Plus 的el-input,封装一个数字输入框组件
前端·javascript
Silence_xl21 分钟前
RACSignal实现原理
前端
柯南二号22 分钟前
【大前端】实现一个前端埋点SDK,并封装成NPM包
前端·arcgis·npm
dangkei23 分钟前
【Wrangler(Cloudflare 的官方 CLI)和 npm/npx 的区别一次讲清】
前端·jvm·npm
乔公子搬砖23 分钟前
小程序开发提效:npm支持、Vant Weapp组件库与API Promise化(八)
前端·javascript·微信小程序·js·promise·vagrant·事件绑定
1024小神1 小时前
使用tauri打包cocos小游戏,并在抖音小玩法中启动,拿到启动参数token
前端
用户游民1 小时前
Flutter Android 端启动加载流程剖析
前端
林太白1 小时前
项目中的层级模块到底如何做接口
前端·后端·node.js
lichenyang4531 小时前
理解虚拟 DOM:前端开发中的高效渲染利器
前端