看完这一篇还不懂Vue的响应式原理你来私信我!

235 阅读4分钟

笔者终于对Vue的响应式原理有了通透的理解,为了防止本人鱼的记忆影响我的学习成果,特此写了这篇文章记录。我可以说,耐下心来看完这篇文章,你对Vue响应式的理解会像笔者一样突飞猛进,手撕源码,吊打面试官!

副作用函数和响应式

在介绍具体原理之前,必须要介绍一下什么是副作用函数:

effect 函数的执行会直接或间接影响其他函数的执行,这时 我们说 effect 函数产生了副作用。

么噶,举个栗子:

// 全局变量 
let val = 1 
function effect() {
    val = 2 // 修改全局变量,产生副作用
}

接着举栗子:假设在一个副作用函数中读取了某个对象的属性,我们希望在属性变化的时候能够重新触发副作用函数的执行。

const obj = {text: 'vue2'}
function effect() {
    // 不执行
    document.body.innerHTML = obj.text
}
obj.text= 'vue3'

很显然,修改属性后effect函数并没有重新执行,那么让我们来给它增加响应式的属性吧,具体步骤如下:

  • 当副作用函数 effect 执行时,会触发字段 obj.text 的读取操作
  • 当修改 obj.text 的值时,会触发字段 obj.text 的设置操作

image.png

image.png

As we know 这种行为的实现就要依赖八股文中的Proxy啦

const bucket = new Set()
const data = { text: 'hello world' }
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    bucket.add(effect)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach((fn) => fn())
    return true
  },
})
function effect() {
  document.body.innerText = obj.text
}

effect()
setTimeout(() => (obj.text = 'hello vue3'), 3000)

创建了一个用于存储副作用函数的桶 bucket,它是 Set 类型。接着定义原始数据 data,obj 是原始数据的代理对象,我们分别设置了 get 和 set 拦截函数,用于拦截读取和设置操作。当读取属性时将副作用函数 effect 添加到桶里,即 bucket.add(effect),然后返回属性值;当设置属性值时先更新原 始数据,再将副作用函数从桶里取出并重新执行,这样我们就实现了响应式数据。

完善响应式原理

硬编码了副作用函数的名字(effect),导致一旦副作用函数的名字不叫 effect,那么这段代码就不能正确地工作了。哪怕副作用函数是一个匿名函数,也能够被正确地收集到 “桶”中。

为了实现这一点,需要提供一个用来注册副作用函数的机制:

const bucket = new Set()
const data = { text: 'hello world' }

let activeEffect

function effect(fn) {
  activeEffect = fn
  fn()
}
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    bucket.add(effect)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    bucket.forEach((fn) => fn())
    return true
  },
})
effect(() => {
  document.body.innerText = obj.text
})

setTimeout(() => (obj.text = 'hello vue3'), 3000)

可以看到使用一个匿名的副作用函数作为 effect 函数的参数。当 effect 函数执行时,首先会把匿名的副作用函数 fn 赋值给 全局变量 activeEffect。

结束了吗?

当然没有!上面的代码还存在着另一个问题:函数与被操作的目标字段之间建立明确的关系

怎么理解这段话呢?假设我们setTimeout函数里修改的是不存在的属性noExist,会发现还是会重新触发effect函数,那么这肯定是不理想的状况,怎么解决呢?

首先说明一下到底是哪些元素之间需要相互关联:

  • target:目标对象
  • key:目标对象的属性
  • effect:目标对象属性的副作用函数

那么就可以得出这么一张关系图:

image.png

图中bucket是一个weakmap(注意:这里使用weakmap是希望用户不需要target的时候,可以做垃圾回收),bucket 里面的key是目标对象,目标对象的存储形式是一个map,map的key是目标对象的属性,而每个属性对应一个Set集合存储着所有的依赖(副作用函数)。

const bucket = new WeakMap()
const data = { text: 'hello world' }

let activeEffect

function effect(fn) {
  activeEffect = fn
  fn()
}
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    if (!activeEffect) return
    let desMap = bucket.get(target)
    if (!desMap) {
      desMap = new Map()
      bucket.set(target, desMap)
    }
    let deps = desMap.get(key)
    if (!deps) {
      deps = new Set()
      desMap.set(key, deps)
    }
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    const desMap = bucket.get(target)
    if (!desMap) return
    const effects = desMap.get(key)
    effects && effects.forEach((fn) => fn())
    return true
  },
})
effect(() => {
  document.body.innerText = obj.text
})

setTimeout(() => (obj.text = 'hello vue3'), 3000)

代码中的get和set函数分别体现了依赖搜集和触发的过程,只有明确关系的依赖才能触发effect函数!

有的人可能在想,我擦,你的代码看的好乱。么问题,我们把搜集依赖的方法和触发副作用的方法分别起个名字:tracktrigger

const bucket = new WeakMap()
const data = { text: 'hello world' }

let activeEffect

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

function track(target, key) {
  let desMap = bucket.get(target)
  if (!desMap) {
    desMap = new Map()
    bucket.set(target, desMap)
  }
  let deps = desMap.get(key)
  if (!deps) {
    deps = new Set()
    desMap.set(key, deps)
  }
}

function trigger(target, key) {
  const desMap = bucket.get(target)
  if (!desMap) return
  const effects = desMap.get(key)
  effects && effects.forEach((fn) => fn())
}
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    if (!activeEffect) return
    track(target, key)
    return target[key]
  },
  set(target, key, newVal) {
    target[key] = newVal
    trigger(target, key)
    return true
  },
})
effect(() => {
  document.body.innerText = obj.text
})

setTimeout(() => (obj.text = 'hello vue3'), 3000)

是不是有种恍然大悟的赶脚!

参考:《Vue.js 设计与实现》and @前端小智

如果有错误,请评论区指出!如果有收获,请点赞收藏加关注!