从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。

相关推荐
XiaoSong15 小时前
React useState 原理和异步更新
前端·react.js
眯眼因为很困啦15 小时前
GitHub Fork 协作完整流程
前端·git·前端工程化
whisper15 小时前
🚀 React Router 7 + Vercel 部署全指南
前端
还债大湿兄16 小时前
huggingface.co 下载有些要给权限的模型 小记录
开发语言·前端·javascript
叶落无痕5216 小时前
Electron应用自动化测试实例
前端·javascript·功能测试·测试工具·electron·单元测试
H@Z*rTE|i16 小时前
elementUi 当有弹窗的时候提示语被覆盖的问题
前端·javascript·elementui
阿奇__16 小时前
vue2+elementUI table多个字段排序
前端·javascript·elementui
hellokatewj16 小时前
React Hooks 全解:原理、API 与应用场景
前端·javascript·react.js
袋鱼不重16 小时前
保姆级教程:让 Cursor 编辑器突破地区限制,正常调用大模型(附配置 + 截图)
前端·后端·cursor