Vue设计与实现阅读笔记

Sep 28 · 100min

Vue3 设计思路

声明式UI

描述声明式UI的两种方式:

  • DOM元素,属性,事件,元素层级结构(模版)

    <h1 @click="handler"><span></span></h1>
  • JS对象

    const vnode = {
      tag: 'div',
      props: {
        onClick: () => alert('hello')
      },
      children: 'click me'
    }

个人理解,声明式UI本质就是描述页面。

JS对象相比模版来说,更加方便灵活,Vue内部就采用JS对象来描述声明式UI,对应的就是Vue里VNode(虚拟dom)。

h函数:

创建虚拟dom的工具函数

import { h } from 'vue'
export default {
	render() {
		return h('h1', { onClick: handler }) // 虚拟 DOM
 	}
}

个人理解:每次都自己敲JS对象,会非常麻烦,如果还有子节点,工作量很大。因此使用h函数来辅助创建JS对象(虚拟DOM)。通过传递参数,就能快速创建虚拟dom,减少心智负担。

渲染器

把虚拟DOM转换为真实DOM

image-20250828152636662

function renderer(vnode, container) {
  // 使用 vnode.tag 作为标签名称创建 DOM 元素
	const el = document.createElement(vnode.tag)
  // 遍历 vnode.props 将属性、事件添加到 DOM 元素
  for (const key in vnode.props) {
    if (/^on/.test(key)) {
      // 如果 key 以 on 开头,那么说明它是事件
      el.addEventListener(
        key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
        vnode.props[key] // 事件处理函数
      )
    }
  }

  // 处理 children
  if (typeof vnode.children === 'string') {
    // 如果 children 是字符串,说明是元素的文本子节点
    el.appendChild(document.createTextNode(vnode.children))
  } else if (Array.isArray(vnode.children)) {
    // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
    vnode.children.forEach(child => renderer(child, el))
  }

  // 将元素添加到挂载点下
  container.appendChild(el)
}

const vnode = {
  tag: 'div',
  props: {
    onClick: () => alert('hello')
  },
  children: 'click me'
}

renderer(vnode, document.body)

组件的本质

组件就是一组 DOM 元素的封装

  • 函数描述

    const myComponent = () => {
        return {
          tag: 'div',
          props: {
            onClick: () => alert('hello')
          },
          children: 'click me'
        }
      }
    
      const vnode = {
        tag: myComponent
      }
  • 对象描述

    const myComponent = {
        render() {
          return {
            tag: 'div',
            props: {
              onClick: () => alert('hello')
            },
            children: 'click me'
          }
        }
      }
    
      const vnode = {
        tag: myComponent
      }

个人理解:本质还是JS对象(虚拟DOM),封装成函数或者对象便于复用

编译器

模板编译为渲染函数

模版

<div @click="handler">
 	click me
</div>

渲染函数

h('div', { onClick: handler }, 'click me')

对于vue来说,template标签包裹的就是模版内容

流程图:

image-20250828162158306

响应系统的作用和实现

响应式数据和副作用函数

  • 副作用函数

    副作用函数effect指的是会产生副作用的函数(废话)

    function effect() {
        document.body.innerText = 'hello vue3'
    }

​ 个人理解:该函数执行之后,会影响某个变量的值

  • 响应式数据

    副作用函数依赖了变量的值,变量的值的改变自动引起副作用函数的执行

    const obj = { text: 'hello world' }
    function effect() {
    	// effect 函数的执行会读取 obj.text
    	document.body.innerText = obj.text
    }

响应式系统

最简单版本

Proxy

Set

使用Proxy来劫持数据的get和set,Set来储存副作用函数

get读的时候收集副作用函数

set改的时候执行副作用函数

// 存储副作用函数的桶
const bucket = new Set()

// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 effect 添加到存储副作用函数的桶中
    bucket.add(effect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    console.log(target, key, newVal)
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    bucket.forEach(fn => fn())
  }
})

function effect() {
  document.body.innerText = obj.text
}
effect()
setTimeout(() => {
  // obj是代理对象Proxy
  obj.text = 'hello vue3'
}, 1000)

第一次优化

问题:当前副作用函数不够灵活,函数名称限定为effect,如果要考虑复用性,也不方便。

新增一个匿名函数activeEffect来储存被注册的副作用函数

// 存储副作用函数的桶
const bucket = new Set()

// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    bucket.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    bucket.forEach(fn => fn())
  }
})


// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text
})

setTimeout(() => {
  obj.text2 = 'hello vue3'
}, 1000)

第二次优化

问题:如果给当前的对象obj新增一个不存在的属性,会触发set函数的执行,正常应该是哪个属性变了就执行那个属性的副作用函数,减少不必要的开销,提升性能。

target-key-effect的关系图:

image-20250829171032670

targert:代理对象

key:对象的key

effect:key的副作用函数

WeakMap

Map

使用WeakMapMap来构建如下关系:

1.WeakMapkey是对象Target

2.Mapkey是对象Targetkey

image-20250829172040735

使用WeakMap来储存target可以防止内存溢出。WeakMapkey是弱引用,不影响垃圾回收(简单理解:内存里不使用的对象,JS会自动销毁此对象),因此如果target不被使用了,就会被垃圾回收掉。如果使用Map,那么会引起不必要的内存开销,如果这个target不被使用了。

代码实现:

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)

    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
  }
})


// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text
})

setTimeout(() => {
  obj.text = 'hello vue3'
}, 1000)

抽离函数:

把收集副作用的函数提取为track和把执行副作用的函数提取为trigger

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text
})

setTimeout(() => {
  trigger(data, 'text')
}, 1000)

第三次优化

如果存在下面这种依赖关系,oktrue,就会显示text字段的内容。

const data = { ok: true, text: 'hello world' }
const obj = new Proxy(data, { /* ... */ })

effect(function effectFn() {
	document.body.innerText = obj.ok ? obj.text : 'not'
})

依赖关系关系图:

image-20250908223300183

textok的副作用函数effect都会在get的时候被依赖收集track

现在存在一个问题,如果把ok改为false,此时派发更新trigger,更新innerText的值为not。但是后续如果修改text的值也会触发trigger,但是此时的innerText一直是not,并不依赖text,因此存在遗留的副作用函数。

graph TD
    subgraph "A. 首次执行 (当 obj.ok 为 true)"
        A1["首次执行 effectFn"] --> A2["(跳过) <b>没有 cleanup 阶段</b>"];
        A2 --> A3["执行用户函数 fn<br>obj.ok ? obj.text : 'not ok'"];
        A3 --> A4["读取 obj.ok (true) 和 obj.text"];
        A4 --> A5["通过 track() 建立依赖关系"];
        A5 --> A6["<b>当前依赖: {obj.ok, obj.text}</b><br>此时依赖是正确的"];
    end

    A6 --> B1["...一段时间后...<br>用户代码执行 <b>obj.ok = false</b>"];
    B1 --> B2["trigger() 触发 effectFn 重新执行"];

    subgraph "B. 第二次执行 (当 obj.ok 为 false)"
        B2 --> C1["effectFn 开始重新执行"];
        C1 --> C2["(跳过) <b>没有 cleanup 阶段</b>"];
        C2 --> C3["执行用户函数 fn<br>obj.ok ? obj.text : 'not ok'"];
        C3 --> C4["只读取 obj.ok (false)"];
        C4 --> C5["通过 track() 重新建立对 obj.ok 的依赖"];
        C5 --> C6["<b>执行结束<br>但对 obj.text 的旧依赖从未被清除</b>"];
    end

    C6 --> D1["<b style='color:red;'>错误状态: 当前依赖仍为 {obj.ok, obj.text}</b><br>但逻辑上应只依赖 obj.ok"];

    subgraph "C. 错误的后果"
        D1 --> E1["...又过了一段时间...<br>用户执行了逻辑上无关的操作:<br><b>obj.text = 'new message'</b>"];
        E1 --> E2["调用 trigger(obj, 'text')"];
        E2 --> E3["trigger 在 obj.text 的依赖集合中<br>找到了那个从未被清除的'僵尸依赖' (effectFn)"];
        E3 --> E4["<b>不必要地重新执行了 effectFn !</b>"];
    end

    %% 样式
    style A2 fill:red,stroke:#333,stroke-width:2px
    style C2 fill:red,stroke:#333,stroke-width:2px
    style D1 fill:#f99,stroke:#b00,stroke-width:4px
    style E4 fill:#f99,stroke:#b00,stroke-width:4px

新增一个cleanup函数来清除遗留的副作用函数

在副作用函数执行之前,先清除之前的遗留副作用函数

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { ok: true, text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

effect(() => {
  console.log('effect run')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

cleanup执行流程图:

graph TD
    A["开始"] --> State0;

    subgraph "第 0 阶段: 初始状态"
        State0("<b>数据初始状态</b><br>bucket: { }<br>effectFn.deps: [ ]")
    end

    State0 --> B;

    subgraph "第 1 阶段: 首次执行 effect (当 obj.ok=true)"
        B["执行 effect(fn)<br>fn = obj.ok ? obj.text : '...'"];
        B --> C["fn 执行, 读取 obj.ok 和 obj.text<br>两次调用 track() 来建立依赖"];
        C --> State1("<b>数据状态 (首次执行后)</b><br>bucket: { obj: {'ok':Set{e}, 'text':Set{e}} }<br>effectFn.deps: [ ref_to_ok_Set, ref_to_text_Set ]");
    end

    State1 --> D;

    subgraph "第 2 阶段: 数据变更"
        D["用户操作: <b>obj.ok = false</b>"];
        D --> E["trigger() 触发 effectFn 重新执行"];
    end

    E --> F;

    subgraph "第 3 & 4 阶段: cleanup 与第二次执行"
        F["<b>cleanup(effectFn) 运行</b><br>遍历 effectFn.deps, 断开旧连接"];
        F --> State2("<b>数据状态 (清理后, 执行前)</b><br>bucket: { obj: {'ok':Set{}, 'text':Set{}} }<br>effectFn.deps: [ ]");
        State2 --> G["<b>effectFn 的 fn 再次运行</b>"];
        G --> H["fn 执行, 只读取 obj.ok<br>只调用一次 track()"];
    end
    
    H --> State3;

    subgraph "第 5 阶段: 最终状态"
        State3("<b>最终数据状态 (第二次执行后)</b><br>bucket: { obj: {'ok':Set{e}, 'text':Set{}} }<br>effectFn.deps: [ ref_to_ok_Set ]");
    end

    State3 --> I["结束"];

依赖收集之后会存在如下关系

image-20250908233301746

effectFn上新增deps储存依赖集合,在依赖收集阶段和activeEffect构建依赖集合。cleanup函数的参数为副作用函数effectFn,在执行副作用函数之前,会清除activeEffect中的deps里的副作用函数依赖集合。effectFn.deps.length = 0清空当前的依赖集合数组。

整个执行流程:

graph TD
    subgraph "A. 外部触发"
        A1["数据变更 (例如 obj.ok = false)"] --> A2["调用 trigger()"]
        A2 --> A3["trigger 找到依赖的 effectFn<br>并准备执行它"]
    end

    A3 --> B["effectFn 开始重新执行"]

    subgraph "B. 清理阶段 (cleanup)"
        direction TB
        B --> C["<b>第一步: 执行 cleanup(effectFn)</b>"];
        C --> D{"effectFn.deps 中是否有旧的依赖?"};
        D -->|"是 (对于后续执行)"| E["遍历 effectFn.deps 数组"];
        E --> F["对于每一个依赖集合 (deps)<br>执行 deps.delete(effectFn)"];
        F --> G["将 effectFn 从所有旧依赖中移除<br><b>(断开旧连接)</b>"];
        G --> H["清空 effectFn.deps 数组"];
        H --> I["清理完成<br>effectFn 处于'干净'状态"];
        D -->|"否 (对于首次执行)"| I;
    end

    subgraph "C. 执行与重建依赖阶段"
        direction TB
        I --> J["<b>第二步: 执行用户传入的函数 fn</b><br>例如: obj.ok ? obj.text : 'not ok'"];
        J --> K["读取 obj.ok, 触发 track()"];
        K --> K_sub["(建立对 obj.ok 的新依赖)"];

        K_sub --> L{"obj.ok 的值是 true 吗?"};
        L -->|是| M["读取 obj.text, 触发 track()"];
        M --> M_sub["(建立对 obj.text 的新依赖)"];
        M_sub --> N["执行结束"];

        L -->|否| O["不读取 obj.text, 直接使用 'not ok'"];
        O --> N;
    end

    N --> P["<b>effectFn 执行完毕<br>依赖关系已精确重建</b>"]

第四次优化

现在还存在一个问题,现在trigger执行副作用函数的时候会导致死循环。

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { ok: true, text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}

effect(() => {
  console.log('effect run')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

trigger遍历effects执行effectFn的时候(此时会读取dataok字段值),执行fn会触发tracktrack里又会执行deps.add(activeEffect),导致effects && effects.forEach(effectFn => effectFn())出现死循环,effects里一直会有副作用函数(当前keyeffects)。

问题关键:

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(effectFn => effectFn()) // 死循环
}

流程如下:

image-20250910231640573

解决方法:

new一个新的set集合,保证内存地址指向不是同一个。

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => effectsToRun.add(effectFn))
  effectsToRun.forEach(effectFn => effectFn())
  // effects && effects.forEach(effectFn => effectFn())
}

image-20250910232840271

第五次优化

effectFn嵌套导致依赖收集的副作用函数出现问题

何时出现effectFn嵌套?当某个组件内部渲染了另外一个组件

image-20250914162423763

此时相当于:

image-20250914162459074

用如下代码举例:

// 原始数据
const data = { foo: true, bar: true }
// 对原始数据的代理
const obj = new Proxy(data, {
// 拦截读取操作
get(target, key) {
  // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
  track(target, key)
  // 返回属性值
  return target[key]
},
// 拦截设置操作
set(target, key, newVal) {
  // 设置属性值
  target[key] = newVal
  // 把副作用函数从桶里取出并执行
  trigger(target, key)
}
}) 
// 省略....
effect(() => {
	console.log('effect run1')
	effect(() => {
		 console.log('effect run2')
		 let temp = obj.bar
	})	
	let temp = obj.foo
})

setTimeout(() => {
	obj.foo = false
}, 1000)

此时effect内部发生了嵌套,此时会导致一个问题,对objfoo进行track的时候,fooeffectFneffectFn2,导致如果修改foo的值为true,执行effectFn2

image-20250914163304948

流程图如下:

image-20250914164958219

优化代码:

使用栈来解决,每次调用副作用函数effectFn之前,把函数放入栈effectStack中,执行完毕之后再弹出栈,并把栈顶指向activeEffect,用于还原之前的值。

流程如下图:

image-20250914165902303

代码:

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []
function effect(fn) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

第六次优化

避免无限递归循环

如果副作用函数如下:

effect(() => {
	obj.foo++  // obj.foo = obj.foo + 1
})

执行流程图如下:

image-20250914220836859

trigger的时候,执行副作用函数,此时effectsToRun.forEach(effectFn => effectFn())还没有执行完毕,执行副作用函数的时候又进行tracktrigger导致无限循环。

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => effectsToRun.add(effectFn))
  effectsToRun.forEach(effectFn => effectFn())
}

解决方法:

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    // 如果当前执行的副作用函数和trigger的副作用函数一样,则不执行
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn) 
    }
  })
  effectsToRun.forEach(effectFn => effectFn())
}

第七次优化

调度执行优化,连续多次修改响应式数据,只触发一次更新。

副作用函数如下:

effect(() => {
  console.log(obj.foo)
})

obj.foo++
obj.foo++

foo++连续两次,我们只关心foo的结果,不关心过程,但这个时候副作用函数会执行两次,输出结果为:

1
2
3

我们只关心最终结果,期望输出:

1
3

解决方法:

新增配置项options,通过配置scheduler来控制调度执行,使用Set(去重,如果有相同的副作用函数)来储存jobQueue任务队列,把副作用函数放入到微任务队列,当trigger的时候执行scheduler,执行jobQueue中的函数

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { foo: 1 }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}




// =========================

const jobQueue = new Set()
const p = Promise.resolve()

let isFlushing = false
function flushJob() {
  if (isFlushing) return
  isFlushing = true
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    isFlushing = false
  })
}


effect(() => {
  console.log(obj.foo)
}, {
  scheduler(fn) {
    jobQueue.add(fn)
    flushJob()
  }
})

obj.foo++
obj.foo++

流程图如下:

image-20250917170439464

computed

effectFn懒执行

options新增配置项lazylazytrue就立即执行effectFn),effect里返回effectFn(用于手动执行副作用函数effectFn),effectFn里返回fn函数执行后的返回值。

现在实现一个简单的computedgetter返回一个函数,函数返回值依赖其他的值,当读取对象obj的值的时候触发get,执行effectFn

  // 存储副作用函数的桶
  const bucket = new WeakMap()

  // 原始数据
  const data = { foo: 1, bar: 2 }
  // 对原始数据的代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
    }
  })

  function track(target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })
  }

  // 用一个全局变量存储当前激活的 effect 函数
  let activeEffect
  // effect 栈
  const effectStack = []

  function effect(fn, options = {}) {
    const effectFn = () => {
      cleanup(effectFn)
      // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
      activeEffect = effectFn
      // 在调用副作用函数之前将当前副作用函数压栈
      effectStack.push(effectFn)
      const res = fn()
      // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]

      return res
    }
    // 将 options 挂在到 effectFn 上
    effectFn.options = options
    // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
    effectFn.deps = []
    // 执行副作用函数
    if (!options.lazy) {
      effectFn()
    }

    return effectFn
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }
  // =========================
  function computed(getter) {
    const effectFn = effect(getter, {
      lazy: true
    })
    const obj = {
      get value() {
        return effectFn()
      }
    }
    return obj
  }
  const sumRes = computed(() => obj.foo + obj.bar)
  console.log(sumRes.value)
  console.log(sumRes.value)

computed懒执行

现在存在一个问题,当我连续读取sumRes.value,会连续多次执行effectFn,但如果这个时候依赖项obj.fooobj.bar没有变化,但也多次执行effectFn,并没有缓存效果。这个时候可以新增一个变量dirty来标志是否需要重新计算值,如果dirtytrue,执行effectFn。如果dirtyfalse则不执行,并重置dirtytrue。如果依赖项obj.fooobj.bar发生了变化,dirty重置为true,这个时候需要去执行effectFn,获取最新的值,可以通过在options里配置scheduler来重置dirtytrue

  // 存储副作用函数的桶
  const bucket = new WeakMap()

  // 原始数据
  const data = { foo: 1, bar: 2 }
  // 对原始数据的代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
      track(target, key)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      trigger(target, key)
    }
  })

  function track(target, key) {
    if (!activeEffect) return
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    activeEffect.deps.push(deps)
  }

  function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
        effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })
  }

  // 用一个全局变量存储当前激活的 effect 函数
  let activeEffect
  // effect 栈
  const effectStack = []

  function effect(fn, options = {}) {
    const effectFn = () => {
      cleanup(effectFn)
      // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
      activeEffect = effectFn
      // 在调用副作用函数之前将当前副作用函数压栈
      effectStack.push(effectFn)
      const res = fn()
      // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
      effectStack.pop()
      activeEffect = effectStack[effectStack.length - 1]

      return res
    }
    // 将 options 挂在到 effectFn 上
    effectFn.options = options
    // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
    effectFn.deps = []
    // 执行副作用函数
    if (!options.lazy) {
      effectFn()
    }

    return effectFn
  }

  function cleanup(effectFn) {
    for (let i = 0; i < effectFn.deps.length; i++) {
      const deps = effectFn.deps[i]
      deps.delete(effectFn)
    }
    effectFn.deps.length = 0
  }
  // =========================
  function computed(getter) {
    let dirty = true
    let value
    const effectFn = effect(getter, {
      lazy: true,
      scheduler(){
          dirty = true
      }
    })
    const obj = {
      get value() {
        if(dirty) {
            value = effectFn()
            dirty = false
        }
        return value
      }
    }
    return obj
  }
  const sumRes = computed(() => obj.foo + obj.bar)
  console.log(sumRes.value)
  console.log(sumRes.value)

  obj.foo++
  console.log(sumRes.value)

流程如下:

image-20250917232301442

track依赖收集的时候,activeEffect此时已被赋值为effectFn,副作用函数是effectFn,函数内部的fngetteroptions的配置是计算属性的参数配置,如下:

{
      lazy: true,
      scheduler(){
          dirty = true
      }
 }

当执行trigger,会执行scheduler来重置dirty

function trigger(target, key) {
    const depsMap = bucket.get(target)
    if (!depsMap) return
    const effects = depsMap.get(key)

    const effectsToRun = new Set()
    effects && effects.forEach(effectFn => {
      if (effectFn !== activeEffect) {
        effectsToRun.add(effectFn)
      }
    })
    effectsToRun.forEach(effectFn => {
      if (effectFn.options.scheduler) {
        // 执行
        effectFn.options.scheduler(effectFn)
      } else {
        effectFn()
      }
    })
  }

computed中的effect嵌套

如果出现effect中使用computed,由于内部obj.fooobj.bareffect的收集不会受外部影响,因为每次都会对当前effectStack进行出栈和入栈,来保证activeEffect指向之前的值。会导致obj.foo++的时候,trigger不会触发() => {console.log(sumRes.value)}的执行。

const sumRes = computed(() => {
	return obj.foo + obj.bar
})
effect(() => {
	console.log(sumRes.value)
})
obj.foo++
3  // 第一次打印 1 + 2

执行流程图如下:

image-20250918231325397

解决方法:

computedobj进行get的时候手动触发track,但依赖项的发生改变,触发trigger的时候再手动执行objtrigger

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}




// =========================

function computed(getter) {
  let value
  let dirty = true

  const effectFn = effect(getter, {
    lazy: true,
    scheduler() {
      if (!dirty) {
        dirty = true
         // 手动对obj进行trigger
        trigger(obj, 'value')
      }
    }
  })
  
  const obj = {
    get value() {
      if (dirty) {
        value = effectFn()
        dirty = false
      }
      // 手动对obj进行track,此时的副作用函数为 () => {console.log(sumRes.value)}
      track(obj, 'value')
      return value
    }
  }

  return obj
}

const sumRes = computed(() => obj.foo + obj.bar)

console.log(sumRes.value)
console.log(sumRes.value)

obj.foo++

console.log(sumRes.value)

effect(() => {
  console.log(sumRes.value)
})

obj.foo++

这个时候会打印两次:

3  // 第一次打印 1 + 2
4  // 第二次打印 2 + 2

watch

wacth文档

watch相比computed,函数的参数不一样,watch的参数可能为对象也可能为一个函数getter,如果为对象,需要遍历读取这个对象,把对象上的所有key都track,因此通过traverse函数来遍历读取对象的所有属性。

traverse的实现:

递归来遍历对象,对象里面可能会嵌套对象。

function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }
  return value
}

wacth可以立即执行,配置options.immediate = true,相当于立即执行job(),此时的oldValue = undefinednewValue = effectFn(),其中effectFn()为该函数首次运行的值。配置options.immediate = false,不会立即执行(并且{ lazy: true }),但是为了触发依赖收集track,必须执行oldValue = effectFn

配置options.immediate = false的执行流程:

graph TD
    A["watch(source, cb) 启动"] --> B{"options.immediate 是否为 true?"};
    B -->|"否 (进入 else 块)"| C["执行 oldValue = effectFn()"];
    
    subgraph C ["执行 oldValue = effectFn()"]
        direction LR
        C1["设置 activeEffect"] --> C2["执行 getter, 读取初始值, 如 obj.foo=1"];
        C2 --> C3["触发 Proxy.get, 调用 track()"];
        C3 --> C4["<b>依赖收集完成</b><br>将 effectFn 与 obj.foo 关联"];
        C4 --> C5["getter 返回初始值 1"];
    end

    C --> D["oldValue 被赋值为 1"];
    D --> E["监听设置完成, 等待变更...<br>此时回调 cb() 未执行"];
    
    E --> F["...一段时间后...<br>用户代码执行 obj.foo++"];
    
    F --> G["触发 Proxy.set, 调用 trigger()"];
    G --> H["trigger 找到依赖, 执行 scheduler"];
    H --> I["scheduler 调用 job()"];
    
    subgraph I ["job() 执行"]
        direction LR
        I1["newValue = effectFn()<br>获取新值 2"] --> I2["执行回调 cb(newValue, oldValue)<br>即 <b>cb(2, 1)</b>"];
        I2 --> I3["更新 oldValue = newValue<br>oldValue 现在是 2"];
    end
    
    I --> J["执行完毕"];

代码实现:

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  const job = () => {
    newValue = effectFn()
    cb(oldValue, newValue)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

watchflush配置,如果options.flush === 'post',把job放入到微任务队列。来确保执行之前DOM的更新已经完成,获取到最新的DOM状态

流程图如下:

graph TD
    A["数据变更触发 scheduler"] --> B{"options.flush === 'post'?"};

    B -->|是| C["创建已解决的 Promise (p = Promise.resolve())"];
    C --> D["将 job() 添加到微任务队列 (p.then(job))"];
    D --> E["当前宏任务继续执行<br>(等待微任务队列清空)"];
    E --> F["当所有同步代码执行完毕,且 DOM 更新完成(若有)"];
    F --> G["执行 job()"];

    B -->|"否 (默认或 'pre'/'sync')"| H["同步执行 job()"];
    H --> I["job() 执行完毕"];
    I --> J["当前宏任务继续执行"];

    G --> K["job() 异步执行完毕"];
    K --> L["当前事件循环迭代结束"];

代码实现:

const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )

过期的副作用:

类似于请求竞态问题, 当快速切换tab页的时候(A->B),如果tabA页的数据的接口响应时间比较长,但tabB页的数据的接口响应时间比较短就会出现竞态问题,导致tab页展示的数据出现问题。

新增一个onInvalidate清理过期副作用,使用cleanup来储存onInvalidate过期回调,job执行之前都会执行cleanup,并在cb执行之后给valid构成闭包,来确保每一次执行cleanup是清理上一次的valid

代码实现:

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  let cleanup
  function onInvalidate(fn) {
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn()
    // 清理上一次的副作用
    if (cleanup) {
      cleanup()
    }
    cb(oldValue, newValue, onInvalidate)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}


watch(() => obj.foo, async (newVal, oldVal, onInvalidate) => {
  let valid = true
  onInvalidate(() => {
    valid = false
  })
  const res = await fetch()

  if (!valid) return

  finallyData = res
  console.log(finallyData)
})

流程图:

%%{init: {'theme': 'dark'}}%%
graph TD
    subgraph "watch 函数初始化"
        A["watch(source, cb, options) 被调用"] --> B{初始化 cleanup = undefined};
        B --> C["创建 effectFn, 设置 scheduler"];
    end

    subgraph "数据变化 -> job 任务调度"
        D["数据变化, scheduler 触发 job"] --> E["job 函数开始"];
        E --> F["计算 newValue"];
        F --> G{"检查 cleanup 是否存在?"};
        G -- "存在 (是)" --> H["执行 cleanup() <br/> (调用上一次的闭包, 令其失效)"];
        H --> I["执行当前 watch 回调 cb()"];
        G -- "不存在 (否)" --> I;
    end

    subgraph "回调函数 cb() 内部"
        I --> J["定义 let valid = true"];
        J --> K["调用 onInvalidate(fn), <br> 将 fn (闭包) 赋值给 cleanup"];
        K --> L["执行异步任务 (如 fetch)"];
    end

    subgraph "异步竞态处理示例"
        direction LR
        subgraph "第一次触发 (请求 A, 耗时 1000ms)"
            P1["cb 执行, cleanup = 清理函数A"];
            P2["请求 A 开始"];
        end
        
        subgraph "第二次触发 (请求 B, 耗时 100ms)"
            S1["job 执行, 调用 cleanup() -> 清理函数A执行"];
            S2["请求 A 的 valid 标志位被设为 false"];
            S3["cb 执行, cleanup = 清理函数B"];
            S4["请求 B 开始"];
            S5["请求 B 完成, valid 为 true, 结果被采纳 ✅"];
        end

        P2 --> S1;
        S5 --> P3["请求 A 完成, valid 为 false, 结果被丢弃 ❌"];
    end

    C --> D;
    L --> P1;

    style H fill:#5e3d5e, stroke:#c599c5
    style S2 fill:#5e3d5e, stroke:#c599c5
    style S5 fill:#2c5c2c, stroke:#8fbc8f
    style P3 fill:#702c2c, stroke:#f08080

最终代码实现:

// 存储副作用函数的桶
const bucket = new WeakMap()

// 原始数据
const data = { foo: 1, bar: 2 }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})

function track(target, key) {
  if (!activeEffect) return
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
  activeEffect.deps.push(deps)
}

function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)

  const effectsToRun = new Set()
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    if (effectFn.options.scheduler) {
      effectFn.options.scheduler(effectFn)
    } else {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn())
}

// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
// effect 栈
const effectStack = []

function effect(fn, options = {}) {
  const effectFn = () => {
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
    activeEffect = effectFn
    // 在调用副作用函数之前将当前副作用函数压栈
    effectStack.push(effectFn)
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并还原 activeEffect 为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]

    return res
  }
  // 将 options 挂在到 effectFn 上
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  if (!options.lazy) {
    effectFn()
  }

  return effectFn
}

function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    const deps = effectFn.deps[i]
    deps.delete(effectFn)
  }
  effectFn.deps.length = 0
}




// =========================

function traverse(value, seen = new Set()) {
  if (typeof value !== 'object' || value === null || seen.has(value)) return
  seen.add(value)
  for (const k in value) {
    traverse(value[k], seen)
  }

  return value
}

function watch(source, cb, options = {}) {
  let getter
  if (typeof source === 'function') {
    getter = source
  } else {
    getter = () => traverse(source)
  }

  let oldValue, newValue

  let cleanup
  function onInvalidate(fn) {
    cleanup = fn
  }

  const job = () => {
    newValue = effectFn()
    if (cleanup) {
      cleanup()
    }
    cb(oldValue, newValue, onInvalidate)
    oldValue = newValue
  }

  const effectFn = effect(
    // 执行 getter
    () => getter(),
    {
      lazy: true,
      scheduler: () => {
        if (options.flush === 'post') {
          const p = Promise.resolve()
          p.then(job)
        } else {
          job()
        }
      }
    }
  )
  
  if (options.immediate) {
    job()
  } else {
    oldValue = effectFn()
  }
}

let count = 0
function fetch() {
  count++
  const res = count === 1 ? 'A' : 'B'
  return new Promise(resolve => {
    setTimeout(() => {
      resolve(res)
    }, count === 1 ? 1000 : 100);
  })
}

let finallyData

watch(() => obj.foo, async (newVal, oldVal, onInvalidate) => {
  let valid = true
  onInvalidate(() => {
    valid = false
  })
  const res = await fetch()

  if (!valid) return

  finallyData = res
  console.log(finallyData)
})

obj.foo++
setTimeout(() => {
  obj.foo++
}, 200);
CC BY-NC-SA 4.0 2024-PRESENT © hujiacheng