Vue3响应式对象是如何实现的(1)

目录
  • 简单的响应式实现
  • Proxy与响应式
    • 为什么需要Proxy?
  • Proxy创建的代理对象与原始对象有何不同?
  • 多副作用函数的响应式实现

简单的响应式实现

为了方便说明,先来看一个简单的例子。

const obj = { text: 'hello vue' }
function effect() {
  document.body.innerText = obj.text
}

这段代码中,如果obj是一个响应式数据,会产生什么效果呢?当obj.text中的内容改变时,document.body.innerText也会随之改变,从而修改页面上显示的内容。因此,如果仅从这个简单的例子出发,在修改obj后,再次执行effect(),就能够实现响应式。此时,effect()被称为副作用函数,其副作用体现在document.body.innerText这个非effect()函数作用域内的值被修改了。

实际使用中,响应式实现要复杂的多。为了进一步实现响应式,让我们基于上文的例子,再来捋一捋响应式实现需要什么。我们需要在修改响应式数据后触发副作用函数。仔细思考这句话,这里其实要求了两个功能:

  • obj读取时,收集副作用函数;
  • obj修改时,触发收集的副作用函数;

问题更清晰了,我们只需要拦截读取和修改操作分别进行收集和触发,就能够实现响应式了。这里我们可以使用Proxy

Proxy与响应式

为什么需要Proxy?

Vue3的核心特征之一就是响应式,而实现数据响应式依赖于底层的Proxy。因此,想要完成Vue的响应式功能,首先需要理解Proxy。

reactive为例,当想要创建一个响应式对象时,仅需要使用reactive对原始对象进行包裹。

例如:

const user = reactive({
  age: 25
})

在Vue3的官方文档中,对reactive的描述是:返回对象的响应式副本。既然是响应式副本,就需要产生一个与原始对象含有相同内容的响应式对象,之后使用这个响应式对象替代原始对象,代替原始对象去做事情。这就体现了Proxy的价值——创建原始对象的代理

Proxy创建的代理对象与原始对象有何不同?

先来看看如何使用Proxy创建代理对象。

let proxy = new Proxy(target, handler)

Proxy可以接收两个参数,其中,target表示被代理的原始对象,handler表示代理配置,代理配置中的捕捉器允许我们拦截重新定义针对代理对象的操作。因此,如果在Proxy中,不进行任何代理配置,Proxy仅会成为原始对象的一个透明包装器(仅创建原始对象副本,但不产生任何其它功能)。因此,为了利用Proxy实现响应式对象,我们必须使用Proxy中的代理配置

在JavaScript规范中,存在一个描述JavaScript运行机制的“内部方法”。其中,读取属性的操作被称为[[Get]]设置属性的操作被称为[[Set]]。通常,我们无法直接使用这些“内部方法”,但Proxy给我们提供了捕捉器,使得对读取和设置操作的拦截成为可能。

const p = new Proxy(obj, {
  get(target, property, receiver) {/*拦截读取属性操作*/}
  set(target, property, value, receiver) {/*拦截设置属性操作*/}
})

其中,target为被代理的原始对象,property为原始对象的属性名,value为设置目标属性的值receiver是属性所在的this对象,即Proxy代理对象。

Proxy是由JavaSciprt引擎实现的ES6特性,因此,没有ES5的polyfill,也无法使用Babel转译。这也是Vue3不支持低版本IE的主要原因。

let fn
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    fn = effect
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    fn()
    return true
  }
})

多副作用函数的响应式实现

如果响应式数据有多个相关的副作用函数应该如何处理。

在上例中,存储一个副作用函数我们可以使用一个fn进行缓存,多个副作用函数,我们在收集时用数组把它装起来就好,需要触发时,再遍历数组挨个触发收集到的副作用函数。

const fns = []
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    fns.push(effect)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    fns.forEach(fn => fn())
    return true
  }
})

这样可以吗?可以是可以,功能上已经实现了。接下来让我们来看看有没有哪些地方可以优化。

第一个可以优化的点是,需要考虑被重复收集的函数。

举个例子:

const effect1 = () => {
  document.body.innerText = obj.text
}
const effect2 = effect1

在这个例子中,effect1effect2都表示同一个函数,但在收集时会被重复收集,执行时也会被重复执行。这些重复显然是不必要的,因此,我们需要在收集副作用函数的时候,干掉重复的函数。去重可以考虑Set

const fns = new Set()
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    fns.push(effect)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    fns.forEach(fn => fn())
    return true
  }
})

不知道大家是否注意到,为了演示方便,我们在收集元素时,都默认被收集的副作用函数名是effect。但实际开发中,函数名肯定不会是固定的或是有规律可循的,我们肯定不能按函数名一个个手动收集,因此,我们要想一种方式能够不依赖于函数名进行副作用函数收集。为了实现这一点,我们将副作用函数进行包裹,用一个activeEffect统一注册

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}

const fns = new Set()
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    if(activeEffect) {
      fns.add(activeEffect)
    }
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    fns.forEach(fn => fn())
    return true
  }
})

function fn() {
  document.body.innerText = obj.text
}
effect(fn)

到这里,我们已经把容易想到的优化都处理了。但这还没完,这里还有个隐蔽的点没有考虑到。此时,我们的响应式粒度还不够细,副作用函数的收集和触发的最小单位是响应式对象,这会导致不必要的副作用函数更新。例如,我们给出一个obj上不存在的属性obj.notExist,对其进行赋值操作。为了方便演示,我们在fn()中加一条打印语句,观察结果。

function fn() {
  document.body.innerText = obj.text
  console.log('Done!')
}

可以看到,副作用函数被触发了。这里obj.notExist属性在obj上是不存在的,更谈不上对应了哪个副作用函数,因此,这里的副作用函数不应该被触发,或者换句话说,副作用函数仅能被对应的响应式对象的属性影响,且仅在该属性被修改时触发

更细粒度的触发条件就要求我们收集副作用函数时,针对响应式对象的属性进行收集。听上去有点无从下手,让我们再来捋一捋。既然是响应式对象,首先还是要保证利用响应式对象的Proxy来进行收集和触发,但在收集的时候,就不能一股脑的把所有属性对应的副作用函数塞到一块了,需要把谁是谁的分清楚(即一个属性对应了哪些副作用函数),让每个属性拥有自己收集和触发副作用函数的能力,具体对应关系如图所示。

让我们从后往前看。首先,属性的副作用函数收集器我们可以沿用上文的思路,使用Set实现。再往前,每个属性都需要配备一个副作用函数收集器,这是一个一对一的关系,我们可以使用将属性值作为key,副作用收集器作为value,使用Map实现,而这个Map就包含了单个响应式对象的全部内容。

回看我们上面的代码,其实我们在进行副作用函数收集的时候,仅使用了一个全局的容器承载。在实际开发中,我们需要尽量避免大量出现全局定义。因此,我们将响应式对象放进一个全局容器中统一管理。我们需要将每一个对象和其对应的Map建立一对一的映射关系。整体关系如下图所示:

按图中思路,我们在代码中重新组织数据结构。

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}

const objsMap = new Map() // 全局容器
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    if(!activeEffect) return
    let propsMap = objsMap.get(target) // 属性容器
    if(!propsMap) {
      objsMap.set(target, (propsMap = new Map()))
    }
    let fns = propsMap.get(key) // 副作用函数收集器
    if(!fns) {
      propsMap.set(key, (fns = new Set()))
    }
    fns.add(activeEffect)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    const propsMap = objsMap.get(target) // 属性容器
    if(!propsMap) return
    const fns = propsMap.get(key) // 副作用函数收集器
    fns && fns.forEach(fn => fn())
    return true
  }
})

function fn() {
  document.body.innerText = obj.text
  console.log('Done!')
}
effect(fn)

执行我们更新的代码,我们就能避免无效更新,只触发与属性相关的更新了。

到这结束了吗?我们其实还能再做一点优化。如果一个响应式对象没有被引用时,说明这个对象不被使用,不被使用的对象应该被JavaScript的垃圾回收器回收,避免造成内存占用。但Map的特性(Map会被其key值持续引用)决定了,即使没有任何引用,Map中的对象也不能被垃圾回收器回收。解决这个问题,只需要用WeakMap来代替MapWeakMap的key是弱引用。到这里我们终于大功告成了,把功能实现完了。

功能实现完接下来要干嘛?对,要重构,要看有没有能重构的地方。getset中的内容有点臃肿了,想想一开始说的,get时收集,set时触发。最后,就让我们来抽离封装这两部分。

let activeEffect
function effect(fn) {
  activeEffect = fn
  fn()
}

const objsMap = new WeakMap()
const data = { text: 'hello vue' }
const obj = new Proxy(data, {
  get(target, key) {
    track(target, key)
    return target[key]
  },
  set(target, key, newValue) {
    target[key] = newValue
    trigger(target, key)
    return true
  }
})
// 收集
function track(target, key) {
  if(!activeEffect) return
  let propsMap = objsMap.get(target)
  if(!propsMap) {
    objsMap.set(target, (propsMap = new Map()))
  }
  let fns = propsMap.get(key)
  if(!fns) {
    propsMap.set(key, (fns = new Set()))
  }
  fns.add(activeEffect)
}
// 触发
function trigger(target, key) {
  const propsMap = objsMap.get(target)
  if(!propsMap) return
  const fns = propsMap.get(key)
  fns && fns.forEach(fn => fn())
}

function fn() {
  document.body.innerText = obj.text
  console.log('Done!')
}
effect(fn)

到此这篇关于Vue3响应式对象是如何实现的的文章就介绍到这了,更多相关Vue3响应式内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 一步一步实现Vue的响应式(对象观测)

    平时开发中,Vue的响应式系统让我们不再去操作DOM,只需关心数据逻辑的处理,极大地降低了代码的复杂度.而响应式系统也是Vue的核心,作为开发者有必要了解其实现原理! 简易版 以watch为切入点 watch是平时开发中使用率非常高的功能,其目的是观测一个数据,当数据变化时执行我们预先定义的回调.使用方式如下: { watch: { obj(val, oldVal) { console.log(val, oldVal); } } } 上面观测了Vue实例的obj属性,当其值发生变化时,打印出新值

  • vue3 响应式对象如何实现方法的不同点

    目录 vue3响应式对象实现方法的不同点 Vue2和Vue3响应式原理对比 响应式原理实现逻辑 Vue2响应式原理简化 Vue2响应式原理弊端 Vue3响应式原理简化 vue3响应式对象实现方法的不同点 vue 响应式对象是利用 defindeProperty 实现,vue3 则是使用 Proxy 来实现响应式的. 二者,虽然都实现了 响应式的功能,但实现方式不一样,解决问题也有些不同. vue2 中,如果想要实现,数组元素的更新,则是需要 用到检测更新的方法 set 之类的,但 vue3的响应

  • Vue3响应式对象是如何实现的(2)

    目录 前言 分支切换的优化 副作用函数嵌套产生的BUG 自增/自减操作产生的BUG 前言 在Vue3响应式对象是如何实现的(1)中,我们已经从功能上实现了一个响应式对象.如果仅仅满足于功能实现,我们就可以止步于此了.但在上篇中,我们仅考虑了最简单的情况,想要完成一个完整可用的响应式,需要我们继续对细节深入思考.在特定场景下,是否存在BUG?是否还能继续优化? 分支切换的优化 在上篇中,收集副作用函数是利用get自动收集.那么被get自动收集的副作用函数,是否有可能会产生多余的触发呢?或者说,我们

  • Vue响应式添加、修改数组和对象的值

    有些时候,不得不想添加.修改数组和对象的值,但是直接添加.修改后又失去了getter.setter. 由于 JavaScript 的限制, Vue 不能检测以下变动的数组: 1. 利用索引直接设置一个项时,例如: vm.items[indexOfItem] = newValue 2. 修改数组的长度时,例如: vm.items.length = newLength 为了避免第一种情况,以下两种方式将达到像 vm.items[indexOfItem] = newValue 的效果, 同时也将触发状

  • vue3响应式Object代理对象的读取示例详解

    目录 正文 读取属性 xx in obj for ... in 正文 从这一章开始,作者将更新深入的讲解响应式,尤其是vue3响应式的具体的实现.其实在前面一章,如果你仔细阅读,你是可以实现一个简单的响应式函数的,类似于@vue/reactive,当然那只是个demo,是个玩具,我能不能在生产环境上去使用的,它差了太多功能和边界条件. 现在,我们才是真正的深入@vue/reactive. 在vue中,obj.a是一个读取操作,但是仔细想来,读取这个操作很宽泛. obj.a // 访问一个属性 '

  • vue响应式Object代理对象的修改和删除属性

    目录 正文 set delete 正文 上一篇文章我们学习了如何代理对象的读取,下面我们学习如何代理对象的修改和删除属性. set set就是修改代理的属性,按照我们之前写的reactive,它大概是这样的 const ITERATE_KEY=symbol() const p = new Proxy(obj,{ set(target,key,newVal,receiver){ const res = Reflect.set(target,key,newVal,receiver) trigger(

  • Vue3响应式对象是如何实现的(1)

    目录 简单的响应式实现 Proxy与响应式 为什么需要Proxy? Proxy创建的代理对象与原始对象有何不同? 多副作用函数的响应式实现 简单的响应式实现 为了方便说明,先来看一个简单的例子. const obj = { text: 'hello vue' } function effect() { document.body.innerText = obj.text } 这段代码中,如果obj是一个响应式数据,会产生什么效果呢?当obj.text中的内容改变时,document.body.i

  • 源码分析Vue3响应式核心之effect

    目录 一.effect用法 1.基本用法 2.lazy属性为true 3.options中包含onTrack 二.源码分析 1.effect方法的实现 2.ReactiveEffect函数源码 三.依赖收集相关 1.如何触发依赖收集 2.track源码 3.trackEffects(dep, eventInfo)源码解读 四.触发依赖 1.trigger依赖更新 2.triggerEffects(deps[0], eventInfo) 3.triggerEffect(effect, debugg

  • 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响应式原理

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

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

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

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

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

  • Vue3 响应式系统实现 computed

    目录 前言 实现 computed 总结 前言 上篇文章我们实现了基本的响应式系统,这篇文章继续实现 computed. 首先,我们简单回顾一下: 响应式系统的核心就是一个 WeakMap --- Map --- Set 的数据结构. WeakMap 的 key 是原对象,value 是响应式的 Map.这样当对象销毁的时候,对应的 Map 也会销毁. Map 的 key 就是对象的每个属性,value 是依赖这个对象属性的 effect 函数的集合 Set.然后用 Proxy 代理对象的 ge

随机推荐