一篇搞懂Vue2、Vue3响应式源码的原理

目录
  • 前言
  • Vue2响应式操作
    • 响应式函数的封装
    • Depend类的封装
    • 监听对象的变化
  • Vue3响应式操作
    • Proxy、Reflect
    • Vue3响应式

前言

我们在编写Vue2,Vue3代码的时候,经常会在data中定义某些数据,然后在template用到的时候,可能会在多处用到这些数据,通过对这些数据的操作,可以达到改变视图的作用,即所谓数据驱动视图。

我们可以通过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类,这个类的作用是:这个类用于管理某一个对象的某一个属性的所有响应式函数。一个对象里面可能会有多个属性并且有他们对应的值,我们可能用到了这个对象里面多个属性,所以我们要给这里的每个用到的属性建立一个属于自己的类,用来管理对这个属性有依赖的所有函数。

所以我们得想办法拿到刚才在响应式函数里面传进去的函数,这里我们可以用activeReactiveFn暂时保存刚才传进去的函数。

所以我们对响应式函数的封装进行重构一下,如下:

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

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

因为某个属性可能会用多个函数进行依赖,所有在这个类的内部我们会定义一个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,在Object.defineProperty第三个参数中,get和set方法中,在set方法中,修改值为新的值之后,之前提到,每个属性都要有属于自己的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后,要调用depend里面的notify方法,使对这个属性有依赖的所有函数执行,也就是对数据进行更新。

在get方法中,在返回属性值之前,要先获取到属性特定的depend后,调用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响应式源码的原理的资料请关注我们其它相关文章!

(0)

相关推荐

  • Vue2 与 Vue3 的数据绑定原理及实现

    目录 介绍 Object.defineProperty Proxy 介绍 数据绑定是一种把用户界面元素(控件)的属性绑定到特定对象上面并使其同步的机制,使开发人员免于编写同步视图模型和视图的逻辑. 观察者模式又称为发布-订阅模式,定义对象间的一种一对多的依赖关系,当它本身的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新.比如用户界面可以作为一个观察者,业务数据是被观察者,用户界面观察业务数据的变化,发现数据变化后,就同步显示在界面上.这样可以确保界面和数据之间划清界限,假定应用程序的需

  • vue2与vue3中生命周期执行顺序的区别说明

    目录 vue2与vue3中生命周期执行顺序区别 生命周期比较 简单例子说明 三种情况下的生命周期执行顺序 1.单页面下生命周期顺序 2.父子.兄弟组件的生命周期顺序 3.不同页面跳转时各页面生命周期的执行顺序 vue2与vue3中生命周期执行顺序区别 生命周期比较 vue2中执行顺序 beforeCreate=>created=>beforeMount =>mounted=>beforeUpdate =>updated=>beforeDestroy=>destro

  • Vue2项目升级到Vue3的详细教程

    目录 应不应该从 Vue 2 升级到 Vue 3 Vue 3 不兼容的那些写法 Vue 3 生态现状介绍 使用自动化升级工具进行 Vue 的升级 总结 应不应该从 Vue 2 升级到 Vue 3 应不应该升级?这个问题不能一概而论. 首先,如果你要开启一个新项目,那直接使用 Vue 3 是最佳选择.后面课程里,我也会带你使用 Vue 3 的新特性和新语法开发一个项目. 以前我独立使用 Vue 2 开发应用的时候,不管我怎么去组织代码,我总是无法避免在 data.template.methods

  • vue2如何实现vue3的teleport

    目录 vue2实现vue3的teleport vue3新特性teleport介绍 teleport是什么 teleport怎么使用 vue2实现vue3的teleport 不支持同一目标上使用多个teleport(代码通过v-if就能实现) 组件 <script>     export default {         name: 'teleport',         props: {             /* 移动至哪个标签内,最好使用id */             to: {

  • Vue mixins混入使用解析

    目录 前言 一.什么是Mixins 二.什么时候使用Mixins 三.如何创建Mixins 四.如何使用Mixins 五.Mixins的特点 六.Mixins合并冲突 七.与vuex的区别 八.与公共组件的区别 前言 当我们的项目越来越大,我们会发现组件之间可能存在很多相似的功能,你在一遍又一遍的复制粘贴相同的代码段(data,method,watch.mounted等),如果我们在每个组件中去重复定义这些属性和方法会使得项目出现代码冗余并提高了维护难度,针对这种情况官方提供了Mixins特性

  • Vue2.x响应式简单讲解及示例

    一.回顾Vue响应式用法 ​ vue响应式,我们都很熟悉了.当我们修改vue中data对象中的属性时,页面中引用该属性的地方就会发生相应的改变.避免了我们再去操作dom,进行数据绑定. 二.Vue响应式实现分析 对于vue的响应式原理,官网上给了出文字描述 https://cn.vuejs.org/v2/guide/reactivity.html . vue内部主要是通过数据劫持和观察者模式实现的 数据劫持: vue2.x内部使用Object.defineProperty https://dev

  • 手写 Vue3 响应式系统(核心就一个数据结构)

    目录 前言 响应式 总结 前言 响应式是 Vue 的特色,如果你简历里写了 Vue 项目,那基本都会问响应式实现原理.而且不只是 Vue,状态管理库 Mobx 也是基于响应式实现的.那响应式是具体怎么实现的呢?与其空谈原理,不如让我们来手写一个简易版吧. 响应式 首先,什么是响应式呢? 响应式就是被观察的数据变化的时候做一系列联动处理.就像一个社会热点事件,当它有消息更新的时候,各方媒体都会跟进做相关报道.这里社会热点事件就是被观察的目标.那在前端框架里,这个被观察的目标是什么呢?很明显,是状态

  • 一文搞懂Vue2中的组件通信

    目录 vue 组件通信方式 1.props传参 2.emit,on通信 3.$ref,$children实例通信 4.$parent通信 5.插槽通信(一般不用) 6.$attr,$listener深层双向通信 7.provide,inject依赖注入深层次单向通信 8.$bus事件总线全局通信 vue 组件通信方式 1.父组件将自己的状态分享给子组件使用: 方法:父组件通过子标签传递数据,子组件通过 props 接收 2.子组件改变父组件的状态; 方法:父组件在子标签上通过@abc 提供一个改

  • 一文详解Vue3响应式原理

    目录 回顾 vue2.x 的响应式 vue3的响应式 Reflect 回顾 vue2.x 的响应式 实现原理: 对象类型:通过object.defineProperty()对属性的读取.修改进行拦截(数据劫持) 数组类型:通过重写更新数组的一系列方法来实现拦截(对数组的变更方法进行了包裹) Object.defineProperty(data,'count ",{ get(){}, set(){} }) 存在问题: 新增属性.删除属性,界面不会更新 直接通过下标修改数组,界面不会自动更新 但是

  • vue3响应式实现readonly从零开始教程

    目录 前言 readonly的实现 重构 结束 前言 前面的章节我们把 effect 部分大致讲完了,这部分我们来讲 readonly以及重构一下reactive. readonly的实现 it("happy path", () => { console.warn = vi.fn(); const original = { foo: 1, }; const observed = readonly({ foo: 1, }); expect(original).not.toBe(ob

  • Vue3响应式方案及ref reactive的区别详解

    目录 一.前言 二.新的方案 1. 缘由 2. Proxy 和 Reflect 1) Proxy 2) Reflect 3. reactive 1) createReactiveObject() 函数 2) mutableHandlers() 函数 -> 对象类型的 handlers 3) mutableInstrumentations() 函数 -> Map Set等类型的 handlers 4. ref 1) createRef() 2) toReactive() 3)proxyRefs(

  • 100行代码理解和分析vue2.0响应式架构

    分享前啰嗦 我之前介绍过vue1.0如何实现observer和watcher.本想继续写下去,可是vue2.0横空出世..所以直接看vue2.0吧.这篇文章在公司分享过,终于写出来了.我们采用用最精简的代码,还原vue2.0响应式架构实现. 以前写的那篇 vue 源码分析之如何实现 observer 和 watcher可以作为本次分享的参考. 不过不看也没关系,但是最好了解下Object.defineProperty 本文分享什么 理解vue2.0的响应式架构,就是下面这张图 顺带介绍他比rea

  • Vue3 响应式侦听与计算的实现

    响应式侦听和计算 有时我们需要依赖于其他状态的状态--在 Vue 中,这是用组件 计算属性 处理的,以直接创建计算值,我们可以使用 computed 方法:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象. 我们先来看看一个简单的例子,关于计算值的方式,同样我们在 src/TemplateM.vue 写下如下代码: <template> <div class="template-m-wrap"> count --->

  • setup+ref+reactive实现vue3响应式功能

    setup 是用来写组合式 api ,内部的数据和方法需要通过 return 之后,模板才能使用.在之前 vue2 中,data 返回的数据,可以直接进行双向绑定使用,如果我们把 setup 中数据类型直接双向绑定,发现变量并不能实时响应.接下来就看看setup如何实现data的响应式功能? 一.ref setup 内的自定义属性不具备响应式能力,所以引入了 ref ,ref 底层通过代理,把属性包装值包装成一个 proxy ,proxy 内部是一个对象,使得基础类型的数据具备响应式能力,使用之

  • 浅析vue3响应式数据与watch属性

    是Vue3的 composition API中2个最重要的响应式API ref用来处理基本类型数据, reactive用来处理对象(递归深度响应式) 如果用ref对象/数组, 内部会自动将对象/数组转换为reactive的代理对象 ref内部: 通过给value属性添加getter/setter来实现对数据的劫持 reactive内部: 通过使用Proxy来实现对对象内部所有数据的劫持, 并通过Reflect操作对象内部数据 ref的数据操作: 在js中要.value, 在模板中不需要(内部解析

随机推荐