前言
最近小管在学Vue3,于是对Vue3中的响应式很感兴趣。在了解Vue3中ref和reactive的实现原理后,今天,小管将手把手带你手搓ref和reactive。当然,由于小管是个小白,所以用js实现一个丐中丐版。
知道定义后,直接!开干!
reactive
先说reactive的基本定义:reactive
只能用于对象、数组和 Map/Set 等引用类型 ,不能直接用于基本数据类型。它会返回一个深度响应式的对象。
reactive.js
我们先来创建一个文件夹reactivity
,再创建一个reactive.js
。
- 抛出一个函数
reactive()
,并传入形参target
(接受一个对象),这个函数返回一个创建一个响应式对象createReactiveObject(target)
函数。
reactive.js
:
js
export function reactive(target){
return createReactiveObject(target)
}
function createReactiveObject(target){
}
createReactiveObject(target)
函数里面的逻辑
- 先判断传入的
target
是不是一个对象,如果不是,立即返回 - 创建一个proxy代理这个
target
,易知当我们创建一个proxy
,应该传入目标对象(target
)和处理程序对象(handler
),我们将这个处理程序对象命名为proxyHandlers
- 则追根溯源,上面的
createReactiveObject()
也传入一个proxyHandlers
,官方命名为mutableHandlers
,为代码维护,这个mutableHandlers
处理程序对象将写在另外一个文件'baseHandlers.js
里,再引入到本reactive.js
中 - 返回这个
proxy
js
import { mutableHandlers } from './baseHandlers.js';
export function reactive(target){
return createReactiveObject(target,mutableHandlers)
}
function createReactiveObject(target,proxyHandlers){
if(typeof target !== 'object'){
return target
}
const proxy = new Proxy(target,proxyHandlers)
return proxy
}
- 创建一个
WeakMap()
对象
- 定义一个弱引用对象
reactiveMap
,将代理完的对象存入reactiveMap
,为方便代码维护,直接把reactiveMap
传入createReactiveObject()
中, - 将原对象和代理完的对象传入
proxyMap
- 定义一个开关变量
existingProxy
,用proxyMap
中的get
方法查看传入的target
是否在proxyMap
,如果存在,立即返回。
js
import { mutableHandlers } from './baseHandlers.js';
export const reactiveMap = new WeakMap();//是弱版本map,
export function reactive(target){
return createReactiveObject(target,mutableHandlers,reactiveMap)
}
function createReactiveObject(target,proxyHandlers,proxyMap){
if(typeof target !== 'object'){
return target
}
const existingProxy = proxyMap.get(target)
if(existingProxy){
return existingProxy
}
const proxy = new Proxy(target,proxyHandlers)
proxyMap.set(target,proxy)
return proxy
}
baseHandlers.js
下面我们来写mutableHandlers
这个对象里面的逻辑,这个对象就是Proxy
中的那个处理程序对象,自带13种方法,下面我列出最常见的五种:
get(target, property, receiver)
set(target, property, value, receiver)
has(target, property)
deleteProperty(target, property)
ownKeys(target)
......
于是我们创建函数createGetter()
和函数 createSetter()
,get
和set
为这两个函数返回的值,get
和set
作为mutableHandlers
中的方法
baseHandlers.js
:
js
const get = createGetter()
const set = createSetter()
function createGetter(){
}
function createSetter(){
}
export const mutableHandlers ={
get,
set
}
createGetter()
createGetter()
将会返回一个函数体 ,返回的这个函数体 会接收三个Proxy
赋予它的参数target, key, receiver
- 返回的函数体 会在被读取值的时候触发,所以返回的将是
target[key]
,这里我们用Reflect.get(target, key,receiver)
,将读取出的值存在res
中,返回这个res
js
function createGetter(){
return function(target, key, receiver){
const res =Reflect.get(target, key,receiver)
return res
}
}
接着我们思考一个问题,如果传入的是一个对象呢?Proxy
只能代理一层,所以我们就得继续让下一层也变为响应式。如果下下一层又是对象呢?我们就会运用递归。所以我们用一个函数来判断一个值是不是对象,为方便维护,写一个工具函数文件夹shared
,并创建一个文件index.js
,抛出一个函数判断一个值是否是对象:
index.js
:
js
export function isObject(obj) {
return typeof obj === 'object' && obj!== null;
}
将这个函数引入原来的baseHandlers.js
- 运用
isObject
判断res
是否是一个对象,如果是,递归调用reactive
js
function createGetter(){
return function(target, key, receiver){
const res =Reflect.get(target, key,receiver)
if(isObject(res)){
return reactive(res)
}
return res
}
}
createSetter()
同理:
createSetter()
将会返回一个函数体 ,返回的这个函数体 会接收四个Proxy
赋予它的参数target, key, value, receiver
- 返回的函数体 会在被读取值的时候触发,所以返回的将是
target[key]
,这里我们用Reflect.set(target, key, value, receiver)
,将读取出的值存在res
中,返回这个res
js
function createSetter(){
return function(target, key,value, receiver){
const res = Reflect.set(target, key, value, receiver)
return res
}
}
track
和trigger
- 当我们读取到响应式对象的属性时,我们就应该收集与之相关的如组件渲染、计算属性等副作用函数.这样,等我们修改这响应式对象的属性时,与之相关联的副作用函数就可以从被我们收集的地方取出来再次修改。副作用函数(如组件渲染、计算属性)与响应式数据之间的关联关系 称之为依赖
- 收集相关副作用函数的行为就叫收集依赖
- 更新相关副作用函数的行为就叫触发依赖(依赖更新)
- 将收集依赖的函数称为
track
,传入三个参数target,'get', key
(第二个参数为操作类型) - 将更新依赖的函数称为
trigger
,传入三个参数target,'set', key
为方便维护,将这两个函数,写在effect.js
文件中,再引入baseHandlers.js
中
baseHandlers.js
我们就写完了,完整代码如下:
js
import { isObject } from "../shared/index.js"
import { reactive } from "./reactive.js"
import { track, trigger} from "./effect.js"
const get = createGetter()
const set = createSetter()
function createGetter(){
return function(target, key, receiver){
const res =Reflect.get(target, key,receiver)
// 在值初次被读取时就要进行依赖收集
track(target,'get', key)
if(isObject(res)){
return reactive(res)
}
return res
}
}
function createSetter(){
return function(target, key,value, receiver){
const res = Reflect.set(target, key, value, receiver)
//触发依赖(更新)
trigger(target,'set',key)
return res
}
}
export const mutableHandlers ={
get,
set
}
effect.js
下面我们来处理effect.js
,里面要抛出两个函数 track()
和trigger()
分别传入三个参数(target,type,key)
- 定义并抛出两个函数
track()
和trigger()
- 声明一个常量
targetMap
,并将其初始化为一个WeakMap
实例。用于存储响应式对象及其属性对应的依赖集合 ,注意,targetMap
中key
就为传入的响应式对象target
(可以用来判断这个对象是否已经存到targetMap
中)。再拿target
这个对象中的属性的key再做key
,这个key
所对应的值将是一个数组,里面存放着和这个key
相关的副作用函数。
伪代码如下:
js
targetMap = {
target:{
key:[effect1,effect2,...]
}
}
代码如下:
js
const targetMap = new WeakMap();
//依赖收集
export function track(target,type,key){
}
//更新依赖
export function trigger(target,type,key){
}
副作用函数
为方便理解,我们先定义一个副作用函数effect
- 声明一个全局变量
activeEffect
,并初始化为null
。activeEffect
用于存储当前正在执行的副作用函数、 - 定义了个
effect
函数,其主要作用是将传入的副作用函数fn
进行包装,在执行fn
之前将其赋值给activeEffect
变量,以便在fn
执行过程中进行依赖收集
js
let activeEffect = null // 副作用函数
export function effect(fn,options={}){
const effectFn = ()=>{
try{
activeEffect=fn
return fn()
}catch(error){
console.log(error);
}
}
effectFn()
}
track()
现在来写track()
,也就是收集依赖里面的逻辑,我们传入(target,type,key)
,分别是目标对象,类型,还有传入的目标对象中的属性。
-
先声明一个
depsMap
,判断targetMap
中是否已经有了目标对象,相当于一个开关变量 -
检查
depsMap
是否为 undefined,如果是,则说明target
对象还没有存入depsMap
。 此时,创建一个新的 Map 对象作为depsMap
,并将其与target
对象关联起来,存储到targetMap
中。 -
从
depsMap
中获取与 key 属性关联的依赖集合(deps)。 -
检查
deps
是否为 undefined,如果是,则说明 key 属性还没有对应的依赖集合。此时,创建一个新的 Set 对象(用Set可以保证值为唯一)作为deps
,用于存储与该属性相关的副作用函数。 -
检查
deps
集合中是否已经包含当前的活跃副作用函数(activeEffect
),并且activeEffect
不为null
或undefined
。 如果deps
中不包含activeEffect
,则将activeEffect
添加到deps
集合中。 -
将更新后的 deps 集合存储到 depsMap 中,与 key 属性关联起来。
js
//依赖收集
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)
}
trigger()
收集完依赖我们就要考虑在每一次响应式对象改变的时候,它的依赖也要进行变更,同理
- 先判断
targetMap
中是否已经有目标对象,如果没有,直接返回 - 如果有,就判断传入的key是否在
targetMap
中,如果没有,又直接返回 - 如果还是有,此时就该调用那个存放着与之相关的所有副作用函数,这里用
forEach
实现
js
//触发依赖
export function trigger(target,type,key){
const depsMap= targetMap.get(target);
if(!depsMap) return
const deps = depsMap.get(key)
if(!deps) return
deps.forEach(effect=>{
effect()
})
}
effect.js文件中的完整代码就为
js
const targetMap = new WeakMap();
let activeEffect = null // 副作用函数
export function effect(fn,options={}){
const effectFn = ()=>{
try{
activeEffect=fn
return fn()
}catch(error){
console.log(error);
}
}
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(effect=>{
effect()
})
}
以上就为小管手搓的reactive
完整代码!
ref
讲完reactive我们就来谈谈ref,你可能会好奇,为什么先讲reavtive而不是ref。别急,我们这就来谈谈为什么。
首先,我们知道reactive
只能用于对象、数组和 Map/Set 等引用类型 ,不能直接用于基本数据类型。所以ref
的出现就填补了基本数据类型不能变成响应式这一缺点。
我们来看它的 定义 :ref
主要用于创建一个响应式的引用,它可以包装任何类型的值,包括基本数据类型(如 number
、string
、boolean
等)和对象、数组等引用类型。
所以当需要响应的值为对象或者数组等引用类型,在ref
的源码里,直接用reactive
就可以,这就是我们为什么要先讲reactive
的实现原理的原因。
- 从
@/shared
模块导入isObject
,用于判断一个值是否为对象。 - 从
./reactive.js
模块导入reactive
,用于创建响应式对象。 - 创建
ref
函数,它接收一个值val
,并返回一个RefImp
类的实例。这个实例就是一个响应式引用。 - 创建
RefImp
类,里面有构造函数constructor
,接收一个val
,为传入的那个应该被变为响应式的值 - 创建一个
convert
函数, 接收一个值val
作为参数。 使用isObject
函数判断val
是否为对象,如果是对象,则使用reactive
函数将其转换为响应式对象并返回;否则直接返回val
。
js
import { isObject } from "@/shared";
import { reactive } from "./reactive.js";
export function ref(val){
return new RefImp(val);
}
class RefImp {
constructor(val){
}
}
function convert(val){
return isObject(val) ? reactive(val):val
}
RefImp
类
下面来写RefImp
类中的逻辑
- 首先用
convert
函数,判断构造函数constructor
中传入的val
是否是一个对象,如果是,直接用reactive
让其变为响应式的对象并将结果赋值给this._val
。若不是,直接将val
赋值给this._val
- 用类中一个特殊的关键字
get
定义一个方法value
,返回这个this._val
。当访问ref
实例的value
属性时会自动调用。
js
export function ref(val){
return new RefImp(val);
}
class RefImp {
constructor(val){
if(convert(val)){
this._val = reactive(val)
}
this._val = val;
}
get value(){
}
}
function convert(val){
return isObject(val) ? reactive(val):val
}
get
关键字定义一个特殊的方法,这个方法在访问对象的某个属性时会自动调用。也就是说不用打那个(),将这个方法直接当成属性调用了,比如如果我们声明一个n
为ref
返回的一个RefImp
实例对象,如果没有get
关键字,我们就需要用()
调用value
js
class RefImp {
constructor(val){
if(convert(val)){
this._val = reactive(val)
}
this._val = val;
}
value(){
}
}
const n = ref(1)
console.log(n.vaule())//如果没有get关键字
class RefImp {
constructor(val){
if(convert(val)){
this._val = reactive(val)
}
this._val = val;
}
get value(){
}
}
consle.log(n.value)
- 和
reactive
原理一样,当我们首次读取到这个ref
实例的value
时,我们也开始依赖收集,于是我们直接调用前面写好的track()
。
js
get value(){
track(this,'get','value')
return this._val;
}
- 接着用一个
set
关键字,它与get
相似,用set
关键字定义一个特殊的方法,在给对象的某个属性赋值时,这个方法会自动被调用,我们也写成value
- 当
val
改变的时候,执行set
定义的value
方法,首先传入新的值将原本的this._val
改变,然后,在value
方法里调用已经写好的trigger()
方法,完成更新依赖
js
set value(newVal){
if(this._val!==newVal){
this._val=newVal
trigger(this,'set','value')
}
大功告成,ref
就这么些东西,完整代码如下:
js
import { isObject } from "@/shared";
import { reactive } from "./reactive.js";
import {track,trigger} from "./effect.js"
export function ref(val){
return new RefImp(val);
}
class RefImp {
constructor(val){
if(convert(val)){
this._val = reactive(val)
}
this._val = val;
}
get value(){
track(this,'get','value')
return this._val;
}
set value(newVal){
if(this._val!==newVal){ref和reactive
this._val=newVal
trigger(this,'set','value')
}
}
}
function convert(val){
return isObject(val) ? reactive(val):val
}
const n = ref(1)
写在最后
恭喜你看到了这里,其实ref
和reactive
也没有我们想象的那么复杂,无非是一层又一层的方法。去繁化简,其实很多时候只是需要我们耐心一点点,其实人生也是同样的道理吧,再耐心一点点,可能事情也没我们想的那么难。(我又在说点奇怪的话了)
ok,今天就聊到这,我是编程小白小管,我们下次聊。
ps:完整代码可以点击这里