Vue3 响应式来!

什么是响应式

js 本身没有响应式,思考这个结果

ini 复制代码
let count = 1
let double = count * 2
console.log(double) 
count = 2
console.log(double)

即使 count 有变化 第二次打印还是 2

Vue 响应式 流程

css 复制代码
┌───────────────────────────────┐
│        组件模板 (Template)     │
│  <span>{{ count }}</span>      │
│  <button @click="count++"/>    │
└──────────────┬────────────────┘
               │ 依赖追踪
               ▼
┌───────────────────────────────┐
│   响应式系统 (Reactive Core)   │
│   ref() / reactive() / effect()│
│   └─ 依赖收集 + 派发更新       │
└──────────────┬────────────────┘
               │ 通知变化
               ▼
┌───────────────────────────────┐
│        虚拟DOM (VNode)        │
│  计算出新的 UI 树             │
│  与旧树进行 diff 比较         │
└──────────────┬────────────────┘
               │
               ▼
┌───────────────────────────────┐
│          真正DOM更新          │
│   innerText / class / style   │
└───────────────────────────────┘

Vue 2 defineProperty

定义个一个对象 obj,使用defineProperty 代理了 count 属性。这样我们就对 obj 对象的 value 属性实现了拦截,读取 count 属性的时候执行 get 函数,修改 count 属性的时候执行 set 函数,并在 set函数内部重新计算了 double

javascript 复制代码
    let getDouble = (n) => n * 3
    let obj = {}
    let count = 1
    let double = getDouble(count)
    Object.defineProperty(obj, 'count', {
      get() {
        return count
      },
      set(val) {
        count = val
        double = getDouble(val)
      },
    })
    console.log(double) // 打印3
    obj.count = 2
    console.log(double) // 打印6 count 变化打印结果也会变

但会存在问题,如果此时删除 obj.count 属性,set 函数就不会执行,double 还是之前的数值。这也是为什么在 Vue 2 中,我们需要 $delete 一个专门的函数去删除数据。

arduino 复制代码
delete obj.count

console.log(double) //4 结果还是之前的值 

Vue 3 基于 Proxy

用户修改数据的时候触发 set 函数,从而实现自动更新 double 的功能。而且 Proxy 还完善了几个 definePropery 的缺陷,比如说可以监听到属性的删除。

Proxy 是针对对象来监听,而不是针对某个具体属性,所以不仅可以代理那些定义时不存在的属性,还可以代理更丰富的数据结构,比如 Map、Set 等,并且我们也能通过deleteProperty 实现对删除操作的代理。

ini 复制代码
    let getDouble = (n) => n * 2
    let count = 1
    let obj = {
      count: 1,
    }
    let double = getDouble(count)

    let proxy = new Proxy(obj, {
      get: function (target, prop) {
        return target[prop]
      },
      set: function (target, prop, value) {
        target[prop] = value
        if (prop === 'count') {
          double = getDouble(value)
        }
      },
      deleteProperty(target, prop) {
        delete target[prop]
        if (prop === 'count') {
          double = NaN
        }
      },
    })
    console.log(obj.count, double) // 1,2
    proxy.count = 2
    console.log(obj.count, double) // 2,4
    delete proxy.count
    // 删除属性后, undefined NaN 此处删除产生了效果
    console.log(obj.count, double)

Vue 3 的 reactive 函数可以把一个对象变成响应式数据,而 reactive 就是基于Proxy 实现的。我们还可以通过 watchEffect,在 obj.count 修改之后,执行数据的打印

javascript 复制代码
    import { reactive, computed, watchEffect } from 'vue'
    let obj = reactive({
      count: 1,
    })
    let double = computed(() => obj.count * 2)
    obj.count = 2
    watchEffect(() => {
      console.log('数据被修改', obj.count, double.value)
    })

Vue3 另一种方式 利用对象的 get 和 set 函数来进行监听,这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现。在下面的代码中,拦截了count 的 value 属性,并且拦截了 set 操作,也能实现类似的功能。

csharp 复制代码
    let getDouble = (n) => n * 2
    let _value = 1
    double = getDouble(_value)
    let count = {
      get value() {
        return _value
      },
      set value(val) {
        _value = val
        double = getDouble(_value)
      },
    }
    console.log(count.value, double) // 1,2
    count.value = 2
    console.log(count.value, double) // 2,4

响应数据进阶

将前面说的todolist 数据存在本地,解决刷新后数据丢失问题

javascript 复制代码
    import { ref, watchEffect, computed } from 'vue'
    let title = ref('')
    let todos = ref(JSON.parse(localStorage.getItem('todos') || '[]'))
    watchEffect(() => {
      localStorage.setItem('todos', JSON.stringify(todos.value))
    })
    function addTodo() {
      todos.value.push({
        title: title.value,
        done: false,
      })
      title.value = ''
    }

还可以直接抽离一个 useStorage 函数,在响应式的基础之上,把任意数据响应式的变化同步到本地存储

javascript 复制代码
    function useStorage(name, value = []) {
      let data = ref(JSON.parse(localStorage.getItem(name) || '[]'))
      watchEffect(() => {
        localStorage.setItem(name, JSON.stringify(data.value))
      })
      return data
    }

把 ref 变成 useStorage,这也是 Composition API最大的优点,也就是可以任意拆分出独立的功能。

csharp 复制代码
    let todos = useStorage('todos', [])
    function addTodo() {
      // writ your code
    }

甚至可以把想要的数据都变成响应式的形式

javascript 复制代码
    export default function useFavicon(newIcon) {
      const favicon = ref(newIcon)
      const updateIcon = (icon) => {
        document.head
          .querySelectorAll(`link[rel*="icon"]`)
          .forEach((el) => (el.href = `${icon}`))
      }
      const reset = () => (favicon.value = '/favicon.ico')
      watch(favicon, (i) => {
        updateIcon(i)
      })
      return { favicon, reset }
    }

    // 使用时
    import useFavicon from './utils/favicon'
    let { favicon } = useFavicon()
    function loading() {
      favicon.value = '/geek.png'
    }

Vueuse

Vue 社区中其实已经有一个类似的工具集合,也就是 VueUse,它把开发中常见的属性都封装成为响应式函数。避免了重复造轮子,可以直接用。

比如这些常用的

javascript 复制代码
// 状态与存储
import { useStorage, useToggle } from '@vueuse/core'

const todos = useStorage('todos', [])
const [visible, toggleVisible] = useToggle(true)
javascript 复制代码
// DOM 与交互
import { useMouse } from '@vueuse/core'

const { x, y } = useMouse() // 响应式坐标
go 复制代码
// 网络与异步
import { useFetch } from '@vueuse/core'

const { data, isFetching, error } = useFetch('/api/todos').json()
javascript 复制代码
// 时间与动画
import { useNow, useIntervalFn } from '@vueuse/core'

const now = useNow()
useIntervalFn(() => console.log('每秒触发'), 1000)
arduino 复制代码
// 剪贴板、窗口与系统
import { useClipboard, useWindowSize } from '@vueuse/core'

const { copy } = useClipboard()
const { width, height } = useWindowSize()
javascript 复制代码
// 组合应用类
import { useDebounceFn } from '@vueuse/core'

const search = useDebounceFn(() => console.log('搜索触发'), 500)
相关推荐
芝士麻雀3 小时前
Zustand 深度解析:原理、源码与最佳实践
前端·react.js·前端框架
Dontla3 小时前
JS睡眠函数(JS sleep()函数、JS单线程、Event Loop事件循环)假睡眠
开发语言·javascript·ecmascript
fruge3 小时前
前端性能优化实战指南:从首屏加载到用户体验的全面提升
前端·性能优化·ux
ZYMFZ3 小时前
Redis主从复制与哨兵集群
前端·git·github
lumi.3 小时前
前端本地存储技术笔记:localStorage 与 sessionStorage 详解
前端·javascript·笔记
旧雨散尘3 小时前
【react】初学react5-react脚手架搭建中的小众知识
前端·react.js·前端框架
炫饭第一名3 小时前
🌍🌍🌍字节一面场景题:异步任务调度器
前端·javascript·面试
烛阴4 小时前
Lua字符串的利刃:模式匹配的艺术与实践
前端·lua
奇舞精选4 小时前
一文了解 Server-Sent Events (SSE):构建高效的服务器推送应用
前端