likes
comments
collection
share

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

作者站长头像
站长
· 阅读数 59

一说到Vue响应式数据或者说响应数据实现原理大家一定会想到两个单词,Vue2 使用了Object.defineProperty, Vue3 使用了Proxy。如果这是一个面试题,这样回答正确吗? 正确,但是如果你只是说到了这两个单词,面试官给你打分的话相信一定是无限趋近于0的,为什么呢?因为时代已经不一样了,放在7、8年前,Vue刚出来那一两年没太大问题。现在Vue 已经成为最流行的前端三大框架之一。只了解到这个程度是远远不够的了。因为相信99.99%的前端开发者都能说得出来,那你有什么优势呢!要想更好的回答这种问题,就必须要对响应式数据有更深的理解了。这就是本文接下来得重点了。本文会从0到实现一个响应式数据系统。

一、什么是响应式数据?

来看一段代码

const obj = {
  content: 'hello 响应式数据'
}
function test () {
  document.body.innerHTML = obj.content
}
test ()
obj.content = 'hello 新的响应式数据'

如果我们修改了obj.content 能够让document.body.innerHTML 重新赋值,那么这就是一个响应式数据。但是。但是目前是不可能的。来运行代码

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

可以看到页面的显示还是原来的数据。因为这是一个普通的对象肯定是不会触发渲染更新的。那么如何才能把obj 变成响应式数据呢?

二、响应式数据的基本实现:

思考: 如果我们能在读属性值和修改属性值时能做一些操作的话,我们是不是就能实现这个功能了呢? test 函数中的obj.conetnt 会触发读取操作,obj.content = 'hello 新的响应式数据' 会触发设置属性操作

那么我们如何拦截对象的读取设置操作呢? 在ES2015 以前只能用Object.defineProperty 来实现。这也是Vue2 中的实现方式。ES2015之后可以使用Proxy 来实现。本文主要讨论Vue3 的实现方式。来看一个简单版的实现。

// 用来存储需要修改属性值后是否需要重新执行的函数集合
const bucket = new Set()

const obj = {
  content: 'hello 响应式数据'
}

const proxy = new Proxy(obj, {
  get (target, key) {
    bucket.add(test)// 读取属性值时,将副作用函数添加到bucket 中
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn()); // 修改属性后遍历重新执行收集到的函数
    return true
  }
})
function test () {
  document.body.innerHTML = proxy.content
}
test()
proxy.content = 'hello 新的响应式数据'

首先我们声明了一个Set 数据类型的变量用来收集需要在修改属性之后重新执行的函数。用Proxy 代理了obj 对象。在get 里面收集函数添加到bucket 里面。在set 里面将收集到的函数取出来遍历执行,所以当我们proxy.content = 'hello 新的响应式数据' 时会触发更新。来看一看效果

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

可以看到页面上显示的数据就是修改过后的数据了。响应式数据的基本功能实现了,我们的实现有没有什么缺陷呢?

问题: 其实是有的,在proxy 的get 中我们将收集的函数名写死了。也就是传说中的硬编码。非常不友好。那么该怎么办呢?

三、代码优化改造硬编码

上面已经说到我们在proxy 的get 中将将副作用函数的名字硬编码了。非常不友好,一旦副作用函数名字不叫test 就没有办法正常使用了。那么该怎么办呢?

解决上面硬编码的方法

  1. 我们可以声明一个库变量来存储当前的副作用函数,
  2. 用一个函数来注册,负责更改这个全局变量。
  3. 在get 操作中我们就可以直接使用这个全局变量来添加到bucket 中。

来看一看具体代码的实现:


let bucket = new Set()
// 用一个全局变量存储被注册的副作用函数
let activeEffect

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

const obj = {
  content: 'hello 响应式数据'
}

const proxy = new Proxy(obj, {
  get (target, key) {
    console.log(activeEffect)
    bucket.add(activeEffect) // 将之前硬编码的函数替换为全局变量
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn())
    return true
  }
})

function test () {
  document.body.innerHTML = proxy.content
}
effect(test)
proxy.content = 'hello 新的响应式数'

可以看到我们声明了activeEffect 全局变量用于存储当前的副作用函数。另外新增了effect 函数用注册副作用函数。在get 中 将bucket.add(test) 改成了bucket.add(activeEffect),这样修之后,我们的响应式系统就不会受到副作用函数名称的限制了。运行代码看看效果

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

可以看到效果和之前一样。

我们现在新增一个不叫test 函数的副作用函数



let bucket = new Set()
// 用一个全局变量存储被注册的副作用函数
let activeEffect

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

const obj = {
  content: 'hello 响应式数据',
  title: 'hello title'
}

const proxy = new Proxy(obj, {
  get (target, key) {
    console.log(activeEffect)
    bucket.add(activeEffect) // 将之前硬编码的函数替换为全局变量
    return target[key]
  },
  set (target, key, newVal) {
    target[key] = newVal
    bucket.forEach(fn => fn())
    return true
  }
})

function test () {
  document.body.innerHTML = proxy.content
}
function test2 () {
  document.title = proxy.title
}
effect(test)
effect(test2)
proxy.content = 'hello 新的响应式数'
proxy.title = 'hello 新title'

来看看效果:

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

发现我们的响应式系统一样好使。现在我们已经实现了副作用函数的名字不需要固定的响应式系统。那么我们实现实现的响应式系统还有其他的缺陷吗?

解决硬编码后还存在的问题: 思考一分钟... 其实还是有的。我们现在分别在test 中打印test, 在test2中打印test2 会发现test1 和test2 都被打印了3次。也就是test 和test2 都被执行了3次。为什么都执行了3次呢?按照我们理想的应该各执行两次就好。初始化的时候执行一次,修改的时候执行一次。

副作用函数执行次数不对的原因分析

  1. 我们在收集副作用函数和修改属性值触发副作函数重新执行时没有和key 之间建立联系,把全部都收集了。
  2. 修改时把所有收集到的副作用函数全部取出来进行了重新执行。

为了解决这个问题我们需要重新设计存储副作用函数的数据结构。来看下具体代码实现:

四、解决有多个副作用函数时, 每个key 对应副作用函数执行次数变成了所有副作用函数执行次数总和的问题。


let bucket = new WeakMap()
// 用一个全局变量存储被注册的副作用函数
let activeEffect

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

const obj = {
  content: 'hello 响应式数据',
  title: 'hello title'
}

const proxy = new Proxy(obj, {
  get (target, key) {
    // acctiveEffect 直接返回
    if (!activeEffect) return target[key]

    // 根据target 从bucket中取出depsMap, 它也是一个Map 类型: key: effects
    let depsMap = bucket.get(target)
    // 如果不存在depsMap,那么新建一个Map 与target 关联
    if (!depsMap) {
      bucket.set(target, depsMap = new Map())
    }
    // 如果deps不存在,新建一个Set 与key关联。
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, deps = new Set())
    }
    deps.add(activeEffect)
    activeEffect = null // 收集完成之后将存储的变量变为空,因为多个副作用函数的时候会篡位

    return target[key]

  },
  set (target, key, newVal) {
    target[key] = newVal

    const depsMap = bucket.get(target)
    if (!depsMap) {
      return
    }
    const effects = depsMap.get(key)
    effects && effects.forEach(fn => fn())
    return true
  }
})

function test () {
  console.log('test')
  document.body.innerHTML = proxy.content
}
function test2 () {
  console.log('test2')
  document.title = proxy.title
}
effect(test)
effect(test2)
proxy.content = 'hello 新的响应式数'
proxy.title = 'hello 新title'

在上述代码中,

1.我们将bucket 存副作用函数的数据结构改成了WeakMap 数据结构,WeakMap 可以以对象作为key值,方便建立target 目标对象和副作用函数集合之间的关系。且WeakMap是弱引用,垃圾回收机制会自动回收。

  1. 接着在proxy 的get 中判断 activeEffect 存不存在,不存在直接返回,存在的话我们会先根据目标对象从bucket 中取出目标对象对应的Map 集合。
  2. 再判断该Map 集合存不存在。不存在的话,判断集合存不存在,不存在就以目标对象作为key值创建一个Map 。
  3. 得到了目标对象对应的集合后,再根据key 取出对应的副作用函数结合。再判断这个集合存不存在。不存在就根据当前的key 作为key 创建一个新的Set集合。
  4. 接着将副作用函数添加到key 对应的Set集合中。
  5. 添加完成之后我们将activeEffect 重置为空,原因是如果不重置,第一个副作用函数触发了副作用函数执行时。也就是test1 执行时。又会触发属性的读取。这时的变量是test2 函数,就会出现错乱问题。
  6. 在set 中我们同样是先根据目标对象先取出depsMap 集合。
  7. 再判断存不存在,不存在直接返回,存在的话再根据当前的key 取出对应的副作用函数进行执行。

下面来看下运行效果:

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇

可以看到执行效果符合预期。

代码优化-将拦截操作抽取成函数:

响应式系统的功能我们基本上都已经实现了。但是来看看我们的代码,我们把收集相关的逻辑的写再get 里面了。把触发副作用函数重新执行的相关逻辑都写在了set 里面。其实这样不太友好。可以将这部分逻辑拆分成一个函数。来看看具体代码。


let bucket = new WeakMap()
// 用一个全局变量存储被注册的副作用函数
let activeEffect

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

const obj = {
  content: 'hello 响应式数据',
  title: 'hello title'
}

const proxy = new Proxy(obj, {
  get (target, key) {
    track(target, key)
    return target[key]

  },
  set (target, key, newVal) {
    target[key] = newVal
    trigger(target, key, newVal)
    return true
  }
})

function track (target, key) {
  // acctiveEffect 直接返回
  if (!activeEffect) return target[key]

  // 根据target 从bucket中取出depsMap, 它也是一个Map 类型: key: effects
  let depsMap = bucket.get(target)
  // 如果不存在depsMap,那么新建一个Map 与target 关联
  if (!depsMap) {
    bucket.set(target, depsMap = new Map())
  }
  // 如果deps不存在,新建一个Set 与key关联。
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, deps = new Set())
  }
  deps.add(activeEffect)
  activeEffect = null // 收集完成之后将存储的变量变为空,因为多个副作用函数的时候会篡位
}

function trigger (target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) {
    return
  }
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}

function test () {
  console.log('test')
  document.body.innerHTML = proxy.content
}
function test2 () {
  console.log('test2')
  document.title = proxy.title
}
effect(test)
effect(test2)
proxy.content = 'hello 新的响应式数'
proxy.title = 'hello 新title'


我们将get 相关的逻辑抽离到了track 函数中。将get中的逻辑抽离到了trigger 中。

来看下代码运行效果:

Vue3响应式数据设计(一)4个步骤写出一个响应式数据系统大家好,本篇将从0开始给大家分享响应式数据的实现,以及可能会遇 和之前一样,但是我们的代码变得更加清晰了。

Vue3响应式数据的核心实现就分享到这里了。感谢收看,一起学习一起进步

转载自:https://juejin.cn/post/7416664040401485851
评论
请登录