响应系统的作用与实现(上)

简介: 响应系统的作用与实现

如何避免无限递归?

为什么需要嵌套的副作用函数?

两个副作用函数之间会产生哪些影响?

4.1 响应式数据与副作用函数

什么是副作用函数

副作用函数指的是会产生副作用的函数

function effect() {
  document.body.innerText = "hello vue3";
}
let val = 1;
function effect() {
  val = 2;
}

effect 函数的执行会直接或间接影响其他函数的执行,这时我们说 effect 函数产生了副作用。副作用很容易产生,例如一个函数修改了全局变量,这其实也是一个副作用。

什么是响应式数据

当值变化后,副作用函数自动重新执行,如果能实现这个目标,那么对象 obj 就是响应式数据。

4.2 响应式数据的基本实现

如何才能让 obj 变成响应式数据呢?

拦截一个对象的读取和设置操作

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

当读取字段 obj.text 时,我们可以把副作用函数 effect 存储到一个“桶”里。

image.png

当设置 obj.text 时,再把副作用函数 effect 从“桶”里取出并执行即可。

image.png

在 ES2015 之前,只能通过 Object.defineProperty 函数实现,这也是 Vue.js 2 所采用的方式。在 ES2015+ 中,我们可以使用代理对象 Proxy 来实现,这也是 Vue.js 3 所采用的方式。

简单实现

<body></body>
<script>
  // 存储副作用函数的桶
  const bucket = new Set()
  // 原始数据
  const data = { text: 'hello world' }
  // 对原始数据的代理
  const obj = new Proxy(data, {
    // 拦截读取操作
    get(target, key) {
      // 将副作用函数 effect 添加到存储副作用函数的桶中
      bucket.add(effect)
      // 返回属性值
      return target[key]
    },
    // 拦截设置操作
    set(target, key, newVal) {
      // 设置属性值
      target[key] = newVal
      // 把副作用函数从桶里取出并执行
      bucket.forEach(fn => fn())
    }
  })
  function effect() {
    document.body.innerText = obj.text
  }
  effect()
</script>

4.3 设计一个完善的响应系统

一个响应系统的工作流程如下:

  • 当读取操作发生时,将副作用函数收集到“桶”中;
  • 当设置操作发生时,从“桶”中取出副作用函数并执行。

注册副作用函数的机制

为了副作用函数是一个匿名函数,也能够被正确地收集到 “桶”中。

<body></body>
<script>
// 存储副作用函数的桶
const bucket = new Set()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    bucket.add(activeEffect)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    bucket.forEach(fn => fn())
  }
})
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text // 匿名副作用函数与字段 obj.text 之间会建立响应联系
})
setTimeout(() => {
  obj.text2 = 'hello vue3' // 匿名副作用函数与字段 obj.text2 之间也会建立响应联系(这是一个问题)
}, 1000)
</script>

重新设计“桶”的数据结构

问题:

当读取属性时,无论读取的是哪一个属性,其实都一样,都会把副作用函数收集到“桶”里;当设置属性时,无论设置的是哪一个属性,也都会把“桶”里的副作用函数取出并执行。副作用函数与被操作的字段之间没有明确的联系。

使用一个 Set 数据结构作为存储副作用函数的“桶”。导致该问题(无差别建立响应问题)的根本原因是,我们没有在副作用函数与被操作的目标字段之间建立明确的联系。

targetkeyeffectFn树形结构。

image.png

<body></body>
<script>
// 存储副作用函数的桶
const bucket = new WeakMap()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    let depsMap = bucket.get(target)
    if (!depsMap) {
      bucket.set(target, (depsMap = new Map()))
    }
    let deps = depsMap.get(key)
    if (!deps) {
      depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect)
    // 返回属性值
    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())
  }
})
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text
})
setTimeout(() => {
  obj.text = 'hello vue3'
}, 1000)
</script>

image.png

WeakMap 和 Map 的区别

WeakMap 对 key 是弱引用,不影响垃圾回收器的工 作。据这个特性可知,一旦 key 被垃圾回收器回收,那么对应的键和 值就访问不到了。所以 WeakMap 经常用于存储那些只有当 key 所引 用的对象存在时(没有被回收)才有价值的信息,例如上面的场景中,如果 target 对象没有任何引用了,说明用户侧不再需要它了,这时垃圾回收器会完成回收任务。但如果使用 Map 来代替 WeakMap,那么即使用户侧的代码对 target 没有任何引用,这个 target 也不会被回收,最终可能导致内存溢出。

将桶逻辑封装


<body></body>
<script>
// 存储副作用函数的桶
const bucket = new WeakMap()
// 原始数据
const data = { text: 'hello world' }
// 对原始数据的代理
const obj = new Proxy(data, {
  // 拦截读取操作
  get(target, key) {
    // 将副作用函数 activeEffect 添加到存储副作用函数的桶中
    track(target, key)
    // 返回属性值
    return target[key]
  },
  // 拦截设置操作
  set(target, key, newVal) {
    // 设置属性值
    target[key] = newVal
    // 把副作用函数从桶里取出并执行
    trigger(target, key)
  }
})
function track(target, key) {
  let depsMap = bucket.get(target)
  if (!depsMap) {
    bucket.set(target, (depsMap = new Map()))
  }
  let deps = depsMap.get(key)
  if (!deps) {
    depsMap.set(key, (deps = new Set()))
  }
  deps.add(activeEffect)
}
function trigger(target, key) {
  const depsMap = bucket.get(target)
  if (!depsMap) return
  const effects = depsMap.get(key)
  effects && effects.forEach(fn => fn())
}
// 用一个全局变量存储当前激活的 effect 函数
let activeEffect
function effect(fn) {
  // 当调用 effect 注册副作用函数时,将副作用函数复制给 activeEffect
  activeEffect = fn
  // 执行副作用函数
  fn()
}
effect(() => {
  console.log('effect run')
  document.body.innerText = obj.text
})
setTimeout(() => {
  trigger(data, 'text')
}, 1000)
</script>

4.4 分支切换与 cleanup

副作用函数遗留情况,每次副作用函数执行时,我们可以先把它从所有与之关联的依赖集合中删除。

image.png

当副作用函数执行完毕后,会重新建立联系,但在新的联系中不会包含遗留的副作用函数。

要将一个副作用函数从所有与之关联的依赖集合中移除,就需要明确知道哪些依赖集合中包含它,因此我们需要重新设计副作用函数。

image.png

const data = {
    text: 'helloWorld',
    ok: true
}
let activeEffect
const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap
function effect(fn) {
    const effectFn = () => {
        // 副作用函数执行之前,将该函数从其所在的依赖集合中删除
        cleanup(effectFn)
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn
        fn()
    }
    effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn()
}
function cleanup(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {
        let deps = effectFn.deps[i] // 依赖集合
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0 // 重置effectFn的deps数组
}
const obj = new Proxy(data, {
    get(target, p, receiver) {
        track(target, p)
        return target[p]
    },
    set(target, p, value, receiver): boolean {
        target[p] = value
        trigger(target, p) // 把副作用函数取出并执行
        return true
    }
})
// track函数
function track(target, key: string | symbol) {
    if (!activeEffect) return // 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去
    // deps就是当前副作用函数存在联系的依赖集合
    // 将其添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
}
// trigger函数
function trigger(target, key: string | symbol) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set(effects)
    effectToRun && effectToRun.forEach(fn => {
        if (typeof fn === 'function') fn()
    })
}
effect(() => {
    console.log('effect run')
    document.body.innerHTML = obj.ok ? obj.text : 'no'
})
setTimeout(() => {
    obj.ok = false
}, 1000)
setTimeout(() => {
    obj.text = 'ds'
}, 2000)

在调用 forEach 遍历 Set 集合 时,如果一个值已经被访问过了,但该值被删除并重新添加到集合, 如果此时 forEach 遍历没有结束,那么该值会重新被访问

<body></body>
<script>
const set = new Set([1])
const newSet = new Set(set)
newSet.forEach(item => {
  set.delete(1)
  set.add(1)
  console.log(999)
})
</script>

4.5 嵌套的 effect 与 effect 栈

用全局变量 activeEffect 来存储通过 effect 函数注册的副作用函数,这意味着同一时刻 activeEffect 所存储的副作用函数只能有一个。当副作用函数发生嵌套时,内层副作用函数的执行会覆盖 activeEffect 的值,并且永远不会恢复到原来的值。这时如果再

有响应式数据进行依赖收集,即使这个响应式数据是在外层副作用函数中读取的,它们收集到的副作用函数也都会是内层副作用函数。

使用副作用栈

image.png

const data = {
    foo: true,
    bar: true
}
let activeEffect,
    effectStack = []
const bucket = new WeakMap() // 副作用函数的桶 使用WeakMap
function effect(fn) {
    const effectFn = () => {
        // 副作用函数执行之前,将该函数从其所在的依赖集合中删除
        cleanup(effectFn)
        // 当effectFn执行时,将其设置为当前激活的副作用函数
        activeEffect = effectFn
        effectStack.push(activeEffect) // 将当前副作用函数推进栈
        fn()
        // 当前副作用函数结束后,将此函数推出栈顶,并将activeEffect指向栈顶的副作用函数
        // 这样:响应式数据就只会收集直接读取其值的副作用函数作为依赖
        effectStack.pop()
        activeEffect = effectStack[effectStack.length - 1]
    }
    effectFn.deps = [] // activeEffect.deps用来存储所有与该副作用函数相关联的依赖集合
    effectFn()
}
function cleanup(effectFn) {
    for (let i = 0, len = effectFn.deps.length; i < len; i++) {
        let deps = effectFn.deps[i] // 依赖集合
        deps.delete(effectFn)
    }
    effectFn.deps.length = 0 // 重置effectFn的deps数组
}
const obj = new Proxy(data, {
    get(target, p, receiver) {
        track(target, p)
        return target[p]
    },
    set(target, p, value, receiver) {
        target[p] = value
        trigger(target, p) // 把副作用函数取出并执行
        return true
    }
})
// track函数
function track(target, key) {
    if (!activeEffect) return // 没有正在执行的副作用函数 直接返回
    let depsMap = bucket.get(target)
    if (!depsMap) { // 不存在,则创建一个Map
        bucket.set(target, depsMap = new Map())
    }
    let deps = depsMap.get(key) // 根据key得到 depsSet(set类型), 里面存放了该 target-->key 对应的副作用函数
    if (!deps) { // 不存在,则创建一个Set
        depsMap.set(key, (deps = new Set()))
    }
    deps.add(activeEffect) // 将副作用函数加进去
    // deps就是当前副作用函数存在联系的依赖集合
    // 将其添加到activeEffect.deps数组中
    activeEffect.deps.push(deps)
}
// trigger函数
function trigger(target, key) {
    const depsMap = bucket.get(target) // target Map
    if (!depsMap) return;
    const effects = depsMap.get(key) // effectFn Set
    const effectToRun = new Set(effects)
    effectToRun && effectToRun.forEach(fn => {
        if (typeof fn === 'function') fn()
    })
}
let tmp1, tmp2
effect(() => {
    console.log('eff1')
    effect(() => {
        console.log('eff2')
        tmp1 = obj.bar
    })
    tmp2 = obj.foo
})
// setTimeout(() => {
//     obj.foo = false
// }, 1000)
// setTimeout(() => {
//     obj.foo = false
// }, 2000)
obj.foo = false
obj.foo = false
obj.bar = false // 此处会执行3次! Vue的bug

响应系统的作用与实现(下)https://developer.aliyun.com/article/1392227

目录
相关文章
|
8天前
接口请求内容改变的问题.
接口请求内容改变的问题.
10 0
|
4月前
|
JSON 前端开发 JavaScript
关于我认识的请求方式
关于我认识的请求方式有三个
42 0
|
5月前
|
缓存 调度
响应系统的作用与实现 (下)
响应系统的作用与实现
48 0
|
5月前
|
API
能否解释授权状态如何影响请求处理?
授权状态对请求处理的影响主要体现在对请求的权限验证上。当一个请求发送到淘宝开放平台时,平台会首先验证该请求的授权状态。如果授权状态为true,表示当前授权有效,请求可以继续处理;如果授权状态为false,表示当前授权无效,请求将被拒绝处理。
|
5月前
全局响应返回处理
全局响应返回处理
20 0
|
10月前
|
JSON PHP 数据格式
响应 方式
响应 方式
怎么修改请求的参数和响应
怎么修改请求的参数和响应
71 0
|
JavaScript 开发者
构建 http 服务程序-根据不同请求做出不同响应|学习笔记
快速学习构建 http 服务程序-根据不同请求做出不同响应
90 0
构建 http 服务程序-根据不同请求做出不同响应|学习笔记
|
前端开发 测试技术
【测试平台开发】21. 完成发送接口请求显示响应头信息
【测试平台开发】21. 完成发送接口请求显示响应头信息
【测试平台开发】21. 完成发送接口请求显示响应头信息
Windiws10系统不显示可用网络的处理方法!
Windiws10系统不显示可用网络的处理方法!
420 0
Windiws10系统不显示可用网络的处理方法!