深入理解Vue3:style中的響應式變數如何工作?

南玖發表於2024-12-03

前言

在很多業務場景中,我們的style樣式可能會根據業務邏輯的變化而變化,這個時候大家最容易想到的方案就是多寫幾個class類,根據不同場景應用不同的類,比如這樣:

<div
  :class="{
  [$style.sign_day]: true,
  [$style.sign_today]: getSignStatus(item) == 1,
  [$style.sign_notyet_day]: getSignStatus(item) == 6,
  [$style.sign_day_dark]: theme == 'dark',
  }"
>
</div>
<style lang="scss" module>
  .sign_day {
    background: red;
  }
  .sign_today {
    background: yellow;
  }
  .sign_notyet_day {
    background: blue;
  }
  .sign_day_dark {
    background: orange;
  }
</style>

這樣雖然也是一種不錯的方式,但是如果型別有非常多的話,那麼你就得在vue模版裡面寫大量的判斷表示式,並且在style中寫大量的class類。

要是在style中也可以直接使用script中的JS變數,那麼這種場景處理起來是不是會更方便一點呢?

Vue2 CSS變數

Vue2中,遇到以上業務場景如果我們不想寫大量的class類的話,可以藉助css中的var()函式來實現

var() 可以插入一個自定義屬性(有時也被稱為“CSS 變數”)的值,用來代替非自定義屬性中值的任何部分。

比如:

在模版中呼叫getStyle函式獲取顏色值,並且定義成css變數

<div
v-for="item in signList"
:key="item.day"
:class="$style.sign_day"
:style="{ '--color': getStyle(item) }"
>
  {{ item.title }}
</div>

生成顏色值

getStyle(item) {
  switch (item.status) {
    case 0:
      return '#f8ae00'
    case 1:
      return '#e5353e'
    case 2:
      return '#1fddf4'
    case 3:
      return '#1ff46a'
    default:
      return '#191919'
  }
},

然後就可以只寫一個css類

.sign_day {
  width: calc((100vw - 72px) / 4);
  height: 80px;
  margin-top: 8px;
  border-radius: 8px;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #f5f5f5;
  color: var(--color);
}

這種方案的原理其實就是藉助了CSS的自定義變數以及CSS的作用域來實現的

所以它需要兩步:

  • 自定義CSS變數(考慮作用域範圍)
  • 使用CSS變數

實際上在Vue3中還有更簡便的方案!

Vue3 v-bind()

在Vue3單檔案元件的 <style> 標籤支援使用 v-bind 函式將 CSS 的值連結到元件中的資料。

所以以上場景還可以這樣實現:

模版:

<div :class="$style.day_item">
  {{ dayItem.title }}
</div>

計算顏色值:

const color = computed(() => {
    switch (props.dayItem.status) {
    case 0:
        return '#f8ae00'
    case 1:
        return '#e5353e'
    case 2:
        return '#1fddf4'
    case 3:
        return '#1ff46a'
    default:
        return '#191919'
    }
})

style 呼叫v-bind()使用setup中的變數

<style lang="scss" module>
.day_item {
    color: v-bind(color);
}
</style>

從該圖我們可以發現Vue3中的v-bind()原理與上面的CSS變數的原理一樣,都是藉助了CSS的自定義變數以及CSS的作用域來實現的

只不過不同的是v-bind()生成的CSS變數前面多了一串hash

Vue3是如何編譯v-bind()的?

猜測流程

我們可以從編譯結果來進行反推

首先是我們的JS部分,編譯成了以下內容:

這裡會比沒使用v-bind()的元件多出一個_useCssVars()函式

_useCssVars((_ctx) => ({
  "5d92a9f9-color": color.value
}));

能不能猜到這個函式的作用是什麼?如果不能,接著看下面一張圖👇

這張圖是元件的style部分編譯之後的產物,可以看到

.day_item {
    color: v-bind(color);
}

編譯成了

"._day_item_1oe25_1 {\n  color: var(--5d92a9f9-color);\n}"

也就是說我們使用的v-bind最終也是編譯成了原生CSS中var函式,原理也是使用CSS的自定義變數

但是這裡只有使用,並沒看到css變數定義的地方🤔,現在能夠猜測到_useCssVars()函式的作用是什麼嗎?大機率就是用來生成css自定義變數了。

接下來我們可以到原始碼中進行驗證:

原始碼驗證

  1. 找到原始碼中的doCompileStyle函式,打上斷點,然後就可以啟動debug模式了

  1. 接著往下走你會看到一個shortId變數,它此時的值是什麼呢?

是不是有點眼熟,沒錯它就是後面會出現在CSS變數前面的那一串hash

  1. 再接著往下走,我們可以看到postcss外掛中新增了一個cssVarsPlugin外掛

這個外掛的作用大家是不是已經猜到是幹嘛的了,接著往下走

  1. cssVarsPlugin這個方法中再加一個斷點

可以看到此時進來的decl引數是:color: v-bind(color)

熟悉postcss的同學應該能知道decl是什麼意思,它表示的是css轉化為AST後的一個節點型別

const vBindRE = /v-bind\s*\(/g;

將CSS宣告中的屬性值v-bind(color) 經過vBindRE正則進行檢測是否為v-bind()語句

再往下,這裡就是v-bind()語句編譯的核心程式碼了

首先是提取變數名

這裡可以看到,執行後的結果是'color',也就是v-bind()括號中的這個變數了

再往下

此時就能看到整個編譯結果了:v-bind(color) ---> var(--5d92a9f9-color)

可以看到v-bind()的編譯其實就是透過正則處理重新生成字串

現在知道v-bind()是如何編譯的,剩下一個重點就是:Vue是如何把style中使用的變數轉換成CSS變數並設定在對應dom節點上的

這個突破點在我們上面猜測流程的第一張圖,裡面有這樣一段程式碼:

_useCssVars((_ctx) => ({
  "5d92a9f9-color": color.value
}));

很明顯,它就是用來生成CSS變數

  1. 接下來我們可以在原始碼中找到這個函式,並打上斷點

在原始碼中搜尋_useCssVars,你會發現什麼也搜不到,這時我們可以嘗試去掉_仔進行搜尋,你會發現有這樣一段程式碼:

const CSS_VARS_HELPER = `useCssVars`;

很明顯,後面在原始碼中我們只需要搜尋CSS_VARS_HELPER就可以,找到以下程式碼,打上斷點,重新整理頁面

我們會發現這一段其實就是生成了我們上面那一段程式碼:

_useCssVars((_ctx) => ({
  "5d92a9f9-color": color.value
}));

走到這裡你會發現好像走不下去了,沒有下一步了,因為最終我們看到的編譯後的程式碼就是這個,具體是怎麼把style中使用的變數轉換成CSS變數並設定在對應dom節點上的這個並不是在編譯時處理的。

想搞清楚這個我們還得在執行時打斷點除錯(這裡換成了火狐瀏覽器進行斷點除錯,不要問為什麼,問就是斷點除錯比谷歌好用)


接著往下走,會來到setVars方法這裡

從方法名我們一眼就能看出它就是用來設定CSS變數的!

再往下走setVars -> setVarsOnVNode -> setVarsOnNode

在這裡最終會呼叫setProperty方法來設定css變數。

到這裡整個流程就結束了!

相關文章