跳到主要内容

响应式✅

Vue2 和 Vue3 响应式系统的区别和原理?

答案
对比项Vue2 响应式Vue3 响应式
实现原理Object.definePropertyProxy
深度监听递归遍历对象,性能较低Proxy 默认支持深度监听,性能更优
动态属性需使用 Vue.set 手动添加支持动态添加属性
数组监听重写数组方法,无法监听索引和长度变化Proxy 默认支持索引和长度变化
支持数据结构仅支持对象和数组支持 Map、Set 等复杂数据结构
性能初始化时递归遍历,性能较低无需递归,性能更优
类型推导类型推导较弱更好的类型推导支持
APIdata, computed, watchreactive, ref, watchEffect

响应式系统核心逻辑包括

  1. 依赖收集 是通过首次执行渲染函数的时候,调用 setupRenderEffect 完成的依赖收集,依赖收集的核心原理就是基于一个全局变量记录当前的副作用函数,在读取数据的时候,判断当前是否存在副作用函数然后进行存储
  2. 副作用执行 依赖收集完成后,再后续数据更新的时候,会基于收集的依赖触发副作用函数执行
  3. 响应式优化 同步多次改变一个值,会 batch 更新,触发一次更新函数, 核心逻辑在 queueJob

一个简化版的响应式系统实现如下:

<script>
// 全员变量用来追踪变化
const trackMap = new WeakMap()
let activeEffect = null
// 自动追踪响应式
function effect (fn) {
  activeEffect = fn
  fn()
  activeEffect = null
}

// 依赖追踪
function track (target, key) {
  // 如果某个副作用函数触发了 get 方法,则将其添加到 trackMap 中
  if (activeEffect) {
    // 没有此 target 对应的追踪则直接添加
    if (!trackMap.has(target)) {
      trackMap.set(target, new Map())
    }
    // 没有对应该 key 的追踪直接添加
    if (!trackMap.get(target).has(key)) {
      trackMap.get(target).set(key, new Set())
    }
    // 添加这个副作用函数
    trackMap.get(target).get(key).add(activeEffect)
  }
}

// 依赖触发
function trigger (target, key) {
  // 如果存在对应的副作用函数,则触发它
  if (trackMap.has(target) && trackMap.get(target).has(key)) {
    // 触发这个 key 的副作用函数
    trackMap.get(target).get(key).forEach(fn => fn())
  }
}

// 一个简单的深层对象代理
function createProxy (data) {
  const proxy = new Proxy(data, {
    has (target, key) {
      track(target, key)
      return key in target
    },
    get (target, key) {
      track(target, key)
      return target[key]
    },
    set (target, key, value) {
      target[key] = value
      trigger(target, key)
      return true
    },
    deleteProperty (target, key) {
      delete target[key]
      trigger(target, key)
      return true
    }
  })
  for (const key in data) {
    const val = data[key]
    if (typeof val === 'object' && val !== null) {
      proxy[key] = createProxy(val)
    }
  }
  return proxy
}

// 返回响应式数据
function reactive (data) {
  return createProxy(data)
}

// 1. 创建响应式数据
const data = reactive({
  count: 0,
  demo: {
    count: 0
  }
})

function log () {
  console.log('count:', data.demo.count, data.count)
}

function logHas () {
  console.log('count has:', 'count' in data)
}

// 2. 追踪数据变化
effect(log) // 追踪   data.demo.count  data.count
effect(logHas) //  in 操作也能触发追踪

// 3. 触发数据变化, 自动触发副作用
data.demo.count++ // 深层属性修改
data.count++ // 浅层修改
delete data.demo.count // 删除触发响应

</script>

延伸阅读

watch 和 computed 有什么区别吗

答案

本质上 watch 和 computed 都是用来绑定一个响应式数据的变化的执行函数。核心差异如下

特性watchcomputed
触发方式数据变化时触发,通过 immediate 在创建 watch 的时候触发懒加载,只有数据变化,调用 .value 才会触发 computed 计算重新计算
触发时机在 Vue 实际上 watch 后续状态变化默认是异步执行的,Vue 会 batch 多次的修改,然后通过 Promise.resolve().then 触发一次副作用函数只有在数据变化时访问 .value 才会执行,且是同步触发
提示

如果直接调用 @vue/reactivity wactch 函数在状态变化后是同步触发的,这个控制是通过一个 scheduler 配置来控制的。在 Vue 中使用 watch 函数时,也可通过修改 { flush: 'sync' } 来实现同步触发。详见 回调触发时机

ref 和 reactive 有何区别吗

答案
  1. 数据类型ref 主要解决原始类型无法进行响应式包裹问题,也可以用于数组对象监听,reactive 用于非原始类型
  2. 取值ref 返回一个对象,在所有非模版中操作需要通过 .value 访问,你也可以使用 unref 函数来解包, reactive 返回的是一个代理对象,可直接访问

watch 和 watchEffect 使用有何区别

答案
功能wacthwatchEffect
依赖追踪需要手动指定依赖自动追踪依赖变化,降低了手动追踪依赖的复杂度
触发时机依赖变化时触发,可以通过 {immediate: true} 在申明时自动触发,默认异步立即执行一次,后续依赖变化时触发,默认异步执行
适用场景关注旧值的场景只关注新值的场景
提示

watch 和 watchEffect 都支持如下配置

interface Option {
flush?: 'pre' | 'post' | 'sync' // 触发时机 默认:'pre'
onTrack?: (event: DebuggerEvent) => void // 侦听器 onTrack 在追踪依赖的时候执行,仅在开发模式下工作。
onTrigger?: (event: DebuggerEvent) => void // 侦听器 onTrigger 在触发依赖的时候执行,仅在开发模式下工作。
}

watchEffect 还提供了 watchPostEffect 函数 对应 flush: 'post' 的配置, wachtSyncEffect 对应 flush: 'sync' 的配置

延伸阅读

provide inject 的使用场景?

答案
  1. provideinject 用于实现跨组件的状态共享。它允许祖先组件将数据“提供”给任意深度的后代组件,避免层层传递 props
  2. 在祖先组件中,通过 provide(key, value) 提供数据,在后代组件中通过 inject(key) 注入数据。 value 可以是任意数据,也可以传递响应式数据。
  3. 注意事项
    1. provide 必须是同步调用,异步调用后续 inject 无法获取到数据
    2. key 可以采用 Symbol 类型,避免命名冲突
    3. 在 typescript 中,可以利用 InjectionKey<T> 定义注入 value 的类型

说下 effectScope ?

答案
  1. effectScope 用来实现对副作用函数的批量管理,实现手动对副作用函数挂载和清除的控制。典型场景如下
    1. 在组件中使用 effectScope 来管理副作用函数,组件卸载时会自动清除 watch 等副作用函数,代码详见 scope.stop
    2. 在组件外部通过 effectScope 批量管理副作用函数
  2. effectScope 主要功能包括
    1. 采用 const scope = effectScope() 创建一个新的副作用域,通过 scope.run(fn) 执行副作用函数,fn 中可以使用 watch, watchEffect, computed 等函数监听响应式数据,调用 scope.stop() 停止副作用函数的执行,注意 effectScope 支持嵌套,停止时候默认嵌套的副作用绑定也会销毁,可以通过 effectScope(false) 来忽略对嵌套副作用域的管理
    2. getCurrentScope() 获取当前的副作用实例
    3. onScopeDispose(fn) 在副作用函数停止时执行的回调函数 fn

参考示例

<template>
  <div class="app-container">
    <h1>Vue 3 effectScope 示例</h1>
    <div class="tab-container">
      <div class="tabs">
        <button 
          v-for="tab in tabs" 
          :key="tab.id"
          :class="{ active: activeTab === tab.id }"
          @click="activeTab = tab.id"
        >
          {{ tab.name }}
        </button>
      </div>
      
      <div class="tab-content">
        <component :is="currentTabComponent"></component>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, markRaw } from 'vue'
import SharedState from './SharedState.vue'

// 标签页定义
const tabs = [
  { id: 'shared', name: '共享状态', component: markRaw(SharedState) },
]

// 当前激活的标签
const activeTab = ref('shared')

// 当前要显示的组件
const currentTabComponent = computed(() => {
  const tab = tabs.find(t => t.id === activeTab.value)
  return tab ? tab.component : null
})
</script>

<style>
.app-container {
  font-family: Arial, sans-serif;
  max-width: 800px;
  margin: 0 auto;
  padding: 20px;
}

.tab-container {
  margin-top: 20px;
  border: 1px solid #ddd;
  border-radius: 4px;
  overflow: hidden;
}

.tabs {
  display: flex;
  background-color: #f5f5f5;
  border-bottom: 1px solid #ddd;
}

.tabs button {
  padding: 10px 15px;
  background: none;
  border: none;
  cursor: pointer;
}

.tabs button.active {
  background-color: #fff;
  border-bottom: 2px solid #42b883;
  font-weight: bold;
}

.tab-content {
  padding: 20px;
}
</style>

延伸阅读

22%