在B站学习vue3 mini-vue实现的学习记录。 视频地址从零开发写个mini-vue
1. Vue3 reactive 的API 相关实现
首先来看Vue3 reactive 的用法 和 作用 1.作用:讲一个对象定义为响应式对象 可以对这个进行依赖收集和相应的响应式处理。
import {reactive } from 'Vue'
let obj = (window.foo = reactive({ count: 0 }));
effect(() => {
console.log('obj.count:', obj.count);
});
调用结果
effect() 方法会初始化执行一次收集依赖 这是watch的区别 接下来就是实现 这个reactive 和 effect。
1. 实现Reactive和effect
- 创建一个reactive.js 文件 定义一个 reactive()方法 接收一个target为参数 首先判断是否是对象还是基本类型 基本类型不做代理
//reactive.js
export function reactive(target) {
//判断是否是对象 还是原始类型 是否做代理
if (!isObject(target)) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver);
.... 依赖收集操作
return res;
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver);
.... 依赖更新通知对应接收者
return res;
},
});
}
这里需要做一下 说明 vue2 对于数据响应式绑定是基于Object.defineProperty() 去设置的 Vue3 这里是基于 proxy 代理去实现数据响应式处理。
上面代码的 isObject 方法是一个工具方法 判断值是否是对象。
//utils.js
export function isObject(target) {
return typeof target === 'object' && target !== null;
}
问题来了?我们下面怎么实现响应式呢?
- 首先,要收集依赖,收集依赖的时机就是
obj.count对属性值做get操作的时候 这个时候我们收集这个方法对于这个属性的依赖track() - 什么时候触发更新呢? 那当然是属性值变化的时候啦 也就是set 的时候 触发
trigger()接下来 我们想想effect怎么实现?effect首先会执行对应回调方法一次 将该回调方法绑定到它所依赖的属性对象上去 然后当里面依赖的对象的数据更新时会再次出发这个回调方法。 所以effect可以初步定义如下:
//effect.js
//当前的回调方法
let activeEffect;
//传入回调方法
export function effect(fn) {
//这里另外包裹一层 是为了配合 track 收集依赖
const effectFn = () => {
try {
activeEffect = effectFn;
return fn();
} finally {
//todo
activeEffect = undefined;
}
};
//首次执行
effectFn();
//返回包含回调
return effectFn;
}
配合实现 trigger 和 track 方法
这里定义一个全局Map收集依赖 结构大概如下
{
[对象]: {
[依赖属性值名称]: {} //set 存放依赖该属性值名称的回调方法
}
}
const targetMap = new WeakMap();
//收集依赖
export function track(target, key) {
//判断是否有依赖回调方法 没有直接返回
if (!activeEffect) {
return;
}
//是否已有该对象map 没有则创建
let depsMap = targetMap.get(target) || new Map();
targetMap.set(target, depsMap);
//是否有该属性值 对应回调方法 Set 集合 没有则创建
let deps = depsMap.get(key) || new Set();
depsMap.set(key, deps);
//将依赖该属性回调方法放到set 集合里面
deps.add(activeEffect);
}
//触发回调
export function trigger(target, key) {
const depsMap = targetMap.get(target);
//判断是否监听该对象
if (!depsMap) return;
const deps = depsMap.get(key);
//是否监听该属性
if (!deps) return;
//通知回调方法
deps.forEach((effectFn) => {
effectFn();
});
}
上面的track() 和 trigger() 方法 是用来处理依赖收集 和 处理数据更新时传递更新通知的方法
上面方法简单的实现了effect和reactive;
但对于一下几种特俗殊情况还需要特别处理
reactive(reactive(obj))这个首先 判断对象是否已经被代理 已经被代理的话直接返回
export function reactive(target) {
//判断对象是否已经被代理 reactive(reactive(obj))
if (isReactive(target)) {
return target;
}
const proxy = new Proxy(target, {
get(target, key, receiver) {
//接收到__isReactive 的get 代理时 返回true 表明该对象已经被代理
if (key === '__isReactive') return true;
}
})
}
//判断一个对象是否已经被代理 reactive(reactive(obj)) 这种情况
export function isReactive(target) {
return !!(target && target.__isReactive);
}
let a = reactive(obj) let b = reactive(obj)
//用于存储 对象和代理对象的对应关系
// 防止 let a = reactive(obj) let b = reactive(obj) 这种情况时重复代理
const proxyMap = new WeakMap();
export function reactive(target) {
//判断对象是否 已经存在代理对象 let a = reactive(obj) let b = reactive(obj)
if (proxyMap.has(target)) {
return proxyMap.get(target);
}
...
const proxy = ()...
...
//放到对应map中
proxyMap.set(target, proxy);
}
- 递归对象
reactive({count1: 0, count: {count2: 2}})
export function reactive(target) {
...
const proxy = new Proxy(target, {
get(target, key, recevier) {
...
//return res; 解决递归依赖 vue3 这里是一种懒处理方式 不同于vue2
return isObject(res) ? reactive(res) : res;
}})
}
- 对象属性赋值不变时 不触发回调操作
export function reactive(target) {
...
const proxy = new Proxy(target, {
set(target, key, value, receiver) {
const oldValue = target[key];
...
//判断新值和旧值是否发生改变
if (hasChanged(oldValue, value)) {
trigger(target, key);
}
}})
}
//utils.js
export function hasChanged(oldValue, value) {
return oldValue !== value && !(Number.isNaN(oldValue) && Number.isNaN(value));
}
- 数组对象 length 监听数组的length 对象
export function reactive(target) {
...
const proxy = new Proxy(target, {
set(target, key, value, receiver) {
let oldLength = target.length;
...
//判断新值和旧值是否发生改变
if (hasChanged(oldValue, value)) {
trigger(target, key);
//判断数组的对象的length 是否改变
if (isArray(target) && hasChanged(oldLength, target.length)) {
trigger(target, 'length');
}
}
}})
}
//utils.js
export function isArray(target) {
return Array.isArray(target);
}
effect嵌套操作 我们得改造一下effect方法 已支持嵌套操作 这里使用一个栈来保存 触发的回调方法 防止回调方法绑定错误监听对象
let activeEffect;
//处理嵌套 effect 问题 使用一个栈存储 activeEffect
const effectStack = [];
//effect 的效果是 会有一次初始触发方法 用来绑定和收集依赖
export function effect(fn, options = {}) {
const effectFn = () => {
try {
activeEffect = effectFn;
//入栈操作
effectStack.push(activeEffect);
return fn();
} finally {
//todo 执行完之后 出栈
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
};
effectFn();
return effectFn;
}
接下来 可以验证下面几个例子
const observed = (window.observed = reactive([1,2,3]));
effect(() => {
console.log('index 3 is:', observed[3]);
});
effect(() => {
console.log('length is:', observed.length);
});
//在控制台push 几个值进去看看
const observed = (window.observed = reactive({count1: 1, count2: 2}));
effect(() => {
effect(() => {
console.log('observed.count2 is:', observed.count2);
});
console.log('observed.count1 is:', observed.count1);
});
//zai控制台 改变count1 和 count2 的值看看
2. 接下来实现ref方法
原理和之前差不多 ref主要是针对基本类型的封装监听
我们定义一个ref.js 定义一个ref导出方法
export function ref(value) {
//是否已经代理
if (isRef(value)) {
return value;
}
//返回响应式封装对象
return new RefImpl(value);
}
//判断是否已经代理改值
export function isRef(value) {
return !!(value && value.__isRef);
}
RefImpl是一个包装类 在这个类里面我们实现对值的依赖收集和监听
//响应式封装对象
class RefImpl {
constructor(value) {
this.__isRef = true;
this._value = convert(value);
}
//因为 .value 获取值 所以set 和 get代理 value 属性
get value() {
//依赖收集
track(this, 'value');
return this._value;
}
set value(newValue) {
//同样判断值是否和之前发生改变
if (hasChanged(newValue, this._value)) {
//这里利用 防止传入的是一个对象 所以使用convert 转换
//这里先赋值 再触发更新通知
this._value = convert(newValue);
trigger(this, 'value');
}
}
}
//判断是否是对象 是的话转换为reactive处理代理对象
function convert(value) {
return isObject(value) ? reactive(value) : value;
}
let foo = (window.foo = ref(10));
effect(() => {
console.log('foo is:', foo.value);
})
//尝试在控制台改变 foo.value 的值 看看
3.接下来实现 computed API 方法
用法一般是下面这种
let foo = (window.foo = ref(10));
let c = window.c = computed(() => {
console.log('foo!!')
return foo.value * 2;
})
只在获取该值的时候 触发做计算操作 并且不改变依赖值 只触发一次回调函数 说明里面有一个缓存 当依赖发生变化的时候 才重新计算
首先我们创建computed.js 定义
computed方法并导出
这里我们 使用一个_dirty 来判断依赖改变 是否需要重新计算值
因为 computed 没有首次触发回调 所以得把effect 方法改造一下 传入参数添加一个options 适应需求
因为 computed 需要在依赖更新后去更新_dirty的值 而不是立马去调用回调 所以需要自己定义回调操作 自己定义依赖更新后的操作 这里在options 里面传入一个scheduler 定义trigger之后的操作
export function computed(getter) {
//同样定义一个代理包装类
return new ComputedImpl(getter);
}
class ComputedImpl {
constructor(getter) {
//存储值
this._value = undefined;
// 判断依赖是否更新 是否需要重新计算值
this._dirty = true;
//计算方法
this.effect = effect(getter, {
lazy: true, //懒计算 是否初次加载
scheduler: () => {
if (!this._dirty) {
this._dirty = true;
//封装对象本身具有响应式
trigger(this, 'value');
}
},
});
}
get value() {
//判断是否依赖发生更新
if (this._dirty) {
//重新计算值
this._value = this.effect();
//更新完毕
this._dirty = false;
//封装对象本身具有 响应式
track(this, 'value');
}
return this._value;
}
}
//effect.js
//effect 的效果是 会有一次初始触发方法 用来绑定和收集依赖
export function effect(fn, options = {}) {
const effectFn = () => {
try {
activeEffect = effectFn;
effectStack.push(activeEffect);
return fn();
} finally {
//todo 执行完之后
effectStack.pop();
activeEffect = effectStack[effectStack.length - 1];
}
};
//判断是否需要首次运行 回调方法
if (!options.lazy) {
effectFn();
}
//绑定调度方法
effectFn.scheduler = options.scheduler;
return effectFn;
}
//触发回调
export function trigger(target, key) {
const depsMap = targetMap.get(target);
if (!depsMap) return;
const deps = depsMap.get(key);
if (!deps) return;
deps.forEach((effectFn) => {
//判断是否有调度方法 有的话执行调度方法
if (effectFn.scheduler) {
effectFn.scheduler(effectFn);
} else {
effectFn();
}
});
}
computed 基本完成 但是还需要考虑立下特殊情况 支持以下类型:
let foo = (window.foo = ref(10));
let c = (window.c = computed({
get() {
console.log('get');
return foo.value * 2;
},
set(value) {
foo.value = value;
},
}));
改造一下 computed.js
computed 需要接收的是一个对象 具有 set 和 get
export function computed(getterOrOption) {
let getter, setter;
//判断是否是函数
if (isFunction(getterOrOption)) {
getter = getterOrOption;
setter = () => [console.log('computed is readonly')];
} else {
//赋值
getter = getterOrOption.get;
setter = getterOrOption.set;
}
return new ComputedImpl(getter, setter);
}
class ComputedImpl {
constructor(getter, setter) {
this._setter = setter;
...
}
...
set value(value) {
//执行setter 方法
this._setter(value);
}
}
//utils.js
export function isFunction(target) {
return typeof target === 'function';
}
OK 完工!