Skip to content

vue.js 设计与实现

根据HCY vue.js 设计与实现做的笔记。

命令式和声明式

编程范式可以分为声明式(Declarative)和命令式(Imperative)两种风格。

ts
// 声明式 更关注过程
$('#app') // 获取 div
	.text('hello world') // 设置文本内容
	.on('click', () => { alert('ok') }) // 绑定点击事件
html
// 命令式 关注结果
<div @click="() => alert('ok')">hello world</div>

vue路由中有编程式路由导航与声明式路由导航

响应式数据与副作用函数

副作用函数 指的是会产生副作用的函数

ts
function effect() {
    document.body.innerText = 'hello vue3'
}
// effect 函数改变了 innerText 内容,当有其他元素调用该 innerText 是则会受到影响,因此 effect() 是一个副作用函数

// 全局变量
let val = 1
function effect() {
    val = 2 // 修改全局变量,产生副作用
}

响应式数据 当副作用函数影响到了某个数据,当该数据改变时,我们希望副作用函数影响的数据一起改变。

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

obj.text = 'hello vue3' // 修改 obj.text 的值,同时希望副作用函数会重新执行

响应式数据基本实现

p9W2qsI.png

实现思路:将副作用函数存储于“桶”中,当发生数据发生改变时,在将副作用函数从“桶”中取出,再次调用副作用函数。

p9W2zFS.png

在 ES2015 之前,只能通过 Object.defineProperty 函 数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使 用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。

ts
let bucket = new Set()

// 原始数据
const data = {
  text: 'hello word'
}

// 对原始数据代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target,key) {
    // 将副作用函数添加到存储桶
    bucket.add(effect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newValue, receiver) {
    // 设置属性值
    target[key] = newValue
    // 提取出副作用函数
    bucket.forEach(fn => fn())
    // 返回 true 代表操作成功
    return true
  }
})

// 副作用函数
function effect() {
  document.body.innerText = obj.text
}

// 调用函数
effect()

// 修改响应式数据
setTimeout(()=> {
  obj.text = 'hello vue3'
},3000)

上述方法虽然能实现数据的响应式,但是我们是直接调用 effect 方法。

解决副作用函数硬编码

一个响应系统工作流程如下:

  1. 当读取操作发生时,将副作用函数收集到桶中;
  2. 当设置操作发生时,从桶中取出副作用函数并执行;

上述响应式系统我们将 effect 函数写死,传入固定的副作用函数,我们需要重新改变副作用函数写法,一旦 函数不叫 effect 那么这个响应式系统就失效。

ts
let bucket = new Set()

// 原始数据
const data = {
  text: 'hello word'
}

// 用一个变量存储被注册的副作用函数
let activeEffect

// effect 用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

// 对原始数据代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target,key) {
    // 将副作用函数添加到存储桶
    bucket.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newValue, receiver) {
    // 设置属性值
    target[key] = newValue
    // 提取出副作用函数
    bucket.forEach(fn => fn())
    // 返回 true 代表操作成功
    return true
  }
})

// 匿名函数注册副作用函数
effect(() => {
  document.body.innerText = obj.text
})

// 修改响应式数据
setTimeout(()=> {
  obj.text = 'hello vue3'
},3000)

这样就完美解决了副作用函数硬编码问题。

但是出现另一个问题。

ts
const data = {
  text: 'hello word'
}

// 匿名函数注册副作用函数
effect(() => {
  console.log('effect running')
  document.body.innerText = obj.text
})

// 修改响应式数据
setTimeout(()=> {
  obj.noExist = 'hello vue3'
},3000)

我们本希望的响应式数据时,当我们代理了整个 data,一旦对象中已有的属性值发生改变,我们希望副作用函数能重新执行,并改变所调用的数据;但是当我们改变对象中不存在的一个属性值,例如 noExist ,我们所希望的副作用函数不会执行,因为副作用函数并没有调用到该数据。从程序结果看,修改一个没有调用的属性值,副作用函数也被执行了。原因是Proxy监测到了对象属性的变化,因此我们需要改写存储桶。

重新设计桶结构

导致上述问题的原因是 没有在副作用函数与被 操作的目标字段之间建立明确的联系。当读取属性时,无论读到哪个属性都会调用副作用函数。解决方法很简单,只需要在副作用函数与被操作的字段之间建立联系即 可,这就需要我们重新设计“桶”的数据结构,而不能简单地使用一个 Set 类型的数据作为“桶”了。

ts
effect(function effectFn() {
    document.body.innerText = obj.text
})

这段代码中存在三个明确的角色。

  1. 被操作(读取)的代理对象 obj。
  2. 被操作(读取)的字段名 text。
  3. effect 函数注册的副作用函数 effectFn

如果用 target 代表一个代理对象所代理的原始对象,key 代表被操作的字段名,effectFn 表示的是被注册的副作用函数。

三者之间的关系是:

txt
target
	└── key
		  └──  effectFn

当如果有两个副作用函数同时读取同一个对象的属性值:

ts
effect(function effectFn1() {
    obj.text
})

effect(function effectFn2() {
    obj.text
})

三者之间的关系是:

txt
target
	└── text
		  └──  effectFn1
		  └──  effectFn2

当一个函数调用同个对象的两个不同属性值

ts
effect(function effectFn() {
    obj.text1
    obj.text2
})

三者之间的关系是:

txt
target
	└── text1
		  └──  effectFn
	└── text2
		  └──  effectFn

当不同的副作用函数调用不同的对象的属性值

ts
effect(function effectFn1() {
    obj1.text1
})

effect(function effectFn2() {
    obj2.text2
})

三者之间的关系是:

txt
obj1
	└── text1
		  └──  effectFn1
obj2
	└── text2
		  └──  effectFn2

重新设置桶结构

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

// 原始数据
const data = {
  text: 'hello word'
}

// 用一个变量存储被注册的副作用函数
let activeEffect

// effect 用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

// 对原始数据代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target,key) {
    // 判断桶中是否有副作用函数 否则 return
    if (!activeEffect) return target[key]
    // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
    let depsMap = bucket.get(target)
    // 如果不存在,则建立一个 Map 与 target 关联
    if (!depsMap) {
      depsMap = new Map()
      bucket.set(target, depsMap)
    }
    // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
    let deps = depsMap.get(key)
    // 如果不存在则建立一个 Set 并与 key 关联
    if (!deps) {
      deps = new Set
      depsMap.set(key, deps)
    }
    // 将副作用函数添加到存储桶
    deps.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newValue, receiver) {
    // 设置属性值
    target[key] = newValue
    // 根据 target 从桶中取出一个 depsMap
    const depsMap = bucket.get(target)
    //
    if (!depsMap) return true
    // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
    const effects = depsMap.get(key)
    // 提取出副作用函数
    effects && effects.forEach(fn => fn())
    // 返回 true 代表操作成功
    return true
  }
})

// 匿名函数注册副作用函数
effect(() => {
  document.body.innerText = obj.text
})

// 修改响应式数据
setTimeout(()=> {
  obj.text = 'hello vue3'
},3000)

从这段代码中看出构建数据结构的方式,利用到了 WeakMap Map Set

WeakMap 由 target --> Map 构成

Map 由 key --> Set 构成

WeakMap 的键是原始对象 target(也就是 obj),WeakMap 的值是一个 Map 实例;而 Map 的键是原始对象 target 的 key(也就是 text), Map 的值是一个由副作用函数组成的 Set 。

p9f7Z5R.png

WeakMap 和 Map

ts
const map = new Map()
const weakMap = new WeakMap();

{
  (function () {
    const foo = {foo: 1}
    const bar = {bar: 2}
    map.set(foo, 1)
    weakMap.set(bar, 2)
  })()
}

当该函数表达式执行完毕后,对于对象 foo 来说,它仍然作为 map 的 key 被引用着,因此垃圾回收器 (grabage collector)不会把它从内存中移除,我们仍然可以通过 map.keys 打印出对象 foo。然而对于对象 bar 来说,由于 WeakMap 的 key 是弱引用,它不影响垃圾回收器的工作,所以一旦表达式执行 完毕,垃圾回收器就会把对象 bar 从内存中移除,并且我们无法获取 weakmap 的 key 值,也就无法通过 weakmap 取得对象 bar。

WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引用的对象存在时(没有被回收)才有价值的信息。

优化代码

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

// 原始数据
const data = {
  text: 'hello word'
}

// 用一个变量存储被注册的副作用函数
let activeEffect

// effect 用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  //
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  effects && effects.forEach(fn => fn())
}

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

// 匿名函数注册副作用函数
effect(() => {
  document.body.innerText = obj.text
})

// 修改响应式数据
setTimeout(()=> {
  obj.text = 'hello vue3'
},3000)

如以上代码所示,分别把逻辑封装到 track 和 trigger 函数 内,这能为我们带来极大的灵活性。

*分支切换与 cleanup

ts
// 原始数据
const data = {
  text: 'hello word',
  ok: true
}

// 匿名函数注册副作用函数
effect(() => {
  console.log('effect running')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

当副作用函数中出现一个三元表达式,即 obj.text 的改变需要经过 obj.ok 的逻辑判断;理想状态,当我们执行完副作用函数后,手动切换 obj.ok = false,不应该执行副作用函数。但实际情况是无论 obj.ok 的值如何改变都会触发副作用函数,即使 obj.text 的值不会在改变。

txt
 data
 	└── ok
 		└── effectFn
 	└── text
 		└── effectFn
p9vCj0A.png

当修改 obj.ok = false 的理想状态

p9vPi6g.png

也就是说,当我们把 字段 obj.ok 的值修改为 false,并触发副作用函数重新执行之后,遗留的副作用函数会导致不必要的更新。解决思路是每次副作用函数执行前,将其从相关联的依赖集合中移除。要将一个副作用函数从所有与之关联的依赖集合中移除,就需要 明确知道哪些依赖集合中包含它。

如何清除掉副作用函数的无效关联关系?

  • 每次副作用函数执行前,可以先把它从所有与之关联的依赖集合中删除,然后清空依赖集合的收集,
  • 当副作用函数执行,所有会重新建立关联。(副作用函数中,会重新执行响应式数据的get操作,从而进行收集依赖)

栈溢出

  1. 当设置响应式对象的值时,触发trigger函数,遍历依赖集合,
  2. 遍历的过程中,每个回合,被包裹的副作用函数执行,
    • cleanup,把副作用函数从依赖集合中删除
    • 触发副作用函数
    • 副作用函数执行触发响应式数据的get操作,重新收集依赖函数
  3. 继续遍历
ts
// 存储副作用函数的桶
let bucket = new WeakMap()

// 原始数据
const data = {
  text: 'hello word',
  ok: false
}

// 用一个变量存储被注册的副作用函数
let activeEffect

// effect 用于注册副作用函数
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数 fn 赋值给 activeEffect
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn)
    // 当 effectFn 执行时,将其设置为当前激活的副作用函数
    activeEffect = effectFn
    // 执行副作用函数
    fn()
  }
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 执行副作用函数
  effectFn()

}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  effectsToRun.forEach(effectFn => effectFn())
  // effects && effects.forEach(effectFn => effectFn()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 匿名函数注册副作用函数
effect(() => {
  console.log('running')
  document.body.innerText = obj.ok ? obj.text : 'not'
})

// 修改响应式数据
setTimeout(()=> {
  obj.text = 'hello vue3'
},3000)

嵌套的 effect 与 effect 栈

effect (注册副作用函数)是可以嵌套的。如:

ts
effect(function effectFn1() {
    console.log('effectFn1')
    effect(function effectFn2() {
        console.log('effectFn2')
    })
})
// 当执行 effectFn1 时,effectFn2 也会被影响并且被执行
ts
// 实际场景
// Foo 组件
const Foo = {
    render() {
        return /*.....*/
    }
}
// 在一个 effect 中执行 Foo 的渲染函数
effect(() => {
    Foo.render()
})

// 当组件发生嵌套时
const Bar = {
    render() {
        return /*.....*/
    }
}
const Foo = {
    render() {
        return <Bar />
    }
}
// 此时 effect 函数也会发生嵌套
effect(() => {
    Foo.render()
    effect(() => {
    	Bar.render()
	})
})

effectFn1 内部嵌套了 effectFn2,很明 显,effectFn1 的执行会导致 effectFn2 的执行。需要注意的是, 我们在 effectFn2 中读取了字段 obj.bar,在 effectFn1 中读取 了字段 obj.foo,并且 effectFn2 的执行先于对字段 obj.foo 的 读取操作。在理想情况下,我们希望副作用函数与对象属性之间的联 系如下:

txt
data
  └── foo
	   └── effectFn1
  └── bar
	   └── effectFn2

在这种情况下,我们希望当修改 obj.foo 时会触发 effectFn1 执行。由于 effectFn2 嵌套在 effectFn1 里,所以会间接触发 effectFn2 执行,而当修改 obj.bar 时,只会触发 effectFn2 执行。

我们用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数,这就是问题所在。

由于当前的 activeEffect 是一个全局遍历,当 effect 函数嵌套时,他始终是指向内层的副作用函数,外层副作用函数则失效。因此解决办法是建立一个副作用函数栈,将当前之前的副作用函数压入栈中,执行完后弹出栈,并始终让 activeEffect 执行栈顶的副作用函数。

p9xN4dH.pngp9xNoFA.png
ts
// 存储副作用函数的桶
let bucket = new WeakMap()

// 原始数据
const data = {
  foo: true,
  bar: true
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

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

}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  effectsToRun.forEach(effectFn => effectFn())
  // effects && effects.forEach(effectFn => effectFn()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

let temp1, temp2

// 匿名函数注册副作用函数
effect(function effectFn1() {
  console.log('effectFn1')
  effect(function effectFn2() {
    console.log('effectFn2')
    temp2 = obj.bar
  })
  temp1 = obj.foo
})

避免无限递归循环

ts
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })

effect(() => obj.foo++)

effect 注册副作用函数是内部有一个自增操作,该操作会引起栈溢出:

txt
Uncaught RangeError: Maximum call stack size exceeded

引发这个错误的原因:

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

在这个语句中,既会读取 obj.foo 的值,又会设置obj.foo 的值。

首先读取 obj.foo 的值,这会触发 track 操作,将当前副作用函数收集到“桶”中,接着将其加 1 后再赋值给 obj.foo,此时会触发 trigger 操作,即把“桶”中的副作用函数取出并执行。但问题是该副作用函数正在执行中,还没有执行完毕,就要开始下一次的执行。这样会导致无限递归地调用自己,于是就产生了栈溢出。

读取和设置 操作是在同一个副作用函数内进行的。此时无论是 track 时收集的副作用函数,还是 trigger 时要触发执行的副作用函数,都是 activeEffect。基于此,我们可以在 trigger 动作发生时增加守卫条件:如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行。

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

// 原始数据
const data = {
  foo: 1
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

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

}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  effectsToRun.forEach(effectFn => {
    if (activeEffect !== effectFn) {
      effectFn()
    }
  })
  // effects && effects.forEach(effectFn => effectFn()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 匿名函数注册副作用函数
effect(() => {
  obj.foo ++
})

调度执行

可调度性是响应系统非常重要的特性。首先我们需要明确什么可调度性。所谓可调度,指的是当 trigger 动作触发副作用函数重新执行时,有能力决定副作用函数执行的时机、次数以及方式。

例如:

ts
const data = { foo: 1 }
const obj = new Proxy(data, { /*...*/ })

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

obj.foo++
console.log('结束了')

// 执行的顺序为 1 2 '结束了'
// 如果我们要将执行的顺序改变为 1 '结束了' 2
ts
// 存储副作用函数的桶
let bucket = new WeakMap()

// 原始数据
const data = {
  foo: 1
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

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

}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  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()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 匿名函数注册副作用函数
effect(() => {
  console.log(obj.foo)
}, {
  // options
  scheduler(fn) {
    setTimeout(fn)
  }
})

obj.foo++
console.log(123)

除了控制函数的执行时机,控制函数的执行次数也是尤为重要,例如:

ts
const data = { foo: 1 }
const obj = new Proxy(data, { /* ... */ })

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

obj.foo++
obj.foo++
// 执行结果 1 2 3
// 我们的需求是跳过中间的过度阶段,期待的打印结果 1 3
ts
// 存储副作用函数的桶
let bucket = new WeakMap()

// 原始数据
const data = {
  foo: 1
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

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

}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  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()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 定义一个队列任务
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,什么都不做
  if (isFlushing) return
  // 第一次执行,将标志设置为 true,后面则会跳过刷新阶段
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 结束后将刷新标志设为 false
    isFlushing = false
  })
}

// 匿名函数注册副作用函数
effect(() => {
  console.log(obj.foo)
}, {
  // options
  scheduler(fn) {
    // 每次调度时,将副作用函数添加到 jobQueue 队列中
    jobQueue.add(fn)
    // 调用 flushJob 刷新队列
    flushJob()
  }
})

obj.foo++
console.log(123)

计算属性与 lazy

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

// 原始数据
const data = {
  foo: 1,
  bar: 2
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

// effect 用于注册副作用函数
function effect(fn, options) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
    activeEffect = effectFn
    // 在执行副作用函数之前,将副作用函数压入栈中
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储于 res
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res
  }
  // 将 options 挂载到 副作用函数
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 如果用户没有设置 lazy 则立即执行副作用函数
  if (!options.lazy) {
    // 执行副作用函数
    effectFn()
  }
  return effectFn
}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  effects && effects.forEach(effectFn => {
    if (effectFn !== activeEffect) {
      effectsToRun.add(effectFn)
    }
  })
  effectsToRun.forEach(effectFn => {
    effectFn()
  })
  // effects && effects.forEach(effectFn => effectFn()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 定义一个计算属性函数
function computed(getter) {
  //把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, {
    lazy: true
  })
  return {
    // 当读取到 value 时,才执行 effectFn
    get value() {
      return effectFn()
    }
  }
}

const sumRes = computed(() => obj.bar + obj.foo)
console.log(sumRes)
ts
// 存储副作用函数的桶
let bucket = new WeakMap()

// 原始数据
const data = {
  foo: 1,
  bar: 2
}

// 用一个变量存储被注册的副作用函数
let activeEffect
// 副作用函数栈
const effectStack = []

// effect 用于注册副作用函数
function effect(fn, options) {
  const effectFn = () => {
    // 调用 cleanup 函数完成清除工作
    cleanup(effectFn)
    // 当调用 effect 注册副作用函数时,将副作用函数赋值给 activeEffect
    activeEffect = effectFn
    // 在执行副作用函数之前,将副作用函数压入栈中
    effectStack.push(effectFn)
    // 将 fn 的执行结果存储于 res
    const res = fn()
    // 在当前副作用函数执行完毕后,将当前副作用函数弹出栈,并把 activeEffect 还原为之前的值
    effectStack.pop()
    activeEffect = effectStack[effectStack.length - 1]
    // 将 res 作为 effectFn 的返回值
    return res
  }
  // 将 options 挂载到副作用函数
  effectFn.options = options
  // activeEffect.deps 用来存储所有与该副作用函数相关联的依赖集合
  effectFn.deps = []
  // 如果用户没有设置 lazy 则立即执行副作用函数
  if (!options.lazy) {
    // 执行副作用函数
    effectFn()
  }
  return effectFn
}

// 在 get 拦截函数内调用 track(跟踪) 函数追踪变化
function track(target, key) {
  // 判断桶中是否有副作用函数 否则 return
  if (!activeEffect) return target[key]
  // 如果有根据 target 从桶中获取一个 depsMap,他是一个 Map 类型 keys --> effects
  let depsMap = bucket.get(target)
  // 如果不存在,则建立一个 Map 与 target 关联
  if (!depsMap) {
    depsMap = new Map()
    bucket.set(target, depsMap)
  }
  // 在根据 key 从 depsMap 中取出 deps,他是一个 Set
  let deps = depsMap.get(key)
  // 如果不存在则建立一个 Set 并与 key 关联
  if (!deps) {
    deps = new Set
    depsMap.set(key, deps)
  }
  // 将副作用函数添加到存储桶
  deps.add(activeEffect)
  // deps 就是一个与当前副作用函数存在联系的依赖集合
  // 将其添加到 activeEffect.deps 数组中
  activeEffect.deps.push(deps)
}

// 在 set 拦截函数内调用 trigger(触发) 函数触发变化
function trigger(target, key) {
  // 根据 target 从桶中取出一个 depsMap
  const depsMap = bucket.get(target)
  if (!depsMap) return true
  // 根据 key 从 depsMap 中取出一个 effects,副作用函数集合
  const effects = depsMap.get(key)
  // 提取出副作用函数
  const effectsToRun = new Set(effects)
  // 如果 trigger 触发执行的副作用函数与当前正在执行的副作用函数相同,则不触发执行
  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()) // 删除
}

// 清空副作用函数的依赖集
function cleanup(effectFn) {
  for (let i = 0; i < effectFn.deps.length; i++) {
    // deps 是依赖集
    const deps = effectFn.deps[i]
    // 将 effectFn 从依赖集中删除
    deps.delete(effectFn)
    // 最后需要重置 effectFn.deps 数组
    effectFn.deps.length = 0
  }
}

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

// 定义一个队列任务
const jobQueue = new Set()
// 使用 Promise.resolve() 创建一个 promise 实例,我们用它将一个任务添加到微任务队列
const p = Promise.resolve()
// 一个标志代表正在刷新队列
let isFlushing = false
function flushJob() {
  // 如果队列正在刷新,什么都不做
  if (isFlushing) return
  // 第一次执行,将标志设置为 true,后面则会跳过刷新阶段
  isFlushing = true
  // 在微任务队列中刷新 jobQueue 队列
  p.then(() => {
    jobQueue.forEach(job => job())
  }).finally(() => {
    // 结束后将刷新标志设为 false
    isFlushing = false
  })
}

// 定义一个计算属性函数
function computed(getter) {
  // value 用来缓存上一次的值
  let value
  // dirty 用来标记是否需要重新执行,true 则意味着脏,需要重新执行
  let dirty = true
  //把 getter 作为副作用函数,创建一个 lazy 的 effect
  const effectFn = effect(getter, {
    lazy: true,
    // 添加调度器,在调度器中将 dirty 重置为 true
    scheduler() {
      dirty = true
      // 当计算属性依赖的响应式数据变化时,手动调用 trigger 函数触发响应
      trigger(obj, 'value')
    }
  })
  return {
    // 当读取到 value 时,才执行 effectFn
    get value() {
      // 只有“脏”时才计算值,并将得到的值缓存到 value 中
      if (dirty) {
        value = effectFn()
        // 将 dirty 设置为 false,下一次访问直接使用缓存到 value 中的值
        dirty = false
      }
      // 当读取 value 时,手动调用 track 函数进行追踪
      track(obj, 'value')
      return value
    }
  }
}

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

Released under the MIT License.