Vue响应式系统的核心依然是对数据进行劫持,只不过Vue3采样点是Proxy类,而Vue2采用的是Object.defineProperty()。Vue3之因此采用Proxy类主要有两个缘由:vue
// 展现使用Object.defineProperty()存在的缺点 const obj = {name: "vue", arr: [1, 2, 3]}; Object.keys(obj).forEach((key) => { let value = obj[key]; Object.defineProperty(obj, key, { get() { console.log(`get key is ${key}`); return value; }, set(newVal) { console.log(`set key is ${key}, newVal is ${newVal}`); value = newVal; } }); }); // 此时给对象新增一个age属性 obj.age = 18; // 由于对象劫持的时候,没有对age进行劫持,因此新增属性没法劫持 delete obj.name; // 删除对象上已经进行劫持的name属性,发现删除属性操做也没法劫持 obj.arr.push(4); // 没法劫持数组的push等方法 obj.arr[3] = 4; // 没法劫持数组的索引操做,由于没有对数组的每一个索引进行劫持,而且因为性能缘由,Vue2并无对数组的每一个索引进行劫持
// 使用Proxy实现完美劫持 const obj = {name: "vue", arr: [1, 2, 3]}; function proxyData(value) { const proxy = new Proxy(value, { get(target, key) { console.log(`get key is ${key}`); const val = target[key]; if (typeof val === "object") { return proxyData(val); } return val; }, set(target, key, value) { console.log(`set key is ${key}, value is ${value}`); return target[key] = value; }, deleteProperty(target, key) { console.log(`delete key is ${key}`); } }); return proxy; } const proxy = proxyData(obj); proxy.age = 18; // 可对新增属性进行劫持 delete proxy.name; // 可对删除属性进行劫持 proxy.arr.push(4); // 可对数组的push等方法进行劫持 proxy.arr[3] = 4; // 可对象数组的索引操做进行劫持
Vue3的响应式系统被放到了一个单独的@vue/reactivity模块中,其提供了reactive、effect、computed等方法,其中reactive用于定义响应式的数据,effect至关因而Vue2中的watcher,computed用于定义计算属性。咱们先来看一下这几个函数的简单示例,如:react
import {reactive, effect, computed} from "@vue/reactivity"; const state = reactive({ name: "lihb", age: 18, arr: [1, 2, 3] }); console.log(state); // 这里返回的是Proxy代理后的对象 effect(() => { console.log("effect run"); console.log(state.name); // 每当name数据变化将会致使effect从新执行 }); state.name = "vue"; // 数据发生变化后会触发使用了该数据的effect从新执行 const info = computed(() => { // 建立一个计算属性,依赖name和age return `name: ${state.name}, age: ${state.age}`; }); effect(() => { // name和age变化会致使计算属性的value发生变化,从而致使当前effect从新执行 console.log(`info is ${info.value}`); });
reactive() 方法本质是传入一个要定义成响应式的target目标对象,而后经过Proxy类去代理这个target对象,最后返回代理以后的对象,如:数组
export function reactive(target) { return new Proxy(target, { get() { }, set() { } }); }
若是咱们代理的仅仅是普通对象或者数组,那么咱们能够直接采用上面的形式,可是咱们还须要代理Set、Map、WeakMap、WeakSet等集合类。因此为了程序的扩展性,咱们须要根据target的类型动态的返回Proxy类的handler。咱们能够改写成以下形式:数据结构
// shared/index.js export const isObject = (val) => val !== null && typeof val === 'object';
import {isObject} from "./shared"; import {mutableHandlers, mutableCollectionHandlers} from "./handlers"; const collectionTypes = new Set([Set, Map, WeakMap, WeakSet]); export function reactive(target) { // 给函数传入不一样的handlers而后经过target类型进判断 return createReactiveObject(target, false, mutableHandlers, mutableCollectionHandlers); } function createReactiveObject(target, isReadonly, baseHandlers, collectionHandlers) { if (!isObject(target)) { // 若是传入的target不是对象,那么直接返回该对象便可 return target; } // 根据传入的target的类型判断该使用哪一种handler,若是是Set或Map则采用collectionHandlers,若是是普通对象或数组则采用baseHandlers const observed = new Proxy(target, collectionTypes.has(target.constructor) ? collectionHandlers : baseHandlers); return observed; }
接下来咱们就须要实现Proxy的handlers,对于普通对象和数组,咱们须要使用baseHandlers即mutableHandlers,Proxy的handler能够代理不少方法,好比get、set、deleteProperty、has、ownKeys,若是将这些方法直接都写在handlers上,那么handlers就会变得很是多代码,因此能够将这些方法分开,以下:函数
// handlers.js const get = createGetter(); const set = createSetter(); function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等价于target[key] console.log(`拦截到了get取值操做`, target, key); return res; } } function createSetter(shallow = false) { return function set(target, key, value, receiver) { const result = Reflect.set(target, key, value, receiver); // 等价于target[key] = value console.log(`拦截到了set设置值操做`, target, key, value); return result; // set方法必须返回一个值 } } export const mutableHandlers = { get, set, // deleteProperty, // has, // ownKeys } export const mutableCollectionHandlers = { }
Proxy的handlers对象中的get和set方法均可以拿到被代理的对象target、获取或修改了对象的哪一个key,设置了新的值value,以及被代理后的对象receiver,目前咱们拦截到用户的get操做后仅仅是从target中取出对应的值并返回回去,拦截到用户的set操做后仅仅是修改了target中对应key的值并返回回去。性能
此时会存在一个问题,若是咱们执行state.arr.push(4)这样的一个操做,会发现仅仅触发了arr的取值操做,并没有收到arr新增了一个值的通知。由于Proxy代理只是浅层的代理,只代理了一层,因此咱们拿到的arr是一个普通数组,此时对普通数组进行操做是不会收到通知的。正是因为Proxy是浅层代理,因此避免了一上来就递归,咱们须要修改get,在取到的值是对象的时候再去代理这个对象,如:ui
+ import { isObject } from "./shared"; + import { reactive } from "./reactive"; function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等价于target[key] console.log(`拦截到了get取值操做`, target, key); + if (isObject(res)) { // 若是取到的值是一个对象,则代理这个值 + return reactive(res); + } return res; } }
此时咱们再次执行state.arr.push(4),能够看到输出结果以下:prototype
拦截到了get取值操做 {name: "lihb", age: 18, arr: Array(3)} arr 拦截到了get取值操做 (3) [1, 2, 3] push 拦截到了get取值操做 (3) [1, 2, 3] length 拦截到了set设置值操做 (4) [1, 2, 3, 4] 3 4 拦截到了set设置值操做 (4) [1, 2, 3, 4] length 4
同时也触发了length的修改,其实咱们将4 push进入数组后,数组的length会自动修改,也就是说不须要再去设置一遍length的值了,一样的咱们执行state.arr[0] = 1也会触发set操做,设置的是一样的值也会触发set操做,因此咱们须要判断一下设置的新值和旧值是否相同,不一样才须要触发set操做。代理
// shared/index.js + export const hasOwn = (target, key) => Object.prototype.hasOwnProperty.call(target, key); + export const hasChanged = (newValue, oldValue) => newValue !== oldValue;
import { isObject, hasOwn, hasChanged } from "./shared"; function createSetter(shallow = false) { return function set(target, key, value, receiver) { const hadKey = hasOwn(target, key); const oldValue = target[key]; // 修改前获取到旧的值 const result = Reflect.set(target, key, value, receiver); // 等价于target[key] = value if (!hadKey) { // 若是当前target对象中没有该key,则表示是新增属性 console.log(`用户新增了一个属性,key is ${key}, value is ${value}`); } else if (hasChanged(value, oldValue)) { // 判断一下新设置的值和以前的值是否相同,不一样则属于更新操做 console.log(`用户修改了一个属性,key is ${key}, value is ${value}`); } return result; } }
此时再次执行state.arr.push(4)就不会触发length的更新了,执行state.arr[0] = 1也不会触发索引为0的值更新了。code
通过前面reactive()方法的实现,咱们已经可以拿到一个响应式的数据对象了,咱们进行get和set操做都可以被拦截。接下来就是实现effect()方法,当咱们修改数据的时候,可以触发传入effect的回调函数执行。
effect()方法的回调函数要想在数据发生变化后可以执行,必须返回一个响应式的effect()函数,因此effect()内部会返回一个响应式的effect。
所谓响应式的effect,就是该effect在执行的时候会在取值以前将本身放入到effectStack收到栈顶,同时将本身标记为activeEffect,以便进行依赖收集与reactive进行关联。
export function effect(fn, options = {}) { const effect = createReactiveEffect(fn, options); // 返回一个响应式的effect函数 if (!options.lazy) { // 若是不是计算属性的effect,那么会当即执行该effect effect(); } return effect; } let uid = 0; let activeEffect; // 存放当前执行的effect const effectStack = []; // 若是存在多个effect,则依次放入栈中 function createReactiveEffect(fn, options) { /** * 所谓响应式的effect,就是该effect在执行的时候会将本身放入到effectStack收到栈顶, * 同时将本身标记为activeEffect,以便进行依赖收集与reactive进行关联 * */ const effect = function reactiveEffect() { if (!effectStack.includes(effect)) { // 防止不停的更改属性致使死循环 try { // 在取值以前将当前effect放到栈顶并标记为activeEffect effectStack.push(effect); // 将本身放到effectStack的栈顶 activeEffect = effect; // 同时将本身标记为activeEffect return fn(); // 执行effect的回调就是一个取值的过程 } finally { effectStack.pop(); // 从effectStack栈顶将本身移除 activeEffect = effectStack[effectStack.length - 1]; // 将effectStack的栈顶元素标记为activeEffect } } } effect.options = options; effect.id = uid++; effect.deps = []; // 依赖了哪些属性,哪些属性变化了须要执行当前effect return effect; }
这里的取值操做就是传入effect(fn)函数的fn的执行,fn中会使用到响应式数据。
此时数据发生变化还没法通知effect的回调函数执行,由于reactive和effect还未关联起来,也就是说尚未进行依赖收集,因此接下来须要进行依赖收集。
① 何时收集依赖?
咱们须要在取值的时候开始收集依赖,因此须要在取值以前将依赖的effect放到栈顶并标识为activeEffect,而前面响应式effect执行的时候已经实现,而执行effect回调取值的时候会在Proxy的handlers的get中进行取值,因此咱们须要在这里进行依赖收集。
+ import { track, trigger } from "./effect"; function createGetter(isReadonly = false, shallow = false) { return function get(target, key, receiver) { const res = Reflect.get(target, key, receiver); // 等价于target[key] console.log(`拦截到了get取值操做`, target, key); + track(target, "get", key); // 取值的时候开始收集依赖 if (isObject(res)) { return reactive(res); } return res; } }
一样的,须要在Proxy类的handlers的set中触发依赖的执行。
function createSetter(shallow = false) { return function set(target, key, value, receiver) { const hadKey = hasOwn(target, key); const oldValue = target[key]; // 修改前获取到旧的值 const result = Reflect.set(target, key, value, receiver); // 等价于target[key] = value if (!hadKey) { // 若是当前target对象中没有该key,则表示是新增属性 console.log(`用户新增了一个属性,key is ${key}, value is ${value}`); + trigger(target, "add", key, value); // 新增了一个属性,触发依赖的effect执行 } else if (hasChanged(value, oldValue)) { // 判断一下新设置的值和以前的值是否相同,不一样则属于更新操做 console.log(`用户修改了一个属性,key is ${key}, value is ${value}`); + trigger(target, "set", key, value); // 修改了属性值,触发依赖的effect执行 } return result; } }
② 如何收集依赖,如何保存依赖?
首先依赖是一个一个的effect函数,咱们能够经过Set集合进行存储,而这个Set集合确定是要和对象的某个key进行对应,即哪些effect依赖了对象中某个key对应的值,这个对应关系能够经过一个Map对象进行保存,即:
// depMap { someKey: [effect1, effect2,..., effectn] // 用集合存储依赖的effect,并放入Map对象中与对象的key相对应 }
若是只有一个响应式对象,那么咱们直接用一个全局的Map对象根据不一样的key进行保存便可,即用上面的Map结构就能够了。
可是咱们的响应式对象是能够建立多个的,而且每一个响应式对象的key也可能相同,因此仅仅经过一个Map结构以key的方式保存是没法实现的。
既然响应式对象有多个,那么就能够以整个响应式对象做为key进行区分,而可以用一个对象做为key的数据结构就是WeakMap,因此咱们能够用一个全局的WeakMap结构进行存储,以下:
// 全局的WeakMap { targetObj1: { someKey: [effect1, effect2,..., effectn] }, targetObj2: { someKey: [effect1, effect2,..., effectn] } ... }
当咱们取值的时候,首先经过该target对象从全局的WeakMap对象中取出对应的depsMap对象,而后根据修改的key获取到对应的dep依赖集合对象,而后将当前effect放入到dep依赖集合中,完成依赖的收集。
// 用一个全局的WeakMap结构以target做为key保存该target对象下的key对应的依赖 const targetMap = new WeakMap(); /** * 取值的时候开始收集依赖,即收集effect */ export function track(target, type, key) { if (activeEffect == undefined) { // 收集依赖的时候必需要存在activeEffect return; } let depsMap = targetMap.get(target); // 根据target对象取出当前target对应的depsMap结构 if (!depsMap) { // 第一次收集依赖可能不存在 targetMap.set(target, (depsMap = new Map())); } let dep = depsMap.get(key); // 根据key取出对应的用于存储依赖的Set集合 if (!dep) { // 第一次可能不存在 depsMap.set(key, (dep = new Set())); } if (!dep.has(activeEffect)) { // 若是依赖集合中不存在activeEffect dep.add(activeEffect); // 将当前effect放到依赖集合中 // 一个effect可能使用到了多个key,因此会有多个dep依赖集合 activeEffect.deps.push(dep); // 让当前effect也保存一份dep依赖集合 } }
触发依赖更新,当修改值的时候,也是经过target对象从全局的WeakMap对象中取出对应的depMap对象,而后根据修改的key取出对应的dep依赖集合,并遍历该集合中的全部effect,并执行effect。
每次effect执行,都会从新将当前effect放到栈顶,而后执行effect回调再次取值的时候,再一次执行track收集依赖,不过第二次track的时候,对应的依赖集合中已经存在当前effect了,因此不会再次将当前effect添加进去了。
/** * 数据发生变化的时候,触发依赖的effect执行 */ export function trigger(target, type, key, value) { const depsMap = targetMap.get(target); // 获取当前target对应的Map if (!depsMap) { // 若是该对象没有收集依赖 console.log("该对象还未收集依赖"); // 好比修改值的时候,没有调用过effect return; } const effects = new Set(); // 存储依赖的effect const add = (effectsToAdd) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { effects.add(effect); }); } }; const run = (effect) => { effect(); // 当即执行effect } /** * 对于effect中使用到的数据,那确定是响应式对象中已经存在的key,当数据变化后确定能经过该key拿到对应的依赖, * 对于新增的key,咱们也不须要通知effect执行。 * 可是对于数组而言,若是给数组新增了一项,咱们是须要通知的,若是咱们仍然以key的方式去获取依赖那确定是没法获取到的, * 由于也是属于新增的一个索引,以前没有对其收集依赖,可是咱们使用数组的时候会使用JSON.stringify(arr),此时会取length属性, * 索引会收集length的依赖,数组新增元素后,其length会发生变化,咱们能够经过length属性去获取依赖 */ if (key !== null) { add(depsMap.get(key)); // 对象新增一个属性,因为没有依赖故不会执行 } if (type === "add") { // 处理数组元素的新增 add(depsMap.get(Array.isArray(target)? "length": "")); } // 遍历effects并执行 effects.forEach(run); }
此时已经完成了effect和active的关联了,当数据发生变化的时候,就会遍历以前收集的依赖,从而从新执行effect,effect的执行必然会致使effect的回调函数执行。
计算属性本质也是一个effect,也就是说,计算属性内部会建立一个effect对象,只不过这个effect不是当即执行,而是等到取值的时候再执行,从以前computed的用法中,能够看到,computed()函数返回一个对象,而且这个对象中有一个value属性,能够进行get和set操做。
import {isFunction} from './shared/index'; import { effect, track, trigger } from './effect'; export function computed(getterOrOptions) { let getter; let setter; if (isFunction(getterOrOptions)) { getter = getterOrOptions; setter = () => {}; } else { getter = getterOrOptions.get; setter = getterOrOptions.set; } let dirty = true; // 默认是脏的数据 let computed; // 计算属性本质也是一个effect,其回调函数就是计算属性的getter let runner = effect(getter, { lazy: true, // 默认是非当即执行,等到取值的时候再执行 computed: true, // 标识这个effect是计算属性的effect scheduler: () => { // 数据发生变化的时候不是直接执行当前effect,而是执行这个scheduler弄脏数据 if (!dirty) { // 若是数据是干净的 dirty = true; // 弄脏数据 trigger(computed, "set", "value"); // 数据变化后,触发value依赖 } } }); let value; computed = { get value() { if (dirty) { value = runner(); // 等到取值的时候再执行计算属性内部建立的effect dirty = false; // 取完值后数据就不是脏的了 track(computed, "get", "value"); // 对计算属性对象收集value属性 } return value; }, set value(newVal) { setter(newVal); } } return computed; }
因为计算属性的effect比较特殊,不是当即执行,因此不能像以前同样,数据发生变化后,都遍历并当即执行effect,须要将计算属性的effect和普通的effect分开处理,若是是计算属性的effect,则执行其scheduler()方法将数据弄脏便可。仅仅修改run()方法便可,如:
/** * 数据发生变化的时候,触发依赖的effect执行 */ export function trigger(target, type, key, value) { const depsMap = targetMap.get(target); // 获取当前target对应的Map if (!depsMap) { // 若是该对象没有收集依赖 console.log("该对象还未收集依赖"); // 好比修改值的时候,没有调用过effect return; } const effects = new Set(); // 存储依赖的effect const add = (effectsToAdd) => { if (effectsToAdd) { effectsToAdd.forEach(effect => { effects.add(effect); }); } }; // const run = (effect) => { // effect(); // 当即执行effect // } // 修改run方法,若是是计算属性的effect则执行其scheduler方法 + const run = (effect) => { + if (effect.options.scheduler) { // 若是是计算属性的effect则执行其scheduler()方法 + effect.options.scheduler(); + } else { // 若是是普通的effect则当即执行effect方法 + effect(); + } + } /** * 对于effect中使用到的数据,那确定是响应式对象中已经存在的key,当数据变化后确定能经过该key拿到对应的依赖, * 对于新增的key,咱们也不须要通知effect执行。 * 可是对于数组而言,若是给数组新增了一项,咱们是须要通知的,若是咱们仍然以key的方式去获取依赖那确定是没法获取到的, * 由于也是属于新增的一个索引,以前没有对其收集依赖,可是咱们使用数组的时候会使用JSON.stringify(arr),此时会取length属性, * 索引会收集length的依赖,数组新增元素后,其length会发生变化,咱们能够经过length属性去获取依赖 */ if (key !== null) { add(depsMap.get(key)); // 对象新增一个属性,因为没有依赖故不会执行 } if (type === "add") { // 处理数组元素的新增 add(depsMap.get(Array.isArray(target)? "length": "")); } // 遍历effects并执行 effects.forEach(run); }