Vue3 框架原理学习实现(一)-reactive 相关API实现

198 阅读8分钟

在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);
});

调用结果

image.png

effect() 方法会初始化执行一次收集依赖 这是watch的区别 接下来就是实现 这个reactive 和 effect。

1. 实现Reactive和effect

  1. 创建一个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;
}

问题来了?我们下面怎么实现响应式呢?

  1. 首先,要收集依赖,收集依赖的时机就是obj.count对属性值做get操作的时候 这个时候我们收集这个方法对于这个属性的依赖 track()
  2. 什么时候触发更新呢? 那当然是属性值变化的时候啦 也就是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;
}

配合实现 triggertrack 方法 这里定义一个全局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() 方法 是用来处理依赖收集 和 处理数据更新时传递更新通知的方法

上面方法简单的实现了effectreactive; 但对于一下几种特俗殊情况还需要特别处理

  1. 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);
}
 
  1. 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);
}
  1. 递归对象 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;
    }})
}
  1. 对象属性赋值不变时 不触发回调操作
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));
}

  1. 数组对象 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);
}
  1. 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;
})

只在获取该值的时候 触发做计算操作 并且不改变依赖值 只触发一次回调函数 说明里面有一个缓存 当依赖发生变化的时候 才重新计算 image.png

image.png 首先我们创建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 完工