Vue3响应式原理详解

343 阅读2分钟

前言

版本:3.0.2

说明:通过源码对Vue3的响应式原理进行阐述。

一、Vue2.x和Vue3.x实现响应式的基础

Object.defineProperty

Vue2.x能够实现双向数据绑定,离不开Object.defineProperty的支持。

局限性:

  • 不能监测到对象属性的添加和删除,Vue2.x之所以能够监测到对象属性的添加和删除,是因为Vue2.x封装了Vue.setvm.$set进行对象属性的添加,使用Vue.deletevm.$delete进行对象的删除。
  • 无法监控数组下标的变化。这个局限性并不是Object.defineProperty的问题,而是Vue2.x放弃了这个特性。至于原因,因为性能问题。

Proxy代理器

Vue3.x使用Proxy来为对象添加一层拦截。

  • 使用:
const proxy = new Proxy(target, handler);

其中,target为目标对象;handler为配置对象,对于每一个被代理的操作,需要提供一个对应的处理函数,该函数将拦截对应的操作。

举个栗子

let person = {
  name: "张三",
  age: 20
}

let newPerson = new Proxy(person, {
  // 拦截取值
  get: function (target /* 目标对象 */, key /* 目标对象的属性名 */, receiver /* newPerson */) {
    return "取值拦截:" + target[key] + `; key => ${key}`;
  },
  
  // 拦截赋值
  set(target /* 目标对象 */, key /* 目标对象的属性名 */, value /* 目标对象的属性值 */, receiver /* newPerson */) {
  	target[key] = value;
    console.log("赋值拦截:" + target[key] + `; key => ${key} ` + ` value => ${value}`); // => 赋值拦截:30; key => age value => 30
  }
});

console.log(newPerson.name); // => 取值拦截:张三; key => name
newPerson.age = 30;
console.log(newPerson.age); // => 取值拦截:30; key => age

二、几个响应式API的使用

reactive

返回对象的响应式副本,深层的响应式代理,将会影响所有嵌套的属性。

html

<div id="app">
  <div>{{ object.person.age }}</div>
  <button @click="add">添加</button>
</div>

JavaScript

const { createApp, reactive } = Vue;
const app = createApp({
  setup() {
    const object = reactive({ 
      person: {
        age: 18
      }
    });
    
    return {
      object
    }
  },
  methods: {
    add() {
      // 修改object.person.age的值,页面实时更新
      this.object.person.age += 1;
    }
  }
}).mount("#app");

readonly

深层只读的响应式代理。只读表示:不可变更。

html

<div id="app">
  <div>{{ reactiveObj.count }}</div>
  <div>{{ readonlyObj.count }}</div>
  <button @click="addReactive">addReactive</button>
  <button @click="addReadonly">addReadonly</button>
</div>

JavaScript

const { createApp, reactive, readonly } = Vue;
const app = createApp({
  setup() {
    const reactiveObj = reactive({ count: 1 });
    const readonlyObj = readonly(reactiveObj);
    return {
      reactiveObj,
      readonlyObj
    }
  },
  methods: {
    addReactive() {
      // 修改reactiveObj.count的值,会影响readonlyObj.count的值
      this.reactiveObj.count += 1;
    },
    addReadonly() {
      // 修改失败,只读对象不可变更,并导致警告
      this.readonlyObj.count += 1;
    }
  }
}).mount("#app");

演示

readonly.gif

shallowReactive

浅层的响应式代理

html

<div id="app">
  <div>{{ shallowReactiveObj.count }}</div>
  <div>{{ shallowReactiveObj.nested.count }}</div>
  <button @click="addShallow">addShallow</button>
  <button @click="addNest">addNest</button>
</div>

JavaScript

const { createApp, shallowReactive } = Vue;
const app = createApp({
  setup() {
    const shallowReactiveObj = shallowReactive({
      count: 1,
      nested: {
        count: 1
      }
    });
    return {
      shallowReactiveObj
    }
  },
  methods: {
    addShallow() {
      // 修改最外层的属性,页面实时响应
      this.shallowReactiveObj.count += 1;
    },
    addNest() {
      // 修改嵌套属性,页面不能实时响应
      this.shallowReactiveObj.nested.count += 1;
      console.log(this.shallowReactiveObj.nested.count);
    }
  }
}).mount("#app");

演示

shallowReactive.gif

shallowReadonly

浅层只读的响应式代理

html

<div id="app">
  <div>{{ shallowReadonlyObj.count }}</div>
  <div>{{ shallowReadonlyObj.nested.count }}</div>
  <button @click="addShallow">addShallowReadonly</button>
  <button @click="addNest">addNestReadonly</button>
</div>

JavaScript

const { createApp, reactive, shallowReadonly } = Vue;
const app = createApp({
  setup() {
    const reactiveObj = reactive({
      count: 1,
      nested: {
        count: 1
      }
    });
    const shallowReadonlyObj = shallowReadonly(reactiveObj);
    return {
      shallowReadonlyObj
    }
  },
  methods: {
    addShallow() {
      // 修改最外层的属性,页面不能实时响应
      this.shallowReadonlyObj.count += 1;
    },
    addNest() {
      // 因为shallowReadonly代理的目标对象是reactiveObj(响应式对象)
      // 修改嵌套属性,页面实时响应
      this.shallowReadonlyObj.nested.count += 1;
    }
  }
}).mount("#app");

演示

shallowReadonly.gif

三、模拟源码

通过模拟源码来一步步实现Vue3的响应式原理。

1、添加拦截,在get时,追踪目标对象,添加响应处理函数;在set时,触发响应式函数。

创建响应式对象,参数为目标对象和配置对象,返回一个代理对象。

function createReactiveObject(target, baseHandlers) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}
  • creative深层响应式代理
// 创建get拦截,第一个参数表示是否为只读,第二个参数表示是否浅层
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);

    if (!isReadonly) {
      track(target, "get" /* GET */ , key);
    }

    // 如果为浅层监听,则直接返回res
    if (shallow) {
      return res;
    }

    // 深层属性添加响应
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  }
}

// 创建set拦截
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];

    // 设置新值
    const result = Reflect.set(target, key, value, receiver);

    // 如果新值和旧值不一样,则触发赋值触发器
    if (hasChanged(value, oldValue)) {
      trigger(target, "set", key, value, oldValue);
    }

    return result;
  }
}

// get拦截
const get = createGetter();
// set拦截
const set = createSetter();
const mutableHandlers = {
  get,
  set,
}
// 创建深层响应式对象
function reactive(target) {
  return createReactiveObject(target, mutableHandlers);
}
  • readonly只读响应式代理

// 只读的get拦截
const readonlyGet = createGetter(true);
const readonlyHandlers = {
  get: readonlyGet,
  set(target, key) {
    {
      // 目标对象是只读的,不能重新赋值
      console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
    }
    return true;
  },
  deleteProperty(target, key) {
    {
      // 目标对象是只读的,不能删除属性
      console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
    }
    return true;
  }
};
// 创建只读的响应式对象
function readonly(target) {
  return createReactiveObject(target, readonlyHandlers);
}

2、追踪目标对象和触发响应式函数

  • effect

// effect栈
const effectStack = [];
// 当前活跃的响应处理函数,在执行effect时添加
let activeEffect;

function createReactiveEffect(fn) {
  const effect = function reactiveEffect() {
    try {
      if (!effectStack.includes(effect)) {
        effectStack.push(effect);
        activeEffect = effect;
        return fn();
      }
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  }
  effect.deps = [];
  return effect;
}

function effect(fn, options) {
  const effect = createReactiveEffect(fn, options);
  effect();
}
  • track
const targetMap = new WeakMap();

function track(target, type, key) {
  if (activeEffect === undefined) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap) = new Map());
  }

  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}
  • trigger
function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }

  const effects = new Set();
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect) {
          effects.add(effect);
        }
      })
    }
  };

  if (key !== void 0) {
    add(depsMap.get(key));
  }

  const run = (effect) => {
    effect();
  };
  effects.forEach(run);
}

四、附:完整代码

html

<div id="app"></div>
<button id="addReactive">addReactive</button>
<button id="addReadonly">addReadonly</button>

JavaScript

function createAppAPI(render) {
  return function createApp(rootComponent) {
    const app = {
      mount(rootContainer) {
        render(rootComponent, rootContainer);
      }
    }

    return app;
  }
}

function ensureRenderer() {

  // 安装effect
  const setupRenderEffect = (rootComponent, rootContainer) => {
    effect(function componentEffect() {
      mountComponent(rootComponent, rootContainer);
    });
  }

  const mountComponent = (rootComponent, rootContainer) => {
    if (!rootComponent.isMounted) {
      // 处理options
      const { setup, methods, template } = rootComponent;
      if (setup) {
        const result = setup();
        rootComponent.data = {};
        Object.keys(result).forEach(key => {
          // 触发get取值
          rootComponent.data[key] = result[key];
        });
      }

      const btnReactive = document.getElementById("addReactive");
      const btnReadonly = document.getElementById("addReadonly");
      btnReactive.addEventListener("click", () => {
        methods.addReactive.call(rootComponent.data);
      });
      btnReadonly.addEventListener("click", () => {
        methods.addReadonly.call(rootComponent.data);
      });
      rootComponent.isMounted = true;
    }

    // 每次响应,重新渲染页面
    rootContainer.innerHTML = rootComponent.data.reactiveObj.person.age;        
  }

  const render = (rootComponent, rootContainer) => {
    setupRenderEffect(rootComponent, rootContainer);
  }
  return {
    render,
    createApp: createAppAPI(render)
  }
}

function createApp(options) {
  const app = ensureRenderer().createApp(options);

  const { mount } = app;

  // 重新定义mount
  app.mount = (containerOrSelector) => {
    const container = normalizeContainer(containerOrSelector);

    if (!container) {
      return;
    }

    mount(container);
  }

  return app;
}

// 格式化容器
function normalizeContainer(container) {

  return document.querySelector(container);
}


const targetMap = new WeakMap();
// effect栈
const effectStack = [];
let activeEffect;

function createReactiveEffect(fn) {
  const effect = function reactiveEffect() {
    try {
      if (!effectStack.includes(effect)) {
        effectStack.push(effect);
        activeEffect = effect;
        return fn();
      }
    } finally {
      effectStack.pop();
      activeEffect = effectStack[effectStack.length - 1];
    }
  }
  effect.deps = [];
  return effect;
}

function effect(fn, options) {
  const effect = createReactiveEffect(fn, options);
  effect();
}

function track(target, type, key) {
  if (activeEffect === undefined) {
    return;
  }
  let depsMap = targetMap.get(target);
  if (!depsMap) {
    targetMap.set(target, (depsMap) = new Map());
  }

  let dep = depsMap.get(key);
  if (!dep) {
    depsMap.set(key, (dep = new Set()));
  }

  if (!dep.has(activeEffect)) {
    dep.add(activeEffect);
    activeEffect.deps.push(dep);
  }
}

function trigger(target, type, key, newValue, oldValue) {
  const depsMap = targetMap.get(target);
  if (!depsMap) {
    return;
  }

  const effects = new Set();
  const add = (effectsToAdd) => {
    if (effectsToAdd) {
      effectsToAdd.forEach(effect => {
        if (effect !== activeEffect) {
          effects.add(effect);
        }
      })
    }
  };

  if (key !== void 0) {
    add(depsMap.get(key));
  }

  const run = (effect) => {
    effect();
  };
  effects.forEach(run);
}

const isObject = (val) => val !== null && typeof val === 'object';
// 创建get拦截,第一个参数表示是否为只读,第二个参数表示是否浅层
function createGetter(isReadonly = false, shallow = false) {
  return function get(target, key, receiver) {
    const res = Reflect.get(target, key, receiver);

    if (!isReadonly) {
      track(target, "get" /* GET */ , key);
    }

    // 如果为浅层监听,则直接返回res
    if (shallow) {
      return res;
    }

    // 深层属性添加响应
    if (isObject(res)) {
      return isReadonly ? readonly(res) : reactive(res);
    }

    return res;
  }
}
const hasChanged = (value, oldValue) => value !== oldValue && (value === value || oldValue === oldValue);

// 创建set拦截
function createSetter() {
  return function set(target, key, value, receiver) {
    const oldValue = target[key];

    // 设置新值
    const result = Reflect.set(target, key, value, receiver);

    // 如果新值和旧值不一样,则触发赋值触发器
    if (hasChanged(value, oldValue)) {
      trigger(target, "set", key, value, oldValue);
    }

    return result;
  }
}

// get拦截
const get = createGetter();
// set拦截
const set = createSetter();
// 只读的get拦截
const readonlyGet = createGetter(true);

const mutableHandlers = {
  get,
  set,
}
const readonlyHandlers = {
  get: readonlyGet,
  set(target, key) {
    {
      // 目标对象是只读的,不能重新赋值
      console.warn(`Set operation on key "${String(key)}" failed: target is readonly.`, target);
    }
    return true;
  },
  deleteProperty(target, key) {
    {
      // 目标对象是只读的,不能删除属性
      console.warn(`Delete operation on key "${String(key)}" failed: target is readonly.`, target);
    }
    return true;
  }
};
// 创建深层响应式对象
function reactive(target) {
  return createReactiveObject(target, mutableHandlers);
}
// 创建只读的响应式对象
function readonly(target) {
  return createReactiveObject(target, readonlyHandlers);
}

function createReactiveObject(target, baseHandlers) {
  const proxy = new Proxy(target, baseHandlers);
  return proxy;
}

const app = createApp({
  setup() {
    const reactiveObj = reactive({
      person: {
        age: 18
      }
    });
    const readonlyObj = readonly({
      name: "张三"
    });

    return {
      reactiveObj,
      readonlyObj
    }
  },
  methods: {
    addReactive() {
      // 页面实时更新
      this.reactiveObj.person.age += 1;
    },
    addReadonly() {
      // 修改失败,只读对象不可变更,并导致警告
      this.readonlyObj.name = "李四";
    }
  }
}).mount("#app");

看完3件事

1、如果文章对你有帮助,可以给博主点个赞。

2、如果你觉得文章还不错,可以动动你的小手,收藏一下。

3、如果想看更多的源码详解,可以添加关注博主。

附录:

1、Vue3.x完整版源码解析:github.com/fanqiewa/vu…

2、其它源码解析:www.fanqiewa.xyz/