大家好,我是歐陽(yáng)!
我們每天都在用v-model,并且大家都知道在vue3中v-model是:modelValue和@update:modelValue的語(yǔ)法糖。那你知道v-model指令是如何變成組件上的modelValue屬性和@update:modelValue事件呢?將v-model指令轉(zhuǎn)換為modelValue屬性和@update:modelValue事件這一過(guò)程是在編譯時(shí)還是運(yùn)行時(shí)進(jìn)行的呢?
下面這個(gè)是我畫(huà)的處理v-model指令的完整流程圖:
圖片
首先會(huì)調(diào)用parse函數(shù)將template模塊中的代碼轉(zhuǎn)換為AST抽象語(yǔ)法樹(shù),此時(shí)使用v-model的node節(jié)點(diǎn)的props屬性中還是v-model。接著會(huì)調(diào)用transform函數(shù),經(jīng)過(guò)transform函數(shù)處理后在node節(jié)點(diǎn)中多了一個(gè)codegenNode屬性。在codegenNode屬性中我們看到?jīng)]有v-model指令,取而代之的是modelValue和onUpdate:modelValue屬性。經(jīng)過(guò)transform函數(shù)處理后已經(jīng)將v-model指令編譯為modelValue和onUpdate:modelValue屬性,此時(shí)還是AST抽象語(yǔ)法樹(shù)。所以接下來(lái)就是調(diào)用generate函數(shù)將AST抽象語(yǔ)法樹(shù)轉(zhuǎn)換為render函數(shù),到此為止編譯時(shí)做的事情已經(jīng)做完了,經(jīng)過(guò)編譯時(shí)的處理v-model指令已經(jīng)變成了modelValue和onUpdate:modelValue屬性。
接著就是運(yùn)行時(shí)階段,在瀏覽器中執(zhí)行render函數(shù)生成虛擬DOM。在生成虛擬DOM的過(guò)程中由于props屬性中有modelValue和onUpdate:modelValue屬性,所以就會(huì)給組件對(duì)象加上modelValue屬性和@update:modelValue事件。最后就是調(diào)用mount方法將虛擬DOM轉(zhuǎn)換為真實(shí)DOM。所以v-model指令轉(zhuǎn)換為modelValue屬性和@update:modelValue事件這一過(guò)程是在編譯時(shí)進(jìn)行的。
vue是一個(gè)編譯時(shí)+運(yùn)行時(shí)一起工作的框架,之前有小伙伴私信我說(shuō)自己傻傻分不清楚在vue中什么時(shí)候是編譯時(shí),什么時(shí)候是運(yùn)行時(shí)。要回答小伙伴的這個(gè)問(wèn)題我們要從一個(gè)vue文件是如何渲染到瀏覽器窗口中說(shuō)起。
我們的vue代碼一般都是寫在后綴名為vue的文件上,顯然瀏覽器是不認(rèn)識(shí)vue文件的,瀏覽器只認(rèn)識(shí)html、css、jss等文件類型。所以第一步就是通過(guò)webpack或者vite將一個(gè)vue文件編譯為一個(gè)包含render函數(shù)的js文件,在這一步中代碼的執(zhí)行環(huán)境是在nodejs中進(jìn)行,也就是我們所說(shuō)的編譯時(shí)。相比瀏覽器端來(lái)說(shuō)能夠拿到的權(quán)限更多,也能做更多的事情。后面就是執(zhí)行render函數(shù)生成虛擬DOM,再調(diào)用瀏覽器的DOM API根據(jù)虛擬DOM生成真實(shí)DOM掛載到瀏覽器上。在第一步后面的這些過(guò)程中代碼執(zhí)行環(huán)境都是在瀏覽器中,也就是我們所說(shuō)的運(yùn)行時(shí)。在客戶端渲染的場(chǎng)景下,一句話總結(jié)就是:代碼跑在nodejs端的時(shí)候就是編譯時(shí),代碼跑在瀏覽器端的時(shí)候就是運(yùn)行時(shí)。
圖片
我們來(lái)看一個(gè)v-model的例子,父組件index.vue的代碼如下:
<template> <CommonChild v-model="inputValue" /> <p>input value is: {{ inputValue }}</p></template><script setup lang="ts">import { ref } from "vue";import CommonChild from "./child.vue";const inputValue = ref();</script>
我們上面是一個(gè)很簡(jiǎn)單的v-model的例子,在CommonChild子組件上使用v-model綁定一個(gè)叫inputValue的ref變量,然后將這個(gè)inputValue變量渲染到p標(biāo)簽上面。
前面我們已經(jīng)講過(guò)了客戶端渲染的場(chǎng)景下,在nodejs端工作的時(shí)候是編譯時(shí),在瀏覽器端工作的時(shí)候是運(yùn)行時(shí)。那我們現(xiàn)在先來(lái)看看經(jīng)過(guò)編譯時(shí)階段處理后,剛剛進(jìn)入到瀏覽器端運(yùn)行時(shí)階段的js代碼是什么樣的。我們要如何在瀏覽器中找到這個(gè)js文件呢?其實(shí)很簡(jiǎn)單直接在network上面找到你的那個(gè)vue文件就行了,比如我這里的文件是index.vue,那我只需要在network上面找叫index.vue的文件就行了。但是需要注意一下network上面有兩個(gè)index.vue的js請(qǐng)求,分別是template模塊+script模塊編譯后的js文件,和style模塊編譯后的js文件。
那怎么區(qū)分這兩個(gè)index.vue文件呢?很簡(jiǎn)單,通過(guò)query就可以區(qū)分。由style模塊編譯后的js文件的URL中有type=style的query,如下圖所示:
圖片
這時(shí)有的小伙伴就開(kāi)始疑惑了不是說(shuō)好的瀏覽器不認(rèn)識(shí)vue文件嗎?怎么這里的文件名稱是index.vue而不是index.js呢?其實(shí)很簡(jiǎn)單,在開(kāi)發(fā)環(huán)境時(shí)index.vue文件是在App.vue文件中import導(dǎo)入的,而App.vue文件是在main.js文件中import導(dǎo)入的。所以當(dāng)瀏覽器中執(zhí)行main.js的代碼時(shí)發(fā)現(xiàn)import導(dǎo)入了App.vue文件,那瀏覽器就會(huì)去加載App.vue文件。當(dāng)瀏覽器加載完App.vue文件后執(zhí)行時(shí)發(fā)現(xiàn)import導(dǎo)入了index.vue文件,所以瀏覽器就會(huì)去加載index.vue文件,而不是index.js文件。
至于什么時(shí)候?qū)ndex.vue文件中的template模塊、script模塊、style模塊編譯成js代碼,我們?cè)?nbsp;通過(guò)debug搞清楚.vue文件怎么變成.js文件文章中已經(jīng)講過(guò)了當(dāng)import加載一個(gè)文件時(shí)會(huì)觸發(fā)@vitejs/plugin-vue包中的transform鉤子函數(shù),在這個(gè)transform鉤子函數(shù)中會(huì)將template模塊、script模塊、style模塊編譯成js代碼。所以在瀏覽器中拿到的index.vue文件就是經(jīng)過(guò)編譯后的js代碼了。
現(xiàn)在我們?cè)跒g覽器的network中來(lái)看剛剛進(jìn)入編譯時(shí)index.vue文件代碼,簡(jiǎn)化后的代碼如下:
import { Fragment as _Fragment, createElementBlock as _createElementBlock, createElementVNode as _createElementVNode, createVNode as _createVNode, defineComponent as _defineComponent, openBlock as _openBlock, toDisplayString as _toDisplayString, ref,} from "/node_modules/.vite/deps/vue.js?v=23bfe016";import CommonChild from "/src/components/vModel/child.vue?t=1710943659056";import "/src/components/vModel/index.vue?vue&type=style&index=0&scoped=0ebe7d62&lang.css";const _sfc_main = _defineComponent({ __name: "index", setup(__props, { expose: __expose }) { __expose(); const inputValue = ref(); const __returned__ = { inputValue, CommonChild }; return __returned__; },});function _sfc_render(_ctx, _cache, $props, $setup, $data, $options) { return ( _openBlock(), _createElementBlock( _Fragment, null, [ _createVNode( $setup["CommonChild"], { modelValue: $setup.inputValue, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => ($setup.inputValue = $event)), }, null, 8, ["modelValue"] ), _createElementVNode( "p", null, "input value is: " + _toDisplayString($setup.inputValue), 1 /* TEXT */ ), ], 64 /* STABLE_FRAGMENT */ ) );}_sfc_main.render = _sfc_render;export default _sfc_main;
從上面的代碼中我們可以看到編譯后的js代碼主要分為兩塊,第一塊是_sfc_main組件對(duì)象,里面有name屬性和setup方法。一個(gè)vue組件在運(yùn)行時(shí)實(shí)際就是一個(gè)對(duì)象,這里的_sfc_main就是一個(gè)vue組件對(duì)象。至于defineComponent函數(shù)的作用是在定義 Vue 組件時(shí)提供類型推導(dǎo)的輔助函數(shù),所以在我們這個(gè)場(chǎng)景沒(méi)什么用。我們接著來(lái)看第二塊_sfc_render,從名字我想你應(yīng)該已經(jīng)猜到了他是一個(gè)render函數(shù)。執(zhí)行這個(gè)_sfc_render函數(shù)就會(huì)生成虛擬DOM,然后再由虛擬DOM生成瀏覽器上面的真實(shí)DOM。
我們?cè)賮?lái)看這個(gè)render函數(shù),在這個(gè)render函數(shù)前面會(huì)調(diào)用openBlock函數(shù)和createElementBlock函數(shù)。他的作用是在編譯時(shí)盡可能的提取多的關(guān)鍵信息,可以減少運(yùn)行時(shí)比較新舊虛擬DOM帶來(lái)的性能開(kāi)銷,我們這篇文章不關(guān)注這點(diǎn),所以我們接下來(lái)會(huì)直接看下面的_createVNode函數(shù)和_createElementVNode函數(shù)。
我們接著來(lái)看render函數(shù)中的_createVNode函數(shù)和_createElementVNode函數(shù),代碼如下:
import { createElementVNode as _createElementVNode, createVNode as _createVNode,} from "/node_modules/.vite/deps/vue.js?v=23bfe016";_createVNode( $setup["CommonChild"], { modelValue: $setup.inputValue, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => ($setup.inputValue = $event)), }, null, 8, ["modelValue"]),_createElementVNode( "p", null, "input value is: " + _toDisplayString($setup.inputValue), 1 /* TEXT */),
從這兩個(gè)函數(shù)的名字我想你也能猜出來(lái)他們的作用是創(chuàng)建虛擬DOM,再仔細(xì)一看這兩個(gè)函數(shù)不就是對(duì)應(yīng)的我們template模塊中的這兩行代碼嗎。
<CommonChild v-model="inputValue" /><p>input value is: {{ inputValue }}</p>
第一個(gè)_createVNode函數(shù)對(duì)應(yīng)的是CommonChild,第二個(gè)_createElementVNode對(duì)應(yīng)的是p標(biāo)簽。我們將重點(diǎn)放在_createVNode函數(shù)上,從import導(dǎo)入來(lái)看_createVNode函數(shù)是從vue中導(dǎo)出的createVNode函數(shù)。你是不是覺(jué)得createVNode這個(gè)名字比較熟悉呢,其實(shí)在 vue官網(wǎng)中有提到。
“
h() 是 hyperscript 的簡(jiǎn)稱——意思是“能生成 HTML (超文本標(biāo)記語(yǔ)言) 的 JavaScript”。這個(gè)名字來(lái)源于許多虛擬 DOM 實(shí)現(xiàn)默認(rèn)形成的約定。一個(gè)更準(zhǔn)確的名稱應(yīng)該是 createVnode(),但當(dāng)你需要多次使用渲染函數(shù)時(shí),一個(gè)簡(jiǎn)短的名字會(huì)更省力。
vue官網(wǎng)中h() 函數(shù)用于生成虛擬DOM,其實(shí)h()函數(shù)底層就是調(diào)用的createVnode函數(shù)。同樣的createVnode函數(shù)和h() 函數(shù)接收的參數(shù)也差不多,第一個(gè)參數(shù)可以是一個(gè)組件對(duì)象也可以是像p這樣的html標(biāo)簽,也可以是一個(gè)虛擬DOM。第二個(gè)參數(shù)為給組件或者h(yuǎn)tml標(biāo)簽傳遞的props屬性或者attribute。第三個(gè)參數(shù)是該節(jié)點(diǎn)的children子節(jié)點(diǎn)。現(xiàn)在我們?cè)賮?lái)仔細(xì)看這個(gè)_createVNode函數(shù)你應(yīng)該已經(jīng)明白了:
_createVNode( $setup["CommonChild"], { modelValue: $setup.inputValue, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => ($setup.inputValue = $event)), }, null, 8, ["modelValue"]),
我們?cè)?nbsp;Vue 3 的 setup語(yǔ)法糖到底是什么東西?文章中已經(jīng)講過(guò)了render函數(shù)中的$setup變量就是setup函數(shù)的返回值經(jīng)過(guò)Proxy處理后的對(duì)象,由于Proxy的攔截處理讓我們?cè)趖emplate中使用ref變量時(shí)無(wú)需再寫.value。在上面的setup函數(shù)中我們看到CommonChild組件對(duì)象也在返回值對(duì)象中,所以這里傳入給createVNode函數(shù)的第一個(gè)參數(shù)為CommonChild組件對(duì)象。
我們?cè)賮?lái)看第二個(gè)參數(shù)對(duì)象,對(duì)象中有兩個(gè)key,分別是modelValue和onUpdate:modelValue。這兩個(gè)key就是傳遞給CommonChild組件的兩個(gè)props,等等這里有兩個(gè)問(wèn)題。第一個(gè)問(wèn)題是這里怎么是onUpdate:modelValue,我們知道的v-model是:modelValue和@update:modelValue的語(yǔ)法糖,不是說(shuō)好的@update怎么變成了onUpdate了呢?第二個(gè)問(wèn)題是onUpdate:modelValue明顯是事件監(jiān)聽(tīng)而不是props屬性,怎么是“通過(guò)props屬性”而不是“通過(guò)事件”傳遞給了CommonChild子組件呢?
因?yàn)樵诰幾g時(shí)處理v-on事件監(jiān)聽(tīng)會(huì)將監(jiān)聽(tīng)的事件首字母變成大寫然后在前面加一個(gè)on,塞到props屬性對(duì)象中,所以這里才是onUpdate:modelValue。所以在組件上不管是v-bind的attribute和prop,還是v-on事件監(jiān)聽(tīng),經(jīng)過(guò)編譯后都會(huì)被塞到一個(gè)大的props對(duì)象中。以on開(kāi)頭的屬性我們都視作事件監(jiān)聽(tīng),用于和普通的attribute和prop區(qū)分。所以你在組件上綁定一個(gè)onConfirm屬性,屬性值為一個(gè)handleClick的函數(shù)。在子組件中使用emit('confirm')是可以觸發(fā)handleClick函數(shù)的執(zhí)行的,但是一般情況下還是不要這樣寫,維護(hù)代碼的人會(huì)看著一臉蒙蔽的。
我們接著來(lái)看傳遞給CommonChild組件的這兩個(gè)屬性值。
{ modelValue: $setup.inputValue, "onUpdate:modelValue": _cache[0] || (_cache[0] = ($event) => ($setup.inputValue = $event)),}
第一個(gè)modelValue的屬性值是$setup.inputValue。前面我們已經(jīng)講過(guò)了$setup.inputValue就是指向setup中定義的名為inputValue的ref變量,所以第一個(gè)屬性的作用就是給CommonChild組件添加:modelValue="inputValue"的屬性。
我們?cè)賮?lái)看第二個(gè)屬性onUpdate:modelValue,屬性值為_(kāi)cache[0] ||(_cache[0] = ($event) => ($setup.inputValue = $event))。這里為什么要加一個(gè)_cache緩存呢?原因是每次頁(yè)面刷新都會(huì)重新觸發(fā)render函數(shù)的執(zhí)行,如果不加緩存那不就變成了每次執(zhí)行render函數(shù)都會(huì)生成一個(gè)事件處理函數(shù)。這里的事件處理函數(shù)也很簡(jiǎn)單,接收一個(gè)$event變量然后賦值給setup中的inputValue變量。接收的$event變量就是我們?cè)谧咏M件中調(diào)用emit觸發(fā)事件傳過(guò)來(lái)的第二個(gè)變量,比如:emit('update:modelValue', 'helllo word')。為什么是第二個(gè)變量呢?是因?yàn)閑mit函數(shù)接收的第一個(gè)變量為要觸發(fā)的事件名稱。所以第二個(gè)屬性的作用就是給CommonChild組件添加@update:modelValue的事件綁定。
前面我們已經(jīng)講過(guò)了在運(yùn)行時(shí)已經(jīng)拿到了key為modelValue和onUpdate:modelValue的props屬性對(duì)象了,我們知道這個(gè)props屬性對(duì)象是在編譯時(shí)由v-model指令編譯而來(lái)的,那在這個(gè)編譯過(guò)程中是如何處理v-model指令的呢?請(qǐng)看下面編譯時(shí)的流程圖:
compile-progress
首先會(huì)調(diào)用parse函數(shù)將template模塊中的代碼轉(zhuǎn)換為AST抽象語(yǔ)法樹(shù),此時(shí)使用v-model的node節(jié)點(diǎn)的props屬性中還是v-model。接著會(huì)調(diào)用transform函數(shù),經(jīng)過(guò)transform函數(shù)處理后在node節(jié)點(diǎn)中多了一個(gè)codegenNode屬性。在codegenNode屬性中我們看到?jīng)]有v-model指令,取而代之的是modelValue和onUpdate:modelValue屬性。經(jīng)過(guò)transform函數(shù)處理后已經(jīng)將v-model指令編譯為modelValue和onUpdate:modelValue屬性,此時(shí)還是AST抽象語(yǔ)法樹(shù)。所以接下來(lái)就是調(diào)用generate函數(shù)將AST抽象語(yǔ)法樹(shù)轉(zhuǎn)換為render函數(shù),到此為止編譯時(shí)做的事情已經(jīng)做完了。
首先是使用parse函數(shù)將template模塊中的代碼編譯成AST抽象語(yǔ)法樹(shù),在這個(gè)過(guò)程中會(huì)使用到大量的正則表達(dá)式對(duì)字符串進(jìn)行解析。我們直接來(lái)看編譯后的AST抽象語(yǔ)法樹(shù)是什么樣子:
圖片
從上圖中我們可以看到使用v-model指令的node節(jié)點(diǎn)中有了name為model和rawName為v-model的props了,明顯可以看出將template中code代碼字符串轉(zhuǎn)換為AST抽象語(yǔ)法樹(shù)時(shí)沒(méi)有處理v-model指令。那么什么時(shí)候處理的v-model指令呢?
其實(shí)是在后面的一個(gè)transform函數(shù)中處理的,在這個(gè)函數(shù)中主要調(diào)用的是traverseNode函數(shù)處理AST抽象語(yǔ)法樹(shù)。在traverseNode函數(shù)中會(huì)去遞歸的去處理AST抽象語(yǔ)法樹(shù)中的所有node節(jié)點(diǎn),這也解釋了為什么還要在transform函數(shù)中再抽取出來(lái)一個(gè)traverseNode函數(shù)。
我們?cè)賮?lái)思考一個(gè)問(wèn)題,由于traverseNode函數(shù)會(huì)處理node節(jié)點(diǎn)的所有情況,比如v-model指令、v-for指令、v-on、v-bind。如果將這些的邏輯全部都放到traverseNode函數(shù)中,那traverseNode函數(shù)的體量將會(huì)是非常大的。所以抽取出來(lái)一個(gè)nodeTransforms的概念,這個(gè)nodeTransforms是一個(gè)數(shù)組。里面存了一組transform函數(shù),用于處理node節(jié)點(diǎn)。每個(gè)transform函數(shù)都有自己獨(dú)有的作用,比如transformModel函數(shù)用于處理v-model指令,transformIf函數(shù)用于處理v-if指令。我們來(lái)看看經(jīng)過(guò)transform函數(shù)處理后的AST抽象語(yǔ)法樹(shù)是什么樣的:
從上圖中我們可以看到同一個(gè)使用v-model指令的node節(jié)點(diǎn),經(jīng)過(guò)transform函數(shù)處理后的和第一步經(jīng)過(guò)parse函數(shù)處理后比起來(lái)node節(jié)點(diǎn)最外層多了一個(gè)codegenNode屬性。
我們接下來(lái)看看codegenNode屬性里面是什么樣的:
從上圖中我們可以看到在codegenNode中還有一個(gè)props屬性,在props屬性下面還有一個(gè)properties屬性。這個(gè)properties屬性是一個(gè)數(shù)組,里面就是存的是node節(jié)點(diǎn)經(jīng)過(guò)transform函數(shù)處理后的props屬性的內(nèi)容。我們看到properties數(shù)組中的每一個(gè)item都有key和value屬性,我想你應(yīng)該已經(jīng)反應(yīng)過(guò)來(lái)了,這個(gè)key和value分別對(duì)應(yīng)的是props屬性中的屬性名和屬性值。從上圖中我們看到第一個(gè)屬性的屬性名key的值為modelValue,屬性值value為$setup.inputValue。這個(gè)剛好就對(duì)應(yīng)上v-model指令編譯后的:modelValue="$setup.inputValue"。
我們?cè)賮?lái)接著看第二個(gè)屬性:
圖片
從上圖中我們同樣也可以看到第二個(gè)屬性的屬性名key的值為onUpdate:modelValue,屬性值value的值拼起來(lái)就是為一串箭頭函數(shù),和我們前面編譯后的代碼一模一樣。第二個(gè)屬性剛好就對(duì)應(yīng)上v-model指令編譯后的@update:modelValue="($event) => ($setup.inputValue = $event)"。
從上面的分析我們看到經(jīng)過(guò)transform函數(shù)的處理后已經(jīng)將v-model指令處理為對(duì)應(yīng)的代碼了,接下來(lái)我們要做的事情就是調(diào)用generate函數(shù)將AST抽象語(yǔ)法樹(shù)轉(zhuǎn)換成render函數(shù)
在generate函數(shù)中會(huì)遞歸遍歷AST抽象語(yǔ)法樹(shù),然后生成對(duì)應(yīng)的瀏覽器可執(zhí)行的js代碼。如下圖:
圖片
從上圖中我們可以看到經(jīng)過(guò)generate函數(shù)處理后生成的render函數(shù)和我們之前在瀏覽器的network中看到的經(jīng)過(guò)編譯后的index.vue文件中的render函數(shù)一模一樣。這也證明了modelValue屬性和@update:modelValue事件塞到組件上是在編譯時(shí)進(jìn)行的。
現(xiàn)在我們可以回答前面提的兩個(gè)問(wèn)題了:
在transform函數(shù)中是調(diào)用transformModel函數(shù)處理v-model指令,這篇文章沒(méi)有深入到transformModel函數(shù)源碼內(nèi)去講解。如果大家對(duì)transformModel函數(shù)的源碼感興趣請(qǐng)?jiān)谠u(píng)論區(qū)留言或者給我發(fā)信息,我會(huì)在后面的文章安排上。
本文鏈接:http://www.www897cc.com/showinfo-26-79600-0.html面試官:只知道v-model是modelValue語(yǔ)法糖,那你可以走了
聲明:本網(wǎng)頁(yè)內(nèi)容旨在傳播知識(shí),若有侵權(quán)等問(wèn)題請(qǐng)及時(shí)與本網(wǎng)聯(lián)系,我們將在第一時(shí)間刪除處理。郵件:2376512515@qq.com
上一篇: SpringCloud微服務(wù)中Feign如何傳遞用戶Token,并保證多線程環(huán)境也可適用?