从vue3 watch开始理解Vue的响应式原理

从vue3 watch开始理解Vue的响应式原理

前言

vue源代码的内容非常之多,包括模板解析(把vue文件的写法解析成一个函数用来插入dom或者调整dom),依赖收集,响应式,事件处理,插槽,组件,指令,等等等。刚开始看时不知道从何看起,这里分享一个个人的学习思路,我们可以从一些关键的api开始逐步学习,这里推荐 reactive,watch。明白了这两个api时如何工作的,那vue的响应式系统就基本理解了。

let`s start

js 复制代码
// demo.js
import { reactive, watch } from './vue-core.js'
const user = reactive({
  name: 'Alice',
  age: 25
})
watch(user, (newValue, oldValue) => {
  console.log(
    `User changed from ${oldValue.name},${oldValue.age} to ${newValue.name},${newValue.age}`
  )
})
setTimeout(() => {
  user.name = 'Bob'
  user.age = 30
}, 1000)

朴素的示例,如果user发生了变化。那么就会触发watch的回调。脱离vue框架,我们需要如何设计呢,首先想到的简单逻辑就是拦截user的set和get。无论是通过Proxy还是Object的set,原理是一样的。

vue-core.js 复制代码
let activeSub = null // 当前活跃的订阅者
const targetMap = new WeakMap()

export function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      track(target, key)
      return Reflect.get(target, key, receiver)
    },
    set(target, key, value, receiver) {
      const res = Reflect.set(target, key, value, receiver)
      trigger(target, key)
      return res
    }
  })
}
// 简化版watch实现,只考虑监听一个reactive对象(类似上面的user例子)的变化
export function watch(source, cb) {
  const getter = () => {
    for (const key in source) {
      source[key]
    }
    return source
  }
  let oldValue
  const effect = new ReactiveEffect(getter)
  const job = () => {
    const newValue = effect.run()
    //todo 对比newValue和oldValue是否有变化
    cb(newValue, oldValue)
    oldValue = newValue
  }
  effect.schedule = job
  oldValue = effect.run()
  console.log('old', oldValue)
}

// 示例解释:搜集user中每个key的下游依赖,当user的name或age变化时,触发对应的依赖更新,从而调用watch的回调函数。
function track(target, key) {
  let depsMap = targetMap.get(target)
  if (!depsMap) {
    targetMap.set(target, (depsMap = new Map()))
  }
  let dep = depsMap.get(key)
  if (!dep) {
    depsMap.set(key, (dep = new Dep()))
  }
  dep.track()
}
function trigger(target, key) {
  const depsMap = targetMap.get(target)
  if (!depsMap) return
  const dep = depsMap.get(key)
  if (dep) {
    dep.trigger()
  }
}

function Dep() {
  this.subs = new Set()
  this.track = () => {
    // 借助全局变量activeSub记录当前活跃的订阅者
    if (activeSub) {
      this.subs.add(activeSub)
    }
  }
  this.trigger = () => {
    this.subs.forEach((sub) => {
      sub.notify()
    })
  }
}

function ReactiveEffect(fn) {
  this.fn = fn
  this.notify = null // 回调函数, 可类比于watch的cb用来辅助理解
  this.schedule = null
  this.run = () => {
    // 标记当前活跃的订阅者为this 通过fn触发每个ref或者reactive对象的getter
    // 从而触发track函数,将当前活跃的订阅者this添加到dep的subs中
    const preActiveSub = activeSub
    activeSub = this
    const value = this.fn()
    activeSub = preActiveSub
    return value
  }
  // 上游的ref或者reactive对象触发setter时,会通过收集者调用trigger函数,从而触发schedule函数,在watch中就是cb函数
  this.trigger = () => {
    this.schedule?.()
  }
  this.notify = () => {
    this.trigger()
  }
}

从上面的demo中我们初步实现了user变动时调用一个回调,deps表示"依赖",通俗的理解就是有多少个下游正在依赖user,例如computed,watch,render函数等等这些的最终结果都是根据上游来变动的。

这里面其实就两个概念需要理解,一个是依赖收集者 Dep,一个是响应副作用管理器 ReactiveEffect。借助例子,Dep 理解为用来管理user中每个key都有哪些watch正在监听。而 ReactiveEffect 理解为当user的某个key发生变化时,需要调用哪些watch的回调函数。

调用watch的过程其实关注的就是 oldValue = effect.run() 这一段代码。在创建watch时,会立即调用effect.run(),从而触发user的getter,从而触发track函数,将当前活跃的订阅者effect添加到user.name和user.age的dep中。这样,当user.name或user.age发生变化时,就会触发trigger函数,从而调用effect的notify函数,从而调用watch的回调函数。

组件渲染其实就是一个特殊的watch,watch的回调函数就是渲染函数。当user的name或age发生变化时,就会触发渲染函数,从而更新dom。

相关推荐
JustHappy19 小时前
古法编程秘籍(七):互联网到底是什么?把两台电脑怎么说话搞懂就够了
前端·后端·网络协议
snow@li19 小时前
SEO-文章标题:写文章时候,分类+主标题+大纲+解释 作为标题 / 不点进去也知道全文覆盖什么 / 标题即架构
前端
kyriewen20 小时前
Git Commit 前自动修复代码风格?配置 Husky + lint-staged,从此 CR 只聊逻辑
前端·git·面试
岁月宁静20 小时前
RAG 文档摄入全链路,从原理到生产落地
vue.js·人工智能·python
小和尚同志20 小时前
AI 自动化测试探索(一):Playwright MCP
前端·人工智能·aigc
老马识途2.020 小时前
在AI的帮助下理解spring的启动过程
java·前端·spring
徐小夕21 小时前
Loop Engineering 深度解析与实战指南(全网最全)
前端·算法·github
运筹vivo@21 小时前
Python ContextVar 底层机制与内存模型拆解
前端·数据库·python
#麻辣小龙虾#1 天前
基于vue3.0开发一款【固废与废气运维管理系统】(支持源码)
前端·vue.js·vue3
Cosolar1 天前
Docsify零构建文档站完全指南:从快速搭建到企业级部署
前端·开源·github