Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537 Warning: error_log(/data/www/wwwroot/hmttv.cn/caches/error_log.php): failed to open stream: Permission denied in /data/www/wwwroot/hmttv.cn/phpcms/libs/functions/global.func.php on line 537
們每天寫的vue代碼都是寫在vue文件中,但是瀏覽器卻只認(rèn)識(shí)html、css、js等文件類型。所以這個(gè)時(shí)候就需要一個(gè)工具將vue文件轉(zhuǎn)換為瀏覽器能夠認(rèn)識(shí)的js文件,想必你第一時(shí)間就想到了webpack或者vite。但是webpack和vite本身是沒有能力處理vue文件的,其實(shí)實(shí)際背后生效的是vue-loader和@vitejs/plugin-vue。本文以@vitejs/plugin-vue舉例,通過debug的方式帶你一步一步的搞清楚vue文件是如何編譯為js文件的,看不懂你來打我。
這個(gè)是我的源代碼App.vue文件:
這個(gè)例子很簡(jiǎn)單,在setup中定義了msg變量,然后在template中將msg渲染出來。
下面這個(gè)是我從network中找到的編譯后的js文件,已經(jīng)精簡(jiǎn)過了:
編譯后的js代碼中我們可以看到主要有三部分,想必你也猜到了這三部分剛好對(duì)應(yīng)vue文件的那三塊。
debug搞清楚如何將vue文件編譯為js文件
大家應(yīng)該都知道,前端代碼運(yùn)行環(huán)境主要有兩個(gè),node端和瀏覽器端,分別對(duì)應(yīng)我們熟悉的編譯時(shí)和運(yùn)行時(shí)。瀏覽器明顯是不認(rèn)識(shí)vue文件的,所以vue文件編譯成js這一過程肯定不是在運(yùn)行時(shí)的瀏覽器端。很明顯這一過程是在編譯時(shí)的node端。
要在node端打斷點(diǎn),我們需要啟動(dòng)一個(gè)debug 終端。這里以vscode舉例,首先我們需要打開終端,然后點(diǎn)擊終端中的+號(hào)旁邊的下拉箭頭,在下拉中點(diǎn)擊Javascript Debug Terminal就可以啟動(dòng)一個(gè)debug終端。
假如vue文件編譯為js文件是一個(gè)毛線團(tuán),那么他的線頭一定是vite.config.ts文件中使用@vitejs/plugin-vue的地方。通過這個(gè)線頭開始debug我們就能夠梳理清楚完整的工作流程。
我們給上方圖片的vue函數(shù)打了一個(gè)斷點(diǎn),然后在debug終端上面執(zhí)行yarn dev,我們看到斷點(diǎn)已經(jīng)停留在了vue函數(shù)這里。然后點(diǎn)擊step into,斷點(diǎn)走到了@vitejs/plugin-vue庫(kù)中的一個(gè)vuePlugin函數(shù)中。我們看到vuePlugin函數(shù)中的內(nèi)容代碼大概是這樣的:
@vitejs/plugin-vue是作為一個(gè)plugins插件在vite中使用,vuePlugin函數(shù)返回的對(duì)象中的buildStart、transform方法就是對(duì)應(yīng)的插件鉤子函數(shù)。vite會(huì)在對(duì)應(yīng)的時(shí)候調(diào)用這些插件的鉤子函數(shù),比如當(dāng)vite服務(wù)器啟動(dòng)時(shí)就會(huì)調(diào)用插件里面的buildStart等函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。更多vite鉤子相關(guān)內(nèi)容查看官網(wǎng)。
我們這里主要看buildStart和transform兩個(gè)鉤子函數(shù),分別是服務(wù)器啟動(dòng)時(shí)調(diào)用和解析每個(gè)模塊時(shí)調(diào)用。給這兩個(gè)鉤子函數(shù)打上斷點(diǎn)。
然后點(diǎn)擊Continue(F5),vite服務(wù)啟動(dòng)后就會(huì)走到buildStart鉤子函數(shù)中打的斷點(diǎn)。我們可以看到buildStart鉤子函數(shù)的代碼是這樣的:
將鼠標(biāo)放到options.value.compiler上面我們看到此時(shí)options.value.compiler的值為null,所以代碼會(huì)走到resolveCompiler函數(shù)中,點(diǎn)擊Step Into(F11)走到resolveCompiler函數(shù)中。看到resolveCompiler函數(shù)代碼如下:
在resolveCompiler函數(shù)中調(diào)用了tryResolveCompiler函數(shù),在tryResolveCompiler函數(shù)中判斷當(dāng)前項(xiàng)目是否是vue3.x版本,然后將vue/compiler-sfc包返回。所以經(jīng)過初始化后options.value.compiler的值就是vue的底層庫(kù)vue/compiler-sfc,記住這個(gè)后面會(huì)用。
然后點(diǎn)擊Continue(F5)放掉斷點(diǎn),在瀏覽器中打開對(duì)應(yīng)的頁面,比如:http://localhost:5173/ 。此時(shí)vite將會(huì)編譯這個(gè)頁面要用到的所有文件,就會(huì)走到transform鉤子函數(shù)斷點(diǎn)中了。由于解析每個(gè)文件都會(huì)走到transform鉤子函數(shù)中,但是我們只關(guān)注App.vue文件是如何解析的,所以為了方便我們直接在transform函數(shù)中添加了下面這段代碼,并且刪掉了原來在transform鉤子函數(shù)中打的斷點(diǎn),這樣就只有解析到App.vue文件的時(shí)候才會(huì)走到斷點(diǎn)中去。
經(jīng)過debug我們發(fā)現(xiàn)解析App.vue文件時(shí)transform函數(shù)實(shí)際就是執(zhí)行了transformMain函數(shù),至于transformStyle函數(shù)后面講解析style的時(shí)候會(huì)講:
繼續(xù)debug斷點(diǎn)走進(jìn)transformMain函數(shù),發(fā)現(xiàn)transformMain函數(shù)中代碼邏輯很清晰。按照順序分別是:
我們先來看看createDescriptor函數(shù),將斷點(diǎn)走到createDescriptor(filename, code, options)這一行代碼,可以看到傳入的filename就是App.vue的文件路徑,code就是App.vue中我們寫的源代碼。
debug走進(jìn)createDescriptor函數(shù),看到createDescriptor函數(shù)的代碼如下:
這個(gè)compiler是不是覺得有點(diǎn)熟悉?compiler是調(diào)用createDescriptor函數(shù)時(shí)傳入的第三個(gè)參數(shù)解構(gòu)而來,而第三個(gè)參數(shù)就是options。還記得我們之前在vite啟動(dòng)時(shí)調(diào)用了buildStart鉤子函數(shù),然后將vue底層包vue/compiler-sfc賦值給options的compiler屬性。那這里的compiler.parse其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的parse函數(shù),這是一個(gè)vue暴露出來的底層的API,這篇文章我們不會(huì)對(duì)底層API進(jìn)行源碼解析,通過查看parse函數(shù)的輸入和輸出基本就可以搞清楚parse函數(shù)的作用。下面這個(gè)是parse函數(shù)的類型定義:
從上面我們可以看到parse函數(shù)接收兩個(gè)參數(shù),第一個(gè)參數(shù)為vue文件的源代碼,在我們這里就是App.vue中的code字符串,第二個(gè)參數(shù)是一些options選項(xiàng)。我們?cè)賮砜纯?/span>parse函數(shù)的返回值SFCParseResult,主要有類型為SFCDescriptor的descriptor屬性需要關(guān)注。
仔細(xì)看看SFCDescriptor類型,其中的template屬性就是App.vue文件對(duì)應(yīng)的template標(biāo)簽中的內(nèi)容,里面包含了由App.vue文件中的template模塊編譯成的AST抽象語法樹和原始的template中的代碼。
我們?cè)賮砜?/span>script和scriptSetup屬性,由于vue文件中可以寫多個(gè)script標(biāo)簽,scriptSetup對(duì)應(yīng)的就是有setup的script標(biāo)簽,script對(duì)應(yīng)的就是沒有setup對(duì)應(yīng)的script標(biāo)簽。我們這個(gè)場(chǎng)景中只有scriptSetup屬性,里面同樣包含了App.vue中的script模塊中的內(nèi)容。
我們?cè)賮砜纯?/span>styles屬性,這里的styles屬性是一個(gè)數(shù)組,是因?yàn)槲覀兛梢栽?/span>vue文件中寫多個(gè)style模塊,里面同樣包含了App.vue中的style模塊中的內(nèi)容。
所以這一步執(zhí)行createDescriptor函數(shù)生成的descriptor對(duì)象中主要有三個(gè)屬性,template屬性包含了App.vue文件中的template模塊code字符串和AST抽象語法樹,scriptSetup屬性包含了App.vue文件中的<script setup>模塊的code字符串,styles屬性包含了App.vue文件中<style>模塊中的code字符串。createDescriptor函數(shù)的執(zhí)行流程圖如下:
我們?cè)賮砜?/span>genScriptCode函數(shù)是如何將<script setup>模塊編譯成可執(zhí)行的js代碼,同樣將斷點(diǎn)走到調(diào)用genScriptCode函數(shù)的地方,genScriptCode函數(shù)主要接收我們上一步生成的descriptor對(duì)象,調(diào)用genScriptCode函數(shù)后會(huì)將編譯后的script模塊代碼賦值給scriptCode變量。
將斷點(diǎn)走到genScriptCode函數(shù)內(nèi)部,在genScriptCode函數(shù)中主要就是這行代碼: const script=resolveScript(descriptor, options, ssr, customElement);。將第一步生成的descriptor對(duì)象作為參數(shù)傳給resolveScript函數(shù),返回值就是編譯后的js代碼,genScriptCode函數(shù)的代碼簡(jiǎn)化后如下:
我們繼續(xù)將斷點(diǎn)走到resolveScript函數(shù)內(nèi)部,發(fā)現(xiàn)resolveScript中的代碼其實(shí)也很簡(jiǎn)單,簡(jiǎn)化后的代碼如下:
這里的options.compiler我們前面第一步的時(shí)候已經(jīng)解釋過了,options.compiler對(duì)象實(shí)際就是vue底層包vue/compiler-sfc暴露的對(duì)象,這里的options.compiler.compileScript()其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的compileScript函數(shù),同樣也是一個(gè)vue暴露出來的底層的API,后面我們的分析defineOptions等文章時(shí)會(huì)去深入分析compileScript函數(shù),這篇文章我們不會(huì)去讀compileScript函數(shù)的源碼。通過查看compileScript函數(shù)的輸入和輸出基本就可以搞清楚compileScript函數(shù)的作用。下面這個(gè)是compileScript函數(shù)的類型定義:
這個(gè)函數(shù)的入?yún)⑹且粋€(gè)SFCDescriptor對(duì)象,就是我們第一步調(diào)用生成createDescriptor函數(shù)生成的descriptor對(duì)象,第二個(gè)參數(shù)是一些options選項(xiàng)。我們?cè)賮砜捶祷刂?/span>SFCScriptBlock類型:
返回值類型中主要有scriptAst、scriptSetupAst、content這三個(gè)屬性,scriptAst為編譯不帶setup屬性的script標(biāo)簽生成的AST抽象語法樹。scriptSetupAst為編譯帶setup屬性的script標(biāo)簽生成的AST抽象語法樹,content為vue文件中的script模塊編譯后生成的瀏覽器可執(zhí)行的js代碼。下面這個(gè)是執(zhí)行vue/compiler-sfc的compileScript函數(shù)返回結(jié)果:
繼續(xù)將斷點(diǎn)走回genScriptCode函數(shù),現(xiàn)在邏輯就很清晰了。這里的script對(duì)象就是調(diào)用vue/compiler-sfc的compileScript函數(shù)返回對(duì)象,scriptCode就是script對(duì)象的content屬性 ,也就是將vue文件中的script模塊經(jīng)過編譯后生成瀏覽器可直接執(zhí)行的js代碼code字符串。
genScriptCode函數(shù)的執(zhí)行流程圖如下:
我們?cè)賮砜?/span>genTemplateCode函數(shù)是如何將template模塊編譯成render函數(shù)的,同樣將斷點(diǎn)走到調(diào)用genTemplateCode函數(shù)的地方,genTemplateCode函數(shù)主要接收我們上一步生成的descriptor對(duì)象,調(diào)用genTemplateCode函數(shù)后會(huì)將編譯后的template模塊代碼賦值給templateCode變量。
同樣將斷點(diǎn)走到genTemplateCode函數(shù)內(nèi)部,在genTemplateCode函數(shù)中主要就是返回transformTemplateInMain函數(shù)的返回值,genTemplateCode函數(shù)的代碼簡(jiǎn)化后如下:
我們繼續(xù)將斷點(diǎn)走進(jìn)transformTemplateInMain函數(shù),發(fā)現(xiàn)這里也主要是調(diào)用compile函數(shù),代碼如下:
同理將斷點(diǎn)走進(jìn)到compile函數(shù)內(nèi)部,我們看到compile函數(shù)的代碼是下面這樣的:
同樣這里也用到了options.compiler,調(diào)用options.compiler.compileTemplate()其實(shí)就是調(diào)用的vue/compiler-sfc包暴露出來的compileTemplate函數(shù),這也是一個(gè)vue暴露出來的底層的API。不過這里和前面不同的是compileTemplate接收的不是descriptor對(duì)象,而是一個(gè)SFCTemplateCompileOptions類型的對(duì)象,所以這里需要調(diào)用resolveTemplateCompilerOptions函數(shù)將參數(shù)轉(zhuǎn)換成SFCTemplateCompileOptions類型的對(duì)象。這篇文章我們不會(huì)對(duì)底層API進(jìn)行解析。通過查看compileTemplate函數(shù)的輸入和輸出基本就可以搞清楚compileTemplate函數(shù)的作用。下面這個(gè)是compileTemplate函數(shù)的類型定義:
入?yún)?/span>options主要就是需要編譯的template中的源代碼和對(duì)應(yīng)的AST抽象語法樹。我們來看看返回值SFCTemplateCompileResults,這里面的code就是編譯后的render函數(shù)字符串。
genTemplateCode函數(shù)的執(zhí)行流程圖如下:
genStyleCode函數(shù)
我們?cè)賮砜醋詈笠粋€(gè)genStyleCode函數(shù),同樣將斷點(diǎn)走到調(diào)用genStyleCode的地方。一樣的接收descriptor對(duì)象。代碼如下:
我們將斷點(diǎn)走進(jìn)genStyleCode函數(shù)內(nèi)部,發(fā)現(xiàn)和前面genScriptCode和genTemplateCode函數(shù)有點(diǎn)不一樣,下面這個(gè)是我簡(jiǎn)化后的genStyleCode函數(shù)代碼:
我們前面講過因?yàn)?/span>vue文件中可能會(huì)有多個(gè)style標(biāo)簽,所以descriptor對(duì)象的styles屬性是一個(gè)數(shù)組。遍歷descriptor.styles數(shù)組,我們發(fā)現(xiàn)for循環(huán)內(nèi)全部都是一堆賦值操作,沒有調(diào)用vue/compiler-sfc包暴露出來的任何API。將斷點(diǎn)走到 return stylesCode;,看看stylesCode到底是什么東西?
通過打印我們發(fā)現(xiàn)stylesCode竟然變成了一條import語句,并且import的還是當(dāng)前App.vue文件,只是多了幾個(gè)query分別是:vue、type、index、scoped、lang。再來回憶一下前面講的@vitejs/plugin-vue的transform鉤子函數(shù),當(dāng)vite解析每個(gè)模塊時(shí)就會(huì)調(diào)用transform等函數(shù)。所以當(dāng)代碼運(yùn)行到這行import語句的時(shí)候會(huì)再次走到transform鉤子函數(shù)中。我們?cè)賮砜纯?/span>transform鉤子函數(shù)的代碼:
當(dāng)query中有vue字段,并且query中type字段值為style時(shí)就會(huì)執(zhí)行transformStyle函數(shù),我們給transformStyle函數(shù)打個(gè)斷點(diǎn)。當(dāng)執(zhí)行上面那條import語句時(shí)就會(huì)走到斷點(diǎn)中,我們進(jìn)到transformStyle中看看。
transformStyle函數(shù)的實(shí)現(xiàn)我們看著就很熟悉了,和前面處理template和script一樣都是調(diào)用的vue/compiler-sfc包暴露出來的compileStyleAsync函數(shù),這也是一個(gè)vue暴露出來的底層的API。同樣我們不會(huì)對(duì)底層API進(jìn)行解析。通過查看compileStyleAsync函數(shù)的輸入和輸出基本就可以搞清楚compileStyleAsync函數(shù)的作用。
我們先來看看SFCAsyncStyleCompileOptions入?yún)ⅲ?/span>
入?yún)⒅饕P(guān)注幾個(gè)字段,source字段為style標(biāo)簽中的css原始代碼。scoped字段為style標(biāo)簽中是否有scoped attribute。id字段為我們?cè)谟^察 DOM 結(jié)構(gòu)時(shí)看到的 data-v-xxxxx。這個(gè)是debug時(shí)入?yún)⒔貓D:
再來看看返回值SFCStyleCompileResults對(duì)象,主要就是code屬性,這個(gè)是經(jīng)過編譯后的css字符串,已經(jīng)加上了data-v-xxxxx。
這個(gè)是debug時(shí)compileStyleAsync函數(shù)返回值的截圖:
genStyleCode函數(shù)的執(zhí)行流程圖如下:
現(xiàn)在我們可以來看transformMain函數(shù)簡(jiǎn)化后的代碼:
transformMain函數(shù)中的代碼執(zhí)行主流程,其實(shí)就是對(duì)應(yīng)了一個(gè)vue文件編譯成js文件的流程。
首先調(diào)用createDescriptor函數(shù)將一個(gè)vue文件解析為一個(gè)descriptor對(duì)象。
然后以descriptor對(duì)象為參數(shù)調(diào)用genScriptCode函數(shù),將vue文件中的<script>模塊代碼編譯成瀏覽器可執(zhí)行的js代碼code字符串,賦值給scriptCode變量。
接著以descriptor對(duì)象為參數(shù)調(diào)用genTemplateCode函數(shù),將vue文件中的<template>模塊代碼編譯成render函數(shù)code字符串,賦值給templateCode變量。
然后以descriptor對(duì)象為參數(shù)調(diào)用genStyleCode函數(shù),將vue文件中的<style>模塊代碼編譯成了import語句code字符串,比如:import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";,賦值給stylesCode變量。
然后將scriptCode、templateCode、stylesCode使用換行符\n拼接起來得到resolvedCode,這個(gè)resolvedCode就是一個(gè)vue文件編譯成js文件的代碼code字符串。這個(gè)是debug時(shí)resolvedCode變量值的截圖:
這篇文章通過debug的方式一步一步的帶你了解vue文件編譯成js文件的完整流程,下面是一個(gè)完整的流程圖。如果文字太小看不清,可以將圖片保存下來或者放大看:
@vitejs/plugin-vue-jsx庫(kù)中有個(gè)叫transform的鉤子函數(shù),每當(dāng)vite加載模塊的時(shí)候就會(huì)觸發(fā)這個(gè)鉤子函數(shù)。所以當(dāng)import一個(gè)vue文件的時(shí)候,就會(huì)走到@vitejs/plugin-vue-jsx中的transform鉤子函數(shù)中,在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。
第一次解析這個(gè)vue文件時(shí),在transform鉤子函數(shù)中主要調(diào)用了transformMain函數(shù)。在transformMain函數(shù)中主要調(diào)用了4個(gè)函數(shù),分別是:createDescriptor、genScriptCode、genTemplateCode、genStyleCode。
createDescriptor接收的參數(shù)為當(dāng)前vue文件代碼code字符串,返回值為一個(gè)descriptor對(duì)象。對(duì)象中主要有四個(gè)屬性template、scriptSetup、script、styles。
genScriptCode函數(shù)為底層調(diào)用vue/compiler-sfc的compileScript函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的<script setup>模塊轉(zhuǎn)換為瀏覽器可直接執(zhí)行的js代碼。
genTemplateCode函數(shù)為底層調(diào)用vue/compiler-sfc的compileTemplate函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的<template>模塊轉(zhuǎn)換為render函數(shù)。
genStyleCode函數(shù)為將vue文件的style模塊轉(zhuǎn)換為import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";樣子的import語句。
然后使用換行符\n將genScriptCode函數(shù)、genTemplateCode函數(shù)、genStyleCode函數(shù)的返回值拼接起來賦值給變量resolvedCode,這個(gè)resolvedCode就是vue文件編譯成js文件的code字符串。
當(dāng)瀏覽器執(zhí)行到import "/src/App.vue?vue&type=style&index=0&scoped=7a7a37b1&lang.css";語句時(shí),觸發(fā)了加載模塊操作,再次觸發(fā)了@vitejs/plugin-vue-jsx中的transform鉤子函數(shù)。此時(shí)由于有了type=style的query,所以在transform函數(shù)中會(huì)執(zhí)行transformStyle函數(shù),在transformStyle函數(shù)中同樣也是調(diào)用vue/compiler-sfc的compileStyleAsync函數(shù),根據(jù)第一步的descriptor對(duì)象將vue文件的<style>模塊轉(zhuǎn)換為編譯后的css代碼code字符串,至此編譯style部分也講完了。
avaScript 是語言,而 React 是工具。
力爭(zhēng)用最簡(jiǎn)潔的方式讓你入門 React,前提是你已了解 JavaScript 和 HTML。
如果你在學(xué)習(xí) React 的同時(shí),也在學(xué)習(xí) JavaScript,那么這里羅列了一些必須要懂的 JavaScript 概念,來幫助你更好地學(xué)習(xí) React。
本文將不會(huì)深入講解關(guān)于 JavaScript 方面的知識(shí),你無需非常精通 JavaScript 才能學(xué)習(xí) React,但以上的一些概念是最適合初學(xué)者掌握的 JavaScript 的重要知識(shí)。
當(dāng)然,你也可以跳過這些基本概念直接進(jìn)入下面章節(jié)的學(xué)習(xí),當(dāng)遇到不理解的問題再回過頭來翻閱這里的概念。
要理解 React 如何工作,首先要搞清楚瀏覽器是如何解釋你的代碼并轉(zhuǎn)化成 UI 的。
當(dāng)用戶訪問一個(gè)網(wǎng)頁時(shí),服務(wù)器將返回一段 html 代碼到瀏覽器,它可能看起來是這樣的:
瀏覽器閱讀了 html 代碼,并將它結(jié)構(gòu)化為 DOM。
DOM 是一個(gè) html 元素的對(duì)象化表達(dá),它是銜接你的代碼和 UI 之間的橋梁,它表現(xiàn)為一個(gè)父子關(guān)系的樹形結(jié)構(gòu)。
你可以使用 JavaScript 或 DOM 的內(nèi)置方法來監(jiān)聽用戶事件,操作 DOM 包括,查詢、插入、更新、刪除界面上特定的元素,DOM 操作不僅允許你定位到特定的元素上,并且允許你修改它的內(nèi)容及樣式。
小問答:你可以通過操作 DOM 來修改頁面內(nèi)容嗎?
讓我們一起來嘗試如何使用 JavaScript 及 DOM 方法來添加一個(gè) h1 標(biāo)簽到你的項(xiàng)目中去。
打開我們的代碼編輯軟件,然后創(chuàng)建一個(gè)新的 index.html 的文件,在文件中加入以下代碼:
<!-- index.html -->
<html>
<body>
<div></div>
</body>
</html>
然后給定 div 標(biāo)簽一個(gè)特定的 id ,便于后續(xù)我們可以定位它。
<!-- index.html -->
<html>
<body>
<div id="app"></div>
</body>
</html>
要在 html 文件中編寫 JavaScript 代碼,我們需要添加 script 標(biāo)簽
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script type="text/javascript"></script>
</body>
</html>
現(xiàn)在,我們可以使用 DOM 提供的 getElementById 方法來通過標(biāo)簽的 ID 定位到指定的元素。
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
const app=document.getElementById('app');
</script>
</body>
</html>
你可以繼續(xù)使用 DOM 的一系列方法來創(chuàng)建一個(gè) h1 標(biāo)簽元素,h1 元素中可以包含任何你希望展示的文本。
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script type="text/javascript">
// 定位到 id 為 app 的元素
const app=document.getElementById('app');
// 創(chuàng)建一個(gè) h1 元素
const header=document.createElement('h1');
// 創(chuàng)建一個(gè)文本節(jié)點(diǎn)
const headerContent=document.createTextNode(
'Develop. Preview. Ship. ',
);
// 將文本節(jié)點(diǎn)添加到 h1 元素中去
header.appendChild(headerContent);
// 將 h1 元素添加到 id 為 app 的元素中去
app.appendChild(header);
</script>
</body>
</html>
至此,你可以打開瀏覽器來預(yù)覽一下目前的成果,不出意外的話,你應(yīng)該可以看到一行使用 h1 標(biāo)簽的大字,寫道:Develop. Preview. Ship.
此時(shí),如果你打開瀏覽器的代碼審查功能,你會(huì)注意到在 DOM 中已經(jīng)包含了剛才創(chuàng)建的 h1 標(biāo)簽,但源代碼的 html 中卻并沒有。換言之,你所創(chuàng)建的 html 代碼中與實(shí)際展示的內(nèi)容是不同的。
這是因?yàn)?HTML 代碼中展示的是初始化的頁面內(nèi)容,而 DOM 展示的是更新后的頁面內(nèi)容,這里尤指你通過 JavaScript 代碼對(duì) HTML 所改變后的內(nèi)容。
使用 JavaScript 來更新 DOM,是非常有用的,但也往往比較繁瑣。你寫了如下那么多內(nèi)容,僅僅用來添加一行 h1 標(biāo)簽。如果要編寫一個(gè)大一些的項(xiàng)目,或者團(tuán)隊(duì)開發(fā),就感覺有些杯水車薪了。
<!-- index.html -->
<script type="text/javascript">
const app=document.getElementById('app');
const header=document.createElement('h1');
const headerContent=document.createTextNode('Develop. Preview. Ship. ');
header.appendChild(headerContent);
app.appendChild(header);
</script>
以上這個(gè)例子中,開發(fā)者花了大力氣來“指導(dǎo)”計(jì)算機(jī)該如何做事,但這似乎并不太友好,或者有沒有更友好的方式讓計(jì)算機(jī)迅速理解我們希望達(dá)到的樣子呢?
以上就是一個(gè)很典型的命令式編程,你一步一步的告訴計(jì)算機(jī)該如何更新用戶界面。但對(duì)于創(chuàng)建用戶界面,更好的方式是使用聲明式,因?yàn)槟菢涌梢源蟠蠹涌扉_發(fā)效率。相較于編寫 DOM 方法,最好有種方法能聲明開發(fā)者想要展示的內(nèi)容(本例中就是那個(gè) h1 標(biāo)簽以及它包含的文本內(nèi)容)。
換句話說就是,命令式編程就像你要吃一個(gè)披薩,但你得告訴廚師該如何一步一步做出那個(gè)披薩;而聲明式編程就是你告訴廚師你要吃什么樣的披薩,而無需考慮怎么做。
而 React 正是那個(gè)“懂你”的廚師!
作為一個(gè) React 開發(fā)者,你只需告訴 React 你希望展示什么樣的頁面,而它會(huì)自己找到方法來處理 DOM 并指導(dǎo)它正確地展示出你所要的效果。
小問答:你覺得以下哪句話更像聲明式?
A:我要吃一盤菜,它要先放花生,然后放點(diǎn)雞肉丁,接著炒一下...
B:來份宮保雞丁
要想在項(xiàng)目中使用 React,最簡(jiǎn)單的方法就是從外部 CDN(如:http://unpkg.com)引入兩個(gè) React 的包:
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script type="text/javascript">
const app=document.getElementById('app');
</script>
</body>
</html>
這樣就無需使用純 JavaScript 來直接操作 DOM 了,而是使用來自 react-dom 中的 ReactDOM.render() 方法來告訴 React 在 app 標(biāo)簽中直接渲染 h1 標(biāo)簽及其文本內(nèi)容。
<!-- index.html -->
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<script type="text/javascript">
const app=document.getElementById('app');
ReactDOM.render(<h1>Develop. Preview. Ship. </h1>, app);
</script>
</body>
</html>
但當(dāng)你在瀏覽器中運(yùn)行的時(shí)候,它會(huì)報(bào)一個(gè)語法錯(cuò)誤:
因?yàn)榇a中的 <h1>Develop. Preview. Ship. </h1> 并不是 JavaScript 代碼,而是 JSX。
JSX 是一種 JS 的語法擴(kuò)展,它使得你可以用類 HTML 的方式來描述界面。你無需學(xué)習(xí) HTML 和 JavaScript 之外的新的符號(hào)和語法等,只需要遵守以下三條規(guī)則即可:
<!-- 你可以使用 div 標(biāo)簽 -->
<div>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
...
</ul>
</div>
<!-- 你也可以使用空標(biāo)簽 -->
<>
<h1>Hedy Lamarr's Todos</h1>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
...
</ul>
</>
<!-- 諸如 img 必須自關(guān)閉 <img />,而包圍類標(biāo)簽必須成對(duì)出現(xiàn) <li></li> -->
<>
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
<ul>
<li>Invent new traffic lights</li>
<li>Rehearse a movie scene</li>
<li>Improve the spectrum technology</li>
</ul>
</>
<!-- 如 stroke-width 必須寫成 strokeWidth,而 class 由于是 react 的關(guān)鍵字,因此替換為 className
<img
src="https://i.imgur.com/yXOvdOSs.jpg"
alt="Hedy Lamarr"
className="photo"
/>
JSX 并不是開箱即用的,瀏覽器默認(rèn)情況下是無法解釋 JSX 的,所以你需要一個(gè)編譯器(compiler),諸如 Babel,來將 JSX 代碼轉(zhuǎn)換為普通的瀏覽器能理解的 JavaScript。
復(fù)制粘貼以下腳本到 index.html 文件中:
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
另外,你還需要告訴 Babel 需要轉(zhuǎn)換哪些代碼,為需要轉(zhuǎn)換的代碼添加類型 type="text/jsx"
<html>
<body>
<div id="app"></div>
<script src="https://unpkg.com/react@17/umd/react.development.js"></script>
<script src="https://unpkg.com/react-dom@17/umd/react-dom.development.js"></script>
<!-- Babel Script -->
<script src="https://unpkg.com/@babel/standalone/babel.min.js"></script>
<script type="text/jsx">
const app=document.getElementById('app');
ReactDOM.render(<h1>Develop. Preview. Ship. </h1>, app);
</script>
</body>
</html>
現(xiàn)在可以再次回到瀏覽器中刷新頁面來確認(rèn)是否能成功展示了。
使用聲明式的 React,你只編寫了以下代碼:
<script type="text/jsx">
const app=document.getElementById("app")
ReactDOM.render(<h1>Develop. Preview. Ship. </h1>, app)
</script>
而命令式代碼如此前編寫的:
<script type="text/javascript">
const app=document.getElementById('app');
const header=document.createElement('h1');
const headerContent=document.createTextNode('Develop. Preview. Ship. ');
header.appendChild(headerContent);
app.appendChild(header);
</script>
相比較后不難發(fā)現(xiàn),你節(jié)省了很多重復(fù)冗余的工作。
這就是 React,一款富含可重用代碼,為你節(jié)省時(shí)間和提高效率的工具。
目前,你還無需過多關(guān)注究竟 React 用了什么神奇的魔法實(shí)現(xiàn)這樣的功能。當(dāng)然如果你感興趣的話,可以參考 React 的官方文檔中的 UI Tree 和 render 兩個(gè)章節(jié)。
在真正上手 React 項(xiàng)目之前,還有三個(gè)最重要的 React 核心概念需要理解
在后續(xù)的章節(jié)中我們將逐一學(xué)習(xí)以上三個(gè)核心概念。
一個(gè)用戶界面可以被分割成更小的部分,我們稱之為“組件”。它是自包含、可重用的代碼塊,你可以把它想象成樂高玩具,獨(dú)立的磚塊可以拼裝成更大的組合結(jié)構(gòu)。如果你要更新界面的某一部分,你可以僅更新特定的組件或“磚塊”。
模塊化讓你的代碼更具有可維護(hù)性,因?yàn)槟憧梢暂p易地添加、修改、刪除特定的組件而無需改動(dòng)程序的其他部分。React 組件其實(shí)就是用 JavaScript 編寫的,接下來我們將學(xué)習(xí)如何編寫一個(gè)從 JavaScript 原生到 React 的組件。
在 React 中,組件就是函數(shù),我們?cè)?script 中插入一個(gè) header 方法:
<script type="text/jsx">
const app=document.getElementById("app")
function header() {}
ReactDOM.render(<h1>Develop. Preview. Ship. </h1>, app)
</script>
組件函數(shù)返回一個(gè)界面元素(即我們前面所提到過的單根節(jié)點(diǎn)的元素),可以使用 JSX 語法,如:
<script type="text/jsx">
const app=document.getElementById("app")
function header() {
return (<h1>Develop. Preview. Ship. </h1>)
}
ReactDOM.render(, app)
</script>
然后將 header 傳入 ReactDOM.render 的第一個(gè)參數(shù)中去:
ReactDOM.render(header, app)
但如果你現(xiàn)在刷新瀏覽器預(yù)覽效果的話,將會(huì)報(bào)錯(cuò),因?yàn)檫€需要做兩件事。
首先,React 組件必須以大寫字母開頭:
// 首字母大寫
function Header() {
return <h1>Develop. Preview. Ship. </h1>;
}
ReactDOM.render(Header, app);
其次,你在使用 React 組件時(shí),也需要使用 JSX 的語法格式,將組件名稱用 <> 擴(kuò)起來:
function Header() {
return <h1>Develop. Preview. Ship. </h1>;
}
<br/>ReactDOM.render(<Header />, app);
一個(gè)應(yīng)用程序通常包含多個(gè)組件,有的甚至是組件嵌套的組件。例如我們來創(chuàng)建一個(gè) HomePage 組件:
function Header() {
return <h1>Develop. Preview. Ship. </h1>;
}
function HomePage() {
return <div></div>;
}
ReactDOM.render(<Header />, app);
然后將 Header 組件放入 HomePage 組件中:
function Header() {
return <h1>Develop. Preview. Ship. </h1>;
}
function HomePage() {
return (
<div>
{/* 嵌套的 Header 組件 */}
<Header />
</div>
);
}
ReactDOM.render(<HomePage />, app);
你可以繼續(xù)以這種方式嵌套 React 組件,以形成一個(gè)更大的組件。
比如上圖中,你的頂層組件是 HomePage,它下面包含了一個(gè) Header,一個(gè) ARTICLE 和一個(gè) FOOTER。然后 HEADER 組件下又包含了它的子組件等等。
這樣的模塊化使得你可以在項(xiàng)目的許多其他地方重用組件。
如果你重用 Header 組件,你將顯示相同的內(nèi)容兩次。
function Header() {
return <h1>Develop. Preview. Ship. </h1>;
}
function HomePage() {
return (
<div><br/> <Header />
<Header />
</div>
);
}
但如果你希望在標(biāo)題中傳入不同的文本,或者你需要從外部源獲取數(shù)據(jù)再進(jìn)行文本的設(shè)置時(shí),該怎么辦呢?
普通的 HTML 元素允許你通過設(shè)置對(duì)應(yīng)標(biāo)簽的某些重要屬性來修改其實(shí)際的展示內(nèi)容,比如修改 <img> 的 src 屬性就能修改圖片展示,修改 <a> 的 href 就能改變超文本鏈接的目標(biāo)地址。
同樣的,你可以通過傳入某些屬性值來改變 React 的組件,這被稱為參數(shù)(Props)。
與 JavaScript 函數(shù)類似,你可以設(shè)計(jì)一個(gè)組件接收一些自定義的參數(shù)或者屬性來改變組件的行為或展示效果,并且還允許通過父組件傳遞給子組件。
?? 注意:React 中,數(shù)據(jù)流是順著組件數(shù)傳遞的。這被稱為單向數(shù)據(jù)流。
在 HomePage 組件中,你可以傳入一個(gè)自定義的 title 屬性給 Header 組件,就如同你傳入了一個(gè) HTML 屬性一樣。
// function Header() {
// return <h1>Develop. Preview. Ship. </h1>
// }
function HomePage() {
return (
<div>
<Header title="Hello React" />
</div>
);
}
// ReactDOM.render(<HomePage />, app)
然后,Header 作為子組件可以接收這些傳入的參數(shù),可在組件函數(shù)的第一個(gè)入?yún)⒅蝎@得。
function Header(props) {
return <h1>Develop. Preview. Ship. </h1>
}
你可以嘗試打印 props 來查看它具體是什么東西。
function Header(props) {
console.log(props) // { title: "Hello React" }
return <h1>Hello React</h1>
}
由于 props 是一個(gè) JS 對(duì)象,因此你可以使用對(duì)象解構(gòu)來展開獲得對(duì)象中的具體鍵值。
function Header({ title }) {
console.log(title) // "Hello React"
return <h1>Hello React</h1>
}
現(xiàn)在你就能使用 title 變量來替換 h1 標(biāo)題中的文本了。
function Header({ title }) {
console.log(title) // "Hello React"
return <h1>title</h1>
}
但當(dāng)你打開瀏覽器刷新頁面時(shí),你會(huì)發(fā)現(xiàn)頁面上展示的是標(biāo)題文本是 title,而不是 title 變量的值。這是因?yàn)?React 不能對(duì)純文本進(jìn)行解析,這就需要你額外地對(duì)文本展示做一些處理。
要在 JSX 中使用你定義的變量,你需要使用花括號(hào) {} ,它允許你在其中編寫 JavaScript 表達(dá)式
function Header({ title }) {
console.log(title) // "Hello React"
return <h1>{ title }</h1>
}
通常,它支持如下幾種方式:
這樣你就能根據(jù)參數(shù)輸出不同的標(biāo)題文本了:
function Header({ title }) {
return <h1>{title ? title : 'Hello React!'}</h1>;
}
function Page() {
return (
<div>
<Header title="Hello JavaScript!" />
<Header title="Hello World!" />
</div>
);
}
通常我們會(huì)有一組數(shù)據(jù)需要展示,它以列表形式呈現(xiàn),你可以使用數(shù)組方法來操作數(shù)據(jù),并生成在樣式上統(tǒng)一的不同內(nèi)容。
例如,在 HomePage 中添加一組名字,然后依次展示它們。
function HomePage() {
const names=['Mike', 'Grace', 'Margaret'];
return (
<div>
<Header title="Develop. Preview. Ship. " />
</div>
);
}
然后你可以使用 Array 的 map 方法對(duì)數(shù)據(jù)進(jìn)行迭代輸出,并使用箭頭函數(shù)來將數(shù)據(jù)映射到每個(gè)迭代項(xiàng)目上。
function HomePage() {
const names=['Mike', 'Grace', 'Margaret'];
return (
<div>
<Header title="Develop. Preview. Ship. " />
<ul>
{names.map((name)=> (
<li>{name}</li>
))}
</ul>
</div>
);
}
現(xiàn)在如果你打開瀏覽器查看,會(huì)看到一個(gè)關(guān)于缺少 key 屬性的警告。這是因?yàn)?React 需要通過 key 屬性來唯一識(shí)別數(shù)組上的元素來確定最終需要在 DOM 上更新的項(xiàng)目。通常我們會(huì)使用 id,但本例子中你可以直接使用 name,因?yàn)樗鼈兊闹狄彩俏ㄒ徊煌摹?/span>
function HomePage() {
const names=['Mike', 'Grace', 'Margaret'];
return (
<div>
<Header title="Develop. Preview. Ship. " />
<ul>
{names.map((name)=> (
<li key={name}>{name}</li>
))}
</ul>
</div>
);
}
首先,我們看下 React 是如何通過狀態(tài)和事件處理來幫助我們?cè)黾咏换バ缘摹?/span>
我們?cè)?HomePage 組件中添加一個(gè)“喜歡”按鈕:
function HomePage() {
const names=['Mike', 'Grace', 'Margaret'];
return (
<div>
<Header title="Develop. Preview. Ship. " />
<ul>
{names.map((name)=> (
<li key={name}>{name}</li>
))}
</ul>
<button>Like</button>
</div>
);
}
要讓按鈕在被點(diǎn)擊的時(shí)候做些什么時(shí),你可以在按鈕上添加 onClick 事件屬性:
function HomePage() {
// ...
return (
<div>
{/* ... */}
<button onClick={}>Like</button>
</div>
);
}
在 React 中,屬性名稱都是駝峰命名式的,onClick 是許多事件屬性中的一種,還有一些其他的事件屬性,如:輸入框會(huì)有 onChange ,表單會(huì)有 onSubmit 等。
你可以定義一個(gè)函數(shù)來處理以上一些事件,當(dāng)它被觸發(fā)的時(shí)候。事件處理函數(shù)可以在返回語句之前定義,如:
function HomePage() {
// ...
function handleClick() {
console.log("I like it.")
}
return (
<div>
{/* ... */}
<button onClick={}>Like</button>
</div>
)
}
接著你就可以在 onClick 中調(diào)用 handleClick 方法了。
function HomePage() {
// ...
function handleClick() {
console.log('I like it.');
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Like</button>
</div>
);
}
React 里有一系列鉤子函數(shù)(Hooks),你可以利用鉤子函數(shù)在組件中創(chuàng)建狀態(tài),你可以把狀態(tài)理解為在界面上隨時(shí)間或者行為變化的一些邏輯信息,通常情況下是由用戶觸發(fā)的。
你可以通過狀態(tài)來存儲(chǔ)和增加用戶點(diǎn)擊喜歡按鈕的次數(shù),在這里我們可以使用 React 的 useState 鉤子函數(shù)。
function HomePage() {
React.useState();
}
useState 返回一個(gè)數(shù)組,你可以使用數(shù)組解構(gòu)來使用它。
function HomePage() {
const []=React.useState();
// ...
}
該數(shù)組的第一個(gè)值是狀態(tài)值,你可以定義為任何變量名稱:
function HomePage() {
const [likes]=React.useState();
// ...
}
該數(shù)組的第二個(gè)值是狀態(tài)修改函數(shù),你可以定義為以 set 為前綴的函數(shù)名,如 setLikes。
function HomePage() {
const [likes, setLikes]=React.useState();
// likes 存儲(chǔ)了喜歡被點(diǎn)擊的次數(shù);setLikes 則是用來修改該次數(shù)的函數(shù)
// ...
}
同時(shí),你可以在定義的時(shí)候給出 likes 的初始值
function HomePage() {
const [likes, setLikes]=React.useState(0);
}
然后你可以嘗試查看你設(shè)置的初始值是否生效
function HomePage() {
// ...
const [likes, setLikes]=React.useState(0);
return (
// ...
<button onClick={handleClick}>Like({likes})</button>
);
}
最后,你可以在每次按鈕被點(diǎn)擊后調(diào)用 setLikes 方法來更新 likes 變量的值。
function HomePage() {
// ...
const [likes, setLikes]=React.useState(0);
function handleClick() {
setLikes(likes + 1);
}
return (
<div>
{/* ... */}
<button onClick={handleClick}>Likes ({likes})</button>
</div>
);
}
點(diǎn)擊喜歡按鈕將會(huì)調(diào)用 handleClick 方法, 然后調(diào)用 setLikes 方法將更新后的新值傳入該函數(shù)的第一個(gè)入?yún)⒅小_@樣 likes 變量的值就變成了新值
本章節(jié)僅對(duì)狀態(tài)做了簡(jiǎn)單的介紹,舉例了 useState 的用法,你可以在后續(xù)的學(xué)習(xí)中了解到更多的狀態(tài)管理和數(shù)據(jù)流處理的方法,更多的內(nèi)容可以參考官網(wǎng)的 添加交互性 和 狀態(tài)管理 兩個(gè)章節(jié)進(jìn)行更深入的學(xué)習(xí)。
小問答:請(qǐng)說出參數(shù)(Props)和狀態(tài)(State)的區(qū)別?
到此為止,你已了解了 React 的三大核心概念:組件、參數(shù)和狀態(tài)。對(duì)這些概念的理解越深刻,對(duì)今后開發(fā) React 應(yīng)用就越有幫助。學(xué)習(xí)的旅程還很漫長(zhǎng),途中若有困惑可以隨時(shí)回看本文,或閱讀以下主題文章進(jìn)行更深入的學(xué)習(xí):
React 的學(xué)習(xí)資源層出不窮,你可以在互聯(lián)網(wǎng)上搜索 React 來獲取無窮無盡的資源,但在我看來最好的仍是官方提供的《React 文檔》,它涵蓋了所有你需要學(xué)習(xí)的主題。
最好的學(xué)習(xí)方法就是實(shí)踐。
eact 作為前端開發(fā)的明星框架,其靈魂之一就是 JSX。今天就來詳細(xì)分析一下什么是 JSX,以及如何在開發(fā)中高效使用它。作為一名程序員,這些技巧你不可不知!
JSX 是 JavaScript XML 的縮寫,它是 React 獨(dú)有的一種語法擴(kuò)展,讓你在 JavaScript 代碼中寫類似 HTML 的標(biāo)記。這不僅讓代碼可讀性更強(qiáng),還能直觀地描述 UI 結(jié)構(gòu)。
示例代碼:
const element=<h1>Hello, world!</h1>;
React 通過將 JSX 轉(zhuǎn)換為虛擬 DOM,再通過對(duì)比虛擬 DOM 和實(shí)際 DOM 的差異,來高效地更新 UI。
示例代碼:
import React from 'react';
import ReactDOM from 'react-dom';
const element=<h1>Hello, world!</h1>;
ReactDOM.render(element, document.getElementById('root'));
代碼解析:
JSX 可以嵌套、包含表達(dá)式,還能直接用于條件渲染和數(shù)組渲染,靈活又強(qiáng)大。
嵌套示例:
const element=(
<div>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</div>
);
包含表達(dá)式示例:
const user={
firstName: 'Harper',
lastName: 'Perez'
};
function formatName(user) {
return user.firstName + ' ' + user.lastName;
}
const element=<h1>Hello, {formatName(user)}!</h1>;
示例代碼:
const MyComponent=()=> {
return <h1>Hello, Component!</h1>;
};
ReactDOM.render(<MyComponent />, document.getElementById('root'));
示例代碼:
const element=(
<>
<h1>Hello, world!</h1>
<p>This is a paragraph.</p>
</>
);
示例代碼:
const name='Josh Perez';
const element=<h1>Hello, {name}!</h1>;
掌握 JSX 是深入學(xué)習(xí) React 的起點(diǎn),它不僅提升代碼可讀性,還能大大提高開發(fā)效率。從簡(jiǎn)單的標(biāo)簽嵌套到復(fù)雜的表達(dá)式嵌套,JSX 讓你在編寫 UI 組件時(shí)如魚得水。大家趕緊動(dòng)手試一試吧!
#如何自學(xué)IT#
*請(qǐng)認(rèn)真填寫需求信息,我們會(huì)在24小時(shí)內(nèi)與您取得聯(lián)系。