普通视图

发现新文章,点击刷新页面。
昨天 — 2025年10月21日首页

第1篇(Ref):搞定 Vue3 Reactivity 响应式源码

作者 CptW
2025年10月21日 17:29

一步步,用 Proxy + 发布订阅 模式,手写 vue3/core/reactivity

实现 ref

源码中的 Ref 是一个工厂函数:

export function ref(value) {
    return RefImpl(value)
}

具体的实现在RefImpl里,它有几个特点:

  • value 是 普通值(Primitive) 和 对象(Object) 时,实现有所不同,因为对象有许多属性,有时要单独订阅对某些属性,value 是对象时交给 reactivity 处理
  • 在访问(get)时“收集依赖”,在赋值(set)时“触发依赖”
  • 通过 RefImpl.value 访问值
class RefImpl {
    privite _value
    
    constructor() {
        // 暂时忽略 Object 情况
        this._value = value
    }
    
    // trackRef 和 triggerRef 的实现放在后面
    get value() {
        trackRef(this)
        return _value
    }
    set value(newValue) {
        this._value = newValue
        triggerRef(this)
    }
}

这里要引入一些概念:

  • refDep,即被订阅者依赖
  • effectSub,即订阅者观察者

代码中把这两者抽象成了接口(interface),即 ref 实现了 Depeffect 实现了 Sub; 而在一次重构中,又使用双向链表实现了他俩,所以接口长这样:

interface Sub {
    // 头
    subs: Link
    // 尾
    subsTail: Link
}

interface Dep {
    // 头
    deps: Link
    // 尾
    depsTail: Link
}

// 链表中 `链` 的抽象
// 直接连接 Dep 和 Sub
export interface Link {
  sub: Sub
  nextSub: Link | undefined
  prevSub: Link | undefined
  dep: Dep
  nextDep: Link | undefined
}

然后,先实现一下 effect,再实现 trackReftriggerRef; 在创建 effect 时,要先执行一遍函数,收集依赖,然后返回一个可以手动执行函数的 handler runner

function effect(fn) {
    const e = new ReactiveEffect(fn)
    e.run()
    
    const runner = () => e.run()
    runner.effect = e
    
    return runner
}

这里同样是工厂函数,好处是分层解耦,代码很灵活; 接下来实现 ReactiveEffect,有个问题:到底 Dep 要被哪个 Sub 收集?

答案是 当前运行的 Sub,我们用一个变量记录:

export let activeSub;

export function setActiveSub(sub) {
    activeSub = sub
}


class ReactiveEffect implement Sub {
    deps: Link
    depsTail: Link
    // effect 有停止机制,即停止收集依赖
    active = true
    // 标记,防止递归依赖
    tracking = false

    constructor(public fn) {}
    
    run() {
        // 停止后,不再收集依赖
        if (!this.active) return this.fn()
        
        // effect 递归触发,要用闭包保存上一个
        let prevSub = activeSub
        activeSub = this
        
        startTrack(this)
        try {
            return this.fn()
        } finally {
            endTrack(this)
            activeSub = prevSub
        }
    }
}

这里的 stratTrackendTrack 是为了在收集依赖时,复用实例&垃圾收集(删除不再依赖的项目),先埋个坑; 下面实现 trackReftriggerRef:

// 这里的 dep 就是 ref
export function trackRef(dep) {
    // 连接 Sub 和 Dep
    if (activeSub) {
        link(dep, activeSub)
    }
}

// 用来存储已经断开连接的 Link 实例,可以复用,避免重复创建 Link
let LinkPool: Link | undefined

export function link(dep, sub) {
    // 当前 sub 的最后一个 Dep
    const currentDep = sub.depsTail
    // 下一个 Dep
    const nextDep = currentDep === undefined ? sub.deps : currentDep.nextDep
    // 尝试复用链表
    if (nextDep && nextDep === dep) {
        sub.depsTail = nextDep
        return
    }
    
    // 不能复用,创建新的 Link
    // 插入 sub 的 dep链表 中在当前 deps 链表中插入一个节点
    let newLink: Link
    if (LinkPool) {
        newLink = LinkPool
        LinkPool = LinkPool.nextDep
        newLink.nextDep = nextDep
        newLink.dep = dep
        newLink.sub = sub
    } else {
        newLink = {
            sub,
            nextSub: undefined,
            prevSub: undefined,
            dep,
            nextDep,
        }
    }
    
    // 插入 dep 的 sub链表 中
    // 连接到 subsTail 后面
    if (dep.subsTail) {
        dep.subsTail.nextSub = newLink
        newLink.prevSub = dep.subsTail
        dep.subsTail = newLink
    } else {
        // 还没有 sub,这是第一个
        dep.subs = newLink
        dep.subsTail = newLink
    }
    
    // 建立 deps 单向链表关系
    if (sub.depsTail) {
        sub.depsTail.nextDep = newLink
        sub.depsTail = newLink
    } else {
        sub.deps = newLink
        sub.depsTail = newLink
    }
}

上面有几个点注意:

  1. dep 是单链表,sub 链表是双链表,因为:dep 清理时只需要从某一点开始删除到末尾,而 sub 需要删除特定的节点;
  2. 贪心策略:尝试直接使用 sub.depsTail.nextDep
  3. 实例收集策略:使用 LinkPool 收集可以重用的 Link

然后是 triggerRef

export function triggerRef(dep) {
    // 仅当有订阅者时执行
    if (dep.subs) {
        // 委托
        propagate(dep.subs)
    }
}

// 传播更新,收集将要触发的 effect,
export propagate(subs: Link) {
    let link = subs
    const queuedEffect = []
    while (link) {
        const sub = link.sub
        // 避免递归触发
        if (!sub.tracking) {
            queuedEffect.push(sub)
        }
        link = link.nextSub
    }
    
    // 触发所有 sub
    queuedEffect.forEach(effect => effect.notify())
}

最后再填坑 startTrackendTrack

export function startTrack(sub: sub) {
    // 标记正在收集依赖
    sub.tracking = true
    sub.depsTail = undefined
}

开始追踪:

  1. tracking 标记当前 sub 正在收集依赖,避免循环依赖问题;
  2. sub.depsTail 设置为空,为后面清理依赖做准备
export endTrack(sub: Sub) {
    sub.tracking = false
    const depsTail = sub.depsTail
    
    // 重新收集依赖后,depsTail 会指向新的最后一个依赖
    if (depsTail && depsTail.next) {
        // 部份清理,新的最后一个依赖后的,都是不再依赖的
        clearTracking(depsTail.nextDep)
        depsTail.nextDep = undefined
    } else if (sub.deps) {
        // depsTail 依旧是空,表示没有任何新的依赖
        clearTracking(sub.deps)
        sub.deps = undefined
    }
}

// 清理包括 link 在内的节点
export function clearTracking(link: Link) {
    while(link) {
        const { prevSub, nextSub, dep, nextDep } = link
        
        // 把 prevSub 接到 link.nextSub
        if (prevSub) {
            prevSub.nextSub = link.nextSub
            link.nextSub = undefined
        } else {
            dep.subsTail = prevSub
        }
        
        // 把 nextSub.prevSub 接到 link.prevSub
        if (nextSub) {
            nextSub.prevSub = link.prevSub
            link.prevSub = undefined
        } else {
            dep.subsTail = prevSub
        }
        
        // 清空 dep/sub
        link.dep = link.sub = undefined
        // 存入复用池
        link.nextDep = LinkPool
        LinkPool = link
        
        link = nextDep
    }
}

几个需要注意的点:

  1. 增量更新:只清理不再需要的,保留仍需要的
  2. 内存优化:复用 Link 实例
  3. 双向链表:支持 O(1) 时间复杂度
  4. 精准清理:通过 depsTail 来精准定位需要清理的范围
昨天以前首页

手撕 Promise 一文搞定

作者 CptW
2025年10月18日 16:54

手撕 Promise,做 2 件事就够了:

  1. 了解 Promise 的行为,理解清晰后就能写出 then 方法
  2. 实现 then 方法,其他方法大多是它的衍生

其他方法,放到最后,很容易能用 then 衍生出来

Promise 行为概览

  1. 通过 new 调用,说明它是一个 class
  2. 构造时传入一个函数 executor
    • 同步执行 executor
    • 它有两个参数 resolvereject
    • 这两个参数负责:改变 Promise 状态、传递数据(分别叫 valuereason)
  3. 有 3 种状态,pendingfulfilledrejected
    • 初始状态是 pending
    • 只能 pending -> fulfilledpending -> rejected
    • 状态一旦改变后,无法继续调用 fulfilledrejected
  4. then 方法
    • 状态改变后,then 方法异步接收 resolve/reject 的回调信息
    • then 内的回调函数实际是 微任务
    • 可以链式(多次)调用
    • 在状态敲定后调用,则立即执行
    • 立刻返回一个新 Promise(pending) 对象(下文称 p):
      • onFulfilledonRejected 将被异步执行,即使状态已被敲定
      • p 的行为取决于上一条的执行结果
        • 返回一个值:以该值作为兑现值
        • 无返回:以 undefined 作为兑现值(value)
        • 抛出错误:以错误作为拒绝值(reason)
        • 返回已兑现的Promise:以该 Promise 作为兑现值
        • 返回已拒绝的 Promise:以该 Promise 作为拒绝值
        • 返回待定(pending)的 Promise:保持待定,并在该 Promise 状态改变后以其值作为兑现/拒绝值

实现 then 方法

const PENDING = 'pending'
const FULFILLED = 'fulfilled'
const REJECTED = 'rejected'

class Kromise {
  constructor(executor) {
    this.status = PENDING
    // value 和 reason 储存到静态属性里,方便 then 访问
    this.value = undefined
    this.reason = undefined
    // 支持链式调用,以数组保存回调
    this.onFulfilledFns = []
    this.onRejectedFns = []

    // value 传递值
    const resolve = (value) => {
      // 仅当 pending 时有效
      if (this.status === PENDING) {
        // 当兑现值是 Promise 时,等待他兑现/拒绝
        if (value instanceof Kromise) {
          value.then(resolve, reject)
          return
        }
        // Keep simple, 省略 thenable 部分

        /**
         * 执行回调,微任务:
         * 1、确保符合规范
         * 2、确保顺序执行 executor 时,不会立即执行 onFulfilled 导致 undefined 错误
         */
        queueMicrotask(() => {
          // 异步锁,防止异步流程中多次改变状态
          if (this.status !== PENDING) return

          // 将改变状态代码放到微任务中,为了确保不会过早敲定状态,导致 then 总执行敲定状态后的代码
          // 敲定状态
          this.status = FULFILLED
          // 储存value
          this.value = value
          this.onFulfilledFns.forEach(fn => fn(this.value))
        })
      }
    }

    const reject = (reason) => {
      if (this.status === REJECTED) {
        queueMicrotask(() => {
          if (this.status !== PENDING) return

          this.status = REJECTED
          this.reason = reason
          this.onRejectedFns.forEach(fn => fn(this.reason))
        })
      }
    }

    // 同步执行 executor,并传递参数
    executor(resolve, reject)
  }

  then(onFulfilled, onRejected) {
    // 返回 Promise,以支持链式调用
    return new Kromise((resolve, reject) => {
      // 如果在状态敲定后再执行 then,则立即执行回调
      if (this.status === FULFILLED && onFulfilled) {
        // 将值传递给返回的 p
        const value = onFulfilled(this.value)
        resolve(value)
      }
      if (this.status === REJECTED && onRejected) {
        const reason = onRejected(this.reason)
        reject(reason)
      }

      // 暂存,等状态改变(即 resolve/reject 执行)时才真正调用
      // try...catch 处理抛出错误的情况
      this.onFulfilledFns.push(() => executeFunctionWithErrorCatch(onFulfilled, this.value, resolve, reject))
      this.onRejectedFns.push(() => executeFunctionWithErrorCatch(onRejected, this.reason, resolve, reject))
    })
  }
}

function executeFunctionWithErrorCatch(fn, value, resolve, reject) {
  try {
    // 将自己的值,传递给返回的 Promise p
    const result = execFn(value)
    resolve(result)
  } catch(err) {
    reject(err)
  }
}

其他方法

catch

实际上是 then(undefined, onRejected) 的简写,但这里有一个边界情况要处理:

如何在then未处理onRejected的情况下,将该错误作为返回值传递给接下来链式调用的catch进行处理?

因为 then 仅注册 onFulfilled 回调时,返回的 p 无法将错误传递下去; 解决方法很简单,只需要提供一个默认的 onRejected 实现,保证错误传递即可

  catch(onRejected) {
    return this.then(undefined, onRejected)
  }
  // 修改 then 的实现
  then(onFulfilled, onRejected) {
    // 返回 Promise,以支持链式调用
    return new Kromise((resolve, reject) => {
      // 如果在状态敲定后再执行 then,则立即执行回调
      if (this.status === FULFILLED && onFulfilled) {
        // 将值传递给返回的 p
        const value = onFulfilled(this.value)
        resolve(value)
      }
      if (this.status === REJECTED && onRejected) {
        const reason = onRejected(this.reason)
        reject(reason)
      }

      const defaultOnFulfilled = (value) => value
      const defaultOnRejected = (reason) => { throw reason }
      onFulfilled = onFulfilled || defaultOnFulfilled
      onRejected = onRejected || defaultOnRejected

      // 暂存,等状态改变(即 resolve/reject 执行)时才真正调用
      // try...catch 处理抛出错误的情况
      if (onFulfilled) this.onFulfilledFns.push(() => executeFunctionWithErrorCatch(onFulfilled, this.value, resolve, reject))
      if (onRejected) this.onRejectedFns.push(() => executeFunctionWithErrorCatch(onRejected, this.reason, resolve, reject))
    })
  }

finally

MDN: Promise实例的 finally()  方法用于注册一个在 promise 敲定(兑现或拒绝)时调用的函数。它会立即返回一个等效的Promise对象,这可以允许我们链式调用其他 promise 方法

finally(onFinally) {
  this.then(onFinally, onFinally)
}
❌
❌