Vue3源码学习--响应式系统(reactive)

159 阅读7分钟

vue3相比较于vue2数据响应式做了较大改动,采用proxy实现数据劫持,定义响应式api的方式主要有reactie, ref,其它api由这2种api衍生而出。本文将介绍vue3响应式系统的实现基本原理(reactive)

vue3响应式系统的入口为reactivity/index。reactive的实现在reactive文件下。

  • reactive

reactive将传入的对象转换为响应式对象,它只能传入对象类型,因为proxy只能代理对象,不能代理基本数据了行,以下分析reactive的实现原理。

reactive传入一个原始对象,返回createReactive的返回值,raw为原始对象,reactiveMap是一个WeakMap,用于存储原始对象和代理对象的映射,可以做缓存处理。baseHandlerproxy的handler

const reactive = (raw) => createReactive(raw, false, reactiveMap, baseHandler);

createReactive用于创建响应式对象。原始值不为对象时直接return,最终返回由proxy创建的响应式对象,并将其存入reactiveMap中。

const createReactive = (raw, isReadonly, proxyMap, handler) => {
  if (!isObject(raw)) {
    console.warn('this is not a object ,cant not be reactive');
    return raw;
  }
  // raw[ReactiveFlags.RAW]已经是响应式对象,普通对象不存在RAW属性,
  if (raw[ReactiveFlags.RAW] && !(isReadonly && raw[ReactiveFlags.REACTIVE])) {
    return raw;
  }
​
  let proxy = proxyMap.get(raw);
  if (proxy) {
    return proxy;//获取缓存值
  }
  if (!proxy) {
    proxy = new Proxy(raw, handler);//不存在? 创建代理对象
    proxyMap.set(raw, proxy);//存入reactiveMap
    return proxy;//返回
  }
};

baseHandler实现

// 对象数据handler ( object,array)
const baseHandler = {
  get,
  set,
  deleteProperty,
  ownKeys,
  has,
};

getset分别通过createGettercreateSetter创建

const get = createGetter(false, false);
const set = createSetter(false, false);

createGetter实现

function createGetter(isReadonly, isShallow) { //这里存在一个闭包
  return (target, key, receiver) => {
    if (key === ReactiveFlags.READONLY) {//判断是否为readonly, isReadonly会用到
      return isReadonly;
    } if (key === ReactiveFlags.REACTIVE) {//判断是否为reactive,isReactive会用到
      return !isReadonly;
    } if (key === ReactiveFlags.SHALLOW) {//判断是否为shallow,isShallow会用到
      return isShallow;
      
      //下面这一坨是用于获取响应式对象的原始对象(源码中是通过三元运算符),receiver为proxy代理出来的对象,通过不同的proxMap获取
    } if (key === ReactiveFlags.RAW && !isShallow && !isReadonly && receiver === reactiveMap.get(target)) { // 这里先分开写
      return target;
    } if (key === ReactiveFlags.RAW && isReadonly && !isShallow && receiver === readonlyMap.get(target)) {
      return target;
    } if (key === ReactiveFlags.RAW && !isReadonly && isShallow && receiver === shallowReactiveMap.get(target)) {
      return target;
    } if (key === ReactiveFlags.RAW && isShallow && isReadonly && receiver === shallowReadonlyMap.get(target)) {
      return target;
    }
​
    // 判断是否调用数组方法
    const targetIsArray = Array.isArray(target);
    if (!isReadonly) {//如果是数组需要处理indexOf等方法,应为经过代理的对象不会是原对象,但是用户不会关心代理对象的查找方式
      if (targetIsArray && arrayInstrumentations.hasOwnProperty(key)) {
        return Reflect.get(arrayInstrumentations, key, receiver);
      }
    }
​
    const res = Reflect.get(target, key, receiver);
    if (!isReadonly) { //不是readonly的情况下需要去做依赖收集
      track(target, key, 'get');
    }
​
    if (isShallow) {//如果是浅层响应式则无需对其对象类型的属性值做响应式处理,直接return
      return res;
    }
    if (isObject(res)) {//如果是对象,则需要再次做响应式处理
      return isReadonly ? readonly(res) : reactive(res);
    }
    return res;
  };
}

处理数组查找方法

function createArrayInstrumentations() {
  const instrumentations = {};
  ['includes', 'indexOf', 'lastIndexOf'].forEach((key) => {
    instrumentations[key] = function (...args) {
      const arr = toRaw(this);
      for (let i = 0; i < this.length; i++) {
        track(arr, `${i}`, 'get'); // 执行查找方法时,需要收集依赖,当用户修改arr[index]时触发;
      }
      const res = arr[key](...args); // 使用原始参数查找arg中有的可能为proxy,
      if (res === -1 || res === false) {
        return arr[key](...(args.map((item) => toRaw(item)))); // 将参数值设置为原始值,再去查找
      }
      return res;
    };
  });
  return instrumentations;
}

// 这里的操作是什么意思呢,举个例子
const obj = {
  name: '我在查找',
};
const proxy = reactive([
  obj, 
]);
console.log(proxy.indexOf(obj));
/*
  这种情况下,obj在reactive中被做了代理,已经不是原始对象obj,所以如果没有createArrayInstrumentations 这里就会返回-1,
  通过createArrayInstrumentations处理,经过代理的对象查找不到就会通过toRaw获取原始对象,再去查找
*/

get和依赖收集相关联,接下来介绍如何依赖收集。介绍依赖之前,先介绍一下依赖的数据结构,首先proxy只能代理对象类型,所以每一个对象会对应一个响应式对象,而对象的每一个键会对应一个依赖,所以就形成以下结构。

112.png

先看几个全局变量

const proxyMap = new WeakMap();//存储对象和对象键值映射dep的map
let activeEffect = void 0;//当前活跃的effect,一般当前正在执行的effect有一个,effect嵌套则通过链表记录上一次活跃情况let shouldTrack = false;//是否应该收集依赖,let effectTrackDepth = 0;//effect嵌套层数
const maxMakerBits = 30; // effect最大嵌套层级, 每嵌套一级则左移一位
export let trackOpBit = 1;//记录effect嵌套时,位移标识const trackStack = [];//存储上一次是否应该收集依赖

track实现

const track = (target, key) => {
  if (!isTracking()) {//是否正在收集依赖
    return;
  }
  let depMap = proxyMap.get(target);//获取对象键对应dep的map
  if (!depMap) {
    proxyMap.set(target, depMap = new Map());//第一次收集则不存在,不存在设置一个空map
  }
  let dep = depMap.get(key);//获取到依赖
  if (!dep) {
    depMap.set(key, dep = createDep());//第一次收集不存在,不存在则创建空dep
  }
  trackEffects(dep);//收集依赖
};

收集依赖

const trackEffects = (dep) => {
  let shouldTrack = false;
  if (effectTrackDepth <= maxMakerBits) {//当前effect嵌套层数小于最大层数,采用优化方案(什么是优化方案?后面介绍)
    if (!newTracked(dep)) {//刚开始收集(dep.n = 0 ,trackOpBit为层数,比如到15层,但是dep.n为0,按位与为0)
      dep.n |= trackOpBit;//n,左移一位,标识该层的这一个dep已经处理过了
      shouldTrack = !wasTracked(dep);//第一次执行dep.w都为0,因为还没收集依赖,deps的长度为0
    }
  } else {
    shouldTrack = !dep.has(activeEffect);
  }
  if (shouldTrack) {
    dep.add(activeEffect);//收集当前effect,
    activeEffect.deps.push(dep);//双向关联,当前effect同时添加dep(为什么要双向关联,后面介绍)
  }
};

先介绍一下依赖的数据结构,依赖是一个set结构,里面存储着effect

const createDep = (effects) => {
  const dep = new Set(effects);
  dep.w = 0;//收集过了
  dep.n = 0;//最新收集
  return dep;
};
const wasTracked = (dep) => (trackOpBit & dep.w) > 0;// 这里注意运算符优先级 (当前层级trackOpbit & 当前dep的w)const newTracked = (dep) => (trackOpBit & dep.n) > 0;// 这里注意运算符优先级(当前层级trackOpbit & 当前dep的w)const initDepMakers = (reactiveEffect) => {//初始化的时候deps长度为0,不执行
  const { deps } = reactiveEffect;
  if (deps.length) {
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i];
      dep.w |= trackOpBit;
    }
  }
};
const finalizeDepMakers = (reactiveEffect) => {//effect执行退出后,dep的标识w,n,还原
  const { deps } = reactiveEffect;
  if (deps.length) {
    let ptr = 0;
    for (let i = 0; i < deps.length; i++) {
      const dep = deps[i];
      if (wasTracked(dep) && !newTracked(dep)) {//之前收集过了但是更新时发现不是新收集的了,则直接删除
        dep.delete(reactiveEffect);
      } else {
        // 当dep中删除effect时,effect同时也要删除dep(从effect的deps中);
        deps[ptr++] = dep;
      }
      dep.w &= ~trackOpBit;
      dep.n &= ~trackOpBit;
    }
  }
};

响应式系统中比较重要的一个类就是ReactiveEffect,看一下ReactiveEfffect结构

class ReactiveEffect {
  constructor(fn, scheduler) {
    //构造器,传入fn(要执行的副作用函数,比如render函数),scheduler调度器,依赖触发时可以选择执行
    
    this.fn = fn;
    this.active = true;
    this.deps = [];//当副作用函数执行时,target->key->dep会存储effect,但是effect也会反过来存储dep,deps就是多个dep
    this.parent = undefined;//上一层活跃的effect,初始化为undefined
    this.scheduler = scheduler;//调度器
  }
  // run的时候会执行副作用函数
  run() {
    if (!this.active) {
      return this.fn();
    }
    const lastShouldTrack = shouldTrack;//将当前存储为上一次
    try {
      this.parent = activeEffect;
      //当前复制给parent,如果是effect嵌套,activeEffect就会被赋值给下一层,防止上一层activEffect丢失
      shouldTrack = true;
      activeEffect = this;//全局活跃effect被赋值为当前effect
​
      trackOpBit = 1 << ++effectTrackDepth; // 每次嵌套一层则左移一位
​
      if (effectTrackDepth <= maxMakerBits) {
        // 初始化的时候这里是不会执行的,因为dep的length1为0,更新的时候每一个dep的w置为trackOpBit
        initDepMakers(this);
      } else {
        cleanupEffect(this);//清除所有副作用(什么意思?,后面阐述)
      }
      const result = this.fn();//执行副作用函数
      return result;
    } finally {//effec执行完毕后,要恢复至执行前的状态,因为每一层的状态都不一样
      if (effectTrackDepth <= maxMakerBits) {
        finalizeDepMakers(this);
      }
      trackOpBit = 1 << --effectTrackDepth;
​
      activeEffect = this.parent;
      shouldTrack = lastShouldTrack;
      this.parent = undefined;
    }
  }
}

什么是清除副作用,为什么要清除副作用,什么是分支切换?举个例子(例子1.1

const user =reactive({
  name:"foo",
  alias:"bar",
  flag:true
})
effect(()=>{//effect是一个全局函数,将传入的fn构造为一个ReactiveEffect对象(这里传入的函数命名为fn)
  let text = user.flag ? user.name : user.alias;
})
/*
  effect执行时,user.flag 为true,则user.flag的dep收集到fn,user.name的dep会收集到 fn,而user.alias不会收集fn,
  同样,effect的实例对象的deps属性会存储user.flag的dep,user.name得flag;
  
  当user.flag变为false时,effect会重新执行,再次收集依赖,user.flag的dep收集到fn,user.alias收集到fn,effect的实例对象
  的deps存储user.flag的dep,user.alias的dep。
  但是此时user.name仍然收集着fn,如果user.flag这辈子再也不会变为true,那么当user.name发生改变时,依赖重新触发,着明显是不对的。
  所以在每次执行副作用函数是,应该清除依赖(user.flag的dep删除fn,user.name的dep删除fn,user.alias的dep删除fn,然后重新收集,
  就不会有遗留的副作用了)
*/

cleanupEffect的实现

const cleanupEffect = (effectFn) => {//传入当前执行的effect实例。找到实例中的dep,再从dep中删除自己(effect)
  for (let i = 0; i < effectFn.deps.length; i++) {
    const dep = effectFn.deps[i];
    dep.delete(effectFn);
  }
};

但是这么做很显然是浪费性能的,因为我只改变了一个属性(alias),但是user.name, user,alias都要重新收集effect(自己)。如果当前effect中有百个属性,那这些属性都要重新收集当前effect,着是没必要的,希望做到只删除那个修改后不会触发依赖重新执行的effect就可以了(本例子中希望user.name的dep删除fn就可以了,user.flag和user.alias不动),那要怎么做呢?这里就涉及到优化方案了,就是前面说的位运算(dep的wn,以及trackOpBit等);那么要怎么做呢?举个例子体会一下按层收集依赖(例子1.2

const user = {
  name: 'app',
  age: 180,
};
/*
 trackOpBit = 1 // 0000 0001
 effectTrackDepth = 0;
*/
// 初始化执行的时候 每嵌套一层effect时会重新 new ReactiveEffect() dep对应的 w 和 n都是 0
effect(() => {
  // 左移一位 trackOpBit = 2 // 0000 0010
  console.log(`--------${user.name}-------`); // name-> w:0000 0000 , n : 0000 0010//这里n的位移在trackEffects里
  console.log(`--------${user.age}--------`); // age->  w:0000 0000 , n : 0000 0010
  effect(() => {
    // 再次左移一位 trackOpBit = 4 // 0000 0100
    console.log(`---------${user.name}--------`); // name-> w:0000 0000, n : 0000 0100
    console.log(`---------${user.age}--------`); // age->   w:0000 0000, n : 0000 0100
    effect(() => {
      // 再次左移一位 trackOpBit = 8 // 0000 1000
      console.log(`--------${user.age}--------`); // age-> w:0000 0000, n : 0000 1000
    });
  });
}); // 初始化完毕之后 所有dep对应的w和n都会被置为 0 (调用源码的finalizeDepMakers方法) 恢复到初始状态 dep.n = 0 , dep.w=0// 响应式数据更新的时候
effect(() => {
  // 左移一位 trackOpBit = 2 // 0000 0010
  /*
    在用户传入的fn执行之前 会初始化depMakers 调用源码中的initDepMakers方法 ,之前收集的dep的w将被标记如下
  */
  console.log(`--------${user.name}-------`); // name-> w:0000 0010 , n : 0000 0010
  console.log(`--------${user.age}--------`); // age->  w:0000 0010 , n : 0000 0010
  effect(() => {
    // 再次左移一位 trackOpBit = 4 // 0000 0100
    console.log(`---------${user.name}--------`); // name-> w:0000 0100, n : 0000 0100
    console.log(`---------${user.age}--------`); // age->   w:0000 0100, n : 0000 0100
    effect(() => {
      // 再次左移一位 trackOpBit = 8 // 0000 1000
      console.log(`--------${user.age}--------`); // age-> w:0000 1000, n : 0000 1000
    });
  });
}); // 更新流程执行完毕之后 所有dep的w和n都会被置为0 (调用源码的finalizeDepMakers方法) 恢复到初始状态 dep.n=0, dep.w=0

这里以例子1.1为例子

初始化effect执行,当前activeEffect为当前effect(fn简称),执行effect的实例方法,trackOpBit左移一位(0000 0010),effectTrackDepth++变为1,1<30,执行优化方案,但是此时effect实例的deps长度为0,user.flag的dep的wn都为0,user.flag触发get,开始track,执行至trackEffects时,effectTrackDepth<30newTracked(dep)的值为false,因为trackOpBit为0000 0010,而user.flag的dep的n为0000 0000 ,按位与为0,不大于0,此时user.flagdeptrackOpBit按位或,此时user.flag的dep的 n 变为

0000 0010,由于user.flagw为0,所以shouldTrakctrue,(user.name收集过程亦是如此)。

user.flag修改为false的时候,重新执行effect的run,initDepMakers执行时,由于deps不为0,user.flag的dep的wtrackOpBit按位或,user.flag的dep的w变为0000 0010,n为0000 0000 ,因为每次run执行后会恢复状态,执行至trackEffects时,newTracked仍然为false,user.flag的dep的ntrackOpBit按位或,变为0000 0010,但是wasTracked变为true(因为w为0000 0010),所以shouldTrack变为false,不会再收集当前effect,user.name不执行,执行至user.alias时,(注意initDepMakers执行在fn之前),会创建一个dep,nw都为0,收集流程和初始化时user.name保持一致。此时执行finalizeDepMakersuser.flagdepw为0000 0010,n为0000 0010。 user.aliasdepw为0000 0000 ,n为0000 0010。 更新时这里重点分析user.name,更新时,会执行initDepMakers,所以user.namew变为 0000 0010,但是由于user.flagfalseuser.name不会执行,则不会执行trackEffects,则它的n为0000 0000,满足删除条件wasTracked(dep) && !newTracked(dep),则当前effectdeps中的dep删除当前effect(自己删除自己)也就是user.name的dep删除当前effect,这样user.name发生改变时,effect就不会重新执行。

依赖触发则需要编写其set函数

function createSetter(isReadonly, isShallow) {
  return (target, key, newValue, receiver) => {
    const oldValue = target[key];// 获取旧值
​
    const hadKey = Array.isArray(target) && isIntegerKey(key) ? Number(key) < target.length : target.hasOwnProperty(key);// 判断是添加值还是设置值
    const res = Reflect.set(target, key, newValue, receiver);
    if (target === toRaw(receiver)) {
      if (!hadKey) {
        console.log('新增属性');
        trigger(target, key, newValue, oldValue, 'add');
      } else if (hasChanged(newValue, oldValue)) { // 前后的值没有发生改变则不需要触发依赖
        console.log('设置属性');
        trigger(target, key, newValue, oldValue, 'set');
      }
    }
    return res;
  };
}

trigger实现

const trigger = (target, key, newValue, oldValue, type) => {
  // 获取对象的键对应的dep(dep是一个set)
  const depMap = proxyMap.get(target);
  if (!depMap) {
    return;
  }
  const deps = [];// 存储所有需要执行的effect,不一定全是 depMap.get(key),比如数组新增元素需要触发length的dep
  if (type === 'clear') {
​
  } else if (key === 'length' && Array.isArray(target)) {
    const newLength = Number(newValue); // 如果是修改了数组长度则获取数组最新长度
    // map遍历时 dep为map的value,key为map的key;
    depMap.forEach((dep, key) => {
      // 当数组的长度由原来的100变为10,则90个dep需要执行;
      // 这里有一个key 为迭代器(之后处理);
      if (key === 'length' || (typeof key !== 'symbol' && key >= newLength)) {
        deps.push(dep);
      }
    });
  } else {
    // 判断key 不为undefined(void 0 === undefinde 防止undefined被修改)
    if (key !== void 0) {
      deps.push(depMap.get(key));
    }
​
    switch (type) {
      case 'add':
        if (!Array.isArray(target)) { // 为对象添加一个属性的时候需要触发他的遍历dep
          deps.push(depMap.get(ITERATE_KEY));
        } else if (isIntegerKey(key)) {
          // 如果是数组添加一个值,则需要触发他length对应的dep;
          deps.push(depMap.get('length'));
        } break;
      case 'delete':
        if (!Array.isArray(target)) { // 为对象删除一个属性的时候需要触发他的遍历dep
          deps.push(depMap.get(ITERATE_KEY));
        }
        break;
      case 'set': // 在修改值的时候不需要触发遍历key的操纵,因为key不变
        break;
      default:
    }
  }
  const effects = [];
  for (const dep of deps) {
    if (dep) {
      effects.push(...dep);
    }
  }
  // 做一个拷贝;
  triggerEffects(createDep(effects));
};
​
const triggerEffects = (dep) => {
  dep.forEach((effect) => {
    if (effect.scheduler) {
      effect.scheduler();//如果调度器存在则执行调度器
    } else {
      effect.run();//负责触发副作用函数
    }
  });
};

删除和遍历时,实现比较简单,主要是在trigger时判断数据操作类型即可。

function deleteProperty(target, key) {
  const hadKey = target.hasOwnProperty(key);
  const oldValue = target[key];
  const result = Reflect.deleteProperty(target, key);
  if (hadKey && result) {
    trigger(target, key, undefined, oldValue, 'delete');
  }
  return result;
}
function ownKeys(target) {
  track(target, Array.isArray(target) ? 'length' : ITERATE_KEY, 'iterate');
  return Reflect.ownKeys(target);
}

至此vue3的数据响应式原理解析完毕,但是源码中还涉及到了集合的代理,这里先不介绍,这里的代码是在源码的基础上做了一个提取,方便理解核心原理,源码中还有许多处理细节的地方。第一次学习源码,希望多多指正

文档中代码地址 Bug-codergb