Vue3 响应式系统 - 1 - 原理与实现

文章探讨了副作用函数的概念,以及如何通过响应式数据来处理这种影响。在Vue2和Vue3中,数据劫持是实现响应式的关键,文章通过代码示例展示了如何使用Proxy进行数据追踪和更改。接着,作者构建了一个简易的响应式系统,包括bucketMap来存储依赖关系,解决了数据改变时不必要的函数执行问题,以及处理了分支切换、清理和避免无限递归循环的策略。最后,文章提供了完整的响应式系统实现代码。

1. 响应式数据与副作用函数

当一个函数的执行直接或间接的影响了其他函数的执行,我们就称其产生了副作用,比如:
 

let val = 1;
const effect = () => {
  val = 2
}

const fn = () => {
  console.log(val)
}

当effect函数执行的时候,会改变val的值,会影响了fn函数中打印的结果。此时我们就称effect函数产生了副作用。理解了副作用函数,再来看下什么是响应式数据。有以下一段代码:

const obj = {
  text: '123'
}

effect = () => {
  console.log(obj.text)
}

我们希望当obj.text重新赋值的时候,effect函数可以重新执行,打印出最新的数据。如果这个需求能够实现的话,那么我们就可以称obj为响应式数据了。

2. 响应式数据的基本实现

通过对以上代码的观察发现,我们的要达到的目标是1. 当effect函数执行的时候,我们会读取obj.text;2. 当obj.text改变时,effect函数要重新执行。

想要完成这个需求就得劫持数据的读取和改变,我们知道vue2是通过object.defineProperty来完成数据劫持的,而vue3是利用了es6中的Proxy对象来完成数据劫持,这两个方案今天就不展开讨论了。如果当effect函数中的obj.text读取时就将effect函数给存储起来,然后当obj.text值改变时,我们把暂存起来的effect函数掏出来运行一遍就ok咯。写段代码尝试一下:

const bucket = new Set();

let activeFuction

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key) {
      if (activeFuction) {
        bucket.add(activeFuction)
      }
      return target[key]
    },
    set (target, key, value) {
      target[key] = value
      bucket.forEach(f => f())
      return true
    }
  })
}

const effect = (fn) => {
  activeFuction = fn
  fn()
}
const data = reactive({
  a: 1,
  b: 2,
  c: 3
})

effect(() => {
  console.log(data.a)
})
data.a++

运行之后发现,控制台打印出了1和2,但是问题来了,如果执行data.b++也会打印触发一次effect中传入的函数。奇了怪了,函数中没有访问data.b,也就是说data.b的get根本就没有触发,为啥会出现这样的现象呢?问题在于bucket和activeFuction!

3. 响应式系统雏形

如果bucket只是一个简单的Set结构,那么所有数据的依赖都会被收集到这个bucket中,当其中一个数据发生了改变就需要把bucket中收集的所有函数全部都执行一遍,这显然是不合理的。那么应该怎么做呢?首先需要一个合理的数据结构来存储这些函数,data、effect和bucketMap对应关系示例如下:
 

const data1 = {
  key1: 1,
  key2: 2
}
const data2 = {
  key1: 1,
  key2: 2
}
const effect1 = () => {
  console.log(data1.key1, data1.key2, data2.key2)
}

const effect2 = () => {
  console.log(data1.key1, data2.key1, data2.key2)
}

const effect3 = () => {
  console.log(data2.key2)
}

const effect4 = () => {
  console.log(data2.key2)
}

// 可以用json对象的形式来描述一下最终的bucketMap
bucketMap = {
  [data1]: {
    [data1.key1]: new Set([effect1, effect2]),
    [data1.key2]: new Set([effect1]),
  },
  [data2]: {
    [data2.key1]: new Set([effect2]),
    [data2.key2]: new Set([effect1, effect2, effect3, effect4]),
  }
}

也可以画个图来简单描述一下这个结构:

 如此设计,data的每一个属性都能有独立的bucket来存储依赖,当然track和trigger的逻辑也要相应的改变。

const bucketMap = new WeakMap()

let activeFuction

const track = (target, key) => {
  if (!activeFuction) return
  let targetMap = bucketMap.get(target)
  if (!targetMap) {
    bucketMap.set(target, new Map())
    targetMap = bucketMap.get(target)
  }
  let deps = targetMap.get(key)
  if (!deps) {
    targetMap.set(key, new Set())
    deps = targetMap.get(key)
  }
  deps.add(activeFuction)
}

const trigger = (target, key) => {
  let targetMap = bucketMap.get(target)
  if (!targetMap) return
  let deps = targetMap.get(key)
  if (!deps) return
  deps.forEach(f => f());
}

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key) {
      track(target, key)
      return target[key]
    },
    set (target, key, value) {
      target[key] = value
      trigger(target, key)
      return true
    }
  })
}

const effect = (fn) => {
  activeFuction = fn
  fn()
  // activeFuction = null
}

const data = reactive({
  a: 1,
  b: 2,
  c: 3
})

effect(() => {
  console.log(data.a)
})

data.a++
data.b++

这样的话,执行data.b++就不会打印data.a了吗? 试了一下 还是会的,为啥呢???是因为activeFuction,我们没有及时将activeFuction置为null,导致data.b++执行的时候首先是触发data.b的get,此时在其track中判断activeFuction是存在的,则将其加入到了data.b的依赖,接下来触发了data.b的set,就把data.b的依赖全都拉出来执行了。所以将以上代码中的activeFuction = null注释打开就可以啦。

4. 分支切换与cleanup

首先我们来明确一下什么是“分支切换”
 

const data = reactive({
  a: 1,
  b: 2,
  c: true
})

effect(() => {
  console.log(data.c ? data.a : data.b)
})

data.b = 10

这段代码执行以后,是不会触发打印的,但把data.b = 10之前加上一个data.c = false呢,至少在我们的预期中,data.b = 10是应该触发打印的,但实际上没有,这种问题就需要分支切换来解决了。

分析一下这几行代码,当effect执行的时候,会把传入的fn赋给activeFunction,然后fn执行,访问data.c,将activeFunction加入到data.c的bucket中,然后访问data.a,将activeFunction加入到data.a的bucket中,最后activeFunction = null。之后再改变任何的值都不会再次操作activeFunction这个变量,所以访问任何属性时都不会再收集依赖了。

要解决这个问题就得让每一次执行依赖的时候都重新操作activeFunction变量

const effect = (fn) => {
  const effectFn = () => {
    activeFuction = effectFn
    fn()
    activeFuction = null
  }
  effectFn()
}

这样一来,加入bucket的其实是effectFn,执行依赖的时候首先会将activeFunction重新赋值然后再执行fn。

const data = reactive({
  a: 1,
  b: 2,
  c: true
})

effect(() => {
  console.log(data.c ? data.a : data.b)
})

data.c = false

data.a = 10

这种情况下,我们的预期是不需要,但实际上打印了,我们需要在本依赖运行之前 在其对应的bucket中删掉该依赖,依赖运行时重新收集就好了,清空bucket的步骤就是cleanup,根据这个思路,我们应该还需要建立一个依赖 -> bucket关系。

const effect = (fn) => {
  const effectFn = () => {
    effectFn.deps.forEach(bucket => bucket.delete(effectFn));
    effectFn.deps = []
    activeFuction = effectFn
    fn()
    activeFuction = null
  }
  effectFn.deps = []
  effectFn()
}

Vue3中使用effectFn.deps来收集bucket,并在track的时候将bucketpush到effectFn.deps中来。

const track = (target, key) => {
  if (!activeFuction) return
  let targetMap = bucketMap.get(target)
  if (!targetMap) {
    bucketMap.set(target, new Map())
    targetMap = bucketMap.get(target)
  }
  let deps = targetMap.get(key)
  if (!deps) {
    targetMap.set(key, new Set())
    deps = targetMap.get(key)
  }
  deps.add(activeFuction)
  activeFuction.deps.push(deps) // 新增
}

运行一下会发现程序出现了无限循环,其实是因为trigger中试图遍历bucket执行依赖A,真正的fn执行之前操作bucket将依赖删除,然后fn执行时访问了data的属性,又在track中往bucket加入了依赖A,这就导致trigger中的遍历永远都不会结束了。

const trigger = (target, key) => {
  let targetMap = bucketMap.get(target)
  if (!targetMap) return
  let deps = targetMap.get(key)
  if (!deps) return
  new Set(deps).forEach(f => f());
}

我们需要在trigger中的new一个Set对象来遍历就可以解决这个问题了。

. 嵌套的effect

跑跑以下示例代码

const data = reactive({
  a: 1,
  b: 2,
  c: true
})

effect(() => {
  console.log('a:', data.a)
  effect(() => {
    console.log('b:', data.b)
  })
  console.log('c:', data.c)
})

data.a++
data.b++
data.c = false

data.a++会把a,b,c全部都打印出来,

data.b++只会把data.b打印出来,

data.c = false则不会打印任何的东西。

这结果显然是不符合预期的,其实解决这个问题的思路比较简单。就是把正在执行的fn全部推入一个栈中,activeFunction取栈顶fn,fn执行结束后将fn出栈,activeFunction继续取栈顶的fn,直到栈内无任何fn。

const effect = (fn) => {
  const effectFn = () => {
    effectFn.deps.forEach(bucket => bucket.delete(effectFn));
effectFn.deps = []
effectStack.push(effectFn)

    activeFuction = effectFn
    fn()
    effectStack.pop()
    activeFuction = effectStack[effectStack.length - 1]
  }
  effectFn.deps = []
  effectFn()
}

6. 避免无限递归循环

运行以下代码

const data = reactive({
  a: 1,
  b: 2,
  c: true
})

effect(() => {
  data.a++
  console.log('a:', data.a)
})


会产生这个问题,原因是fn中访问了data.a触发了get方法,把fn包了一层作为一个依赖收集到到了data.a对应的bucket中,然后又触发了data.a的set方法,于是又把bucket中的依赖拉出来执行,依赖执行的过程中又重复了以上的过程,最终导致栈溢出。其实这个问题的解决思路也不难想到,只需要在tigger方法遍历deps的时候加个activeFuction判断就可以了。 

const trigger = (target, key) => {
  let targetMap = bucketMap.get(target)
  if (!targetMap) return
  let deps = targetMap.get(key)
  if (!deps) return
  new Set(deps).forEach(f => {
    if (f !== activeFuction) f()
  });
}

7. 简易的响应式系统就此完成,完整代码如下。
 

const bucketMap = new WeakMap()
const effectStack = []
let activeFuction

const track = (target, key) => {
  if (!activeFuction) return
  let targetMap = bucketMap.get(target)
  if (!targetMap) {
    bucketMap.set(target, new Map())
    targetMap = bucketMap.get(target)
  }
  let deps = targetMap.get(key)
  if (!deps) {
    targetMap.set(key, new Set())
    deps = targetMap.get(key)
  }
  deps.add(activeFuction)
  activeFuction.deps.push(deps) // 新增
}

const trigger = (target, key) => {
  let targetMap = bucketMap.get(target)
  if (!targetMap) return
  let deps = targetMap.get(key)
  if (!deps) return
  new Set(deps).forEach(f => {
    if (f !== activeFuction) f()
  });
}

const reactive = (data) => {
  return new Proxy(data, {
    get (target, key) {
      track(target, key)
      return target[key]
    },
    set (target, key, value) {
      target[key] = value
      trigger(target, key)
      return true
    }
  })
}

const effect = (fn) => {
  const effectFn = () => {
    effectFn.deps.forEach(bucket => bucket.delete(effectFn));
    effectFn.deps = []
    effectStack.push(effectFn)

    activeFuction = effectFn
    fn()
    effectStack.pop()
    activeFuction = effectStack[effectStack.length - 1]
  }
  effectFn.deps = []
  effectFn()
}
评论 1
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值