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)
相关推荐
hh随便起个名6 小时前
力扣二叉树的三种遍历
javascript·数据结构·算法·leetcode
我是小路路呀7 小时前
element级联选择器:已选中一个二级节点,随后又点击了一个一级节点(仅浏览,未确认选择),此时下拉框失去焦点并关闭
javascript·vue.js·elementui
程序员爱钓鱼7 小时前
Node.js 编程实战:文件读写操作
前端·后端·node.js
PineappleCoder7 小时前
工程化必备!SVG 雪碧图的最佳实践:ID 引用 + 缓存友好,无需手动算坐标
前端·性能优化
JIngJaneIL8 小时前
基于springboot + vue古城景区管理系统(源码+数据库+文档)
java·开发语言·前端·数据库·vue.js·spring boot·后端
敲敲了个代码8 小时前
隐式类型转换:哈基米 == 猫 ? true :false
开发语言·前端·javascript·学习·面试·web
澄江静如练_8 小时前
列表渲染(v-for)
前端·javascript·vue.js
JustHappy8 小时前
「chrome extensions🛠️」我写了一个超级简单的浏览器插件Vue开发模板
前端·javascript·github
Loo国昌8 小时前
Vue 3 前端工程化:架构、核心原理与生产实践
前端·vue.js·架构