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