Vue3响应式原理彻底掰开揉碎!

前言

笔者在准备面试的时候,被vue3响应式原理困扰许久,参考多篇文章终于弄懂,今日来对此进行一个总结,本篇文章会从proxy reflect map weakmap是什么?怎么用,给大家一步一步推出如何手写出响应式原理

Proxy Reflect

Proxy是什么?

proxy顾名思义,就是代理。代理就是目标对象的抽象,也可以理解为是目标对象的替身,但是又独立于目标对象。跟火影忍者中呐力多(鸣人)的影分身很像,默认情况下,在代理对象上进行的所有操作都会无障碍地传递给目标对象。目标对象既可以被直接操作,也可以通过代理进行操作,只不过直接操作目标对象不能得到代理赋予的拦截行为(后续会介绍)

Proxy主要目的

使用proxy的主要目的就是可以定义捕获器(trap),所谓捕获器就是------对基础操作的拦截器,每个操作都有对应的拦截器。每次在代理对象调用这些基础操作的时候,proxy内部就会触发一个个捕获器,从而拦截以及修改相应的操作。

!注意只有在代理对象上执行这些操作才会触发捕获器,在目标对象上执行则不会触发

基本用法

首先我们需要先new一个proxy对象 ,并且传入对应需要进行侦听的对象,以及一个处理对象,可以称之为handle。

语法:const proxy = new Proxy(target,handle)

参数:

1.target:要创建代理对象的目标对象

2.handle:各对象属性中的函数(比如get set)分别定义了在执行各种操作时代理proxy的行为。


由于篇幅有限,这里只演示部分捕获器,其他的大家感兴趣可以去js高级程序设计了解

get set捕获器

get函数有三个参数:

1.target:目标对象

2.propety:被获取的属性key

3.receiver:调用的代理对象

set函数有四个参数:

1.target:目标对象

2.propety:被获取的属性key

3.value:新属性值

4.receiver:调用的代理对象

arduino 复制代码
const proxy = {
  name: "rabbit",
  height:1.88
}

const objProxy = new Proxy(obj, {
  set: function(target, key, value) {
    console.log(`代理对象被进行set操作了!!!!,快点处理!`, target, key, value)
  },
  get: function(target, key) {
    console.log(`代理对象被进行get操作了!!!!,快点处理!`, target, key)
  }
})

objProxy.name = "kobe"
objProxy.height = 1.98

console.log(objProxy.name)
console.log(objProxy.height)

其他捕获器

proxy一共是有13个捕获器,例如

  • handler.getPrototypeOf()
    • Object.getPrototypeOf 方法的捕捉器。
  • handler.setPrototypeOf()
    • Object.setPrototypeOf 方法的捕捉器。
  • handler.isExtensible()
    • Object.isExtensible 方法的捕捉器。
  • handler.preventExtensions()
    • Object.preventExtensions 方法的捕捉器。
  • handler.getOwnPropertyDescriptor()
    • Object.getOwnPropertyDescriptor 方法的捕捉器。
  • handler.defineProperty()
    • Object.defineProperty 方法的捕捉器。
  • handler.ownKeys()
    • Object.getOwnPropertyNames 方法和 Object.getOwnPropertySymbols 方法的捕捉器。
  • handler.has()
    • in 操作符的捕捉器。
  • handler.get()
    • 属性读取操作的捕捉器。
  • handler.set()
    • 属性设置操作的捕捉器。
  • handler.deleteProperty()
    • delete 操作符的捕捉器。
  • handler.apply()
    • 函数调用操作的捕捉器。
  • handler.construct()
    • new 操作符的捕捉器。
javascript 复制代码
const objProxy = new Proxy(obj, {
  has: function(target, key) {
    console.log("has捕捉器", key)
    // 还原原始操作
    return key in target
  },
  set: function(target, key, value) {
    console.log("set捕捉器", key)
    // 还原原始操作
    target[key] = value
  },
  get: function(target, key) {
    console.log("get捕捉器", key)
    // 还原原始操作
    return target[key]
  },
  deleteProperty: function(target, key) {
    console.log("delete捕捉器")
    // 还原原始操作
    delete target[key]
  }
})

console.log("name" in objProxy)
objProxy.name = "kobe"
console.log(objProxy.name)
delete objProxy.name

Reflect是什么?

顾名思义就是反射,是一个ES6之后新增API

主要目的

因为早期ECMA规范里面没有考虑到这种对对象本身 的操作规范,所以直接一股脑地所有对对象操作的API塞进了Object,又因为Object本身是一个构造函数,把这些API放在上面总归有些不合适,所以ES6之后就增加的Reflect,把对对象的一系列操作全转换到Reflect身上。

与Proxy搭配的作用

为proxy的捕获器重建原始操作

例如get操作重现

ini 复制代码
const target = {
      foo: 'bar'
    };
    const handler = {
      get(trapTarget, property, receiver) {
        return trapTarget[property];
      }
    };
    const proxy = new Proxy(target, handler);
    console.log(proxy.foo);   // bar
    console.log(target.foo); // bar

虽然get操作重现逻辑简单,但并非所有捕获器的行为都像get这样简单,因此想每次都通过手写代码来还原操作的话,是不现实的,而Reflect则是解决了这个问题,Reflect对象上以及封装了和原始操作同名的方法来进行原始操作的重建

javascript 复制代码
const target = {
      foo: 'bar'
    };
    const handler = {
      get(trapTarget, property, receiver) {
        return Reflect.get(trapTarget, property, receiver);
      }
    };
    const proxy = new Proxy(target, handler);
    console.log(proxy.foo);    // bar
    console.log(target.foo);   // bar

常见方法------和Proxy是一一对应的

  • Reflect.getPrototypeOf(target)
    • 类似于 Object.getPrototypeOf()。
  • Reflect.setPrototypeOf(target, prototype)
    • 设置对象原型的函数. 返回一个 Boolean, 如果更新成功,则返回true。
  • Reflect.isExtensible(target)
    • 类似于 Object.isExtensible()
  • Reflect.preventExtensions(target)
    • 类似于 Object.preventExtensions()。返回一个Boolean。
  • Reflect.getOwnPropertyDescriptor(target, propertyKey)
    • 类似于 Object.getOwnPropertyDescriptor()。如果对象中存在该属性,则返回对应的属性描述符, 否则返回 undefined.
  • Reflect.defineProperty(target, propertyKey, attributes)
    • 和 Object.defineProperty() 类似。如果设置成功就会返回 true
  • Reflect.ownKeys(target)
    • 返回一个包含所有自身属性(不包含继承属性)的数组。(类似于 Object.keys(), 但不会受enumerable影响).
  • Reflect.has(target, propertyKey)
    • 判断一个对象是否存在某个属性,和 in 运算符 的功能完全相同。
  • Reflect.get(target, propertyKey[, receiver])
    • 获取对象身上某个属性的值,类似于 target[name]。
  • Reflect.set(target, propertyKey, value[, receiver])
    • 将值分配给属性的函数。返回一个Boolean,如果更新成功,则返回true。
  • Reflect.deleteProperty(target, propertyKey)
    • 作为函数的delete操作符,相当于执行 delete target[name]。
  • Reflect.apply(target, thisArgument, argumentsList)
    • 对一个函数进行调用操作,同时可以传入一个数组作为调用参数。和 Function.prototype.apply() 功能类似。
  • Reflect.construct(target, argumentsList[, newTarget])
    • 对构造函数进行 new 操作,相当于执行 new target(...args)。

把之前Proxy案例中还原对象的操作都改成用Reflect来操作

javascript 复制代码
const obj = {
  name: "rabbit",
  height: 1.o8,
  set height(newValue) {
    
  }
}

const objProxy = new Proxy(obj, {
  has: function(target, key) {
    return Reflect.has(target, key)
  },
  set: function(target, key, value) {
    return Reflect.set(target, key, value)
  },
  get: function(target, key) {
    return Reflect.get(target, key)
  },
  deleteProperty: function(target, key) {
    return Reflect.deleteProperty(target, key)
  }
})

console.log("name" in objProxy)
objProxy.name = "kobe"
console.log(objProxy.name)
delete objProxy.name
console.log(objProxy)

补充:receiver的作用

如果我们的原对象obj里面有setter getter的访问器属性,那么可以通过receiver来改变里面的this,他们的this原来是指向obj的,而不是objProxy,如果我们在使用Reflect的时候传入了receiver参数,就会把this的指向指回objProxy。------毕竟我们希望所有的操作都是在代理对象中执行

csharp 复制代码
const obj = {
  name: "lebi",
  age: 18,
  _height: 1.88,
  set height(newValue) {
    this._height = newValue
  },
  get height() {
    return this._height
  }
}

接下来补充两个ES6之后新增的数据结构------Map WeakMap

Map

基本介绍:

Map用法------存储映射关系

哎,存储映射关系又不是只有Map可以,对象也可以啊。那么他和对象的区别在哪呢?

最大的区别就是------ 对象只能用字符串(ES6新增了Symbol)作为属性名,不能用对象作为属性名,而Map则可以。

常用方法

set(key, value):在Map中添加key、value,并且返回整个Map对象

get(key):根据key获取Map中的value

has(key):判断是否包括某一个key,返回Boolean类型;

delete(key):根据key删除一个键值对,返回Boolean类型;

clear():清空所有的元素;

forEach(callback, [, thisArg]):通过forEach遍历Map;

WeakMap

基本介绍

和Map相似,有以下两个区别

区别一:WeakMap的key只能使用对象,不接受其他的类型作为key

区别二:WeakMap的key对对象想的引用是弱引用,如果没有其他引用引用这个对象,那么GC可以回收该对象;

常用方法

set(key, value):在Map中添加key、value,并且返回整个Map对象;

get(key):根据key获取Map中的value;

has(key):判断是否包括某一个key,返回Boolean类型;

delete(key):根据key删除一个键值对,返回Boolean类型;


前置知识补充完,现在该进入正题,Vue3响应式原理


响应式

什么是响应式

某个变量,某个对象的属性,发生改变时,与该变量/属性有相关依赖的代码会重新执行一遍

就比如对象obj的name属性发生了改变之后,五六两行代码开始执行

arduino 复制代码
const obj = {
    name:'rabbit',
    height:1.88
}

obj.name = 'james'

console.log(obj.name,'name变了啊!');
console.log('那我也要执行了');

响应式函数封装

那么我们可以把5,6两行代码放入函数fn中,现在我们把问题转换成了------** name属性发生改变时,函数fn执行

接着我们就可以来封装一个函数watchFn,将那些需要进行响应式处理的函数进行统一收集和保存

php 复制代码
const obj = {
    name:'rabbit',
    height:1.88
}

obj.name = 'james'

function foo() {
    console.log('name变了啊!');
    console.log('那我也要执行了');
}

const reactiveFns = []

function watchFn(fn) {
    reactiveFns.push(fn)
    fn()
}

watchFn(foo)

依赖收集类Depend的封装

接着我们发现,要是我们有很多个对象,对象又有很多个属性,我们就需要创建一大堆reactiveFns数组去管理那些响应式函数,想想就可怕!所以我们需要进行优化!所以需要创建一个类,用来管理每一个对象的某一个属性的所有响应式函数

类Depend:

每一个对象属性相关的依赖函数,都可以存在一个depend里面,不污染对象属性的依赖,控制哪一个属性变化就调用哪些相关依赖,封装depend notify函数,使得外部操作简单,直接调用函数即可

javascript 复制代码
class Depend {
    constructor() {
        this.reactiveFns = []
    }

    addDepend(reactive) {
        this.reactiveFns.push(reactive)
    }

    notify() {
        this.reactiveFns.forEach(fn => fn())
    }
}

// 封装一个响应式函数
const depend = new Depend()
function watchFn(fn) {
    depend.addDepend(fn)
}

// 对象的响应式
const obj = {
    name: 'sph',
    age: 18
}

watchFn(function() {
    const newName = obj.name
    console.log('你好啊!乐比');
    console.log('你好啊!兔兔');
})

obj.name = 'lebi'
depend.notify()

于是,这样我们就创建好了一个类来管理对象属性的响应式函数,接着,我们进行下一步------自动监听对象变化

自动监听对象变化

这里就用到了上面提到Proxy和Reflect了,同时设计函数reactive,只要传入某一个对象,就能让它变成被侦听的响应式对象

vbnet 复制代码
const objProxy = new Proxy(obj,{
        get: function(target,key,receiver) {
            return Reflect.get(obj,key,receiver)
        },
        set: function(target,key,newValue,receiver) {
            Reflect.set(target,key,newValue,receiver)
        }
    })

objProxy改变属性值的时候,在set函数里面执行依赖函数

vbnet 复制代码
const objProxy = new Proxy(obj,{
        get: function(target,key,receiver) {
            return Reflect.get(obj,key,receiver)
        },
        set: function(target,key,newValue,receiver) {
            Reflect.set(target,key,newValue,receiver)
            depend.notify()
        }
    })

接下来我们来试试对obj的两个属性name,height进行侦听,于是我们只能再new一个Depend出来,用于管理height的依赖函数

javascript 复制代码
const depend1 = new Depend()
// 创建一个函数用来添加执行对象属性的依赖函数
function watchFn(depend,fn) {
    depend.addDepend(fn)
}

watchFn(depend1,function() {
    const newName = objProxy.name
    console.log('你好啊!乐比');
    console.log('你好啊!兔兔');
})

const depend2 = new Depend()

watchFn(depend2,function() {
    const newAge = objProxy.age
    console.log('你好啊!乐比');
    console.log('你好啊!兔兔');
})

所以意思就是说若是一个对象有一百个属性需要侦听,我就得重复写一百个以上的watchFn函数,这当然是不现实,那我们应该思考,该如何收集属性的依赖函数?

我们之前是在watchFn里面收集依赖函数的,但是这种收集方式不能 自动识别哪一个key需要把哪些函数作为依赖函数进行添加

javascript 复制代码
const depend = new Depend()

function watchFn(fn) {
    depend.addDepend(fn)
}

watchFn(function() {
    const newName = obj.name
    console.log('你好啊!乐比');
    console.log('你好啊!兔兔');
})

我们可以封装一个getDepend的函数,用来区分对象的不同属性需要添加的fn

scss 复制代码
// getDepend初级
const map = new Map()
function getDepend(key) {
    let depend = map.get(key)
    if(!depend) {
        depend = new Depend()
        map.set(key,depend)
    }
    return depend
}

** 那么,接下来应该去哪里收集属性的依赖函数呢?去get里面收集!

因为当一个函数中使用了对象的某个key,那么他就是这个key的依赖对象,在key发生变化的时候,这个函数就应该重新执行一遍。

vbnet 复制代码
const objProxy = new Proxy(obj,{
        get: function(target,key,receiver) {
          // 根据key 获取对应的depend
            const depend = getDepend(key)
          // 给depend对象添加响应函数?
            return Reflect.get(obj,key,receiver)
        },
        set: function(target,key,newValue,receiver) {
            Reflect.set(target,key,newValue,receiver)
          // 根据key 获取对应的depend
            const depend = getDepend(key)
            depend.notify()
        }
    })

在每次传入依赖函数时,都需要调用一下,来告诉objProxy需要收集一下依赖函数了

php 复制代码
function watchFn(fn) {
    fn()
}

那么这时候已经已经拿到对应属性的depend了,该为depend对象添加响应函数了

可是现在问题在于,如何拿到watchFn里面的依赖函数fn呢? 可以这样操作

csharp 复制代码
let activeReactiveFn = null
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

然后修改get函数的代码

vbnet 复制代码
const objProxy = new Proxy(obj,{
        get: function(target,key,receiver) {
          // 根据key 获取对应的depend
            const depend = getDepend(key)
          // 给depend对象添加响应函数?
          depend.addDepend(activeReactiveFn)
            return Reflect.get(obj,key,receiver)
            const depend = getDepend(key)
            depend.notify()
        }
    })
          

那么至此对单个对象的多个进行侦听就完成了,但是我们在开发总又不可能只有一个对象,大概率需要对很多个对象的不同属性进行响应式处理。

所以需要对多个对象进行侦听,可进行以下优化来批量生成代理对象

javascript 复制代码
const reactive = (obj) => {
    return new Proxy(obj,{
        get: function(target,value,receiver) {
            const depend = getDepend(key)
            depend.addDepend(activeReactiveFn)
            return Reflect.set(target,value,receiver)
        },
        
        set: function(target,value,newValue,receiver) {
            Reflect.set(target,value,newValue,receiver)
            depend.notify()
        }
    })
}

那么如何判断一个依赖函数是哪一个obj的哪一个属性的依赖?

这时就需要对对象依赖进行管理,重点来了,提起精神往下看

依赖收集管理

整个依赖收集框架图

于是,接着我们就可以对getDepend进行优化

scss 复制代码
const targetMap = WeakMap()
function getDepend(target,key) {
    // 通过target 拿到第一层数据结构
    let map = targetMap.get(target)
    if(!map) {
        map = new Map()
        targetMap.set(target,map)
    }
  // 再拿第二层数据结构
    let depend = map.get(key)
    if(!depend) {
        depend = new Depend()
        map.set(key,depend)
    }

    return depend
}

再对set get函数代码进行调整,重新获取depend

javascript 复制代码
// 封装一个响应式函数------------正确收集依赖
let activeReactiveFn = null
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

const reactive = function(obj) {
    return new Proxy(obj,{
        get: function(target,key,receiver) {
            const depend = getDepend(target,key)
            depend.addDepend(activeReactiveFn)
            return Reflect.get(obj,key,receiver)
        },
        set: function(target,key,newValue,receiver) {
            Reflect.set(target,key,newValue,receiver)
            const depend = getDepend(target,key)
            depend.notify()
        }
    })
}

最后进行一步优化

问题:如果一个依赖函数里面两次用到一个属性,这个依赖函数就会被重复添加进depend里面

解决办法:把reactiveFns换成一个新的数据结构Set,用Set结构代替数组达到不重复执行依赖函数的目的

代码如下

javascript 复制代码
class Depend {
    constructor() {
        // 用set代替数组就是为了不多次调用同一个函数
        this.reactiveFns = new Set()
    }

    depend(fn) {
        if(activeReactiveFn) {
            this.reactiveFns.add(fn)
        }
    }
}

完结撒花了!

相关推荐
崔庆才丨静觅9 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606110 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了10 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅10 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅10 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅11 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment11 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅11 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊11 小时前
jwt介绍
前端
爱敲代码的小鱼11 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax