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

相关推荐
搬砖的阿wei1 天前
CSS常用选择器总结
前端·css
Trae1ounG1 天前
Vue Iframe
前端·javascript·vue.js
阿部多瑞 ABU1 天前
`tredomb`:一个面向「思想临界质量」初始化的 Python 工具
前端·python·ai写作
比特森林探险记1 天前
React API集成与路由
前端·react.js·前端框架
爱上妖精的尾巴1 天前
8-1 WPS JS宏 String.raw等关于字符串的3种引用方式
前端·javascript·vue.js·wps·js宏·jsa
hvang19881 天前
某花顺隐藏了重仓涨幅,通过chrome插件计算基金的重仓涨幅
前端·javascript·chrome
Async Cipher1 天前
TypeScript 的用法
前端·typescript
web打印社区1 天前
vue页面打印:printjs实现与进阶方案推荐
前端·javascript·vue.js·electron·html
We་ct1 天前
LeetCode 30. 串联所有单词的子串:从暴力到高效,滑动窗口优化详解
前端·算法·leetcode·typescript
木卫二号Coding1 天前
Docker-构建自己的Web-Linux系统-Ubuntu:22.04
linux·前端·docker