当前位置:   article > 正文

浅谈Vue3响应式原理_vue3 reaction

vue3 reaction

什么是响应式?

响应式,就是一个变量依赖了其他变量,当被依赖的其他变量更新后该变量也要响应式的更新。

实现响应式思路

响应式的思路一般都是这样一个模型

  1. 定义某个数据为响应式数据,它会拥有收集访问它的函数的能力
  2. 定义观察函数,在这个函数内部去访问响应式数据,访问到响应式数据的某个key的时候,会建立一个依赖关系key -> reaction观察函数
  3. 检测到响应式数据的key的值更新的时候,会去重新执行一遍它所收集的所有reaction观察函数

我们看个例子:

// 响应式数据
const state = reactive({
  count: 0,
  age: 18
})
// 观察函数
const effect = effect(() => {
  console.log('effect: ' + state.count)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 用reactive包裹的数据叫做响应式数据
  • 在effect内部执行的函数叫观察函数

定义

reactive({ count: 0, age: 18 }),会让{ count: 0, age: 18 }这个普通的对象变成一个proxy,而后续对于这个proxy所有的get、set等操作都会被我们内部拦截下来。

访问

effect函数会先开启一个开始观察的开关,然后去执行
console.log('effect: ’ + state.count),执行到state.count的时候,proxy的get拦截到了对于state.count的访问,这时候就可以知道访问者是const effect = effect(() => { console.log('effect: ’ + state.count) })这个函数,那么就把这个函数作为count这个key值的观察函数收集在一个地方。

修改

下次对于state.count修改时,会去找count这个key下所有的观察函数,轮流执行一遍。

这样就实现了响应式模型。

Vue2和Vue3的区别

我们通过一个例子看下:

Object.defineProperty

<template>
  {{ obj.b }}
</template>
<script>
export default {
  data: {
    obj: { a: 0 },
  },
  mounted() {
    this.obj.b = 5
  }
}
</script>

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14

Object.defineProperty必须对于确定的key值进行响应式的定义。这就导致了如果data在初始化的时候没有b属性,那么后续对于b属性的赋值都不会触发Object.defineProperty中对于set的劫持。只能用一个额外的api Vue.set来解决。

Proxy

const raw = {}
const data = new Proxy(raw, {
    get(target, key) { },
    set(target, key, value) { }
})
  • 1
  • 2
  • 3
  • 4
  • 5

从例子中可以看出来:Proxy在定义的时候并不用关心key值,只要定义了get方法,那么后续对于data上任何属性的访问(哪怕是不存在的),都会触发get的劫持,set也是同理。这样Vue3中,对于需要定义响应式的值,初始化时的要求就没那么高了,只要保证它是个可以被Proxy接受的对象或者数组类型即可。

前置知识

Proxy

具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Proxy

Reflect

具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Reflect

WeakMap

具体可以参考MDN文档:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/WeakMap

dep

是dependence的缩写,也就是依赖

effect

指因某种原因导致产生结果,着重持续稳定的影响

track

指追踪、踪迹

trigger

指触发

我们先来看一张经典的Vue3响应式原理图,该图清晰的描述了vue3响应式原理的具体实现过程。

下面将围绕这个图仔细讲解

初始化阶段

在这里插入图片描述

初始化做了哪些事情呢?

我们先看来个例子:

首先clone Vue3源码,在 packages/reactivity 模块下调试,在项目根目录运行yarn dev reactivity,然后进入packages/reactivity目录找到产出的 dist/reactivity.global.js 文件,创建index.html文件,内容如下:
在这里插入图片描述

<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>响应式demo测试</title>
</head>
<body>
  <script src="./dist/reactivity.global.js"></script>
<script>
  const { reactive, effect } = VueReactivity
  const origin = {
    count: 0
  }
  const state = reactive(origin)
  const fn = () => {
    const count = state.count
    console.log(`set count to ${count}`)
  }
  effect(fn)
</script>
</body>
</html>
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

通过浏览器打开,在控制台我们输入state.count++,会输出set count to 1,即实现了响应式。

从上述代码中可以总结下初始化做了什么:

  1. 把origin对象转化成响应式的Proxy对象state
  2. 把函数fn()作为一个响应式的effect函数

如何把origin对象转化成响应式的Proxy对象state

Vue 3.0使用Proxy来代替之前的Object.defineProperty(),改写了对象的 getter/setter,完成依赖收集和响应触发。reactive() 函数主要实现逻辑如下:

import { handler } from './handlers.js'
export function reactive(target) {
  // handler包括get和set
  const observed = new Proxy(target, handler)
  return observed
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
proxy的handler

handler中主要包括get和set。

get收集依赖
  • 依赖收集在get操作的时候要调用。
  • 依赖收集中有依赖收集栈

get实现代码如下:

/** 劫持get访问 收集依赖 */
/**
 * 
 * @param {*} target 原始对象
 * @param {*} key 当前赋值属性
 * @param {*} receiver 可以简单理解为响应式proxy本身
 * @returns 
 */
function createGetter (target, key, receiver) {
  const res = Reflect.get(target, key, receiver) // 求值
  track(target, 'get', key)
  // 深层数据的劫持
  // 即在深层访问的时候,若访问的数据是个对象,就把这个对象也用reactive包装成proxy再返回,这样在最里面属性进行赋值操作的时候,也可以是响应式的
  return isObject(res)
    ? reactive(res)
    : res
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
set触发更新

set赋值操作的时候,本质上就是去检查这个key收集到了哪些reaction观察函数,然后依次触发。

set实现代码如下:

/** 劫持set访问 触发收集到的观察函数 */
function createSetter (target, key, value, receiver) {
  const hadKey = hasOwn(target, key) // 先检查一下这个key是不是新增的
  const oldValue = target[key] // 拿到旧值
  const result = Reflect.set(target, key, value, receiver) // 设置新值
  if (!hadKey) {
    // 新增key值时触发观察函数
    trigger(target, 'add', key)
  } else if (value !== oldValue) { 
    // 已存在的key的值发生变化时触发观察函数
    trigger(target, 'set', key)
  }
  return result
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
set核心逻辑总结
  • 当响应式数据进行赋值操作时,会收集对应的执行函数作为当前赋值属性的依赖
  • 当进行赋值操作后state.count = 1的操作,会触发对于state的set劫持,此时就会从key值的依赖收集里面找到观察函数,再重新执行一遍

把函数fn()作为一个响应式的effect函数

  • 普通的函数fn() 被effect()包裹之后,就会变成一个响应式的effect函数,而fn()也会被立即执行一次
  • fn() 里面有引用到Proxy对象的属性,所以这一步会触发对象的 getter,从而启动依赖收集
  • effect函数也会被压入一个名为"activeReactiveEffectStack"(此处为 effectStack)的栈中,供后续依赖收集的时候使用

effect.js实现如下:

// 接受用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖
/**
 * 观察函数
 * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖
 * 下次访问的key发生变化的时候 就会重新运行这个函数
 * @param {*} fn 
 * @returns 
 */
export function effect (fn) {
  // 构造一个effect
  // effect是包装了原始函数之后的观察函数
  // 在run的上下文中执行原始函数 可以收集到依赖
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  // 立即执行一次
  effect()

  // 返回出去 让外部也可以手动调用
  return effect
}

// 收集响应依赖的的函数,在get操作的时候要调用
/** 把函数包裹为观察函数 */
export function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      // 往栈里放入当前effect
      // 把当前的观察函数推入栈内 开始观察响应式proxy
      effectStack.push(effect)
      // 立即执行一遍fn()
      // fn()执行过程会完成依赖收集,会用到effect
      // 运行用户传入的函数 这个函数里访问proxy就会收集effect函数作为依赖了
      return fn(...args)
    } finally {
      // 完成依赖收集后从栈中扔掉这个effect
      // 运行完了永远要出栈
      effectStack.pop()
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41

到这里,整个初始化过程就结束了。

依赖收集阶段

先看下依赖收集阶段做了什么?

在这里插入图片描述

触发时机

就是在effect被立即执行,其内部的fn() 触发了Proxy对象的getter的时候。简单说,只要执行到类似state.count时,就会触发state的getter。

依赖收集的目的

建立一份”依赖收集表“,即图中的"targetMap"。targetMap是一个 WeakMap,其key值是当前的Proxy对象state代理前的对象origin,而 value则是该对象所对应的depsMap。

depsMap是一个Map,key值为触发getter时的属性值(这里是 count),而value则是触发过该属性值所对应的各个 effect函数。

通过一段伪代码看下:

const state = reactive({
  count: 0,
  age: 18
})
const effect1 = effect(() => {
  console.log('effect1: ' + state.count)
})
const effect2 = effect(() => {
  console.log('effect2: ' + state.age)
})
const effect3 = effect(() => {
  console.log('effect3: ' + state.count, state.age)
})
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13

在这段代码中targetMap表示如下:

即,{ target -> key -> dep } 的对应关系就建立起来了,依赖收集也就完成了。

targetMap、depsMap、dep 之间的关系可以通过一张经典的图来看下:


具体代码实现如下:

export const targetMap = new WeakMap()
export const effectStack = [] // 栈结构,依赖收集栈,供后续依赖收集的时候使用

export function track (target, operationType, key) {
  /** 从栈的末尾取到正在运行的observe包裹的函数(即观察函数) */
  const effect = effectStack[effectStack.length - 1]
  if (effect) {
    let depsMap = targetMap.get(target)
    if (depsMap === void 0) {
      targetMap.set(target, (depsMap = new Map()))
    }

    let dep = depsMap.get(key)
    if (dep === void 0) {
      // 如果这个key之前没有收集过观察函数 就新建一个, 然后set到整个value的存储里去
      depsMap.set(key, (dep = new Set()))
    }
    if (!dep.has(effect)) {
      // 把当前key对应的观察函数收集起来
      dep.add(effect)
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23

总结

  • 把effect推入effectStack后开始执行用户传入的函数
  • 在函数内访问响应式proxy的属性,又会触发get的拦截
  • 这时候get去effectStack找当前正在运行的effect,就可以成功的收集到依赖了。

effect观察函数

该函数接受一个用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖。

代码实现如下:

// 接受用户传入的函数,在这个函数内访问响应式数据才会去收集观察函数作为自己的依赖
/**
 * 观察函数
 * 在传入的函数里去访问响应式的proxy 会收集传入的函数作为依赖
 * 下次访问的key发生变化的时候 就会重新运行这个函数
 * @param {*} fn 
 * @returns 
 */
export function effect (fn) {
  // 构造一个effect
  // effect是包装了原始函数之后的观察函数
  // 在run的上下文中执行原始函数 可以收集到依赖
  const effect = function effect(...args) {
    return run(effect, fn, args)
  }
  // 立即执行一次
  effect()
  // 返回出去 让外部也可以手动调用
  return effect
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20

核心逻辑在run函数中:

// 收集响应依赖的的函数,在get操作的时候要调用
/** 把函数包裹为观察函数 */
export function run(effect, fn, args) {
  if (effectStack.indexOf(effect) === -1) {
    try {
      // 往栈里放入当前effect
      // 把当前的观察函数推入栈内 开始观察响应式proxy
      effectStack.push(effect)
      // 立即执行一遍fn()
      // fn()执行过程会完成依赖收集,会用到effect
      // 运行用户传入的函数 这个函数里访问proxy就会收集effect函数作为依赖了
      return fn(...args)
    } finally {
      // 完成依赖收集后从栈中扔掉这个effect
      // 运行完了永远要出栈
      effectStack.pop()
    }
  }
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19

核心思路总结

  • 把effect推入effectStack后开始执行用户传入的函数
  • 在函数内访问响应式proxy的属性,又会触发get的拦截
  • 这时get去effectStack找当前正在运行的effect,就可以成功的收集到依赖了

通过例子我们看下

const { reactive, effect } = VueReactivity
const origin = {
   count: 0
 }
 const state = reactive(origin)
 const fn = () => {
   const count = state.count
   console.log(`set count to ${count}`)
 }
 effect(fn)
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10

上述代码中,effect内部对于state的key值count的访问,会收集fn作为count的依赖。state.count++的操作,会触发对于state的set劫持,此时就会从key值的依赖收集里面找到fn,再重新执行一遍。

响应阶段

先通过一张图看下响应阶段做了什么?

在这里插入图片描述

说明

  • 当修改对象的某个属性值的时候,会触发对应的setter
  • setter里面的trigger() 函数会从依赖收集表里找到当前属性对应的各个 dep,然后把它们推入到effects和computedEffects(计算属性)队列中,最后通过scheduleRun()挨个执行里面的effect

代码实现

export function trigger (target, operationType, key) {
  // 取得对应的depsMap
  const depsMap = targetMap.get(target)
  if (depsMap === void 0) {
    return
  }
  // 取得对应的各个 dep
  const effects = new Set()
  if (key !== void 0) {
    const dep = depsMap.get(key)
    dep && dep.forEach(effect => {
      effects.add(effect)
    })
  }
  // 触发某些由循环触发的观察函数收集
  if (operationType === 'add' || operationType === 'set') {
    const iterationKey = Array.isArray(target) ? 'length' : Symbol('iterate')
    const dep = depsMap.get(iterationKey)
    dep && dep.forEach(effect => {
      effects.add(effect)
    })
  }
  // 简化版scheduleRun,挨个执行effect, 值更新时触发观察函数
  effects.forEach(effect => {
    effect()
  })
}
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27

总结

  1. 初始化reactive:new一个Proxy,参数是target和handler,target是原始对象,handler包括set和get
  2. 依赖收集:get函数中执行track进行依赖收集,建立依赖收集表,即收集观察函数推入到栈中。
  3. 响应阶段:set函数触发观察函数,遍历栈中的函数,执行effect,effect中执行run函数

参考网址:

https://juejin.cn/post/6938702983014121485#heading-7
https://juejin.cn/post/6844903959660855309#heading-2(精简非常详细)
https://juejin.cn/post/6844904050014552072#heading-14
https://juejin.cn/post/6844904050912133133

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/知新_RL/article/detail/283904
推荐阅读
相关标签
  

闽ICP备14008679号