新知識(shí)Get,Vue3是如何實(shí)現(xiàn)在Style中使用響應(yīng)式變量?
前言
vue2的時(shí)候想必大家有遇到需要在style模塊中訪問script模塊中的響應(yīng)式變量,為此我們不得不使用css變量去實(shí)現(xiàn)。現(xiàn)在vue3已經(jīng)內(nèi)置了這個(gè)功能啦,可以在style中使用v-bind指令綁定script模塊中的響應(yīng)式變量,這篇文章我們來講講vue是如何實(shí)現(xiàn)在style中使用script模塊中的響應(yīng)式變量。注:本文中使用的vue版本為3.4.19。
看個(gè)demo
我們來看個(gè)簡(jiǎn)單的demo,index.vue文件代碼如下:
<template>
<div>
<p>222</p>
<span class="block">hello world</span>
</div>
</template>
<script setup lang="ts">
import { ref } from "vue";
const primaryColor = ref("red");
</script>
<style scoped>
.block {
color: v-bind(primaryColor);
}
</style>
我們?cè)趕cript模塊中定義了一個(gè)響應(yīng)式變量primaryColor,并且在style中使用v-bind指令將primaryColor變量綁定到color樣式上面。
我們?cè)跒g覽器的network面板中來看看編譯后的js文件,如下圖:
圖片
從上圖中可以看到在network面板中編譯后的index.vue文件有兩個(gè),并且第二個(gè)里面有一些query參數(shù),其中的type=style就表示當(dāng)前文件的內(nèi)容對(duì)應(yīng)的是style模塊。第一個(gè)index.vue對(duì)應(yīng)的是template和script模塊中的內(nèi)容。
我們來看看第一個(gè)index.vue,如下圖:
圖片
從上圖中可以看到setup函數(shù)是script模塊編譯后的內(nèi)容,在setup函數(shù)中多了一個(gè)_useCssVars函數(shù),從名字你應(yīng)該猜到了,這個(gè)函數(shù)的作用是和css變量有關(guān)系。別著急,我們接下來會(huì)詳細(xì)去講_useCssVars函數(shù)。
我們?cè)賮砜纯吹诙€(gè)index.vue,如下圖:
圖片
從上圖中可以看到這個(gè)index.vue確實(shí)對(duì)應(yīng)的是style模塊中的內(nèi)容,并且原本的color: v-bind(primaryColor);已經(jīng)變成了color: var(--c845efc6-primaryColor);。
很明顯瀏覽器是不認(rèn)識(shí)v-bind(primaryColor);指令的,所以經(jīng)過編譯后就變成了瀏覽器認(rèn)識(shí)的css變量var(--c845efc6-primaryColor);。
我們接著在elements面板中來看看此時(shí)class值為block的span元素,如下圖:
圖片
從上圖中可以看到color的值為css變量var(--c845efc6-primaryColor),這個(gè)我們前面講過。不同的是這里從父級(jí)元素div中繼承過來一個(gè)--c845efc6-primaryColor: red;。
這個(gè)就是聲明一個(gè)名為--c845efc6-primaryColor的css變量,變量的值為red。
還記得我們?cè)趕cript模塊中定義的響應(yīng)式變量primaryColor嗎?他的值就是red。
所以這個(gè)span元素最終color渲染出來的值就是red。
接下來我們將通過debug的方式帶你搞清楚在style中是如何將指令v-bind(primaryColor)編譯成css變量var(--c845efc6-primaryColor),以及_useCssVars函數(shù)是如何生成聲明值為red的css變量--c845efc6-primaryColor。
doCompileStyle函數(shù)
在前面的文章中我們講過了style模塊實(shí)際是由doCompileStyle函數(shù)函數(shù)處理的,具體如何調(diào)用到doCompileStyle函數(shù)可以查看我之前的文章: 掉了兩根頭發(fā)后,我悟了!vue3的scoped原來是這樣避免樣式污染。
我們需要給doCompileStyle函數(shù)打個(gè)斷點(diǎn),doCompileStyle函數(shù)的代碼位置在:node_modules/@vue/compiler-sfc/dist/compiler-sfc.cjs.js。
還是一樣的套路啟動(dòng)一個(gè)debug終端。這里以vscode舉例,打開終端然后點(diǎn)擊終端中的+號(hào)旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。
圖片
在debug終端執(zhí)行yarn dev,在瀏覽器中打開對(duì)應(yīng)的頁面,比如:http://localhost:5173/ 。
此時(shí)斷點(diǎn)將停留在doCompileStyle函數(shù)中,在我們這個(gè)場(chǎng)景中doCompileStyle函數(shù)簡(jiǎn)化后的代碼如下:
import postcss from "postcss";
function doCompileStyle(options) {
const {
filename,
id,
postcssOptions,
postcssPlugins,
} = options;
const source = options.source;
const shortId = id.replace(/^data-v-/, "");
const plugins = (postcssPlugins || []).slice();
plugins.unshift(cssVarsPlugin({ id: shortId, isProd }));
const postCSSOptions = {
...postcssOptions,
to: filename,
from: filename,
};
let result;
try {
result = postcss(plugins).process(source, postCSSOptions);
return result.then((result) => ({
code: result.css || "",
// ...省略
}));
} catch (e: any) {
errors.push(e);
}
}
在前面的文章掉了兩根頭發(fā)后,我悟了!vue3的scoped原來是這樣避免樣式污染中我們講過了,這里id的值為使用了scoped后給html增加的自定義屬性data-v-x,每個(gè)vue文件生成的x都是不一樣的。在doCompileStyle函數(shù)中使用id.replace方法拿到x賦值給變量shortId。
接著就是定義一個(gè)plugins插件數(shù)組,并且將cssVarsPlugin函數(shù)的返回結(jié)果push進(jìn)去。
這里cssVarsPlugin函數(shù)就是返回了一個(gè)自定義的postcss插件。
最后就是執(zhí)行result = postcss(plugins).process(source, postCSSOptions)拿到經(jīng)過postcss轉(zhuǎn)換編譯器處理后的css。
可能有的小伙伴對(duì)postcss不夠熟悉,我們這里來簡(jiǎn)單介紹一下。
postcss 是 css 的 transpiler(轉(zhuǎn)換編譯器,簡(jiǎn)稱轉(zhuǎn)譯器),它對(duì)于 css 就像 babel 對(duì)于 js 一樣,能夠做 css 代碼的分析和轉(zhuǎn)換。同時(shí),它也提供了插件機(jī)制來做自定義的轉(zhuǎn)換。
在我們這里主要就是用到了postcss提供的插件機(jī)制來完成css scoped的自定義轉(zhuǎn)換,調(diào)用postcss的時(shí)候我們傳入了source,他的值是style模塊中的css代碼。并且傳入的plugins插件數(shù)組中有個(gè)cssVarsPlugin插件,這個(gè)自定義插件就是vue寫的用于處理在css中使用v-bind指令。
在執(zhí)行postcss對(duì)css代碼進(jìn)行轉(zhuǎn)換之前我們?cè)赿ebug終端來看看此時(shí)的css代碼是什么樣的,如下圖:
source
從上圖中可以看到此時(shí)的options.source中還是v-bind(primaryColor)指令。
cssVarsPlugin插件
cssVarsPlugin插件在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:
const vBindRE = /v-bind\s*\(/g;
const cssVarsPlugin = (opts) => {
const { id, isProd } = opts;
return {
postcssPlugin: "vue-sfc-vars",
Declaration(decl) {
const value = decl.value;
if (vBindRE.test(value)) {
vBindRE.lastIndex = 0;
let transformed = "";
let lastIndex = 0;
let match;
while ((match = vBindRE.exec(value))) {
const start = match.index + match[0].length;
const end = lexBinding(value, start);
if (end !== null) {
const variable = normalizeExpression(value.slice(start, end));
transformed +=
value.slice(lastIndex, match.index) +
`var(--${genVarName(id, variable, isProd)})`;
lastIndex = end + 1;
}
}
decl.value = transformed + value.slice(lastIndex);
}
},
};
};
這里的id就是我們?cè)赿oCompileStyle函數(shù)中傳過來的shortId,每個(gè)vue文件對(duì)應(yīng)的shortId值都是不同的。
這里使用到了Declaration鉤子函數(shù),css中每個(gè)具體的樣式都會(huì)觸發(fā)這個(gè)Declaration鉤子函數(shù)。
給Declaration鉤子函數(shù)打個(gè)斷點(diǎn),當(dāng)post-css處理到color: v-bind(primaryColor);時(shí)就會(huì)走到這個(gè)斷點(diǎn)中。如下圖:
圖片
將字符串v-bind(primaryColor)賦值給變量value,接著執(zhí)行if (vBindRE.test(value))。vBindRE是一個(gè)正則表達(dá)式,這里的意思是當(dāng)前css的值是使用了v-bind指令才走到if語句里面。
接著就是執(zhí)行while ((match = vBindRE.exec(value)))進(jìn)行正則表達(dá)式匹配,如果value的值符合vBindRE正則表達(dá)式,也就是value的值是v-bind綁定的,那么就走到while循環(huán)里面去。
看到這里有的小伙伴會(huì)問了,這里使用if就可以了,為什么還要使用while循環(huán)呢?
答案是css的值可能是多個(gè)v-bind指令組成的,比如border: v-bind(borderWidth) solid v-bind(primaryColor);。這里的css值就由兩個(gè)v-bind組成,分別是v-bind(borderWidth)和v-bind(primaryColor);。
為了處理上面這種多個(gè)v-bind指令組成的css值,所以就需要使用while循環(huán)搭配exec方法。正則表達(dá)式使用了global標(biāo)志位的時(shí)候,js的RegExp 對(duì)象是有狀態(tài)的,它們會(huì)將上次成功匹配后的位置記錄在 lastIndex 屬性中。使用此特性,exec() 可用來對(duì)單個(gè)字符串中的多次匹配結(jié)果進(jìn)行逐條的遍歷。
在debug終端來看看此時(shí)的match數(shù)組是什么樣的,如下圖:
圖片
從上圖中可以看到match[0]的值是正則表達(dá)式匹配的字符串,在我們這里匹配的字符串是v-bind(。match.index的值為匹配到的字符位于原始字符串的基于 0 的索引值。
看到這里有的小伙伴可能對(duì)match.index的值有點(diǎn)不理解,我舉個(gè)簡(jiǎn)單的例子你一下就明白了。
還是以v-bind(borderWidth) solid v-bind(primaryColor)為例,這個(gè)字符串就是原始字符串,第一次在while循環(huán)中正則表達(dá)式匹配到第一個(gè)bind,此時(shí)的match.index的值為0,也就是第一個(gè)v在原始字符串的位置。第二次在while循環(huán)中會(huì)基于第一次的位置接著向后找,會(huì)匹配到第二個(gè)v-bind指令,此時(shí)的match.index的值同樣也是基于原始字符串的位置,也就是第二個(gè)v-bind中的v的位置,值為26。
在while循環(huán)中使用const start = match.index + match[0].length給start變量賦值,match.index的值是v-bind中的v的位置。match[0]是正則匹配到的字符串 v-bind(。所以這個(gè)start的位置就是v-bind(primaryColor)中primaryColor變量的開始位置,也就是p所在的位置。
接著就是執(zhí)行l(wèi)exBinding函數(shù)拿到v-bind(primaryColor)中primaryColor變量的結(jié)束位置,賦值給變量end。在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的lexBinding函數(shù)代碼如下:
function lexBinding(content: string, start: number) {
for (let i = start; i < content.length; i++) {
const char = content.charAt(i);
if (char === `)`) {
return i;
}
}
return null;
}
簡(jiǎn)化后的lexBinding函數(shù)也很簡(jiǎn)單,使用for循環(huán)遍歷v-bind(primaryColor)字符串,如果發(fā)現(xiàn)字符串)就說明找到了primaryColor變量的結(jié)束位置。
接著來看拿到end變量后的代碼,會(huì)執(zhí)行const variable = normalizeExpression(value.slice(start, end))。這里先執(zhí)行了value.slice(start, end)根據(jù)start開始位置和end結(jié)束位置提取出v-bind指令綁定的變量,接著normalizeExpression函數(shù)對(duì)其進(jìn)行trim去除空格。
在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的normalizeExpression函數(shù)代碼如下:
function normalizeExpression(exp) {
exp = exp.trim();
return exp;
}
將從v-bind指令中提取出來的變量賦值給variable變量,接著執(zhí)行字符串拼接拿到由v-bind指令轉(zhuǎn)換成的css變量,代碼如下:
transformed +=
value.slice(lastIndex, match.index) +
`var(--${genVarName(id, variable, isProd)})`;
這里的value是css變量值v-bind(primaryColor),在我們這里lastIndex的值為0,match.index的值也是0,所以value.slice(lastIndex, match.index)拿到的值也是空字符串。
接著來看后面這部分,使用字符串拼接得到:var(--變量)。這個(gè)看著就很熟悉了,他就是一個(gè)css變量。變量名是調(diào)用genVarName函數(shù)生成的,genVarName函數(shù)代碼如下:
import hash from "hash-sum";
function genVarName(id, raw, isProd) {
if (isProd) {
return hash(id + raw);
} else {
return `${id}-${getEscapedCssVarName(raw)}`;
}
}
這個(gè)id是根據(jù)當(dāng)前vue組件路徑生成的,每個(gè)vue組件生成的id都不同。這個(gè)raw也就是綁定的響應(yīng)式變量,在這里是primaryColor。isProd表示當(dāng)前是不是生產(chǎn)環(huán)境。
如果是生產(chǎn)環(huán)境就根據(jù)id和變量名使用哈希算法生成一個(gè)加密的字符串。
如果是開發(fā)環(huán)境就使用字符串拼接將id和變量名primaryColor拼接起來得到一個(gè)css變量。getEscapedCssVarName函數(shù)的代碼也很簡(jiǎn)單,是對(duì)變量中的特殊字符進(jìn)行轉(zhuǎn)義,以便在 CSS 變量名中使用。代碼如下:
const cssVarNameEscapeSymbolsRE = /[ !"#$%&'()*+,./:;<=>?@[\\\]^`{|}~]/g;
function getEscapedCssVarName(key: string) {
return key.replace(cssVarNameEscapeSymbolsRE, (s) => `\\${s}`);
}
這也就是為什么不同組件的primaryColor生成的css變量名稱不會(huì)沖突的原因了,因?yàn)樵谏傻腸ss變量前面拼接了一個(gè)id,每個(gè)vue組件生成的id值都不同。
拿到轉(zhuǎn)換成css變量的css值后,并且將其賦值給變量transformed。接著就是執(zhí)行l(wèi)astIndex = end + 1,在我們這里lastIndex就指向了字符串的末尾。
最后就是執(zhí)行decl.value = transformed + value.slice(lastIndex);將v-bind指令替換成css變量,由于lastIndex是指向了字符串的末尾,所以value.slice(lastIndex)的值也是一個(gè)空字符串。
所以在我們這里實(shí)際是執(zhí)行了decl.value = transformed,執(zhí)行完這句話后color的值就由v-bind(primaryColor)轉(zhuǎn)換成了var(--c845efc6-primaryColor)。
生成useCssVars函數(shù)
前面我們講過了編譯后的setup函數(shù)中多了一個(gè)useCssVars函數(shù),實(shí)際在我們的源代碼中是沒有這個(gè)useCssVars函數(shù)的。接下來我們來看看編譯時(shí)處理script模塊時(shí)是如何生成useCssVars函數(shù)的。
在之前的 為什么defineProps宏函數(shù)不需要從vue中import導(dǎo)入?文章中我們講過了vue的script模塊中的代碼是由compileScript函數(shù)處理的,當(dāng)然你沒看過那篇文章也不影響這篇文章的閱讀。
給compileScript函數(shù)打個(gè)斷點(diǎn),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的compileScript函數(shù)代碼如下:
function compileScript(sfc, options) {
const ctx = new ScriptCompileContext(sfc, options);
const startOffset = ctx.startOffset;
ctx.s.prependLeft(
startOffset,
`
${genCssVarsCode(sfc.cssVars, ctx.bindingMetadata, scopeId, !!options.isProd)}
`
);
}
首先調(diào)用ScriptCompileContext類new了一個(gè)ctx上下文對(duì)象,我們這里來介紹一下需要使用到的ctx上下文對(duì)象中的兩個(gè)方法:ctx.s.toString、ctx.s.prependLeft。
- ctx.s.toString:返回此時(shí)由script模塊編譯成的js代碼。
- ctx.s.prependLeft:給編譯后的js代碼在指定index的前面插入字符串。
給ctx.s.prependLeft方法打個(gè)斷點(diǎn),在debug終端使用ctx.s.toString方法來看看此時(shí)由script模塊編譯成的js代碼是什么樣的,如下圖:
圖片
從上圖中可以看到此時(shí)生成的js代碼code字符串只有一條import語句和定義primaryColor變量。
由于篇幅有限我們就不深入到genCssVarsCode函數(shù)了,這個(gè)genCssVarsCode函數(shù)會(huì)生成useCssVars函數(shù)的調(diào)用。我們?cè)赿ebug終端來看看生成的code代碼字符串是什么樣的,如下圖:
圖片
從上圖中可以看到genCssVarsCode函數(shù)生成了一個(gè)useCssVars函數(shù)。
執(zhí)行ctx.s.prependLeft函數(shù)后會(huì)將生成的useCssVars函數(shù)插入到生成的js code代碼字符串的前面,我們?cè)赿ebug終端來看看,如下圖:
圖片
從上圖中可以看到此時(shí)的js code代碼字符串中已經(jīng)有了一個(gè)useCssVars函數(shù)了。
執(zhí)行useCssVars函數(shù)
前面我們講過了編譯時(shí)經(jīng)過cssVarsPlugin這個(gè)post-css插件處理后,v-bind(primaryColor)指令就會(huì)編譯成了css變量var(--c845efc6-primaryColor)。這里只是使用css變量值的地方,那么這個(gè)css變量的值又是在哪里定義的呢?答案是在useCssVars函數(shù)中。
在開始我們講過了編譯后的setup函數(shù)中多了一個(gè)useCssVars函數(shù),所以我們給useCssVars函數(shù)打個(gè)斷點(diǎn),刷新瀏覽器此時(shí)代碼就會(huì)走到斷點(diǎn)中了。如下圖:
圖片
從上圖中可以看到執(zhí)行useCssVars函數(shù)時(shí)傳入了一個(gè)回調(diào)函數(shù)作為參數(shù),這個(gè)回調(diào)函數(shù)返回了一個(gè)對(duì)象。
將斷點(diǎn)走進(jìn)useCssVars函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的useCssVars函數(shù)代碼如下:
function useCssVars(getter) {
const instance = getCurrentInstance();
const setVars = () => {
const vars = getter(instance.proxy);
setVarsOnVNode(instance.subTree, vars);
};
watchPostEffect(setVars);
}
在useCssVars函數(shù)中先調(diào)用getCurrentInstance函數(shù)拿到當(dāng)前的vue實(shí)例,然后將setVars函數(shù)作為參數(shù)傳入去執(zhí)行watchPostEffect函數(shù)。
這個(gè)watchPostEffect函數(shù)大家應(yīng)該知道,他是watchEffect() 使用 flush: 'post' 選項(xiàng)時(shí)的別名。
為什么需要使用 flush: 'post'呢?
答案是需要在setVars回調(diào)函數(shù)中需要去操作DOM,所以才需要使用 flush: 'post'讓回調(diào)函數(shù)在組件渲染完成之后去執(zhí)行。
給setVars函數(shù)打個(gè)斷點(diǎn),組件渲染完成后斷點(diǎn)將會(huì)走進(jìn)setVars函數(shù)中。
首先會(huì)執(zhí)行g(shù)etter函數(shù),將返回值賦值給變量vars。前面我們講過了這個(gè)getter函數(shù)是調(diào)用useCssVars函數(shù)時(shí)傳入的回調(diào)函數(shù),代碼如下:
_useCssVars((_ctx) => ({
"c845efc6-primaryColor": primaryColor.value
}))
在這個(gè)回調(diào)函數(shù)中會(huì)返回一個(gè)對(duì)象,對(duì)象的key為c845efc6-primaryColor,這個(gè)key就是css變量var(--c845efc6-primaryColor)括號(hào)中的內(nèi)容。
對(duì)象的值是ref變量primaryColor的值,由于這個(gè)代碼是在watchPostEffect的回調(diào)函數(shù)中執(zhí)行的,所以這里的ref變量primaryColor也被作為依賴進(jìn)行收集了。當(dāng)primaryColor變量的值變化時(shí),setVars函數(shù)也將再次執(zhí)行。這也就是為什么在style中可以使用v-bind指令綁定一個(gè)響應(yīng)式變量,并且當(dāng)響應(yīng)式變量的值變化時(shí)樣式也會(huì)同步更新。
接著就是執(zhí)行setVarsOnVNode(instance.subTree, vars)函數(shù),傳入的第一個(gè)參數(shù)為instance.subTree。他的值是當(dāng)前vue組件根元素的虛擬DOM,也就是根元素div的虛擬DOM。第二個(gè)參數(shù)為useCssVars傳入的回調(diào)函數(shù)返回的對(duì)象,這是一個(gè)css變量組成的對(duì)象。
接著將斷點(diǎn)走進(jìn)setVarsOnVNode函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的代碼如下:
function setVarsOnVNode(vnode: VNode, vars) {
setVarsOnNode(vnode.el, vars);
}
在setVarsOnVNode函數(shù)中是調(diào)用了setVarsOnNode函數(shù),不同的是傳入的第一個(gè)參數(shù)不再是虛擬DOM。而是vnode.el虛擬DOM對(duì)應(yīng)的真實(shí)DOM,也就是根節(jié)點(diǎn)div。
將斷點(diǎn)走進(jìn)setVarsOnNode函數(shù),在我們這個(gè)場(chǎng)景中簡(jiǎn)化后的setVarsOnNode函數(shù)代碼如下:
function setVarsOnNode(el: Node, vars) {
if (el.nodeType === 1) {
const style = el.style;
for (const key in vars) {
style.setProperty(`--${key}`, vars[key]);
}
}
}
在setVarsOnNode函數(shù)中先使用if語句判斷el.nodeType === 1,這個(gè)的意思是判斷當(dāng)前節(jié)點(diǎn)類型是不是一個(gè)元素節(jié)點(diǎn),比如<p>和<div>。如果是就走進(jìn)if語句里面,使用el.style拿到根節(jié)點(diǎn)的style樣式。
這里的vars是css變量組成的對(duì)象,遍歷這個(gè)對(duì)象。對(duì)象的key為css變量名稱,對(duì)象的value為css變量的值。
接著就是遍歷css變量組成的對(duì)象,使用style.setProperty方法給根節(jié)點(diǎn)div增加內(nèi)聯(lián)樣式,也就是--c845efc6-primaryColor: red;。span元素由于是根節(jié)點(diǎn)div的子節(jié)點(diǎn),所以他也繼承了樣式--c845efc6-primaryColor: red;。
由于span元素的color經(jīng)過編譯后已經(jīng)變成了css變量var(--c845efc6-primaryColor),并且從根節(jié)點(diǎn)繼承過來css變量--c845efc6-primaryColor的值為red,所以最終span元素的color值為red。
總結(jié)
下面這個(gè)是我總結(jié)的流程圖,如下(搭配流程圖后面的文字解釋一起服用效果最佳):
圖片
編譯階段script模塊是由compileScript函數(shù)處理的,compileScript函數(shù)會(huì)去執(zhí)行一個(gè)genCssVarsCode函數(shù)。這個(gè)函數(shù)會(huì)返回一個(gè)useCssVars函數(shù)的調(diào)用。然后在compileScript函數(shù)中會(huì)調(diào)用ctx.s.prependLeft方法將生成的useCssVars函數(shù)插入到編譯后的setup函數(shù)中。
編譯階段style模塊是由doCompileStyle函數(shù)處理的,在doCompileStyle函數(shù)中會(huì)調(diào)用postcss對(duì)css樣式進(jìn)行處理。vue自定義了一個(gè)名為cssVarsPlugin的postcss插件,插件中有個(gè)Declaration鉤子函數(shù),css中每個(gè)具體的樣式都會(huì)觸發(fā)這個(gè)Declaration鉤子函數(shù)。
在Declaration鉤子函數(shù)中使用正則表達(dá)式去匹配當(dāng)前css值是不是v-bind綁定的,如果是就將匹配到的v-bind綁定的變量提取出來賦值給變量variable。還有一個(gè)id變量,他是根據(jù)當(dāng)前vue組件的路徑生成的加密字符串。使用字符串拼接就可以得到var(--${id}-${variable}),他就是由v-bind編譯后生成的css變量。最終生成的css變量類似這樣:var(--c845efc6-primaryColor)。
運(yùn)行時(shí)階段初始化的時(shí)候會(huì)去執(zhí)行setup函數(shù),由于在編譯階段setup函數(shù)中插入了一個(gè)useCssVars函數(shù)。使用在運(yùn)行時(shí)階段初始化時(shí)useCssVars函數(shù)會(huì)被執(zhí)行。
在useCssVars函數(shù)中執(zhí)行了watchPostEffect函數(shù),他是watchEffect() 使用 flush: 'post' 選項(xiàng)時(shí)的別名。
由于我們需要在回調(diào)中操作DOM,所以才需要使用flush: 'post',讓回調(diào)函數(shù)在組件渲染之后去執(zhí)行。由于在回調(diào)函數(shù)中會(huì)去讀取v-bind綁定的響應(yīng)式變量,所以每次綁定的響應(yīng)式變量值變化后都會(huì)再次執(zhí)行調(diào)用watchPostEffect傳入的回調(diào)函數(shù),以此讓響應(yīng)式變量綁定的樣式保存更新。
在watchPostEffect傳入的回調(diào)函數(shù)中會(huì)通過當(dāng)前vue組件實(shí)例拿到真實(shí)DOM的根節(jié)點(diǎn),然后遍歷css變量組成的對(duì)象,將這些css變量逐個(gè)在根節(jié)點(diǎn)上面定義,類似這樣:--c845efc6-primaryColor: red;。由于css可以繼承,所以子節(jié)點(diǎn)都繼承了這個(gè)css定義。
我們的<span>標(biāo)簽在編譯階段由color: v-bind(primaryColor);編譯成了css變量color: var(--c845efc6-primaryColor)。并且在運(yùn)行時(shí)由于useCssVars函數(shù)的作用在根節(jié)點(diǎn)生成了css變量的定義--c845efc6-primaryColor: red;。由于css繼承,所以span標(biāo)簽也繼承了這個(gè)css變量的定義,所以span標(biāo)簽渲染到頁面上的color值最終為red。