3分钟带你深入浅出了解Vue.js 3.0的响应式系统原理

Vue.js 3.0的响应式系统原理拿捏

Vue.js 响应式回顾

vue.js 3.0 重写了响应式系统,和vue.js 2.x 相比主要变化体现在以下几个方面。

主要变化
Vue.js 3.0的响应式系统 底层采用Proxy对象实现属性监听,在初始化的时候不需要遍历所有的属性再把属性通过 defineProperty()转化成 gettersetter
如果有 多层属性嵌套的话,只有访问某个属性的时候才会递归处理下一级的属性,所以Vue.js 3.0中的响应式系统性能要比Vue.js 2.x好。
默认监听动态添加的属性
默认监听属性的删除
默认监听数组的索引和length属性
可以作为单独的模块使用

核心函数/方法
reactive/ ref/ toRefs/ computed(创建响应式数据或响应式对象)
effect( watch/ watchEffect底层所使用的函数)
track(收集依赖)
trigger(触发更新)

Proxy对象回顾

重点关注两个小问题

问题1: Proxy对象的使用中,setdeleteProperty中需要返回布尔类型的值;在严格模式下,如果返回false的话会出现Type Error的异常。

'use strict'
const target = {
	foo: 'xxx',
	bar: 'yyy'
}
// `set`和`deleteProperty`中需要返回布尔类型的值;
// 在严格模式下,如果返回`false`的话会出现`Type Error`的异常。
const proxy = new Proxy(target, {
	get(target, key, receiver){
		// return target[key]
		return Reflect.get(target, key, receiver)
	},
	set(target, key, value, receiver){
		// target[key] = value
		return Reflect.set(target, key, value, receiver)
	},
	deleteProperty(target, key){
		// delete target[key]
		return Reflect.deleteProperty(target, key)
	}
})

proxy.bar = 'zzz'
// delete target.foo

从上面的Proxy对象的创建我们可以看到,创建时除了要被代理的目标对象以外,我们还需要传递第二个参数,它是一个对象,我们可以叫做处理器或者监听器。其中的get/set/deleteProperty可以分别监听:对属性的访问、赋值以及删除操作get/set这两个方法最后有一个参数叫receiver,在这里代表的是当前的Proxy对象或者继承Proxy的对象。在获取或设置值的时候使用了Reflect,分别调用了Reflect中的同名getset这两个方法。Reflect是反射的意思,是es6中新增的成员。是在代码运行期间用来获取或设置对象中的成员。过去es6之前JavaScript中并没有反射,它可以很随意的把一些方法挂载到Object中,如Object.getPrototypeOf()方法,Reflect中也有对应的方法Reflect.getPrototypeOf(),方法的作用是一样的,只是表达语义的问题。如果在Reflect中有对应的Object中的方法,我们都建议使用Reflect中的方法。所以例子中在创建Proxy对象传入的处理器中都是使用了Reflect来操作对象中的成员。Vue.js 3.0中的源码也是使用的这种方式来操作对象的成员的。

问题2:ProxyReflect中使用的receiver相关
Proxy中的receiver:Proxy或者继承 Proxy的对象,它是当前创建的proxy对象或者继承自当前proxy的子对象。
Reflect中的receiver:如果target对象设置了gettergetter中的this指向receiver

// Proxy中的`receiver`:Proxy或者继承 Proxy的对象,它是当前创建的proxy对象或者继承自当前proxy的子对象。
// Reflect中的`receiver`:如果`target`对象设置了`getter`,`getter`中的`this`指向`receiver`
const obj = {
	get foo() {
		console.log(this)
		return this.bar
	}
}
const proxy = new Proxy(obj, {
	get(target, key, receiver) {
		if(key === 'bar') {
			return 'value - bar'
		}
		// return Reflect.get(target, key) // 这里Reflect.get没有传递receiver, 上面的obj没有bar属性,所以会返回undefined
		return Reflect.get(target, key, receiver) // 这里向Reflect.get传递了receiver, 获取foo属性时,foo中的this指向了receiver,也就是当前的创建的proxy, 它的getter中对key值做了判断,所以会返回'value - bar'
	}
})
  • return Reflect.get(target, key)
    在这里插入图片描述
  • return Reflect.get(target, key, receiver)
    在这里插入图片描述

Vue.js 3.0的响应式源码中,在获取或设置值的时候,都会传入receiver,以防止类似的意外发生。


响应式原理函数/方法的模拟实现

reactive函数模拟实现

原理分析
  • 接收一个参数target,判断这个参数是否是对象,如果不是的话直接返回。
  • 创建拦截器对象handler,设置 get/set/deleteProperty
    • 在构建处理get/set时,如果当前处理的属性成员也是一个对象,那么需要递归调用reactive函数对其进行响应式处理。
    • get 中收集依赖
    • set中触发更新
    • deleteProperty中触发更新
  • 返回new Proxy(target, handler)代理对象
  • 注意⚠️:reactive只能把对象转换成响应式对象
Created with Raphaël 2.3.0 开始 接收一个 target 参数 判断这个参数 target 是否是对象? 创建拦截器对象 handler,设置 get/set/deleteProperty 返回 new Proxy(target, handler) 代理对象 结束 返回非对象的参数 target 本身 yes no
// utils functions
function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}
function convert(target) {
  return isObject(target) ? reactive(target) : target;
}
function hasOwnProperty(key) {
  return Object.prototype.hasOwnProperty(key);
}
function hasOwn(target, key) {
  return hasOwnProperty.call(target, key);
}

export function reactive(target) {
  if (!isObject(target)) return target;

  const handler = {
    get(target, key, receiver) {
      // 收集依赖 Todo
      console.log('get', key);
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      return convert(result);
    },
    set(target, key, value, receiver) {
      const oldVal = Reflect.get(target, key, receiver);
      // set and deleteProperty must return a boolean
      let result = true;
      if (oldVal !== value) {
        result = Reflect.set(target, key, value, receiver);
        // 触发更新 Todo
        trigger(target, key);
        console.log('set', key, value);
      }
      return result;
    },
    deleteProperty(target, key) {
      const haskey = hasOwn(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (haskey && result) {
        // 触发更新
        trigger(target, key);
        console.log('delete', key);
      }
      return result;
    },
  };
  return new Proxy(target, handler);
}

依赖收集

在依赖收集的过程会创建三个集合: targetMap/ depsMap/ dep
targetMap 的作用时用来记录目标对象和一个字典(也就是depsMap),类型是 WeakMap,即弱引用的Map,里面的key是就是我们的target对象。因为是弱引用,当目标对象失去引用之后,可以销毁。
targetMap的值是depsMap。又是一个字典,类型是 Map,这个字典中的key是目标对象中的属性名称,值是一个 Set集合。
dep 它是一个Set集合,其中存储元素不会重复。它里面存储的是 effect函数。因为我们可以多次调用一个effect,在effect中访问同一个属性,那这个时候该属性会收集多次依赖,对应多个effect函数。

所以通过这种结构,我们可以存储目标对象,目标对象的的属性,以及属性对一个的Effect函数。一个属性可能对应多个函数。那么将来触发更新的时候,我们可以来这个结构中根据目标对象的属性找到effect函数,然后执行。
在这里插入图片描述

收集依赖 effecttrack 实现原理

  • effect函数

    • 它接收一个函数作为参数callback,
    • effect中,首先要执行一次传入的函数参数callback
    • callback中会访问响应式对象属性,收集依赖
    • 在收集依赖的过程中要把callback存储起来,所以要想办法让之后的track函数能够访问到这里的的callback
    • 需要一个在外部定义的变量来记录callback,这个变量叫做activeEffect,默认值为null
    • effect中把callback存储到activeEffect
    • (callback被调用)依赖收集完毕之后,需要把activeEffect重置为null,因为收集依赖的时候如果有嵌套属性的话,是一个递归的过程。
    • effect函数定义如下:
    let activeEffect = null;
    export function effect(callback) {
      activeEffect = callback;
      // 初始化生时会被调用一次
      callback(); // 访问响应式对象属性,去收集依赖
      activeEffect = null;
    }
    
  • track函数:它的作用是收集依赖,它需要往targetMap中添加记录effect函数的callback。

    • track函数接收两个参数,一个是目标对象target,还有一个是要跟踪的属性key
    • 需要先去判断activeEffect,因为最终我们要去保存activeEffect。如果activeEffectnull则直接返回,说明没有要收集的依赖。
    • 否则的话,我们要到targetMap中根据当前的target来找depsMap,因为我们当前的target就是我们targetMap中的键。
    • 接下来我们还要继续判断是否找到了depsMap,因为当前的target可能没有收集过依赖。如果没有找到的话,那么就要为当前的target创建一个对应的depsMap,来存储对应的属性keydep对象(也就是我们要执行的那些effect函数)。
    • 然后再把它添加到targetMap中。
    • 接下来,再要根据属性key,查找对应的dep对象。在depsMap里边根据属性key作为健来查找dep,判断dep是否存在。dep是一个Set类型的集合,用来存储我们属性对应的那些effect函数。如果没有找到对的话,也要跟上面一样,要创建一个新的dep集合,并且把它添加到depsMap中。
    • effect函数添加到dep集合中。dep.add(activeEffect)
    • 最后还要在代理对象的 get 中来调用一下这个 track 函数,进行收集依赖。
    • track(target, key)函数定义,如下:
    let targetMap = new WeakMap();
    export function track(target, key) {
      if (!activeEffect) return;
      let depsMap = targetMap.get(target);
      if (!depsMap) {
        targetMap.set(target, (depsMap = new Map()));
      }
      let dep = depsMap.get(key);
      if (!dep) {
        depsMap.set(key, (dep = new Set()));
      }
      dep.add(activeEffect);
    }
    
  • trigger函数:它的作用是触发更新,它要去targetMap中找到属性对应的的effect函数,然后来执行。

    • trigger函数接收两个参数,一个是目标对象target,还有一个是要跟踪的属性key
    • trigger函数中,我们要根据targettargetMap中找到depsMap,即把target作为键,来targetMap中找到depsMapdepsMap中存储的是我们的属性以及dep集合(键值对)。dep集合存储的就是我们属性对应的那些effect函数。
    • 首先要判断是否找到了depsMap,没有找到直接返回。
    • 否则,再根据key来找对应的dep集合。
    • 接着,要判断找到的dep集合中是否有值。如果有值的话就要遍历dep集合,然后执行它里边的每一个effect函数。
    • 最后,还要在代理对象的setdeleteProperty方法中,调用trigger函数触发更新。
    • trigger函数的定义,如下:
    export function trigger(target, key) {
      const depsMap = targetMap.get(target);
      if (!depsMap) return;
      const dep = depsMap.get(key);
      if (!dep) return;
      dep.forEach((effect) => {
        effect();
      });
    }
    

演示案例

import { reactive, effect } from './reactivity';

const obj = {
  name: 'huiquan',
  age: '18',
  message: 'hi',
};
const reactiveObj = reactive(obj);
let himsg = '';
effect(() => {
  himsg = `${reactiveObj.name}, i'm ${reactiveObj.age}`;
});

console.log(himsg);
reactiveObj.age = 28;
console.log(reactiveObj.age);
console.log(himsg);
reactiveObj.name = 'gg';

console.log(himsg);
演示结果

在这里插入图片描述


以上函数都在一个模块reactivity.js中定义:

// utils functions
function isObject(obj) {
  return obj !== null && typeof obj === 'object';
}
function convert(target) {
  return isObject(target) ? reactive(target) : target;
}
function hasOwnProperty(key) {
  return Object.prototype.hasOwnProperty(key);
}
function hasOwn(target, key) {
  return hasOwnProperty.call(target, key);
}

export function reactive(target) {
  if (!isObject(target)) return target;

  const handler = {
    get(target, key, receiver) {
      // 收集依赖 Todo
      console.log('get', key);
      track(target, key);
      const result = Reflect.get(target, key, receiver);
      return convert(result);
    },
    set(target, key, value, receiver) {
      const oldVal = Reflect.get(target, key, receiver);
      // set and deleteProperty must return a boolean
      let result = true;
      if (oldVal !== value) {
        result = Reflect.set(target, key, value, receiver);
        // 触发更新 Todo
        trigger(target, key);
        console.log('set', key, value);
      }
      return result;
    },
    deleteProperty(target, key) {
      const haskey = hasOwn(target, key);
      const result = Reflect.deleteProperty(target, key);
      if (haskey && result) {
        // 触发更新
        trigger(target, key);
        console.log('delete', key);
      }
      return result;
    },
  };
  return new Proxy(target, handler);
}

let activeEffect = null;
export function effect(callback) {
  activeEffect = callback;
  // 初始化生时会被调用一次
  callback(); // 访问响应式对象属性,去收集依赖
  activeEffect = null;
}

let targetMap = new WeakMap();
export function track(target, key) {
  if (!activeEffect) return;
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()));
  }
  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }
  dep.add(activeEffect);
}

export function trigger(target, key) {
  const depsMap = targetMap.get(target);
  if (!depsMap) return;
  const dep = depsMap.get(key);
  if (!dep) return;
  dep.forEach((effect) => {
    effect();
  });
}

【在线案例演示地址】

  • 2
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值