vue3响应式原理源码简单剖析
vue3响应式原理源码简单剖析
讲Vue3的响应式原理之前,我们先回顾一下vue2的数据响应式原理
Vue2的响应式原理:
对象的响应式原理
Vue内部定义了一个defineReactive函数,函数内容是通过Object的defineProperty函数对对象的属性遍历进行拦截
function walk (value) {
for (let key of Object.keys(value)) {
defineReactive(value, key)
}
}
/* 定义响应式操作 */
function defineReactive (obj, key, val) {
/* 闭包
get和set函数想访问外部变量val
get和set函数引用了defineReactive函数中的val变量
*/
/* 属性拦截 */
/* 后面想访问obj对象里面的属性的时候,通过get函数返回对应的数据
后面想设置obj对象里面的属性的时候,通过set函数进行设置值
此时就可以理解为obj中的key就被拦截了,从而在get和set函数中
做我们想做的事情
*/
Object.defineProperty(obj, key, {
get () {
console.log('get:', key);
return val
},
set (v) {
console.log('set:', key);
if (v !== val) {
val = v
}
}
})
}数组的响应式原理:
重写了数组的7个方法。大致流程是:
简单参考一下(具体代码还是查看原文进行查看)
import { def } from './utils.js'
/* 获取数组中的原型 */
const arrayProperty = Array.prototype;
/* 以Array.prototype为原型创建arrayMethods对象 */
export const arrayMethods = Object.create(arrayProperty)
const methodsNeedChange = [
'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'
];
/* 其实这里做的是,给arrayMethods对象新增这7个方法,用户访问数组中的这7个方法的时候,会优先使用我们定义的这7个方法
而不会使用数组的中那7个方法。
原型链---->
其实就是重写了数组中的那7个方法
*/
methodsNeedChange.forEach(methodName => {
// 备份原来的方法
const original = arrayProperty[methodName];
// 定义新的方法
/* 给对象定义一个不可枚举的函数 */
def(arrayMethods, methodName, function () {
const args = [...arguments];
/* 恢复原来的功能(原有数组的中的方法功能不变) */
const result = original.apply(this, args);
/* 把这个数组身上的__ob__取出来 */
const ob = this.__ob__;
/* 三个特殊的方法,特殊处理的目的是,这些数据也需要是可侦测的 */
let inserted = [];
switch (methodName) {
case 'push':
case 'unshift':
inserted = args;
break;
case 'splice':
inserted = [...args].slice(2);
break;
}
if (inserted) {
/* 需要是插入的数据也是响应式的 */
ob.observeArray(inserted);
/* 此时为什么只需要取出当前调用对象的ob实例,而不是新的Observes对象的实例?
因为当前对象已经是可侦测的了
*/
}
/* 在这里可以干其他操作 */
return result;
}, false)
})
console.log(arrayMethods);获取数组中的原型
以Array.propertype(数组的原型)为原型创建一个对象
对数组的7个方法进行遍历,先执行数组中原方法的功能,而后再去做一些想做的操作(更新页面)
回顾完Vue2的数据响应式,我们再看看Vue3的数据响应式原理
之前我在学Vue3 的响应式原理的时候,给我的第一印象就是,Vue3的响应式原理就是Proxy,不再使用Object.defineProperty方法。然而并非如此
Vue3定义响应式对象的时候,我们通常会使用到 ref 和reactive 这两个函数
那我们就看看ref是怎么定义一个数据为响应式的
// 定义了一个ref函数
function ref(value) {
return createRef(value, false);
}
// 创建ref
function createRef(rawValue, shallow) {
//判断数据是不是已经是一个响应式对象,是就直接返回
if (isRef(rawValue)) {
return rawValue;
}
//不是的话,就返回一个RefImpl实例对象
return new RefImpl(rawValue, shallow);
}
// RefImpl类
class RefImpl {
constructor(value, __v_isShallow) {
this.__v_isShallow = __v_isShallow;
this.dep = undefined;
this.__v_isRef = true;
this._rawValue = __v_isShallow ? value : toRaw(value);
// 这里是我们需要关注的
this._value = __v_isShallow ? value : toReactive(value);
}
get value() {
trackRefValue(this);
return this._value;
}
set value(newVal) {
newVal = this.__v_isShallow ? newVal : toRaw(newVal);
if (hasChanged(newVal, this._rawValue)) {
this._rawValue = newVal;
this._value = this.__v_isShallow ? newVal : toReactive(newVal);
triggerRefValue(this, newVal);
}
}
}
// 判断value值是是否是对象,如果是对象,需要调reactive的方法(这个后面看),否则就返回自己
const toReactive = (value) => isObject(value) ? reactive(value) : value;
从上往下看代码,在这个ref的对数据初始化进行响应式操作的流程中,我们并没有看到Proxy吧。我们看到的是创建了一个RefImpl类的实例对象,RefImpl类中对value属性进行了get和set操作,即对对象的value的属性进行读取或者修改的时候,进行拦截该属性的行为。由此可见,ref对数据的响应式操作是本质上还是对象的属性拦截,和Object.defineProperty方法是一样的
在RefImpl类的构造器中 判断了value值是否是对象,之前分析的值不是对象,是基本类型,那么就要看一下对象的情况,啊哈,我们看到了调用的是reactive函数,那么正好可以把reactive也一起分析了。
//定义一个reactive的函数
function reactive(target) {
// if trying to observe a readonly proxy, return the readonly version.
if (isReadonly(target)) {
return target;
}
return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers, reactiveMap);
}
// 创建Reactive对象
function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers, proxyMap) {
// 传入的值必须是一个引用数据类型,不能是基本数据类型
if (!isObject(target)) {
if ((process.env.NODE_ENV !== 'production')) {
console.warn(`value cannot be made reactive: ${String(target)}`);
}
return target;
}
// 不需要代理两次
// target is already a Proxy, return it.
// exception: calling readonly() on a reactive object
if (target["__v_raw" /* RAW */] &&
!(isReadonly && target["__v_isReactive" /* IS_REACTIVE */])) {
return target;
}
// target already has corresponding Proxy
const existingProxy = proxyMap.get(target);
if (existingProxy) {
return existingProxy;
}
// only a whitelist of value types can be observed.
const targetType = getTargetType(target);
if (targetType === 0 /* INVALID */) {
return target;
}
//创建一个代理对象
const proxy = new Proxy(target, targetType === 2 /* COLLECTION */ ? collectionHandlers : baseHandlers);
proxyMap.set(target, proxy);
return proxy;
}
在这里我们终于看到了 Proxy 了,证明 对象的响应式操作是通过Proxy来实现的。
总结:
调用ref函数定义基本数据类型使之成为响应式对象的时候,它还是通过Object.defineProperty的方式进行的。
调用ref函数定义引用数据类型使之成为响应式对象的时候,通过Object.defineProperty的方式进行的,而里面的值是 再 通过Proxy代理的方式进行的。
调用reactive函数定义基本数据类型使之成为响应式对象的时候,直接返回,打印错误
调用reactive函数定义引用数据类型使之成为响应式对象的时候,它是通过Proxy代理的方式进行的。
数组也是引用类型的数据,故直接使用reactive函数进行调用即可
简单的实现vue2的响应式代码,可以点击 查看原文