《Vue.js设计与实现》笔记 第4章:响应系统的作用与实现

Vue.js设计与实现 第4章:响应系统的作用与实现

本章导读

Vue 最核心的能力:

数据变化
↓
视图自动更新

例如:

<template>
  <h1>{{ count }}</h1>
</template>

<script setup>
const count = ref(0)

setTimeout(() => {
  count.value++
}, 1000)
</script>

当数据变化时:

count
↓
自动更新DOM

开发者无需手动操作页面。

这种机制称为:

响应式(Reactivity)

本章将一步一步实现:

effect
track
trigger

三个核心模块。


一、什么是响应式

最简单的理解

响应式:

数据变化
↓
自动执行相关逻辑

例如:

let text = 'hello'

function update() {
  document.body.innerText = text
}

text = 'world'

update()

这里需要:

手动调用 update()

并不是真正响应式。


理想状态

希望:

text = 'world'

自动执行:

update()

即:

修改数据
↓
自动更新

这就是响应式系统。


二、副作用函数(Effect)

什么是副作用

副作用:

函数执行时
会影响外部环境

例如:

document.body.innerText = 'Vue'

修改了页面。


Effect概念

例如:

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

这里:

effect依赖obj.text

当:

obj.text

变化时:

effect需要重新执行

Effect本质

依赖响应式数据的函数

三、最简单的响应式实现

原始数据

const data = {
  text: 'hello world'
}

使用Proxy代理

const obj = new Proxy(data, {
  get(target, key) {
    return target[key]
  },

  set(target, key, newVal) {
    target[key] = newVal
    return true
  }
})

注册副作用函数

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

执行:

effect()

页面显示:

hello world

问题

修改:

obj.text = 'Vue3'

页面不会更新。

因为:

set触发后
不知道执行哪个effect

四、建立响应联系

核心目标:

obj.text
↓
effect

建立映射关系。


全局变量保存当前Effect

let activeEffect

注册Effect

function effect(fn) {
  activeEffect = fn
  fn()
}

使用:

effect(() => {
  document.body.innerText = obj.text
})

执行过程

effect()
↓
activeEffect记录
↓
读取obj.text
↓
建立依赖

五、依赖收集(track)

读取属性时收集依赖

修改:

get(target, key) {
  track(target, key)
  return target[key]
}

创建依赖集合

const bucket = new Set()

track实现

function track() {
  bucket.add(activeEffect)
}

工作流程

effect执行
↓
读取obj.text
↓
触发get
↓
track()
↓
收集effect

当前结果

bucket

┌──────────┐
│ effect() │
└──────────┘

六、触发更新(trigger)

修改属性时触发

set(target,key,newVal){
  target[key] = newVal

  trigger()

  return true
}

trigger实现

function trigger() {
  bucket.forEach(fn => fn())
}

整体流程

obj.text = 'Vue3'
↓
set
↓
trigger()
↓
effect()
↓
页面更新

第一个完整版本

const bucket = new Set()

let activeEffect

const data = {
  text: 'hello'
}

const obj = new Proxy(data, {
  get(target, key) {
    bucket.add(activeEffect)
    return target[key]
  },

  set(target, key, newVal) {
    target[key] = newVal

    bucket.forEach(fn => fn())

    return true
  }
})

function effect(fn) {
  activeEffect = fn
  fn()
}

七、分支切换问题

示例

const data = {
  ok: true,
  text: 'hello'
}

副作用:

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

初次收集

依赖:

ok
text

修改

obj.ok = false

页面显示:

not

再修改

obj.text = 'Vue3'

理论上:

不应该重新执行effect

因为:

当前已经不依赖text

实际却会执行

原因:

旧依赖没有清理

八、依赖清理

Effect增加deps属性

function effect(fn){
  const effectFn = () => {
    cleanup(effectFn)

    activeEffect = effectFn

    fn()
  }

  effectFn.deps = []

  effectFn()
}

cleanup实现

function cleanup(effectFn) {
  effectFn.deps.forEach(deps => {
    deps.delete(effectFn)
  })

  effectFn.deps.length = 0
}

track改进

function track(target,key){

  deps.add(activeEffect)

  activeEffect.deps.push(deps)
}

效果

重新执行effect时:

先删除旧依赖
↓
重新收集依赖

避免无效更新。


九、嵌套Effect问题

示例

effect(() => {

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

  console.log(obj.foo)
})

问题

执行内部effect后:

activeEffect
被覆盖

导致:

外层effect丢失

十、Effect栈

解决方案:

effectStack

创建栈

const effectStack = []

effect实现

function effect(fn) {

  const effectFn = () => {

    cleanup(effectFn)

    activeEffect = effectFn

    effectStack.push(effectFn)

    fn()

    effectStack.pop()

    activeEffect =
      effectStack[
        effectStack.length - 1
      ]
  }

  effectFn()
}

工作流程

外层effect
↓
入栈

内层effect
↓
入栈

执行结束
↓
出栈

恢复外层effect

十一、避免无限递归

问题

effect(() => {
  obj.count++
})

执行:

effect
↓
count++
↓
trigger
↓
effect
↓
count++
↓
无限循环

解决方案

trigger时跳过当前Effect

effectsToRun.forEach(effectFn => {
  if (effectFn !== activeEffect) {
    effectFn()
  }
})

十二、调度执行(Scheduler)

问题

obj.foo++
obj.foo++

会触发:

effect执行两次

Vue解决方案

允许用户指定:

scheduler

effect实现

effect(
  fn,
  {
    scheduler(effectFn) {

    }
  }
)

trigger修改

if(effectFn.options.scheduler){

  effectFn.options.scheduler(
    effectFn
  )

}else{

  effectFn()

}

优势

开发者可以:

  • 控制执行时机
  • 合并更新
  • 异步更新

十三、计算属性与Lazy

Lazy执行

默认:

effect(fn)

立即执行。


支持:

effect(fn,{
  lazy:true
})

实现

if (!options.lazy) {
  effectFn()
}

返回值

return effectFn

这样:

const runner = effect(fn,{
  lazy:true
})

执行:

runner()

才会运行。


十四、计算属性(Computed)思想

例如:

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

本质:

lazy effect
+
缓存机制

工作流程

读取value
↓
执行getter
↓
缓存结果

再次读取
↓
直接返回缓存

依赖变化
↓
缓存失效

十五、Watch实现思想

示例

watch(
  () => obj.foo,
  (newVal, oldVal) => {

  }
)

本质:

effect
+
scheduler

工作流程

数据变化
↓
trigger
↓
scheduler
↓
执行回调

第四章核心知识图谱

响应式系统

Proxy
│
├── get
│    └── track()
│
└── set
     └── trigger()

track()
│
└── 收集依赖

trigger()
│
└── 触发依赖

effect()
│
├── activeEffect
├── cleanup
├── effectStack
├── scheduler
└── lazy

高级能力

├── computed
└── watch

高频面试题

什么是响应式?

数据变化
自动触发相关逻辑

effect作用是什么?

注册副作用函数。


track作用是什么?

收集依赖。


trigger作用是什么?

触发依赖执行。


为什么需要cleanup?

解决分支切换导致的遗留依赖问题。


为什么需要effectStack?

解决嵌套effect覆盖问题。


scheduler作用是什么?

控制effect执行时机。


computed本质是什么?

lazy effect
+
缓存

watch本质是什么?

effect
+
scheduler

本章总结

Vue3响应式系统核心链路:

响应式数据
     │
     ▼
   Proxy
     │
 ┌───┴────┐
 ▼        ▼
track   trigger
 │         │
 ▼         ▼
effect ←───┘

核心实现:

  1. effect(副作用函数)
  2. track(依赖收集)
  3. trigger(触发更新)
  4. cleanup(依赖清理)
  5. effectStack(嵌套处理)
  6. scheduler(调度器)

后续章节将在此基础上扩展:

  • 第5章:非原始值响应式(Object、Array、Map、Set)
  • 第6章:原始值响应式(ref、toRef、toRefs)
  • 第7章:渲染器实现

第四章是整本《Vue.js设计与实现》中最重要的章节之一,也是 Vue 面试源码题出现频率最高的部分。

posted @ 2025-03-03 14:08  Li_pk  阅读(6)  评论(0)    收藏  举报