{T}

Vue3 响应式机制

响应式

响应式一直都是 Vue 的特色功能之一。但是在 JavaScript 里面的变量,是没有响应式这个概念的

javascript
let count = 1
let double = count * 2
console.log(double) // 2
count = 2
console.log(double) // 2

double 的值是根据 count 的值乘以二计算而得到的,如果想让 doube 能够跟着 count 的变化而变化,那么就需要在每次 count 的值修改后,重新计算 double

先把计算 doube 的逻辑封装成函数,然后在修改完 count 之后,再执行一遍,你就会得到最新的 double 值

javascript
let count = 1

// 计算过程封装成函数
let getDouble = n=>n*2
let double = getDouble(count)
console.log(double)

count = 2
// 重新计算double,这里不能自动执行对double的计算
double = getDouble(count)
console.log(double)

使用 JavaScript 的某种机制把 count 包裹一层,每当对 count 进行修改时,就去同步更新 double 的值,这就算是响应式的雏形

响应式原理

Vue 中用过三种响应式解决方案,分别是 defineProperty、Proxy 和 value setter

defineProperty

javascript
let getDouble = n=>n*2
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)  // 打印 2
obj.count = 2
console.log(double) // 打印 4  有种自动变化的感觉

defineProperty API 作为 Vue 2 实现响应式的原理,它的语法中也有一些缺陷。比如下面代码中删除 obj.count 属性,set 函数就不会执行,double 还是之前的数值。这也是为什么在 Vue 2 中需要 $delete 一个专门的函数去删除数据

javascript
delete obj.count
console.log(double) // doube还是4

Proxy

Vue3 响应式机制是基于 Proxy 实现的,Proxy 的重要意义在于它解决了 Vue 2 响应式的缺陷

javascript
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)
proxy.count = 2
console.log(obj.count,double) 

delete proxy.count
// 删除属性后,我们打印log时,输出的结果就会是 undefined NaN
console.log(obj.count,double) 

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

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

Vue3 的 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)
})

对象的 get 和 set 函数

在 Vue 3 中还有另一个响应式实现的逻辑,就是利用对象的 get 和 set 函数来进行监听。这种响应式的实现方式,只能拦截某一个属性的修改,这也是 Vue 3 中 ref 这个 API 的实现

javascript
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)
count.value = 2
console.log(count.value,double)

三种实现原理的对比

实现原理definePropertyProxyvalue setter
实际场景Vue 2 响应式Vue3 reactiveVue3 ref
优势兼容性基于 Proxy 实现真正的拦截实现简单
劣势数组和属性删除等拦截不了兼容不了 IE11只拦截 value 属性
实际应用Vue 2Vue3 复杂数据结构Vue3 简单属性

定制响应式数据

使用 <script setup> 重构之后的 todolist 的代码,使用 watchEffect 数据变化之后会把数据同步到 localStorage 之上,这样就实现todolist 和本地存储的同步

Vue SFC
<template>
<div>
  <input type="text" v-model="title" @keydown.enter="addTodo"/>
  <button v-if="active < all" @click="clear">清理</button>
  <ul v-if="todos.length">
    <li v-for="todo in todos">
      <input type="checkbox" v-model="todo.done"/>
      <span :class="{ done: todo.done }"> {{ todo.title }}</span>
  </li>
  </ul>
  <div v-else>暂无数据</div>
  <div>
    全选<input type="checkbox" v-model="allDone"/>
    <span> {{ active }} / {{ all }} </span>
  </div>
  </div>
</template>

<script setup>
  import {ref, computed} from "vue";
  import {useStorage} from '../use/useStorage'
  let todos = useStorage('todos', [])

  function addTodo() {
    todos.value.push({
      title: title.value,
      done: false,
    });
    title.value = "";
  }

  let title = ref("");

  function clear() {
    todos.value = todos.value.filter((v) => !v.done);
  }

  let active = computed(() => {
    return todos.value.filter((v) => !v.done).length;
  });

  let all = computed(() => todos.value.length);

  let allDone = computed({
    get: function () {
      return active.value === 0;
    },
    set: function (value) {
      todos.value.forEach((todo) => {
        todo.done = value;
      });
    },
  });
</script>

useStorage 函数

useStorage 函数在响应式的基础之上,把任意数据响应式的变化同步到本地存储。ref 从本地存储中获取数据,封装成响应式并且返回,watchEffect 中做本地存储的同步,useStorage 这个函数可以抽离成一个文件,放在工具函数文件夹中

javascript
import {ref, watchEffect, computed} from "vue";

export function useStorage(name, value = []) {
  let data = [];
  
  if (localStorage.getItem(name)!="undefined"){
    data = ref(JSON.parse(localStorage.getItem(name)))
  }else{
    data=ref(value)
  }
  
  watchEffect(() => {
    localStorage.setItem(name, JSON.stringify(data.value))
  })
  
  return data
}

社区也有非常优秀的 Vueuse 工具库,包含了大量类似 useStorage 的工具函数库

可以把日常开发中用到的数据,无论是浏览器的本地存储,还是网络数据,都封装成响应式数据,统一使用响应式数据开发的模式。基于响应式的开发模式,还可以按照类似的原理,把需要修改的数据,都变成响应式。比如可以在 loading 状态下,去修改浏览器的小图标 favicon。和本地存储类似,修改 favicon 时需要找到 head 中有 icon 属性的标签

useFavicon 函数

对图标的对应修改的操作封装成了 useFavicon 函数,并且通过 ref 和 watch 的包裹,我们还把小图标变成了响应式数据

javascript
import {ref,watch} from 'vue'

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 }
} 

可以通过响应式的方式去修改和使用小图标,通过对 faivcon.value 的修改就可以随时更换网站小图标。实现了在点击按钮之后,修改了网页的图标的操作

Vue SFC
<script setup>
  import useFavicon from './utils/favicon'
  let {favicon}  = useFavicon()
  function loading(){
    favicon.value = '/demo.png'
  }
</script>

<template>
  <button @click="loading">123</button>
</template>

Vueuse 工具包

VueUse 官方的介绍说这是一个 Composition API 的工具集合,适用于 Vue 2.x 或者 Vue 3.x,用起来和 React Hooks 还挺像的

bash
npm install @vueuse/core

使用 useFullscreen 来返回全屏的状态和切换全屏的函数。这样就不需要考虑浏览器全屏的 API,而是直接使用 VueUse 响应式数据和函数就可以很轻松地在项目中实现全屏功能

Vue SFC
<template>
  <h1 @click="toggle">click</h1>
</template>

<script setup>
  import { useFullscreen } from '@vueuse/core'
  const { isFullscreen, enter, exit, toggle } = useFullscreen()
</script>

useFullscreen 的封装逻辑和 useStorage 类似,都是屏蔽了浏览器的操作,把所有需要用到的状态和数据都用响应式的方式统一管理