深度解析?Vue3?的響應(yīng)式機(jī)制
什么是響應(yīng)式
響應(yīng)式一直都是 Vue 的特色功能之一;與之相比,JavaScript 里面的變量,是沒有響應(yīng)式這個(gè)概念的;你在學(xué)習(xí) JavaScript 的時(shí)候首先被灌輸?shù)母拍?,就是代碼是自上而下執(zhí)行的;
我們看下面的代碼,代碼在執(zhí)行后,打印輸出的兩次 double 的結(jié)果也都是 2;即使 我們修改了代碼中 count 的值后,double 的值也不會有任何改變
let?count?= 1 let?double?= count?* 2 count?= 2
double 的值是根據(jù) count 的值乘以二計(jì)算而得到的,如果現(xiàn)在我們想讓 doube 能夠跟著 count 的變化而變化,那么我們就需要在每次 count 的值修改后,重新計(jì)算 double
比如,在下面的代碼,我們先把計(jì)算 doube 的邏輯封裝成函數(shù),然后在修改完 count 之 后,再執(zhí)行一遍,你就會得到最新的 double 值
let?count?= 1 // 計(jì)算過程封裝成函數(shù) let?getDouble?= n=>n*2 //箭頭函數(shù) let?double?= getDouble(count) count?= 2 //?重新計(jì)算double?,這里我們不能自動執(zhí)行對double的計(jì)算 double?= getDouble(count)
實(shí)際開發(fā)中的計(jì)算邏輯會比計(jì)算 doube 復(fù)雜的多,但是都可以封裝成一個(gè)函數(shù)去執(zhí)行;下 一步,我們要考慮的是,如何讓 double 的值得到自動計(jì)算
如果我們能讓 getDouble 函數(shù)自動執(zhí)行,也就是如下圖所示,我們使用 JavaScript 的某種機(jī)制,把 count 包裹一層,每當(dāng)對 count 進(jìn)行修改時(shí),就去同步更新 double 的值,那 么就有一種 double 自動跟著 count 的變化而變化的感覺,這就算是響應(yīng)式的雛形了
響應(yīng)式原理
響應(yīng)式原理是什么呢?Vue 中用過三種響應(yīng)式解決方案,分別是 defineProperty、Proxy 和 value setter我們首先來看 Vue 2 的 defineProperty API
這里我結(jié)合一個(gè)例子來說明,在下面的代碼中,我們定義個(gè)一個(gè)對象 obj,使用 defineProperty 代理了 count 屬性;這樣我們就對 obj 對象的 value 屬性實(shí)現(xiàn)了攔截,讀取 count 屬性的時(shí)候執(zhí)行 get 函數(shù),修改 count 屬性的時(shí)候執(zhí)行 set 函數(shù),并在 set 函數(shù)內(nèi)部重新計(jì)算了 double
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 有種自動變化的感覺
這樣我們就實(shí)現(xiàn)了簡易的響應(yīng)式功能,在課程的第四部分,我還會帶著你寫一個(gè)更完善的響應(yīng)式系統(tǒng)
但 defineProperty API 作為 Vue 2 實(shí)現(xiàn)響應(yīng)式的原理,它的語法中也有一些缺陷;比如在下面代碼中,我們刪除 obj.count 屬性,set 函數(shù)就不會執(zhí)行,double 還是之前的數(shù)值;這也是為什么在 Vue 2 中,我們需要 $delete
一個(gè)專門的函數(shù)去刪除數(shù)據(jù)
delete obj.count console.log(double) // doube還是4
Vue 3 的響應(yīng)式機(jī)制是基于 Proxy 實(shí)現(xiàn)的;就 Proxy 這個(gè)名字來說,你也能看出來這是代理的意思,Proxy 的重要意義在于它解決了 Vue 2 響應(yīng)式的缺陷
我們看下面的代碼,在其中我們通過 new Proxy 代理了 obj 這個(gè)對象,然后通過 get、set 和 deleteProperty 函數(shù)代理了對象的讀取、修改和刪除操作,從而實(shí)現(xiàn)了響應(yīng)式的功能
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時(shí),輸出的結(jié)果就會是 undefined NaN console.log(obj.count,double)
我們從這里可以看出 Proxy 實(shí)現(xiàn)的功能和 Vue 2 的 definePropery 類似,它們都能夠在用戶修改數(shù)據(jù)的時(shí)候觸發(fā) set 函數(shù),從而實(shí)現(xiàn)自動更新 double 的功能。而且 Proxy 還完善了幾個(gè) definePropery 的缺陷,比如說可以監(jiān)聽到屬性的刪除
Proxy 是針對對象來監(jiān)聽,而不是針對某個(gè)具體屬性,所以不僅可以代理那些定義時(shí)不存在的屬性,還可以代理更豐富的數(shù)據(jù)結(jié)構(gòu),比如 Map、Set 等,并且我們也能通過 deleteProperty 實(shí)現(xiàn)對刪除操作的代理
當(dāng)然,為了幫助你理解 Proxy,我們還可以把 double 相關(guān)的代碼都寫在 set 和 deleteProperty 函數(shù)里進(jìn)行實(shí)現(xiàn),在課程的后半程我會帶你做好更完善的封裝;比如下面代碼中,Vue 3 的 reactive 函數(shù)可以把一個(gè)對象變成響應(yīng)式數(shù)據(jù),而 reactive 就是基于 Proxy 實(shí)現(xiàn)的;我們還可以通過 watchEffect,在 obj.count 修改之后,執(zhí)行數(shù)據(jù)的打印
import {reactive,computed,watchEffect} from 'vue' let obj = reactive({ count:1 }) let double = computed(()=>obj.count*2) obj.count = 2 watchEffect(()=>{ console.log('數(shù)據(jù)被修改了',obj.count,double.value) })
有了 Proxy 后,響應(yīng)式機(jī)制就比較完備了;但是在 Vue 3 中還有另一個(gè)響應(yīng)式實(shí)現(xiàn)的邏輯,就是利用對象的 get 和 set 函數(shù)來進(jìn)行監(jiān)聽,這種響應(yīng)式的實(shí)現(xiàn)方式,只能攔截某一個(gè)屬性的修改,這也是 Vue 3 中 ref 這個(gè) API 的實(shí)現(xiàn)
在下面的代碼中,我們攔截了 count 的 value 屬性,并且攔截了 set 操作,也能實(shí)現(xiàn)類似的功能
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)
三種實(shí)現(xiàn)原理的對比表格如下,幫助你理解三種響應(yīng)式的區(qū)別
實(shí)現(xiàn)原理 | defineProperty | Proxy | value setter |
---|---|---|---|
實(shí)際場景 | Vue 2 響應(yīng)式 | Vue 3 reactive | Vue 3 ref |
優(yōu)勢 | 兼容性 | 基于proxy實(shí)現(xiàn)真正的攔截 | 實(shí)現(xiàn)簡單 |
劣勢 | 數(shù)組和屬性刪除等攔截不了 | 兼容不了 IE11 | 只攔截了 value 屬性 |
實(shí)際應(yīng)用 | Vue 2 | Vue 3 復(fù)雜數(shù)據(jù)結(jié)構(gòu) | Vue 3 簡單數(shù)據(jù)結(jié)構(gòu) |
定制響應(yīng)式數(shù)據(jù)
簡單入門響應(yīng)式的原理后,接下來我們學(xué)習(xí)一下響應(yīng)式數(shù)據(jù)在使用的時(shí)候的進(jìn)階方式;我們看下使用 <script setup>
重構(gòu)之后的 todolist 的代碼;這段代碼使用 watchEffect,數(shù)據(jù)變化之后會把數(shù)據(jù)同步到 localStorage 之上,這樣我們就實(shí)現(xiàn)了 todolist 和本地存儲的同步
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 = ""; }
更進(jìn)一步,我們可以直接抽離一個(gè) useStorage 函數(shù),在響應(yīng)式的基礎(chǔ)之上,把任意數(shù)據(jù)響應(yīng)式的變化同步到本地存儲;我們先看下面的這段代碼,ref 從本地存儲中獲取數(shù)據(jù),封裝成響應(yīng)式并且返回,watchEffect 中做本地存儲的同步,useStorage 這個(gè)函數(shù)可以抽離成一個(gè)文件,放在工具函數(shù)文件夾中
function useStorage(name, value=[]){ let data = ref(JSON.parse(localStorage.getItem(name)||'[]')) watchEffect(()=>{ localStorage.setItem(name,JSON.stringify(data.value)) }) return data }
在項(xiàng)目中我們使用下面代碼的寫法,把 ref 變成 useStorage,這也是 Composition API 最大的優(yōu)點(diǎn),也就是可以任意拆分出獨(dú)立的功能
let todos = useStorage('todos',[]) function addTodo() { ...code }
現(xiàn)在,你應(yīng)該已經(jīng)學(xué)會了在 Vue 內(nèi)部進(jìn)階地使用響應(yīng)式機(jī)制,去封裝獨(dú)立的函數(shù);在后續(xù)的實(shí)戰(zhàn)應(yīng)用中,我們也會經(jīng)常對通用功能進(jìn)行封裝;如下圖所示,我們可以把日常開發(fā)中用到的數(shù)據(jù),無論是瀏覽器的本地存儲,還是網(wǎng)絡(luò)數(shù)據(jù),都封裝成響應(yīng)式數(shù)據(jù),統(tǒng)一使用響應(yīng)式數(shù)據(jù)開發(fā)的模式;這樣,我們開發(fā)項(xiàng)目的時(shí)候,只需要修改對應(yīng)的數(shù)據(jù)就可以了
基于響應(yīng)式的開發(fā)模式,我們還可以按照類似的原理,把我們需要修改的數(shù)據(jù),都變成響應(yīng)式;比如,我們可以在 loading 狀態(tài)下,去修改瀏覽器的小圖標(biāo) favicon;和本地存儲類似,修改 favicon 時(shí),我們需要找到 head 中有 icon 屬性的標(biāo)簽
在下面的代碼中,我們把對圖標(biāo)的對應(yīng)修改的操作封裝成了 useFavicon 函數(shù),并且通過 ref 和 watch 的包裹,我們還把小圖標(biāo)變成了響應(yīng)式數(shù)據(jù)
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}`) } watch( favicon, (i) => { updateIcon(i) } ) return {favicon,reset} }
這樣在組件中,我們就可以通過響應(yīng)式的方式去修改和使用小圖標(biāo),通過對 faivcon.value 的修改就可以隨時(shí)更換網(wǎng)站小圖標(biāo);下面的代碼,就實(shí)現(xiàn)了在點(diǎn)擊按鈕之后,修改了網(wǎng)頁的圖標(biāo)為 geek.png 的操作
<script setup> import useFavicon from './utils/favicon' let {favicon} = useFavicon() function loading(){ favicon.value = '/geek.png' } </script> <template> <button @click="loading">123</button> </template>
Vueuse 工具包
我們自己封裝的 useStorage,算是把 localStorage 簡單地變成了響應(yīng)式對象,實(shí)現(xiàn)數(shù)據(jù)的更新和localStorage 的同步;同理,我們還可以封裝更多的類似 useStorage 函數(shù)的其他 use 類型的函數(shù),把實(shí)際開發(fā)中你用到的任何數(shù)據(jù)或者瀏覽器屬性,都封裝成響應(yīng)式數(shù)據(jù),這樣就可以極大地提高我們的開發(fā)效率
Vue 社區(qū)中其實(shí)已經(jīng)有一個(gè)類似的工具集合,也就是 VueUse,它把開發(fā)中常見的屬性都封裝成為響應(yīng)式函數(shù)
VueUse 趁著這一波 Vue 3 的更新,跟上了響應(yīng)式 API 的潮流;VueUse 的官方的介紹說這是一個(gè) Composition API 的工具集合,適用于 Vue 2.x 或者 Vue 3.x,用起來和 React Hooks 還挺像的
在項(xiàng)目目錄下打開命令行里,我們輸入如下命令,來進(jìn)行 VueUse 插件的安裝:
npm install @vueuse/core
然后,我們就先來使用一下 VueUse;在下面這段代碼中,我們使用 useFullscreen 來返回全屏的狀態(tài)和切換全屏的函數(shù);這樣,我們就不需要考慮瀏覽器全屏的 API,而是直接使用 VueUse 響應(yīng)式數(shù)據(jù)和函數(shù)就可以很輕松地在項(xiàng)目中實(shí)現(xiàn)全屏功能
<template> <h1 @click="toggle">click</h1> </template> <script setup> import { useFullscreen } from '@vueuse/core' const { isFullscreen, enter, exit, toggle } = useFullscreen() </script>
useFullscreen 的封裝邏輯和 useStorage 類似,都是屏蔽了瀏覽器的操作,把所有我們需要用到的狀態(tài)和數(shù)據(jù)都用響應(yīng)式的方式統(tǒng)一管理,VueUse 中包含了很多我們常用的工具函數(shù),我們可以把網(wǎng)絡(luò)狀態(tài)、異步請求的數(shù)據(jù)、動畫和事件等功能,都看成是響應(yīng)式的數(shù)據(jù)去管理
到此這篇關(guān)于深度解析 Vue3 的響應(yīng)式機(jī)制的文章就介紹到這了,更多相關(guān)Vue3 響應(yīng)式機(jī)制內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
el-date-picker時(shí)間清空值為null處理方案
本文介紹關(guān)于Vue.js項(xiàng)目中時(shí)間選擇器組件的問題,當(dāng)選擇后清空導(dǎo)致值變?yōu)閚ull,進(jìn)而引發(fā)后臺接口報(bào)錯(cuò),通過監(jiān)聽`overallForm.time`的值并設(shè)置為空數(shù)組,成功解決此問題,確保了數(shù)據(jù)正確性,同時(shí),建議避免直接監(jiān)聽整個(gè)對象以優(yōu)化性能,感興趣的朋友一起看看吧2024-08-08element-ui 限制日期選擇的方法(datepicker)
本篇文章主要介紹了element-ui 限制日期選擇的方法(datepicker),小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧2018-05-05Vue實(shí)現(xiàn)懸浮框自由移動+錄音功能的示例代碼
這篇文章主要為大家詳細(xì)介紹了如何利用Vue實(shí)現(xiàn)懸浮框自由移動+錄音功能,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以動手嘗試一下2022-07-07詳解Vue里循環(huán)form表單項(xiàng)實(shí)例
本文主要介紹了Vue里循環(huán)form表單項(xiàng)實(shí)例,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2021-09-09