普通视图

发现新文章,点击刷新页面。
昨天 — 2026年4月4日首页

Vue3.5设计理念和响应式原理(上)

作者 乘方
2026年4月4日 12:37

VUE 设计理念

  1. 声明式框架

    • 描述组件该长什么样子,不用关心具体怎么实现。
  2. 采用虚拟 DOM

    • 使用虚拟 DOM 作为声明式渲染到真实 DOM 的中间层
    • 直接操作真实 DOM 非常昂贵(性能开销大),而虚拟 DOM 是在 JS 层面进行计算和比较,再将批量更新应用回 DOM。它让 Vue 能以声明式的方式实现高效的 UI 更新,同时为跨平台(如 Weex、NativeScript)提供了可能。
  3. 编译时和运行时

    • 编译时: 工程化中使用 @vue/compiler-sfc 调用 @vue/compiler-dom 模块,将 SFC 中的模板编译为渲染函数。

    • 运行时:(@vue/runtime-core)负责创建组件实例、执行渲染函数、生成虚拟 DOM、对比并更新真实 DOM。

    在 Vue 3 中,运行时和编译时是解耦的:你可以直接手写渲染函数(不经过模板编译),也可以使用 JSX(通过插件编译)。但官方推荐的模板 + 编译时优化,能让运行时更轻量、更快速。

响应式实现方式的改变

Object.defineProperty:

  • 用于精确控制对象属性行为的方法。它可以定义一个新属性,或者修改一个已有属性,并允许设置该属性的描述符(如可枚举、可配置、可写等),其中最关键的存取描述符(get / set)正是实现对象劫持的基础
  • 直接修改原来对象,给对象的属性都添加 getter/setter 方法,进行读写时的劫持;
    • vue2 将一个普通 data 对象传入 Vue 实例时,Vue 会递归遍历该对象的所有属性(包括嵌套对象)。
    • 对每个属性调用 Object.defineProperty,替换其原有的属性描述符,加上自定义的 get 和 set。
    • 动态添加的属性不会自动劫持(需用 Vue.set)。
    • 对象属性删除(delete)无法被检测(需用 Vue.delete)。
  • 对数组操作
    1. 可以捕获到
      • 通过索引访问/赋值(如果预先为索引定义了 getter/setter) 数组索引本质上就是对象的属性名("0"、"1"等)。你可以用 Object.defineProperty 为某个索引添加存取描述符:
      • 劫持已有索引的赋值行为(包括通过原生方法隐式赋值) 如果某个索引已经定义了 setter,那么任何改变该索引值的操作(包括 arr[0]=x、arr.splice(0,1,10) 等)都会触发 setter。因为 splice 内部最终也是通过属性赋值修改索引。
    2. 不能捕获到
      • 数组的变异方法(push, pop, shift, unshift 等)

      • 修改 length 属性 数组的 length 属性默认是 不可配置(configurable: false) 且 不可枚举,因此无法通过 Object.defineProperty 重新定义它的 getter/setter

      • 动态新增的索引

      • 删除属性(delete arr[0])

    vue2 考虑到性能问题(数组可能很大,一个一个劫持索引有很大消耗),就不做监听,但是对数组中对象的属性会对它内部属性进行监听。 vue2 中重写了 push/pop 等 7 个数组方法,手动触发响应式。

Proxy

  • Proxy 是 ES6 引入的一个新特性,可以拦截并重新定义对象的基本操作(如属性读写、增删、读写原型、函数调用、描述符相关等)
  • 原生 Proxy 对数组没有特殊分支:数组只是 target;读写下标和 length、以及方法触发的多次内部赋值,都会按你实现的 trap 规则执行;
    • 一次方法调用 ≠ 一次 trap,push/splice 等会在引擎内部触发多次 set
    • Vue 3 在数组上的补丁,核心是 7 个变异方法(统一触发与避免误 track)+ includes / indexOf / lastIndexOf(补全依赖与修正比较),都通过 Proxy get 分发到 instrumentations,而不是污染全局 Array。
  • 在 vue3 中
    • 保持代理的引用:在整个应用中,应始终使用由 reactive 或 ref 返回的代理对象进行数据操作,而不是操作原始对象,否则响应性会丢失。
    • 解构会丢失响应性:直接解构 reactive 对象会使其失去响应式能力。可以使用 toRefs 或 toRef API 将其转换为 ref 来保持响应性。

get 中为什么不要使用 target[key]receiver[key] 要用 Reflect

const obj = {
  a: 1,
  get b() {
    return this.a;
  },
};
  • 如果用 target[key] 取 b,this 指向原对象 obj,内部访问 this.a 会绕过代理,可能导致依赖收集不完整。
  • receiver 通常就是 Proxy 实例本身。当你读取 receiver[key] 时,会再次触发当前 Proxy 的 get 陷阱,导致无限递归,最终栈溢出。
  • Reflect.get(..., receiver),内部实现区分了“读取属性”和“调用 getter”这两个步骤,this 绑定到代理对象 receiver,this.a 会再次走代理 get,依赖才能正确追踪。

响应式实现原理

reactive:定义响应式对象

  • 将数据变为响应式的,数据修改后检测到数据发生改变,从而让页面重新渲染
  • 每一个由 reactive 包裹的对象,都返回一个 proxy 对象,对 get/set 进行拦截。
export function reactive(target) {
  return createReactiveObject(target);
}

function createReactiveObject(target) {
  // 检测target是否为对象
  if (!isObject(target)) {
    return target;
  }

  // 放置代理过的对象重复代理
  if (target[ReactiveFlags.IS_REACTIVE]) {
    return target;
  }
  // 优化:同一个对象只能代理一次
  const existProxy = reactiveMap.get(target);
  if (existProxy) {
    return existProxy;
  }

  let proxy = new Proxy(target, mutableHandlers);
  reactiveMap.set(target, proxy);
  return proxy;
}

// in mutableHandlers
export const mutableHandlers: ProxyHandler<any> = {
  /**
   *
   * @param target 代理目标对象
   * @param key 获取的哪个属性
   * @param recevier 返回的代理对象
   * @returns
   */
  get(target, key, recevier) {
    if (key === ReactiveFlags.IS_REACTIVE) {
      return true; // 响应式 get 的结果
    }
    // Reflect 让this指向Proxy对象(recevier),避免重复触发get,导致死循环。
    let res = Reflect.get(target, key, recevier);

    // *当取得的值也是对象的时候,对这个对象进行递归代理
    if (isObject(res)) {
      return reactive(res);
    }
    return res;
  },

  set(target, key, value, recevier) {
    let result = Reflect.set(target, key, value, recevier);
    return result;
  }
}

effect:副作用函数

  • 数据变化后 可以让 effect 重新执行,组件,watch、computed、都是基于 effect 来实现的
  • 在 Vue3 中,每个组件的模板编译成的渲染函数,会被一个内部的 effect(称为“渲染 effect”或“组件更新 effect”)自动包裹。
  • 属于底层API,编写 Vue 插件或构建自定义响应式系统,作为框架底层使用。普通业务开发基本用不到。
  • effect 会将里面的响应式数据进行关联
// state 为响应式数据
// effect1
effect(() => {
  app.innerHTML = `姓名${state.name} 年龄${state.age}`;
});
// effect2
effect(() => {
  main.innerHTML = `姓名${state.age}`;
});

state.age++;

步骤:

  1. 执行 effect 函数,会生成一个 effect 实例,运行 effect.run()。
  2. run(): 会将 effect 实例放入到全局,并调用 fn(effect 中的回调)执行。
  3. 执行到 state.name 触发 name 的 get。完成依赖收集器 dep(name)依赖(effect1) 的收集。
  4. 同理,后面 dep(age)会对 effect1 和 effect2 进行挨个收集。
// 依赖收集的数据结构(三 Map 结构)
targetMap (WeakMap) : {
  // 原始对象
  '{name: '', age: ''}' : {
    // 依赖收集器 dep(name)
    'name':{
      effect1: effect1._trackId
    },
    // 依赖收集器 dep(age)
    'age': {
      effect1: effect1._trackId,
      effect2: effect2._trackId
    }
  }
}

effect1._trackId:指的是当前 effect 的执行次数,相同 effect 中 trackId 的值相同

  1. 并将 dep 添加到 effect 上的 deps 数组,实现 响应式 和 依赖 的双向收集(循环引用)
effect.deps[effect._depsLength++] = dep;
  1. 在 执行到 state.age++ 后,触发代理对象 age 的 set,并执行 trigger,将 age 的依赖(effect1、effect2)取出依次执行。
// 触发更新
export function triggerEffects(dep) {
  // 将映射表中的effect拿出来依次执行
  for (const effect of dep.keys()) {
    if (effect.scheduler && effect._runner === 0) {
      effect.scheduler(); // -> _effect.run() -> 重新执行 fn
    }
  }
}

effect._runner: 是防止 effet 中触发响应式set的标识,为 0 表示没有 effect 在执行中。可以进行触发依赖的执行更新。

其他问题

1. 条件渲染

// state 为响应式数据 flag = true
effect(() => {
  app.innerHTML = state.flag ? state.name : state.age;
});

state.flag = false;
  1. effect 执行前的前置清理
function preCleanEffect(effect) {
  effect._depsLength = 0; // 身上的依赖收集器数组的长度置空
  effect._trackId++; // 每次执行前 trackId 都加1,如果同一个 effect 执行,trackId 就是相同的
}
  1. 在第一次执行挨个添加依赖收集器 dep(flag,name),并将其保存到 effect.deps
  2. flag 发生改变,触发 set 重新执行 effect。
  3. 先添加收集器 flag,与之前保存的deps中的第一个dep进行比对,发现相同,则复用。
  4. 再添加收集器 age,与之前第二个 dep 进行对比,发现不同,删除老dep(name)中的此次依赖(effect),删除后若发现 dep(name)为空,则删除dep(name)。并将新的dep(age),放到depsLength = 2 的位置。

    [flag, name] ===> [flag, age]

export function trackEffect(effect, dep) {
  // 相同 trackId 则跳过收集
  if (dep.get(effect) !== effect._trackId) {
    // 收集到相同的依赖,只更新 trackId 的次数
    dep.set(effect, effect._trackId);

    let oldDep = effect.deps[effect._depsLength]; // 获取上次的旧 dep

    if (oldDep !== dep) {
      if (oldDep) {
        // 删除老的
        cleanDepEffect(oldDep, effect);
      }
      effect.deps[effect._depsLength++] = dep; // 永远按照本次**最新**的来存
    } else {
      effect._depsLength++;
    }
  }
}

function cleanDepEffect(dep, effect) {
  dep.delete(effect);
  if (dep.size === 0) {
    dep.cleanup(); // 如果map为空,则删除这个属性
  }
}
  1. 执行完 effect 后的清理,以维护的 _depsLength 为准,清理掉多余的 dep。
function postCleanEffect(effect) {
  if (effect.deps.length > effect._depsLength) {
    for (let i = effect._depsLength; i < effect.deps.length; i++) {
      cleanDepEffect(effect.deps[i], effect); // 删除映射表中对应的effect
    }
    effect.deps.length = effect._depsLength; // 更新依赖列表的长度
  }
}

2. 嵌套 effect 的依赖收集的实现

// 实例:effect1
effect(() => {
  effect(() => {}); // effect2
});
// -------------------

// 全局上保存当前执行的 effect
let activeEffect;

// run方法
run() {
  let lastEffect = activeEffect; // *
  try {
    this._runner ++;
    activeEffect = this;
    preCleanEffect(this);
    return this.fn();
  } finally {
    postCleanEffect(this);
    activeEffect = lastEffect;
    this._runner --;
  }
}
  1. 老的版本,使用来实现,执行 effect1 进栈,执行 effect2 进栈,收集完毕挨个出栈,栈顶则是当前的 activeEffect。
  2. 新版本,用 lastEffect 记录上一次的 effect 实例,结束后再重新复制给当前 activeEffect。

3. effect 的调度执行

  • effect 可以传入 scheduler 选项,控制响应式数据变化时是 立即执行 fn 还是走 自定义调度(如 watch 的 flush)
// 做法
const runner = effect(
  () => {
    app.innerHTML = `姓名${state.name} 年龄${state.age}`;
  },
  {
    scheduler: () => {
      console.log("触发了更新,暂时不做处理"); // 切片编程思想,首先覆盖掉默认的 scheduler 执行,加上自己逻辑
      runner(); // 拿到暴露出来的runner后,某个时刻触发更新
    },
  },
);

// in effect
export function effect (fn, options?) {
  // 创建一个effect 实例,只要依赖的属性发生变化就要执行回调scheduler,就是 run() 方法
  const _effect = new ReactiveEffect(fn, () => {
    // 默认 scheduler 调度器,run 方法中执行 fn()
    _effect.run();
  });

  _effect.run();

  if (options) {
    Object.assign(_effect, options); // 将用户定义的scheduler覆盖掉内置的
  }

  const runner = _effect.run.bind(_effect);
  runner.effect = _effect;
  return runner; // 外面可以拿到调度执行 effect 的方法。
}
昨天以前首页

Promise/A+ 解析

作者 乘方
2026年3月24日 20:24

关于 Promise 要掌握的点

Promise 中有三种状态

  • 分别是 pending fulfilled rejected
  • 状态一旦从 pending 变为 fulfilled/rejected ,就会“凝固”,不能再改变。
  • executor() resolve() reject() 默认都是同步执行的。
const executor = (resolve, reject) => {
  resolve("fulfilled"); // 将状态变为 '成功'
  reject("rejected"); // 不会再执行了
};
new Promise(executor);

executor 中执行 reject() 和 throw new Error()

  • executor 的同步阶段,throw new Error() 会抛出一个异常,Promise 内部会用 try...catch 捕获这个异常,然后自动调用 reject(error)
  • 所以在同步代码里,reject(reason)throw new Error() 都会让 Promise 进入 rejected
  • catchthen(_, onRejected) 捕获后,如果返回普通值,后续链会转为 fulfilled
Promise.reject("初始错误")
  .catch((err) => {
    console.log("捕获到错误:", err);
    return "恢复后的值"; // 返回普通值,后续链会进入 fulfilled 状态
  })
  .then((value) => {
    console.log("后续 then 收到:", value); // 输出: 后续 then 收到: 恢复后的值
    return "继续传递";
  })
  .catch((err) => {
    console.log("这个 catch 不会执行,因为错误已被处理");
  });
  • 但在异步回调里直接 throw new Error(),不属于 Promise 构造时的同步执行上下文,通常不会被这个 Promise 的 .catch 捕获。
// 异步 throw 无法被捕获
new Promise((resolve, reject) => {
  setTimeout(() => {
    throw new Error("异步错误"); // 全局未捕获异常
  }, 0);
}).catch((e) => console.log(e)); // 不会执行

// 正确写法是把异步错误显式交给 reject:
new Promise((resolve, reject) => {
  setTimeout(() => {
    try {
      // ...可能报错的逻辑
      throw new Error("异步错误");
    } catch (e) {
      reject(e);
    }
  }, 0);
}).catch((e) => console.log("捕获到:", e.message));

then(onFulfilled, onRejected) 回调的执行时机

  • p.then(...) 执行也是同步的。
  • Promise 实例 p 的状态决定是否执行或者执行哪个 reaction(onFulfilled / onRejected)
    1. p 的状态确定后,进入 then 方法,根据状态将对应 reaction 回调。通过 queueMicrotask 加入到微任务队列,结束 then,返回一个新的 Promise 实例。

      queueMicrotask 是一个全局函数,用于将一个回调函数添加到微任务队列(microtask queue)中。是 HTML 标准和 Node.js 都支持的 API,在 ECMAScript 2020 中被正式纳入规范。

    2. p 的状态还未定:比如在 executor 中用定时器包裹了resolve() / reject(),因为 then 是同步的,此时进入then,p 的状态是 pending 还未凝固。Promise 将 reactionasyncRun处理,暂存到 Promise 类的全局的回调数组。待真正触发resolve() / reject() 后,将队列的方法依次取出,并执行 asyncRunreaction 放入微任务队列。此时回调还未执行,只是放到了微任务队列里面,等待同步任务执行完毕,才会依次执行

      asyncRun 的作用就是将回调函数交给 queueMicrotask 处理的“包装器”。 因此 reaction 回调的执行是异步的。

// 加入微任务队列
function asyncRun(fn) {
  if (typeof queueMicrotask === "function") {
    queueMicrotask(fn);
    return;
  }
  setTimeout(fn, 0);
}
// In Promise.then()
/**
 * 执行 fulfilled 分支
 * - 用微任务/异步任务包装,保证 then 回调异步执行
 * - 回调结果 x 交给 resolvePromise 统一处理
 */
const runFulfilled = () => {
  asyncRun(() => {
    try {
      const x = realOnFulfilled(this.value); // x: 第一个 then 中的 onFulfilled 返回值,它决定 promise2 的状态
      resolvePromise(promise2, x, resolve, reject);
    } catch (error) {
      reject(error);
    }
  });
};
// 判断是哪种情况
if (this.state === FULFILLED) {
  runFulfilled();
} else if (this.state === REJECTED) {
  runRejected();
} else {
  // pending:先存回调,等 resolve/reject 时再统一触发
  this.onFulfilledCallbacks.push(runFulfilled);
  this.onRejectedCallbacks.push(runRejected);
}

Promise.then() 的链式调用,都做了些什么

示例:

const p2 = p1.then(fn1, fn2);
const p3 = p2.then(fn3, fn4);

问题:由上面可知,这种链式关系,在执行同步代码就已经形成。但是在链式中 fn 的执行受什么影响?

  • 一个 Promise 实例的状态由 resolve() / reject() 来决定。

  • 实例 p1 的状态,决定执行 fn1 / fn2, 并且返回一个新的实例 p2;

  • 同理 p2 的状态,影响着后续 fn3 / fn4 的执行。因此 then 中核心则是如何决定 p2 状态

  • 新返回 promise2 实例的状态受回调函数 fn 返回值影响。(通过控制其 resolve() / reject() 来实现)

    • 如果 fn 的返回值 x 不是 thenable 类型(实现了 then 接口,可以被 then 调用),那么 p2 fulfilled(x)给 fn3。
    • 如果 x 是对象或者函数,那么 x 就有可能是 thenable 类型
    • thenable 类型的 x 的状态是自己实现的,由自己控制,且 p2 的状态就会和 x 的状态进行挂钩。
    • 如果外层 Promise 直接 resolve(内层Promise)。规范要求外层应“跟随”内层最终结果,而不是把“Promise 对象本身”当普通值传下去,通过递归调用 resolvePromise,可以持续“剥开”嵌套的 thenable,直到获得一个普通值,或者遇到拒绝状态时立即终止。这就是“展平(flatten)”语义。
    // 示例
    const p = new Promise((resolve, reject) => {
      const value = Promise.reject("rejected");
      resolve(value);
    });
    p.then(
      (val) => {
        console.log("成功", val); // 不执行
      },
      (reason) => {
        console.log("失败", reason); // `失败 rejected` 可以看出,p 的状态被后面 value 的状态的接管
      },
    );
    
  • 双重保险

    • resolve 中优先处理“同构 Promise”(如 value instanceof Promise)是优化。
    • resolvePromise 中通过 then.call 处理通用 thenable 是规范核心。
    • 两者结合,既有性能优化,也保证兼容外部实现/用户自定义 thenable
// resolve 实现
const resolve = (value) => {
  // 同化 Promise 实例:当前 promise 跟随 value 的最终结果
  if (value instanceof Promise) {
    value.then(resolve, reject);
    return;
  }
  // ...
};

// resolvePromise 实现
function resolvePromise(promise2, x, resolve, reject) {
  // 1. 防止 p.then(() => p2) 这类自解析死循环
  if (promise2 === x) {
    reject(new TypeError("Chaining cycle detected for promise"));
    return;
  }

  // 2. 只有对象/函数才可能是 thenable
  if (x !== null && (typeof x === "object" || typeof x === "function")) {
    // 防止 thenable 同时/重复调用 resolve 和 reject
    let called = false;

    try {
      // 取 then 时也可能抛错(例如 getter 抛异常)
      const then = x.then;

      if (typeof then === "function") {
        // 按 thenable 协议调用:then.call(x, resolveFn, rejectFn)
        then.call(
          x,
          (y) => {
            if (called) return;
            called = true;
            // 递归解析 y,直到拿到普通值或最终拒绝
            resolvePromise(promise2, y, resolve, reject);
          },
          (r) => {
            if (called) return;
            called = true;
            reject(r);
          },
        );
        return; // thenable 分支处理完毕
      }
    } catch (error) {
      // 取 then 或调用 then 过程抛错,且未决过,转 reject
      if (called) return;
      called = true;
      reject(error);
      return;
    }
  }

  // 3. 普通值:直接 fulfilled
  resolve(x);
}

Promise 的其他 API

resolve

  • 如果本来就是 Promise,直接返回
  • 否则包装成 fulfilled 的 Promise
static resolve(value) {
  if (value instanceof Promise) return value;
  return new Promise((resolve) => resolve(value));
}

reject

  • Promise 上的静态方法
  • 直接返回 rejected 的 Promise
static reject(reason) {
  return new Promise((_, reject) => reject(reason));
}

catch

  • 语法糖,等价于 then(null, onRejected)
catch(onRejected) {
  return this.then(null, onRejected);
}

finally

  • 不改变前一个 Promise 的值/错(除非 finally 自己抛错/返回 rejected)
  • 无论成功失败都会执行 onFinally
finally(onFinally) {
  const handler =
    typeof onFinally === "function" ? onFinally : () => undefined;
  return this.then(
    // 成功:先执行 finally,再把原 value 传下去
    (value) => Promise.resolve(handler()).then(() => value),
    // 失败:先执行 finally,再把原 reason 继续抛出
    (reason) =>
      Promise.resolve(handler()).then(() => {
        throw reason;
      }),
  );
}

all

  • 全部 fulfilledfulfilled(按输入顺序收集结果)
  • 任意一个 rejected 立即 rejected
static all(iterable) {
  return new Promise((resolve, reject) => {
    const items = Array.from(iterable);

    // 空数组直接 fulfilled []
    if (items.length === 0) {
      resolve([]);
      return;
    }

    const result = new Array(items.length);
    let count = 0;

    items.forEach((item, index) => {
      // 统一 Promise 化,兼容普通值
      Promise.resolve(item).then(
        (value) => {
          result[index] = value;
          count += 1;
          if (count === items.length) resolve(result);
        },
        (reason) => reject(reason),
      );
    });
  });
}

race

  • 谁先落态(fulfilled/rejected)就采用谁的结果
static race(iterable) {
  return new Promise((resolve, reject) => {
    Array.from(iterable).forEach((item) => {
      Promise.resolve(item).then(resolve, reject);
    });
  });
}
❌
❌