Vue2与Vue3响应式原理源码解析

作者:山岚

时间:2026-10-26

浏览:486

本文深入解析Vue2与Vue3响应式系统的实现原理。Vue2基于Object.defineProperty劫持对象属性,通过Dep类管理依赖;Vue3采用ES6的Proxy和Reflect,解决数组监听及新增属性限制。通过源码级封装演示Watcher收集与更新机制,帮助开发者理解数据驱动视图的核心逻辑。

文章目录

前言

在开发Vue2和Vue3应用时,开发者通常会在data选项中定义数据,并在模板中多处引用。通过操作这些数据,可以自动更新视图,实现数据驱动视图的效果。

利用Mustache语法,可以将data中的数据渲染到页面。当data中的数据发生变化时,视图会自动同步更新。

这种响应式机制在Vue2和Vue3中是如何具体实现的?

Vue2响应式操作

响应式函数的封装

在实现响应式之前,需要先封装一个响应式函数。该函数接收一个函数作为参数,传入的函数将被标记为需要响应式处理,而默认定义的普通函数则不需要。

我们需要使用一个数组来收集这些函数(目前暂用数组,后续优化为Set),代码如下:

// 封装一个响应式的函数
let reactiveFns = []
function watchFn(fn) {
  reactiveFns.push(fn)
}

当需要执行这些函数时(具体触发时机稍后说明),可以遍历该数组并执行每个函数:

reactiveFns.forEach(fn => {<!--{cke_protected}{C}%3C!%2D%2D%20%2D%2D%3E-->
fn()
})

Depend类的封装

我们需要封装一个Depend类,用于管理某个对象特定属性的所有响应式函数。由于一个对象可能包含多个属性,且每个属性都可能被依赖,因此需要为每个被使用的属性创建一个独立的Depend实例,以管理对该属性的所有依赖函数。

为了获取响应式函数中传入的函数,我们可以使用activeReactiveFn变量暂时保存当前正在执行的函数。

基于此,我们对响应式函数的封装进行重构,如下所示:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null

// 封装一个响应式的函数
function watchFn(fn) {
  activeReactiveFn = fn
  fn()
  activeReactiveFn = null
}

由于某个属性可能被多个函数依赖,我们在Depend类内部定义一个Set集合reactiveFns = new Set()。使用Set而非数组是因为Set自动去重,从而避免重复执行依赖函数。

这里定义了一个depend方法,当activeReactiveFn存在时,将其加入reactiveFns集合中;notify函数则负责遍历并执行所有收集到的函数。

class Depend {
  constructor() {
    this.reactiveFns = new Set()
  }
  depend() {
    if (activeReactiveFn) {
      this.reactiveFns.add(activeReactiveFn)
    }
  }

  notify() {
    this.reactiveFns.forEach(fn => {
      fn()
    })
  }
}

监听对象的变化

在Vue2中,监听对象变化主要依赖Object.defineProperty方法。

我们可以封装一个reactive函数,接收一个对象作为参数。该函数遍历对象的所有属性及其值,并对每个属性使用Object.defineProperty进行监听。在defineProperty的第三个参数中,通过get和set方法实现劫持。在set方法中,当值被修改为新值后,需要获取该属性对应的Depend对象。

这里存在一个问题:如果有多个对象,且每个对象包含多个属性,该如何管理这些Depend对象?

可以定义一个WeakMap来存储各个对象,将对象保存为Map形式。在每个单一对象内部,使用Map形式保存其属性的Depend类实例,如图所示:

前言

那么如何根据对象名和属性名获取对应的depend呢?可以通过getDepend函数实现,该函数接收对象和属性名作为参数,代码如下:

// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
  // 根据target对象获取map的过程
  let map = targetMap.get(target)
  if (!map) {
    map = new Map()
    targetMap.set(target, map)
  }

  // 根据key获取depend对象
  let depend = map.get(key)
  if (!depend) {
    depend = new Depend()
    map.set(key, depend)
  }
  return depend
}

获取到属性特定的depend后,回到set方法。当值被修改为新值后,获取该属性对应的depend,并调用其notify方法,从而执行所有对该属性有依赖的函数,实现数据更新。

在get方法中,在返回属性值之前,先获取该属性对应的depend,并调用其depend方法,将当前依赖的函数保存下来。

代码如下:

function reactive(obj) {
  Object.keys(obj).forEach(key => {
    let value = obj[key]
    Object.defineProperty(obj, key, {
      get: function() {
        const depend = getDepend(obj, key)
        depend.depend()
        return value
      },
      set: function(newValue) {
        value = newValue
        const depend = getDepend(obj, key)
        depend.notify()
      }
    })
  })
  return obj
}

至此,Vue2的响应式操作原理已实现。

所有代码以及测试代码如下:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
    constructor() {
        this.reactiveFns = new Set()
    }
    depend() {
        if (activeReactiveFn) {
            this.reactiveFns.add(activeReactiveFn)
        }
    }

    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

// 封装一个响应式的函数
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}

// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
    // 根据target对象获取map的过程
    let map = targetMap.get(target)
    if (!map) {
        map = new Map()
        targetMap.set(target, map)
    }

    // 根据key获取depend对象
    let depend = map.get(key)
    if (!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

function reactive(obj) {
    Object.keys(obj).forEach(key => {
        let value = obj[key]
        Object.defineProperty(obj, key, {
            get: function() {
                const depend = getDepend(obj, key)
                depend.depend()
                return value
            },
            set: function(newValue) {
                value = newValue
                const depend = getDepend(obj, key)
                depend.notify()
            }
        })
    })
    return obj
}

// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
    name: "cy", // depend对象
    age: 18 // depend对象
})

const infoProxy = reactive({
    address: "安徽省",
    height: 1.88
})

watchFn(() => {
    console.log(infoProxy.address)
})

infoProxy.address = "北京市"

const foo = reactive({
    name: "foo"
})

watchFn(() => {
    console.log(foo.name)
})

foo.name = "aaa"
foo.name = "bbb"

// 安徽省
// 北京市
// foo
// aaa
// bbb

Vue3响应式操作

Proxy、Reflect

Proxy: 在Vue2中,使用Object.defineProperty来监听对象的变化,但这种方法存在哪些局限性?

首先,Object.defineProperty的设计初衷并非用于监听一个对象的所有属性。
在定义某些属性时,初衷是定义普通属性,但后来强行将其转换为数据属性描述符。
其次,如果想监听更丰富的操作,如新增属性或删除属性,Object.defineProperty无法做到。
因此,我们需要明确,数据描述符的设计初衷并不是为了监听完整对象的所有变化。

在ES6中,新增了一个Proxy类,从名称即可看出,它用于创建代理对象:

也就是说,如果我们希望监听对象的相关操作,可以先创建一个代理对象(Proxy对象);

之后对该对象的所有操作都通过代理对象完成,代理对象可以监听我们对原对象进行的操作;

如果我们想要侦听某些具体操作,可以在handler中添加对应的捕捉器(Trap): set函数有四个参数:

target:目标对象(被监听的对象); property:将被设置的属性key; value:新属性值; receiver:调用的代理对象;

get函数有三个参数:

target:目标对象(被监听的对象); property:被获取的属性key; receiver:调用的代理对象

实例代码如下:

const obj = {
  name: "cy",
  age: 18
}

const objProxy = new Proxy(obj, {
  // 获取值时的捕获器
  get: function(target, key) {
    console.log(`监听到对象的${key}属性被访问了`, target)
    return target[key]
  },

  // 设置值时的捕获器
  set: function(target, key, newValue) {
    console.log(`监听到对象的${key}属性被设置值`, target)
    target[key] = newValue
  }
})

console.log(objProxy.name)
console.log(objProxy.age)

objProxy.name = "kobe"
objProxy.age = 30

console.log(obj.name)
console.log(obj.age)
// 监听到对象的name属性被访问了 { name: 'cy', age: 18 }
// cy
// 监听到对象的age属性被访问了 { name: 'cy', age: 18 }
// 18
// 监听到对象的name属性被设置值 { name: 'cy', age: 18 }
// 监听到对象的age属性被设置值 { name: 'kobe', age: 18 }
// kobe
// 30

Reflect:

Reflect也是ES6新增的一个API,它是一个对象,字面意思是反射。

那么Reflect有什么用呢?

它主要提供了许多操作JavaScript对象的方法,类似于Object中操作对象的方法;
例如Reflect.getPrototypeOf(target)类似于Object.getPrototypeOf();
例如Reflect.defineProperty(target, propertyKey, attributes)类似于Object.defineProperty();

既然已有Object可以执行这些操作,为什么还需要新增Reflect对象?

这是因为在早期的ECMA规范中,未充分考虑对对象本身的操作如何设计更为规范,因此将这些API放置在了Object上;
但Object作为构造函数,这些操作放在其上并不合适;
此外,还包含一些类似in、delete等操作符,使得JS代码看起来有些奇怪;
因此在ES6中新增了Reflect,将这些操作集中到Reflect对象上;

Reflect中常见的方法:

前言

我们可以将之前Proxy案例中对原对象的操作,都修改为使用Reflect来执行;

在使用getter和setter时,有一个receiver参数,它的作用是什么?

如果源对象(obj)包含setter和getter访问器属性,可以通过receiver来改变内部的this指向。

Vue3响应式

Vue3响应式使用Proxy,我们需要在Vue2的reactive函数基础上进行一些调整:

function reactive(obj) {
  return new Proxy(obj, {
    get: function(target, key, receiver) {
      // 根据target.key获取对应的depend
      const depend = getDepend(target, key)
      // 给depend对象中添加响应函数
      depend.depend()
  
      return Reflect.get(target, key, receiver)
    },
    set: function(target, key, newValue, receiver) {
      Reflect.set(target, key, newValue, receiver)
      // depend.notify()
      const depend = getDepend(target, key)
      depend.notify()
    }
  })
}

其他方面的代码与Vue2基本无异,Vue3的响应式操作即已实现。

所有代码以及测试代码如下:

// 保存当前需要收集的响应式函数
let activeReactiveFn = null
class Depend {
    constructor() {
        this.reactiveFns = new Set()
    }
    depend() {
        if (activeReactiveFn) {
            this.reactiveFns.add(activeReactiveFn)
        }
    }

    notify() {
        this.reactiveFns.forEach(fn => {
            fn()
        })
    }
}

// 封装一个响应式的函数
function watchFn(fn) {
    activeReactiveFn = fn
    fn()
    activeReactiveFn = null
}
// 封装一个获取depend函数
const targetMap = new WeakMap()

function getDepend(target, key) {
    // 根据target对象获取map的过程
    let map = targetMap.get(target)
    if (!map) {
        map = new Map()
        targetMap.set(target, map)
    }

    // 根据key获取depend对象
    let depend = map.get(key)
    if (!depend) {
        depend = new Depend()
        map.set(key, depend)
    }
    return depend
}

function reactive(obj) {
    return new Proxy(obj, {
        get: function(target, key, receiver) {
            // 根据target.key获取对应的depend
            const depend = getDepend(target, key)
                // 给depend对象中添加响应函数
            depend.depend()

            return Reflect.get(target, key, receiver)
        },
        set: function(target, key, newValue, receiver) {
            Reflect.set(target, key, newValue, receiver)
                // depend.notify()
            const depend = getDepend(target, key)
            depend.notify()
        }
    })
}

// 监听对象的属性变量: Proxy(vue3)/Object.defineProperty(vue2)
const objProxy = reactive({
    name: "cy", // depend对象
    age: 18 // depend对象
})

const infoProxy = reactive({
    address: "安徽省",
    height: 1.88
})

watchFn(() => {
    console.log(infoProxy.address)
})

infoProxy.address = "北京市"

const foo = reactive({
    name: "foo"
})

watchFn(() => {
    console.log(foo.name)
})

foo.name = "bar"
// 安徽省
// 北京市
// foo
// bar

以上就是一篇搞懂Vue2、Vue3响应式源码的原理的详细内容,更多关于Vue2、Vue3响应式源码的原理的资料请关注本站其它相关文章!

实用工具推荐

电子包浆

绿化、压缩、噪点和虚拟水印,自由调节做旧程度。

使用 ›
图片压缩

选择画质和输出格式,预览压缩前后的大小。

使用 ›
相关文章