likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 3

前言

笔者在准备面试的时候,被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:调用的代理对象

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 操作符的捕捉器。
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操作重现

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对象上以及封装了和原始操作同名的方法来进行原始操作的重建

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来操作

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。——毕竟我们希望所有的操作都是在代理对象中执行

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属性发生了改变之后,五六两行代码开始执行

const obj = {
    name:'rabbit',
    height:1.88
}

obj.name = 'james'

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

响应式函数封装

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

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

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函数,使得外部操作简单,直接调用函数即可

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,只要传入某一个对象,就能让它变成被侦听的响应式对象

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函数里面执行依赖函数

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的依赖函数

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需要把哪些函数作为依赖函数进行添加

const depend = new Depend()

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

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

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

// 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发生变化的时候,这个函数就应该重新执行一遍。

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需要收集一下依赖函数了

function watchFn(fn) {
    fn()
}

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

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

let activeReactiveFn = null
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

然后修改get函数的代码

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()
        }
    })
          

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

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

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的哪一个属性的依赖?

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

依赖收集管理

整个依赖收集框架图

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

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

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

// 封装一个响应式函数————正确收集依赖
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结构代替数组达到不重复执行依赖函数的目的

代码如下

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

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

完结撒花了!