前言
此筆記參考尚硅谷2021版React技術全家桶全套完整版(零基礎入門到精通/男神天禹老師親授)_嗶哩嗶哩_bilibili學習視頻,感謝視頻發布者和天禹老師!
react是什么?
React是一個將數據渲染為HTML視圖的開源JavaScript庫。
誰開發的?
1.起初由Facebook的軟件工程師Jordan Walke 創建。
2.於2011年部署於Facebook 的newsfeed。
3.隨后在2012年部署於Instagram.
4.2013年5月宣布開源。
為什么要學?
1.原生JavaScript操作DOM繁瑣、效率低(DOM-API操作UI) 。
2.使用JavaScript直接操作DOM,瀏覽器會進行大量的重繪重排。
3.原生JavaScript沒有組件化編碼方案,代碼復用率低。
這里的組件化借用視頻里的例子進行說明:
假設一個開發組里,A已經寫好了一個導航欄,而B想使用A寫好的導航欄,如果在傳統的開發模式(未引入組件化概念),則需要拷貝html代碼,雖然js和css文件可以復用,但是html代碼還是需要寫入,如果引入了組件化模塊,組件會把導航欄里所需要的一切包括但不限於html、css、js或者img等等,都包含在內,此時導入組件就使得開發十分高效,便捷。
特點
1.采用組件化模式、聲明式編碼,提高開發效率及組件復用率。傳統方式使用的是命令式編碼。
2.在React Native中可以使用React語法進行移動端開發。
3.使用虛擬DOM和優秀的Diffing算法(可以簡單理解為高效地比較新舊虛擬DOM不同點的算法),盡量減少與真實DOM的交互,所以瀏覽器的重繪重排次數減少,從而提高效率。
關於第三點的總結:虛擬DOM通過Diffing算法,找出新舊虛擬DOM的區別,只將差別部分渲染給真實DOM,從而最大程度使用真實DOM上已經渲染的部分,從而最小化瀏覽器的重繪重排。
語法
React入門使用(不標准)
作為入門,並未使用webpack環境,故只作為一種入門。
使用react實現一個最簡單的功能,顯示hello react這一段字符串
代碼
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<title>Hello_react</title>
<!-- 引入react引入核心庫, 需要按順序引入 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom, 用於支持react操作DOM -->
<script type='text/javascript' src='../js/react-dom.development.js'></script>
<!-- 引入babel,用於將jsx轉為js -->
<script type='text/javascript' src='../js/babel.min.js'></script>
</head>
<body>
<!-- 准備好一個容器 -->
<div id="test"></div>
<script type='text/babel'> //此處必須寫babel,因為script內部寫的是JSX而非JS
// 1. 創建一個虛擬DOM
let VDOM = <h1>Hello, React!</h1> // 此處不用加引號,因為是虛擬DOM,而且使用的是JSX語法。JSX可以使得js語法和標簽混用
// 2. 讓React 將虛擬DOM(VDOM)插入頁面
// ReactDOM.render(虛擬DOM,目標容器)
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
</body>
</html>
兩種創建虛擬DOM的方式
每個 JSX 元素只是調用 React.createElement(component, props, ...children)
的語法糖。
因此,使用 JSX 可以完成的任何事情都可以通過純 JavaScript 完成。
使用JSX
<script type='text/babel'>
// 1. 創建一個虛擬DOM
let VDOM = (
<h1 id="atguigu">
<span>Hello, React!</span>
</h1>
)
// 2. 讓React 將虛擬DOM(VDOM)插入頁面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
使用js語法創建
<script type='text/javascript'>
// 1.創建虛擬DOM(標簽名,屬性,內容)
let VDOM = React.createElement('h1',{id:'atguigu'},'Hello, React!')
// 2.渲染虛擬DOM到頁面
ReactDOM.render(VDOM,document.getElementById('test'))
</script>
虛擬DOM和真實DOM
關於虛擬DOM和真實DOM
1.虛擬DOM比較“輕”,真實DOM比較“重”(這里的輕和重主要體現在屬性個數上),因為虛擬DOM是React在用,無需真實DOM上那么多屬性。
2.虛擬DOM最終一定會轉為真實DOM放入頁面。
console.log(typeof VDOM); // object
console.log(VDOM instanceof Object); // true
const TDOM = document.getElementById('test')
console.log('虛擬DOM:', VDOM);
console.log('真實DOM:', TDOM);
瀏覽器顯示如下:
JSX
簡介
JSX:全稱JavsScript XML。
是react定義的一種類似於XML的JS擴展語法:JS + XML 本質是
React.createElement(component, props, ...children)方法的語法糖。
jsx語法規則
1.創建虛擬DOM時,不要寫引號;
2.標簽中要混入【js表達式】(函數的調用、變量的調取等等),而非js語句(比如if for while等等),要使用{}。
const data = `hello!`
//虛擬DOM
const VDOM = <h1>{data}</h1>
ReactDOM.render(VDOM, document.getElementById('test'));
PS:js表達式和js語句的區別:
①.JS表達式:一個表達式會產生一個值,可以放在任何一個需要值的地方
(1) a
(2) a + b
(3) demo(1)
(4) arr.map(),用於加工數組,不會影響原數組。
(5) function test(){}
②.JS語句(代碼):
(1) if 判斷語句
(2) for(){} 循環語句
(3) switch(){case:xxx} 分支語句
3.標簽中樣式的類名要用className來指定
const VDOM = <h1 className="title">{data}</h1>
4.標簽中的內聯樣式要用style={{color:'white'}},屬性名轉為小駝峰。比如style里的字體大小font-size在JSX中得轉換為fontSize。
- 正常情況下,在html定義內聯樣式的方式
<div id="test" style="color:white;"></div>
- 在JSX下需要用小駝峰和雙括號,此處第一對花括號代表第一對花括號里寫的是js代碼,第二對花括號表示的是js語法里的對象的意思,white加單引號表示字符串而非white變量。
const VDOM = <h1 className="title" style={{color:'white'}}>{data}</h1>
-
若有多個style樣式,並且需要轉換小駝峰時,注意這里font-size是fontSize!!
let VDOM = <h1 style={{ color: 'red', fontSize: '16px' }} className="title">hello~!</h1>
5.VDOM每次創建只能有一個根標簽。
(1)如果VDOM有多個標簽,必須用div包裹起來才行
const VDOM=(
<h1>你好</h1>
<span>中國!</span>
)
以上的寫法是錯誤的,必須用div包裹起來才可行!
const VDOM = (
<div>
<h1>你好</h1>
<span>中國!</span>
</div>
)
6.標簽必須閉合(單標簽加 / 自閉合)
此處以舉例,在html中input標簽不會自動閉合會以以下形式呈現:
<input type="text">
這種寫法在html中可以使用,但是在react中,必須要閉合標簽,第一種是自閉合(推薦),第二種是用標簽對閉合,如果不閉合直接寫成上面的形式,則會報錯!
const VDOM = (
<div>
<h1>你好</h1>
<span>中國!</span>
<input type="text"/>
<input type="text"></input>
</div>
)
7.關於標簽首字母:
1) 若首字母小寫,那么React就會去尋找與之同名的<html標簽>
· 若找見,直接轉為html同名元素
· 若未找見,報錯
2) 若首字母大寫,那么React就會去尋找與之同名的組件(component),
· 若找到就使用
· 若沒有就會報錯
8.注釋時先用{}包起來變成js表達式再注釋,在html里面注釋react語句會比較麻煩,在后期使用腳手架之后,直接在.jsx文件里注釋就方便了。
const VDOM = (
<div>
{/*<h1>你好</h1>*/}
<span>中國!</span>
</div>
)
JSX練習
1.模擬從數據(此處用數組進行模擬)中取數據再呈現到頁面上:
const data=['Angular','React','Vue']
const VDOM = (
<div>
<h1>前端JS框架列表</h1>
<ul>
{
/*注意這里只能寫js表達式,不能寫js語句,故使用數組的map方法*/
data.map((item,idex)=>{
return <li key={index}>{item}</li>
})
/*因為在react中標簽里摻雜js變量的時候需要加{},這里加key是因為react使用了diffing算法,需要用key來比較,如果添加了相同的key或者不添加key的話,就會彈出警告!*/
}
</ul>
</div>
)
ReactDOM.render(VDOM, document.getElementById('test'))
因為只有一個返回值,箭頭函數可以進一步簡寫為:
data.map((item,idex)=><li key={index}>{item}</li>)
(1)注意點:
①react里只能寫js表達式而不能寫js語句!
②li標簽里需要添加不同的key屬性以供react的diffing算法比較
③在JSX或者說react語法中,標簽里摻雜js變量的時候需要加{},這是react的特性與ES6的新特性無關。在ES6中與這個比較相像的特性就是模板字符串中可以用${}來取js變量,正如下面所示!
2.如果用js實現該功能的話
const arr = ['Angular','React','Vue']
let arr2 = arr.map((item,index)=>{
return `<li>${item}</li>`
})
// map加工數組,但不影響原數組
document.getElementById("test").innerHTML=arr2.join().replaceAll(",","");
React編程所需的工具
需要瀏覽器的一個插件,名叫React Developer Tools,可以直接從插件商店里下載。
定義組件的兩種方式
組件的定義:實現頁面局部功能全部代碼和資源的集合。
使用函數定義
該方法便捷,但是有缺陷,雖然有補救措施,但是不推薦。
js中調用函數的幾種方式
1.直接調用
function test(){
console.log(this);
}
直接使用函數名加小括號的方式調用該函數。
test();
此時test函數里的this指向的就是window。
2.通過函數名.call()方式調用,這種調用方式會改變函數中的this指向,參數依次傳入,完整調用格式如下:
最直接的體現就是如果第一個例子中改為test.call(1),控制台就會打印如下:
3.通過函數名.apply()調用,同樣可以改變this指向,參數以數組方式傳入。
4.通過bind函數返回一個新的函數再調用
以上四種具體區別可以總結為以下幾點:
該部分參考(80條消息) JS中的call()和apply()方法和區別_世平那個張的博客-CSDN博客_js中apply和call的作用和區別是什么,感謝作者世平那個張!
具體寫法
<body>
<div id="test"></div>
<script type="text/babel">
// 1.定義一個組件(函數式)
function Demo(){
console.log(this) // 此處的this是undefined,因為經過babel翻譯,開啟了嚴格模式
return <h2>我是用函數定義的組件(適用於【簡單組件】的定義)</h2> // 組件標簽
}
// 2.渲染組件到頁面
ReactDOM.render(<Demo/>,document.getElementById('test'))
</script>
</body>
注意點:
(1)構成組件的函數名稱的首字母必須大寫,具體原因與babel翻譯有關。
此處參考(80條消息) react聲明組件時,第一個字母必須大寫,為什么呢_codingWeb的博客-CSDN博客_react 組件大寫,感謝作者codingWeb!
總結來說,如果不首字母大寫,babel在轉義時就會把其當成一個字符串傳入React.createElement()這個方法,如果首字母大寫了,babel在轉義時就會傳入一個變量。具體如下圖所示:
(2)在渲染時,組件名必須使用<組件名/>的這種閉包形式(當然<組件名></組件名>也可以)。
(3)在執行了ReactDOM.render(
①React解析組件標簽,尋找Demo組件的定義位置
②React發現Demo組件是用函數定義的,隨后React去直接調用Demo函數,將【返回的值】渲染到頁面
(4)該函數形式定義組件所存在的殘疾的部分就是:函數內部的this是undefined,具體原因是babel翻譯時開啟了嚴格模式,不允許直接指向window。
使用類定義
寫法較為繁瑣,但是功能完備,不殘疾,推薦。
復習js中的類
1.類中的構造器不是必須要寫的,如果想給實例添加一些自己獨有的屬性,那么就要寫構造器;
2.如果A類繼承了B類,且A類寫了構造器,那么在A類的構造器中必須調用super。
3.類中的方法是放在類的原型對象上的,供實例使用,如果是通過實例調用的方法,那么方法中的this就是實例對象。
具體寫法
<div id="test"></div>
<script type="text/babel">
// 1.定義一個組件(類式)
class Demo extends React.Component{
/**
* 1. render方法是放在哪里的? ———— Demo的原型對象上,供實例使用
* 2. render中的this是誰? ———— Demo的實例對象
*/
render(){
console.log(this); // Demo類的實例對象 <=> Demo組件的實例對象 <=> 也稱“Demo組件對象”
return <h2>我是用類定義的組件(適用於【復雜組件】的定義)</h2>
}
}
// 2.渲染組件到頁面
ReactDOM.render(<Demo/>, document.getElementById('test'))
</script>
注意點:
①類組件必須繼承React.Component。
②類組件內部必須重新render方法,同時該render方法必須有返回值,因為React會調用該組件的render方法來獲取返回值。
③執行了ReactDOM.render(
1.React解析組件標簽,尋找Demo組件的定義
2.React發現Demo組件是用類定義的,React實例創建了一個Demo的實例對象-D
3.通過D去調用到了render方法,D.render()。
組件實例三大屬性
注意:這里的三大屬性主要是針對通過類定義的組件!
state
簡介
1.state是組件對象最重要的相互ing,值是對象(可以包含多個key-value的組合)
2.組件又被稱為‘狀態機’,通過更新組件的state來更新對應的頁面顯示(重新渲染組件)
代碼
實現需求:現在有一段話“今天天氣很涼爽”,當鼠標點擊涼爽時,這兩個字就會變成炎熱,點擊炎熱時這兩個字就會變成涼爽。
一般實現
<script type="text/babel">
class Weather extends React.Component{
constructor(props) {
super(props)
this.state = {isHot:false} // 初始化狀態
}
render(){
return <h1 onClick={this.changeWeather}>今天天氣很{this.state.isHot ? '炎熱' : '涼爽'}</h1>
}
changeWeather(){
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
}
ReactDOM.render(<Weather/>,document.getElementById('test'))
</script>
注意點:
1.原生屬性比如是onclik在react中需要寫成onClick,以駝峰的方式命名!
render(){
return <h1 onClick={this.changeWeather}>今天天氣很{this.state.isHot ? '炎熱' : '涼爽'}</h1>
}
2.react中事件綁定的方式與原生js有些不同。
在原生js和html中,可以直接寫函數名加小括號,這樣的效果是當我們點擊時才會調用show函數,不會主動調用。
<h1 onclick="show()">今天天氣不錯</h1>
但是在react中,要想實現以上效果,則函數名后面不能加小括號。若加了小括號,函數就會自動調用無論你是否有點擊事件的發生。
render(){
return <h1 onClick={this.changeWeather}>今天天氣很{this.state.isHot ? '炎熱' : '涼爽'}</h1>
}
3..在JSX代碼里,在組件類外部直接寫function,然后function中的this因為babel的嚴格模式,指向的會是undefined!即:在嚴格模式下禁止this關鍵字指向全局對象!
4.在js中,類中函數方法的調用者是類的實例對象,如果類中的一個函數方法中內部要調用另一個函數方法,必須要用this.函數名()的形式來調用。
class dog{
bark(){
console.log("bark");
}
cry(){
this.bark();//必須加上this,才能調用一個類中的另一個方法
console,log("bark");
}
}
5.js中的類中的this詳解,此處代碼沒有被babel包裹
class Dog {
constructor(name,age){
this.name = name
this.age = age
}
bark(){
console.log('bark的this是:',this);
}
}
const d = new Dog('DOGE',4)
d.bark();//此時this指向的就是dog
(1)當類中的函數方法被類的實例對象調用時,this指向的就是類的實例。但是如果通過第二個變量得到了堆中函數的位置,直接通過第二個變量直接調用,雖然是全局調用,但是這里this指向的就是undefined了,因為類中所有定義的方法,瀏覽器在運行時,全都加上了use strict,在嚴格模式禁止this關鍵字指向全局對象。
const d = new Dog('DOGE',4)
const x = d.bark
x()
(2)但如果是直接是對象的話,同樣條件下調用,this得到的就是window了
let obj = {
a:1,
b:2,
c:function(){
console.log(this);
}
}
const y = obj.c
y()//會打印windo
(3)嚴格模式可以局部開啟的,如下面的代碼所示
function demo1(){
'use strict' //局部開啟嚴格模式
console.log('demo1:',this);
}
function demo2(){
console.log('demo2:',this);
}
demo1()
demo2()
可以看到,開啟了嚴格模式的函數直接調用this的話指向的就不是window了。
6.類中定義的函數方法都是在原型上的
7.通過bind改變this指向,bind改完this指向后會返回一個新的改過this的函數
this.changeWeather = this.changeWeather.bind(this)
這句話先讀賦值語句的右側,先從構造器的this指向(就是Weather)的原型中找到changeWeather,把該changeWeather中的this改好了之后再賦值給自己,這樣Weather的實例對象就有了changeWeahter方法了。做完上述步驟后,就會有兩個changeWeather,一個在對象的內部原型外部,還有一個在原型內部,前面一個的this是正常的不受嚴格模式約束的,后面一個this是受嚴格模式約束的。
8.state無法直接修改,需要用setState來修改。這是state的一大特性!
簡便寫法
<div id="test"></div>
<script type="text/babel">
class Weather extends React.Component {
state = { isHot: false }
render() {
console.log('render里的this:', this)
return <h1 onClick={this.changeWeather}>今天天氣很{this.state.isHot ? '炎熱' : '涼爽'}</h1>
}
// 組件類中程序員定義的事件回調,必須寫成賦值語句+箭頭函數
// 避免了this為undefined的問題
changeWeather = ()=> {
console.log('changeWeather里的this', this);
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
}
ReactDOM.render(<Weather />, document.getElementById('test'))
</script>
說明
1.在類中直接寫一個賦值語句例如a=1,等價於給這個類新增一個屬性a,其值為1。
2.changeWeather也可以用同樣的方式簡寫,但是不能用傳統的function的方式聲明,這樣會被babel強制應用嚴格模式。所以必須使用箭頭函數,箭頭函數內部沒有this,當在箭頭函數內部使用this時會調用箭頭函數外部的this。
這樣就是不行的!
changeWeather = function() {
console.log('changeWeather里的this', this);
const isHot = this.state.isHot
this.setState({isHot:!isHot})
}
props
定義
props是專門設計為從組件外部收集信息傳遞給組件內部的類組件里的對象。
用法
在類組件中使用props
目前有個需求就是在網頁上顯示兩個人的姓名、性別和年齡。
1.分別傳遞標簽屬性
<div id="test"></div>
<div id="test2"></div>
<script type="text/babel">
// 1.定義一個Person組件
class Person extends React.Component{
render(){
const {name,sex,age} = this.props
return (
<ul>
<li>NAME: {name}</li>
<li>GENDER: {sex}</li>
<li>AGE: {age}</li>
</ul>
)
}
}
// 2.渲染組件到頁面(分別傳遞標簽屬性)
ReactDOM.render(<Person name="老劉" sex="female" age="23" />,document.getElementById('test'))
</script>
效果圖:
2.批量傳遞標簽屬性
// (模擬一個服務器返回人的數據)
const p1 = {
name:'強哥',
sex:'女',
age: 19
}
// 批量傳遞標簽屬性
ReactDOM.render(<Person {...p1}/>, document.getElementById('test2'))
在函數式組件中使用props
可以通過函數參數的形式把props傳入到函數內部,但是對props的限制的語句就只能寫在外部了,不能像類組件那樣寫在組件的內部。
<script type="text/babel">
function Person(props) { // 用參數傳入props
const { name, age, sex } = props
return (
<ul>
<li>姓名:{name}</li>
<li>性別:{sex}</li>
<li>年齡:{age}</li>
</ul>
)
}
// 限制只能寫在外面
Person.propTypes = {
name: PropTypes.string.isRequired, // .isRequired要求必須傳
age: PropTypes.number,
asex: PropTypes.string
}
Person.defaultProps = { // 默認值
age: 18
}
const p1 = {
name: 'Nicolas',
sex: 'male',
age: 27,
}
ReactDOM.render(<Person {...p1} />, document.getElementById('test'))
</script>
對props進行限制
react提供方法給props進行限制,不過需要引入prop-types.js。
對props限制既可以限制類型,也可以限制必須傳哪些屬性,也可以設置屬性的默認值。
<script type="text/babel">
class Person extends React.Component {
state = {} // 任何時候狀態放第一
static propTypes = {
name: PropTypes.string.isRequired, // .isRequired要求必須傳
age: PropTypes.number,
sex: PropTypes.string
}
static defaultProps = { // 默認值
age: 18
}
render() {
let { name, sex, age } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性別:{sex}</li>
<li>年齡:{age + 1}</li>
</ul>
)
}
}
const p1 = {
name: 'Nicolas',
sex: 'male',
age: 27,
}
ReactDOM.render(<Person {...p1} />, document.getElementById('test'))
</script>
static propTypes中可以限制props屬性的類型和是否是必須傳入的,static defaultProps可以設置props的默認值。假如沒有傳入指定類型,雖然頁面可以顯示但是瀏覽器控制台就會報錯!
比如給age傳入了一個字符串
注意點和補充復習知識
1.props對象中的屬性里的值是只讀的,在賦值之后無法再修改。
比如props里有一個name屬性,則以下代碼會在瀏覽器中報錯!
2.關於ES7中的打包和拆包功能復習:
有關具體的展開功能的詳細介紹,可以查看MDN的鏈接:展開語法 - JavaScript | MDN (mozilla.org)
(1)拆包
①...展開語法可以對數組使用。
let arr = [4,5,6,7]
console.log(arr); // (5) [1, 3, 5, 7, 9]
console.log(...arr); // 1 3 5 7 9 (拆包)
let arr2 = [1,2,3,...arr,8] // 拆包
console.log(arr2);// (8) [1, 2, 3, 4, 5, 6, 7, 8]
②在原生js中,展開語法無法對對象展開,因為對象並沒有迭代器。
let obj = {name:'圓圓的海峰',age:18,sex:'女'}
let obj2 = {nationality:'Chinese',height:'180cm'}
console.log(...obj)//這樣寫就會報錯
③展開語法可以對對象使用,條件是要包在一個對象內部。比如
console.log(...obj); // {name:'圓圓的海峰',age:18,sex:'女'}
console.log({...obj,...obj2}); // {name: '圓圓的海峰', age: 18, sex: '女', nationality: 'Chinese', height: '180cm'}
console.log({...obj,name:'LX'}); // {name: 'LX', age: 18, sex: '女'}
(2)打包,最典型的使用場景就是當一個函數不知道自己接受的參數有多少個時,可以用...params來將參數打包起來。
function demo(...params){ // 打包
console.log('我收到的參數為:', params);
}
demo(1,2,3)
3.在原生里...無法展開對象,但是在react里可以,但僅適用於傳遞標簽屬性,為什么要加{}呢?是因為是模板語法,JSX的基本語法中遇到 HTML 標簽(以 < 開頭),就用 HTML 規則解析;遇到代碼塊(以 { 開頭),就用 JavaScript 規則解析;
所以在react中{...obj}就等價於js中的...obj。
4.關於class中的static屬性
在class中,未加static的屬性會出現在類的實例對象的屬性中,而加了static的屬性就會出現在類的屬性中,例如:
class Dog {
constructor(){
}
run(){
}
a = 1
static b = 2
}
const d = new Dog()
console.log(d);
console.log(Dog.b);//2 即 b是在Dog類上的,可以通過類名加上屬性名進行調用
這也是限制props的幾個屬性前面要加static的原因,因為加了的話就會隨着類走,也比較方便。
5.類式組件中的構造器完全可以省略。若寫了構造器則super必須調用,且當需要在構造器中通過this.props取值時,那么props要傳給super。
refs與事件處理
簡單來說ref就是react中標簽中id的替代者,不過react建議能不用ref就不用ref,因為ref會占用的內存比較大!下面會有解決辦法的!
字符串形式的refs
不推薦使用,略過。
回調形式的refs
refs寫成函數形式后,react來幫我們調用,函數的參數就是包含ref屬性的標簽。但也有點問題,不推薦。
createRef形式的ref
使用React.createRef()在類內部創建容器后,將需要操作的ref存入容器中,最后再在函數內部直接調用容器內部的ref。
<script type="text/babel">
class Demo extends React.Component{
container = React.createRef()
container2 = React.createRef()
render(){
return(
<div>
<input type="text" ref={this.container} />
<button onClick={this.show}>點我提示左側數據</button>
<input type="text" ref={this.container2} onBlur={this.show2} placeholder="失去焦點提示數據"/>
</div>
)
}
show = ()=>{
console.log(this.container); // {current: input}
alert(this.container.current.value)
}
show2 = ()=>{
alert(this.container2.current.value)
}
}
ReactDOM.render(<Demo/>,document.getElementById('test'))
</script>
事件處理
- 通過onXxxx屬性指定事件處理函數(注意大小寫)
(1) React使用的是自定義(合成)事件,而不是使用的原生DOM事件這樣有利於提高效率,因為原生事件中如果有用不到的屬性,它還是會給你顯示出來,而react的自定義事件中的屬性,用到了才會給你,若沒有用到就不會給,提高了效率。
(2) React中的事件是通過事件委托方式處理的(委托給組件最外層的元素) - 通過event.target得到事件發生的DOM元素對象
非受控組件和受控組件
非受控組件
定義
表單中的數據,在需要的時候“現用現取”,即通過ref獲得到節點,進而訪問到value值。這里的非受控主要指的是數據與組件的state沒有建立起聯系來,如果一輸入,value值就往狀態state里面跑,那就不是非受控組件了。
示例代碼
這里的ref使用了回調形式。
<script type="text/babel">
class Login extends React.Component{
render(){
return(
<form onSubmit={this.handleLogin}>
{
}
用戶名:<input type="text" ref={c => this.userNameNode = c} /><br/>
密碼:<input type="password" ref={c => this.passwordNode = c}/><br/>
<button>登陸</button>
</form>
)
}
handleLogin = ()=>{
const {userNameNode,passwordNode} = this
console.log(userNameNode);
alert(`用戶名:${userNameNode.value}, 密碼:${passwordNode.value}` )
}
}
ReactDOM.render(<Login/>,document.getElementById('test'))
</script>
受控組件
定義
表單中輸入類的DOM,隨着用戶的輸入,將值自動收集到State中,那么就稱為受控組件。
代碼
最垃圾的寫法,有一個屬性就寫一個函數
1.以下代碼有弊端,有一個屬性就要寫一個save函數比較繁瑣。
為什么不寫this.saveDate("password")來用一個函數來完成各個函數的功能呢?因為如果寫了saveDate("password")的話就是一個函數調用了,react就會自動調用這個函數,並把這個函數的返回值給onchange,在這里的場景下與我們的需求相悖。
class Login extends React.Component{
state = {
username: "",
password: "123"
}
render(){
return(
<form onSubmit={this.handleLogin}>
{
}
用戶名:<input type="text" onChange={this.saveUsername} /><br/>
密碼:<input type="password" onChange={this.savePassword} /><br/>
<button>登陸</button>
</form>
)
}
// 保存用戶名到State中
saveUsername = (event)=>{
this.setState({username:event.target.value})
//setState不會造成其他state屬性值的改變或者丟失,只會改變setState函數傳入的屬性值。
}
// 保存密碼到state中
savePassword = (event)=>{
this.setState({password:event.target.value})
}
handleLogin = (event)=>{
event.preventDefault()
const {username,password} = this.state
alert(`用戶名:${username}, 密碼:${password}`)
}
}
ReactDOM.render(<Login/>,document.getElementById('test'))
注意:setState不會造成其他state屬性值的改變或者丟失,只會改變setState函數傳入的屬性值。
使用高級函數和函數的柯里化
1.高級函數和函數柯里化的定義:
(1)高階函數:如果一個函數符合下面2個規范中的任意一個,該函數即為高階函數
①若A函數接收的參數是一個函數,那么A即為高階函數
②若A函數調用的返回值依然是一個函數,那么A為高階函數
常見的高階函數:Promise,setTimeout,arr.map(),bind
(2)函數的柯里化:通過函數調用繼續返回函數的方式,實現多次接收參數最后統一處理的函數編碼形式。
比如:
//正常函數寫法
function sum(a,b,c){
return a+b+c
}
如果使用柯里化寫法和高級函數:
function sum(a)
{
return (b) => {
return (c) => {
return a + b + c ;
}
}
}
2.使用這兩項技術來解決受控組件問題
class Login extends React.Component{
state = { // 初始化狀態
username: "",
password: "123"
}
render(){
return(
<form onSubmit={this.handleLogin}>
{
}
用戶名:<input type="texnCht" onChange={this.saveFormData("username")} /><br/>
密碼:<input type="password" onChange={this.saveFormData("password")} /><br/>
<button>登陸</button>
</form>
)
}
saveFormData = (type) =>{
return (event)=>{
this.setState({[type]:event.target.value})
{/*這里的type加中括號的原因看下面的備注*/}
}
}
handleLogin = (event)=>{
event.preventDefault()
const {username,password} = this.state
alert(
`用戶名:${username}, 密碼:${password}`
)
}
}
ReactDOM.render(<Login/>,document.getElementById('test'))
3.在對象中,我們給對象的屬性賦值有兩種方式,假如我想給一個Person對象賦值一個屬性,屬性名字叫name而其中的value值是張三
(1)第一種賦值方式,直接用對象.加上屬性名的方式給對象添加屬性。
let Person = {}
Person.name="張三"
注意如果有一個字符串變量叫name,用.的方式的話就會失效!
let a='name'
Person.a="張三"//這樣就會變成Person里有個屬性a,這個屬性的值為張三
(2)第二賦值屬性的方式,使用中括號,這種方式可以讀取變量作為屬性的名稱
Person['name']='張三'
//等價於
let a = 'name'
Person[a]='張三'
所以這里也是上面的type要加中括號的原因!
不使用高級函數和柯里化
我們研究一下JSX或者說reac中標簽里onChange后面跟的東西,首先跟着的必須是一個函數,但是如果是傳統寫法即函數名加上()的話就會直接調用,使用傳統方式的話必須不能加括號,這就導致了不能傳遞type的值。
如果使用箭頭函數,因為箭頭函數寫出來並不是調用的,在箭頭函數的函數體內部寫一個統一的處理data的函數,該函數接收event和type做參數,則就可以解決問題了。
代碼如下:
用戶名:<input type="texnCht" onChange={event => this.saveFormData(event, "username")} /><br />
密碼:<input type="password" onChange={event => this.saveFormData(event, "password")} /><br />
同時saveFormData函數如下所示:
saveFormData = (event, type) => {
this.setState({ [type]: event.target.value})
}
這樣就解決了,既沒有返回一個函數,又沒有以函數作為參數。
組件的生命周期
舊生命周期
圖示
簡而言之,在render函數執行的前后階段react會自動調用一些函數,只不過我們沒有寫的話就默認是沒有函數體的函數。
比如componentWillMount就是在render之前要執行的,那個函數的意思就是組件將要掛載時執行。
例子
假設現有一個需求,要實現頁面上的一段文字漸變成透明的,並且還有一個按鈕,點擊按鈕之后組件就卸載並且消失。
漸變可以通過定時器循環執行來遞減文字的透明度,但是這個遞減過程放在哪里比較關鍵,不能放在render中。
<script type="text/babel">
class Life extends React.Component {
state = {
opacity:1
}
render() { // 1+n次,隨着狀態改變即調用
console.log('render');
const {opacity} = this.state
return (
<div>
<h1 style={{opacity}}>分手了怎么辦?</h1>
<button onClick={this.death}>不活了</button>
</div>
)
}
componentDidMount(){ // 1次
console.log('componentDidMount');
this.timer = setInterval(() => {
//1.獲取原來的opacity
let {opacity} = this.state
//2.遞減
opacity -= 0.1
if(opacity <= 0) opacity = 1
//3.賦回去
this.setState({opacity})
}, 200);
}
componentWillUnmount(){ // 1次
console.log('componentWillUnmount');
clearInterval(this.timer)
}
death = ()=>{
ReactDOM.unmountComponentAtNode(document.getElementById('test'))
}
}
ReactDOM.render(<Life />, document.getElementById('test'))
</script>
componentWillUnmount()是將要卸載時執行的函數,componentDidMount()是已經掛載完畢后執行的函數,這些生命周期函數(鈎子函數)都僅執行一次。
總結鈎子函數
1.初始化階段:由ReactDOM.render()觸發 --- 初次渲染
(1) constructor()
(2)componentWillMount()(在新版本中被不推薦使用了)
(3)render()
(4)componentDidMount(),重要,比如開啟定時器、發送ajax請求、消息訂閱等等
- 更新階段:由組件內部this.setState()或父組件重新render觸發
(1) shouldComponentUpdate()
(2)componentWillUpdate()也比較重要,主要做一些收尾的事情,例如關閉定時器。(在新版本中被不推薦使用了)
(3)render(),老大哥地位!
(4)componentDidUpdate() - 卸載組件:由ReactDOM.unmountComponentAtNode()觸發
(1)componentWillUnmount()(在新版本中被不推薦使用了)
注意點
1.render函數會執行1+n次,其中的1表示一上來就會調用1次,n次表示如果組件里的state改變了,那就會render一次。
2.透明度遞減函數不能放在render里,因為會遞歸調用很多很多次,會導致頁面頻繁渲染。放在構造器里雖然可行但是並不是正確的放置方式。正確的放置位置應在鈎子函數中即組件的生命周期函數中。
3.shouldComponentUpdate可以看做是組件更新的一個閥門,該函數返回為true時,組件才能繼續更新否則組件不會更新。不過forceUpdate()可以繞過鈎子,強制更新
4.閥門關閉之后,調用setState()只是不render但是內部的state值還是會根據具體setState()函數進行修改調整。如果強制更新的話,會直接更新為最新的state值。
5.componentWillReceiveProps表示的是父組件給子組件傳props參數時在子組件中調用的函數,並且只有在父組件第二次及以上調用render時才會調用這個函數,父組件第一次調用render時並不會自動調用這個鈎子函數。
新生命周期
從react17.0之后就是新生命周期了,但是依然可以用舊鈎子,不過並不推薦,在18.0版本及以后,舊鈎子必須加上UNSAFE_前綴才能正常奏效工作了。(此處添加UNSAFE與安全性並無直接關系,而是表示使用這些生命周期的代碼在React的未來版本中有可能出現bug,尤其是在啟用異步渲染之后)
componentWillMount(),componentWillUpdate(),componentWillUnmount()
官網上,把不常用的鈎子函數去掉之后的生命周期圖如下所示:
因為新生命周期中,getDerivedStateFromProps和getSnapshotBeforeUpdate很少用。
getDerivedStateFromProps可以接收到props,然后可以由props派生狀態以及值並且更新原有的state值,通過return狀態對象的方式。使用這個方法之后,setstate和初始化等改變狀態的方法都不管用了,最終還是以getDerivedStateFromProps的return的狀態對象為准。
使用這個函數的場景是:當組件中的state完全取決於外部傳來的props時。
簡單來說沒錘子用。
getSnapshotBeforeUpdate用的也很少,所以就不多介紹了。
React腳手架
有關概念
1.xxx腳手架:用來幫助程序員快速創建一個基於xxx庫的模板項目
(1)包含了所有需要的配置(語法檢查、jsx編譯、devServer...)
(2)下載好了所有相關的依賴
(3)可以直接運行一個簡單效果
2.react提供了一個用於創建react項目的腳手架庫:create-react-app
3.項目的整體技術架構為:react+wbpack+es6+eslint
4.使用腳手架開發的項目的特點:模塊化、組件化、工程化
有關命令
創建項目並啟動
1.全局安裝:npm install -g create-react-app
2.切換到想創項目的目錄:使用命令:create-react-app hello-react
3.進入項目文件夾:cd hello-react
4.啟動項目:npm start
官方項目目錄分析
1.分析示例代碼可以得到以下信息
(1)所有組件都當做是App組件的孩子,這樣渲染組件的時候只需要渲染App即可
(2)使用了大量的模塊化語句進行編寫,使用了默認暴露
(3)reportWebVitals主要用於分析網頁性能
(4)App組件外層包了一對<React.StrictMode></React.StrictMode>用於將代碼開啟react的嚴格模式,只要寫了不被建議的寫法,就會飄紅報錯!學的時候我們並不開啟
(5)setupTests.js主要用於單元測試
(6)index.js是webpack打包的入口,而App.js是react的App根組件,這兩個js文件是我們最需要關注的兩個文件。
(7)App.test.js也是做測試用的,不推薦使用了。
(8)robots.txt,爬蟲協議文件,用於說明哪些爬蟲可以獲取,哪些是爬蟲無法獲取的
2.他的id為root的div是寫在index.html內,下面是對該html的一些分析
(1)這一行中的href后面跟了%PUBLIC_URL%,表示的是public文件夾路徑,該寫法僅在react腳手架中奏效。
(2)是在開啟理想視口,一般用於移動端網頁的開發(后期會詳細講解)
(3)主要是調整瀏覽器地址欄的顏色,僅用於調整原生安卓手機上瀏覽器的地址欄顏色。很雞肋不用太管。
(4)meta name="description"主要用於描述網頁,主要用於提供給爬蟲
(5) ios系統中網頁添加到桌面后的圖標
(6)用於加殼的,給網頁加個殼,然后當做app發布(ios和安卓都有),其實質上就是一個網頁。而manifest主要用於配置一些加殼后的app的一些特征,比如開啟app后的要顯示的圖片
(7)<noscript>
標簽里的內容在瀏覽器不支持js時會顯示出來
功能界面的組件化編碼流程
1.拆分組件:拆分界面(拆組件的原則:按功能點拆分並且適於取名),抽取組件
2.實現靜態組件:使用組件實現靜態頁面效果
3.實現動態組件
(1)動態顯示初始化數據
①先思考數據是什么類型
②數據名稱
③保存在哪個組件?比如有一個狀態數據A組件用,B組件也用,C組件也用,那么就適合放在A,B,C共同的父親組件里(還沒學高級組件通信時),即狀態提升
(2)交互(從綁定事件監聽開始)
天氣案例
首先把官方案例不需要的文件都刪了,只留下的文件結構如圖所示:
App.jsx,根組件名稱
import React, {Component} from 'react';
import Weather from "./components/Weather";
export default class App extends Component {
render() {
return (
<div>
<Weather/>
</div>
);
}
}
Weather.jsx
import React, {Component} from 'react';
export default class Weather extends Component {
state={
isHot:true
}
changeWeather = ()=>{
const {isHot} = this.state
this.setState({isHot:!isHot})
}
render() {
return (
<div>
<h1>今天天氣很{this.state.isHot ? '炎熱':'涼爽'}</h1>
<button onClick={this.changeWeather}>切換天氣</button>
</div>
);
}
}
index.js,該文件主要作為入口文件使用
import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(<App />, document.getElementById('root'));
index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<meta name="theme-color" content="#000000" />
<meta
name="description"
content="Web site created using create-react-app"
/>
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
todolist案例
代碼部分
1.app.jsx
import React, { Component } from 'react'
import Add from './components/Add'
import List from './components/List'
import Footer from './components/Footer'
import './App.css'
export default class App extends Component {
//初始化狀態,因為Add要用todos(添加),List也要讀取數據
state = {
tasklist: [
{ id: '001', name: '抽煙', done: true },
{ id: '002', name: '喝酒', done: false },
{ id: '003', name: '燙頭', done: false },
]
}
add = (newtask)=>{
const {tasklist} = this.state
this.setState({tasklist:[newtask, ...tasklist]})
}
delete = (id)=>{
const {tasklist} = this.state
const newList = tasklist.filter((todoObj)=>{
return todoObj.id !== id
})
this.setState({tasklist:newList})
}
update = (id,done)=>{
const {tasklist} = this.state
const newList = tasklist.map((todoObj)=>{
if(todoObj.id === id) todoObj.done = done
return todoObj
})
this.setState({tasklist:newList})
}
checkAbove = (done)=>{
const {tasklist} = this.state
const newList = tasklist.map((todoObj)=>({...todoObj,done}))
this.setState({tasklist:newList})
}
clearDone = ()=>{
const {tasklist} = this.state
const newList = tasklist.filter((todoObj)=>{
return !todoObj.done
})
this.setState({tasklist:newList})
}
render() {
return (
<div className="todo-container">
<div className="todo-wrap">
<Add add={this.add}/>
<List
todos={this.state.tasklist}
deleteItem={this.delete}
updateItem={this.update}
/>
<Footer
todos={this.state.tasklist}
checkAbove={this.checkAbove}
clearDone={this.clearDone}
/>
</div>
</div>
)
}
}
2.Add組件
import React, { Component } from 'react'
import { v4 as uuidv4 } from 'uuid';
import './index.css'
export default class Add extends Component {
handleKeyUp = (event)=>{
if(event.keyCode === 13){
if(event.target.value.trim() === ''){
alert('輸入內容不能為空')
} else {
const todoObj = {
id: uuidv4(),
name: event.target.value,
done: false,
}
this.props.add(todoObj)
event.target.value = ''
}
}
}
render() {
return (
<div className="todo-header">
<input type="text" onKeyUp={this.handleKeyUp} placeholder="請輸入你的任務名稱,按回車鍵確認" />
</div>
)
}
}
3.Footer組件
import React, { Component } from 'react'
import './index.css'
export default class Footer extends Component {
checkAll = (event)=>{
this.props.checkAbove(event.target.checked)
}
clearAllDone = ()=>{
if(window.confirm('確定刪除全部已完成的任務嗎?')){
if(this.props.todos.length > 0){
this.props.clearDone()
} else {
alert('抱歉,沒有可刪除的任務')
}
}
}
render() {
const {todos} = this.props
const doneCount =
todos.reduce((preValue,current)=>{
return preValue + (current['done'] === true ? 1 : 0)
},0)
const all = todos.length
return (
<div className="todo-footer">
<label>
<input
type="checkbox"
checked={all === doneCount && all > 0}
onChange={this.checkAll}/>
</label>
<span>
<span>已完成{doneCount}</span> / 全部{all}
</span>
<button className="btn btn-danger" onClick={this.clearAllDone}>清除已完成任務</button>
</div>
)
}
}
4.Item組件
import React, { Component } from 'react'
import './index.css'
export default class Item extends Component {
state = {mouseIsEnter:false}
handleMouse = (mouseIsEnter)=>{
return ()=>{
this.setState({mouseIsEnter})
}
}
handleDelete = (id)=>{
return () => {
if(window.confirm('確定刪除嗎?')){
this.props.deleteItem(id)
}
}
}
handleCheck = (id)=>{
return (event) =>{
this.props.updateItem(id,event.target.checked)
}
}
render() {
const {id, name, done} = this.props
const {mouseIsEnter} = this.state
return (
<li
onMouseEnter={this.handleMouse(true)}
onMouseLeave={this.handleMouse(false)}
className={mouseIsEnter? 'active' : ''}
>
<label>
<input
type="checkbox"
checked={done}
onChange={this.handleCheck(id)} />
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: mouseIsEnter ? "block" : "none" }}
onClick={this.handleDelete(id)}
>刪除</button>
</li>
)
}
}
5.List組件
import React, { Component } from 'react'
import Item from '../Item'
import "./index.css"
export default class List extends Component {
render() {
const {todos,deleteItem,updateItem} = this.props
return (
<ul className="todo-main">
{
todos.map((todoObj,index)=>{
return <Item
key={todoObj.id}
index={index}
{...todoObj}
deleteItem={deleteItem}
updateItem={updateItem}
/>
})
}
</ul>
)
}
}
注意事項
1.組件拆分后可以按照文件夾划分,在文件夾中可以默認創建index.js和index.css等,當引入時不指定文件夾里的哪個js,那么react就會默認引用index.js。
2.【父組件】給【子組件】傳遞數據使用props傳遞
【子組件】給【父組件】傳遞數據:通過props傳遞,要求父組件提前給子組件傳遞一個函數
【兄弟組件】之間通信可以借助共同的父親
3.JSX標簽里的一些onClick或者onChange屬性值后面跟的函數一旦要傳數據進去,那么這個函數就得寫成高階函數+箭頭函數的形式,即函數的返回值又是一個函數。
4.reduce函數復習:
arr.reduce((preValue, current, index, arr)=>{},initialValue)
arr: 當前操作的數組。
preValue:第一次執行回調時為給定的初始值initialValue,以后是上一次執行回調的返回值。
備注:若沒有傳入initialValue,則第一次的preValue值是數組中第一個元素的值。
current: 表示當前正在處理的元素。
index:表示當前正在處理的數組元素的索引,若傳入了initialValue值,則為0,否則為1。
array: 當前操作的數組(就是arr)。
initialValue: 表示初始值,一般做數學運算時設置為0,若篩選最值可以不傳。
<script type="text/javascript">
let person = [
{ id: "001", name: "Tom", age: "16" },
{ id: "002", name: "John", age: "17" },
{ id: "003", name: "Rechael", age: "18" },
{ id: "004", name: "Hank", age: "19" },
]
let arr = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
//reduce 可以進行累加/累乘
const result =
arr.reduce((preValue, current, index, arr) => {
console.log(
'preValue:', preValue,
'| current:', current,
'| index:', index,
'| arr:', arr
);
return preValue + current
})
console.log(result); // 55
//reduce 可以進行條件求和
const result2 =
arr.reduce((preValue, current) => {
return preValue + ( current % 2 == 0 ? current : 0)
},0)
console.log('數列偶數之和為',result2);
//reduce 可以進行條件統計
const result3 =
arr.reduce((preValue, current) => {
return preValue + ( current % 2 !== 0 ? 1 : 0)
},0)
console.log('數列奇數個數為',result3,'個');
//reduce 可以篩選最值
const result4 =
arr.reduce((preValue, current) => {
return Math.max(preValue,current)
})
console.log('數列最大值為',result4);
</script>
5.在JSX中,箭頭函數只有一個語句,即返回語句且返回的是一個對象時,簡寫時不能用花括號,使用花括號會被當成函數體,應該在花括號外面加一層小括號即可!
todos.map((todoObj)=>({...todoObj,done}))
6.defaultchecked只能執行一次(適合用於勾選框不會改變的情形)。
寫了checked就必須要寫onchange!因為數據狀態驅動着頁面。總結下來就是onchange不能配合defaultChecked使用。
7.爺爺組件想給孫子組件傳函數的話,現在必須得通過父親組件來傳,但是以后有新的解決辦法!
跨域問題
啟動node服務器,即教程里給的測試用服務器,在根目錄下用node ./server1.js啟動
定義及發生時機
1.定義和發生實際:
只要http(對應不同的是https),ip地址,端口號不同,就會產生跨域,但僅限於在瀏覽器端借助Ajax(也就是xhr)發送請求給服務器。
注意跨域問題只會導致瀏覽器無法獲得數據但是發送請求是可以正常發送的,服務器也能接受到這個請求。
2.特殊情況:
(1)服務器與服務器之間不同源且互相請求時不會產生跨域問題
(2)使用form表單請求時不會產生跨域問題,因為form表單不是通過xhr實現的。
如何解決問題
在react腳手架中有以下幾種方式
1.jsonp
2.cor
3.借助代理:原理是借助代理,代理與目標服務器之間是服務器與服務器之間的對話,所以不會有跨域問題,若是瀏覽器與服務器之間的交互,則會有跨域問題。
第一種方式,直接在package.json中追加一行配置
在package.json中追加如下配置(端口號是目標服務器的端口號):
"proxy":"http://localhost:5000"
請求的地址改為本地地址+目標后綴
url:'http://localhost:3000/students',
method:'GET'
說明:
- 優點:配置簡單,前端請求資源時可以不加任何前綴
- 缺點:不能配置多個代理。
- 工作方式:上述方式配置代理,當用Ajax請求了3000不存在的資源時,該請求會轉發給5000(如果3000存在資源,則優先匹配前端資源,localhost:3000 即是public路徑)
第二種方式,編寫setupProxy.js配置具體代理規則
1.需要安裝http-proxy-middleware插件,使用npm安裝即可
2.編寫setupProxy.js配置具體代理規則:
const {createProxyMiddleware} = require('http-proxy-middleware')
module.exports = function(app) {
app.use(
createProxyMiddleware(
'/api1', // 只要/api 開頭的請求,才轉發給后端服務器
{
target:'http://localhost:5000',
changeOrigin:true, // 控制服務器接收到的請求頭中host字段的值
// false(默認值):服務器請求來自於原地址 localhost:3000
// true:服務器請求來自於5000(請求目標地址),可迷惑目標服務器
pathRewrite:{'^/api1':''} // 重寫路徑(目的:去掉api前綴)
}),
createProxyMiddleware(
'/api2',
{
target:'http://localhost:5001',
changeOrigin:true,
pathRewrite:{'^/api2':''}
})
)
}
額外說明:
pathRewrite可以覆蓋替換用於標識該地址是用於轉發的部分。
舉個例子:假如有個一個騰訊的api,這個騰訊api的位置是(不關注地址)/OCR_text,現在我本地想請求這個api,我需要給它做個標識,我請求的可能就是 http://localhost:3000/tx/OCR_text,這個時候腳手架知道/tx開頭的都需要轉發給騰訊的api地址,但是如果不加pathRewrite的話,直接轉發到騰訊服務器的就 是/tx/OCR_text,但是正確的地址應該是/OCR_text,那這個時候就出現問題了;使用pathRewrite就可以把前面的/tx去掉,然后正常訪問騰訊的api地址。
(提示:目前的場景是3000通過代理去跨域請求5000里的數據)
changeOrigin:true加了之后,服務器收到的請求頭中的host是localhost:5000,設置為 false時,請求頭收到的就是原來的地址localhost:3000。
changeOrigin默認值為false但我們一般把它設置為true,可以撒謊欺騙5000即請求服務器。
注意事項
1.在腳手架中,會自動處理瀏覽器訪問不存在資源時的情況:如果要訪問的資源不存在(通過瀏覽器中輸入資源地址訪問時),腳手架會自動重定向去index.html。如果是ajax請求不存在的地址,那么會正常地顯示404錯誤,
2..在react腳手架里,public文件夾就是http://localhost:3000的根目錄,訪問http://localhost:3000/a.jpg就等於在訪問public下的a.jpg。所以會出現問題,如果前端地址里有這個資源並且后端地址也有這個資源,那么代理不生效,ajax會自動取到本地也就是前端地址的資源。
消息訂閱機制
通過消息訂閱機制可以解決兄弟組件之間通信需要通過父親組件的問題 。消息訂閱機制可以適用於任意兩個組件之間的通信。
安裝以及使用
我們這里使用pubsub消息訂閱插件
1.首先安裝這個插件
npm install pubsub-js
2.先訂閱再發布。需要接收信息的組件是訂閱方,需要傳送數據出去的組件是發布方。
PubSub.publish('status',data)
this.msgis = PubSub.subscribe('status',函數(_,data))
PubSub.unsubscribe(this.msgid) // 取消訂閱
github搜索用戶名案例
項目結構如下
1.App.jsx
import React, { Component } from 'react'
import Search from './components/Search'
import List from './components/List'
export default class App extends Component {
render() {
return (
<div className="container">
<Search/>
<List/>
</div>
)
}
}
2.Search組件
import React, {Component} from 'react';
import axios from 'axios'
import PubSub from 'pubsub-js'
export default class Search extends Component {
keyWordContainer = React.createRef()
//通過createRef來使用Ref
search = ()=> {
// 1. 獲取用戶輸入
const {value} = this.keyWordContainer.current
// 2. 校驗數據
if(!value.trim()) return alert('please input a word')
// this.props.updateAppState({isFirst:false, isLoading:true})
PubSub.publish("user_date_of_github",{isFirst:false, isLoading:true})
// 3. 發送請求獲取數據
axios.get(`https://api.github.com/search/users?q=${value}`).then(
response => {
const {items} = response.data
// 請求成功后,通知app存儲用戶數據, 更改isLoading
// this.props.updateAppState({users:items, isLoading:false})
PubSub.publish("user_date_of_github",{users:items, isLoading:false})
},
error => {
console.log(error);
// 請求失敗后,存儲錯誤信息、將isLoading變為false
// 注意:此處的error是一個對象,真正的錯誤信息在error.message
// this.props.updateAppState({errorMsg:error.message, isLoading:false})
PubSub.publish("user_date_of_github",{errorMsg:error.message, isLoading:false})
}
)
}
render() {
return (
<section className="jumbotron">
<h3 className="jumbotron-heading">Search Github Users</h3>
<div>
<input type="text" ref={this.keyWordContainer} placeholder="enter the name you search"/>
<button onClick={this.search}>Search</button>
</div>
</section>
)
}
}
3.List組件
import React, {Component} from 'react';
import PubSub from 'pubsub-js'
export default class List extends Component {
state = {
users:[],
isFirst:true,
isLoading:false,
errorMsg:''
}
componentDidMount() {
//這個組件一掛載完畢就開始訂閱
this.pub_id = PubSub.subscribe('user_date_of_github',(_,data)=>{
this.setState(data)
})
// data前面必須有一個頂位的標識,因為消息發布傳過來給回調函數的參數是(msg,data),
// 所以第二個參數才是data
}
componentWillUnmount() {
PubSub.unsubscribe(this.pub_id)
}
render() {
const {isFirst,isLoading,errorMsg,users} = this.state
return (
<div className="row">
{
isFirst ? <h1>Welcome!</h1> :
isLoading ? <h1>正在加載</h1> :
errorMsg ? <h1>{errorMsg}</h1> :
users.map((userObjs)=>{
// 函數體
return (
<div className="card" key={userObjs.id} style={{width:"100px"}}>
<a href={userObjs.html_url} target="_blank" rel="noreferrer">
<img alt='avatar' src={userObjs.avatar_url} style={{width:'100px'}} />
</a>
<p className="card-text">{userObjs.login}</p>
</div>
)
})
}
</div>
)
}
}
注意事項
1.react腳手架會預先檢查通過ES6引入的CSS里的所有資源哪怕沒用到,只要沒找到其中某一個資源,它就會報錯。如果是引入第三方的css庫,建議放到public中,在index.html中通過link的形式引入css庫。
2.三元表達式可以嵌套使用。
3.對象是不可以直接在react中顯示的,會報錯:
React路由
教程里的文檔可以參考這篇博客React Router 6 | BruceBlog (gitee.io)
這里主要介紹的是六代路由了,npm install react-route-dom默認安裝的是六代路由。
官方在這里更推崇函數式組件!函數式組件里this的殘疾現象被hooks改進了,這個部分會在后面詳細介紹。
概念理解
SPA
SPA指的是Single Page Web Application SPA,整個應用只有一個完整的頁面,點擊頁面中的鏈接不會刷新頁面,只會做頁面的局部更新,數據都需要通過ajax請求獲取,並在前端異步展現。
路由
1.何為路由?
一個路由就是一個映射關系(key:value),key為路徑,value可能是element(element里可以有component或者Navigate)
2.路由分類:
(1)后端路由:
value 是 function, 用來處理客戶端提交的請求。
工作過程:當node接收到一個請求時,根據請求路徑找到匹配的路由,調用路由中的函數來處理請求,返回響應數據。
(2)前端路由
瀏覽器端路由,value是component,用於展示頁面內容。可以使用Route路由組件實現或者使用路由表實現。
路由使用的原則
1.明確好界面中的導航區、展示區。
2.按照實際需求將a標簽改為Link或者NaviLink路由組件(即 編寫路由鏈接)
3.展示區寫Route標簽或者在路由表里寫好對應的路徑之后通過Outlet插槽組件來顯示(即 注冊路由)。
4.
<BrowserRouter>
或
<HashRouter>
NavLink和Link
1.NavLink和Link都可以實現編寫路由鏈接的過程,區別是NavLink可以更加自由地控制點擊后該標簽的class的內容,比如點擊后class中多一個active屬性或者其他的屬性值。
<NavLink className={({isActive}) => {
return isActive ? "list-group-item atcsq" : "list-group-item"
}} to="/home">Home</NavLink>
注意這里接受的是一個對象,點擊NavLink后,NavLink會給回調函數傳一個對象{isActive:true},所以接收參數的時候要注意!
2.Link組件寫法類似
<Link children={m.title} to={"details"}/>
Navigate
該組件主要是用於重定向,Navigate只要被渲染就會修改路徑從而導致視圖的切換。
<Navigate to={"/About"}/>
Route和Routes
1.Route和Routes必須搭配使用,且必須使用Routes包裹Route,這樣在匹配路由的時候,一旦匹配到響應的路由,react就不會繼續往下匹配了,提高了效率
2.Route是用於注冊路由,寫了Route的地方,在路由鏈接被點擊的時候,Route組件就會被替換成Route里element里面的組件。
<Routes>
<Route path="/about" element={<About/>}/>>
<Route path="/home" element={<Home/>}/>>
<Route path={"/"} element={<Navigate to={"about"} />}/>
{/*用於重定向的Navigate,Navigate還可以寫replace屬性,寫上了之后就是replace模式*/}
</Routes>
路由表和嵌套路由
路由表
使用路由表對路由進行統一管理會比較方便。
假如我們想把上述路由轉換成路由表的形式的話:
我們需要使用到useRoutes這個模塊,然后我們可以把路由寫在另一個路由文件中以默認方式暴露
我們在src目錄下創建routes文件夾,再在routes文件夾中創建index.js
import About from "../pages/About";
import Home from "../pages/Home";
import News from "../pages/News";
import Details from "../pages/Details";
import Message from "../pages/Message";
import {Navigate} from "react-router-dom";
import React from "react";
export default [
{
path:'/about',
element:<About/>
},
{
path:'/home',
element:<Home/>,
children:[
{
path:'news',
element:<News/>
},
{
path:'message',
element: <Message/>,
children:[
{
path:'details',
element:<Details/>
}
]
}
],
},
// 用於重定向的route
{
path:'/',
element:<Navigate to={"/About"}/>
},
{
//這里用於home組件內部的重定向,默認轉到home組件后轉到news
path:'/home/',
element: <Navigate to={"/home/news"}/>
}
]
回到需要使用路由表的App.js,引入useRoutes后,往useRoutes傳入路由數組(即 src/routes/index.js中默認暴露的內容),接收為變量后,寫在原先Routes的位置。
import React from 'react';
import {NavLink,useRoutes} from "react-router-dom";
import routes from "./routes"
import Header from "./pages/Header";
export default function App(props) {
//路由表,寫在routes文件夾下了
const routes_table = useRoutes(routes)
return (
<div>
<div className="row">
<Header/>
</div>
<div className="row">
<div className="col-xs-2 col-xs-offset-2">
<div className="list-group">
{/*<NavLink className="list-group-item" to="/about">About</NavLink>*/}
{/*<NavLink className="list-group-item " to="/home">Home</NavLink>*/}
{/*NavLink在被點擊的時候自動給自己加上active類名,如果想實現自定義的話,
需要把className寫成一個函數,函數接受的就是一個對象,即{isActive:true}*/}
{/*這里atcsq就是我自己自定義的樣式*/}
<NavLink className={({isActive}) => {
return isActive ? "list-group-item atcsq" : "list-group-item"
}} to="/home">Home</NavLink>
<NavLink className={({isActive}) => {
return isActive ? "list-group-item atcsq" : "list-group-item"
}} to="/about" children={"About"}/>
</div>
</div>
<div className="col-xs-6">
<div className="panel">
<div className="panel">
{routes_table}
</div>
</div>
</div>
</div>
</div>
</div>
);
}
嵌套路由
假如home路徑下還有嵌套路由,可以在路由表里以children的形式呈現出來。
不過要注意:嵌套路由需要使用
比如下面Message組件就是使用
import React,{useState} from 'react';
import {Link,Outlet,useNavigate} from "react-router-dom";
export default function Message(props) {
//使用useNavegate可以實現編程式路由,即通過編程來觸發路由的跳轉,
// 而不一定非得通過用戶點擊來觸發路由
const Navigate = useNavigate()
const [messages]=useState([
{id:"001",title:"消息一",content:"我愛你中國"},
{id:"002",title:"消息二",content:"親愛的母親"},
{id:"003",title:"消息三",content:"我為你自豪"}
])
function showDetail(m){
//實現了編程式路由,同時還可以通過state方式來傳遞參數
Navigate('details',{
replace:false,
state:m
})
}
return (
<div>
<ul>
{
messages.map((m)=>{
return (
<li key={m.id}>
<Link children={m.title} to={"details"} state={m}/>
<button onClick={()=>{showDetail({...m})}}>查看</button>
{/* state里等價於state={{id:m.id,title:m.title,content:m.content}}*/}
</li>
)
})
}
</ul>
<Outlet/>
{/* 指出組件呈現的位置*/}
</div>
);
}
解決樣式丟失問題
在二級或者嵌套路由可能會出現樣式丟失的問題,為了避免這個問題,
我們需要把index.html中引入的css寫成/xxx的格式,不要寫成./xxx的格式。
也可以使用%PUBLIC_URL%。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>路由實現</title>
<link rel="icon" href="%PUBLIC_URL%/favicon.ico">
<link rel="stylesheet" href="/bootstrap.css">
<style>
.atcsq{
background-color: #c7254e !important;
color: #4cae4c !important;
/* 可以通過!important來將樣式優先級提升到最高為了避免被bootstrap樣式覆蓋的情況*/
}
</style>
</head>
<body>
<div id="root"></div>
</body>
</html>
編程式路由
之前我們進行路由跳轉都是需要用戶點擊路由再進行跳轉,如何用編程實現跳轉呢?
比如我想實現一個功能,在一個頁面展示5秒后自動跳轉到另一個頁面。
這時我們需要使用useNavigate組件。
在函數體內部使用 const Navigate = useNavigate()接收變量之后:
Navigate(1)命令式前進1個頁面,Navigate(-1)就是退后一個頁面。
Navigate('路徑',{
replace:false,
state:{xxxxx}
})
上面的命令就是跳轉到指定路徑后並且攜帶上state屬性值。
給路由傳遞參數
這里只介紹使用state傳遞
在六代里非常方便,只需在Link或者NaviLink組件中多寫一個state屬性即可:
<Link children={m.title} to={"details"} state={{id:"002",title:"消息一",content:"我愛你中國"}}/>
下一級路由接收state參數時需要使用useLocation組件
import React from 'react';
import {useLocation} from "react-router-dom";
export default function Details(props) {
//使用useLocation來讀取路由通過state傳遞過來的信息
//這里使用解構賦值
const {id,title,content} = useLocation().state
return (
<div>
<ul>
<li>消息編號:{id}</li>
<li>消息名稱:{title}</li>
<li>消息內容:{content}</li>
</ul>
</div>
);
}
BrowserRouter與HashRouter的區別
- 底層原理不一樣:
- BrowserRouter使用的是H5的history API,不兼容IE9及以下版本。
- HashRouter使用的是URL的哈希值
- path表現形式不一樣
- BrowserRouter的路徑中沒有#
- HashRouter的路徑包含#
- 刷新對路由state參數的影響
- (1)BrowserRouter無影響,因為state保存在history對象中
- (2)HashRouter刷新后會導致路由state參數的丟失!!!
- 備注:HashRouter可以用於解決一些路徑錯誤相關的問題。
ReactUI組件庫
這里主要介紹antd,除了antd外還有ElementUI for react和vant(專門用於移動端開發)。
ant-design
螞蟻金服開發的用於react的UI組件庫。
官方網址:Ant Design - 一套企業級 UI 設計語言和 React 組件庫
使用方法
首先先在腳手架里安裝該模塊。
npm install antd
隨后在使用過程中不僅要引入模塊,也要引入對應的樣式
import {Button} from 'antd';
import 'antd/dist/antd.css';
具體各個組件的樣式和使用方法可以參考官網:組件總覽 - Ant Design
這里以Button為例:
import React from 'react';
import {useNavigate} from "react-router-dom";
import {Button} from 'antd';
import 'antd/dist/antd.css';
//不僅要引入組件,而且還要引入樣式
export default function Header(props) {
const Navigate = useNavigate()
return (
<div className="col-xs-offset-2 col-xs-8">
<div className="page-header"><h2>React Router Demo</h2></div>
<Button type="primary" onClick={()=>{Navigate(1)}}>前進</Button>
{/*通過Navigate(1)來實現前進和后退*/}
<Button type="primary" danger onClick={()=>{Navigate(-1)}}>后退</Button>
</div>
);
}
按需引入組件
在 create-react-app 中使用 - Ant Design參考這篇文章里的高級配置一欄。
直接引入antd.css可能會太大,如果想要繼續優化的可以采用按需引入的方式,但是需要暴露腳手架的默認配置(暴露的行為是不可逆的)。
自定義主題
因為antd的默認主題顏色是與支付寶一致的藍色,如果想改成其他顏色的話,我們需要進行自定義主題,同樣這部分的具體操作參考在 create-react-app 中使用 - Ant Design下的自定義主題部分。
PS:less是啥?
Less 是一門 CSS 預處理語言,它擴展了 CSS 語言,增加了變量、Mixin、函數等特性,使 CSS 更易維護和擴展。
Less 可以運行在 Node 或瀏覽器端。
Redux
redux的內容暫時跳過,下面一段引用阮一峰的話:
首先明確一點,Redux 是一個有用的架構,但不是非用不可。事實上,大多數情況,你可以不用它,只用 React 就夠了。
曾經有人說過這樣一句話。
"如果你不知道是否需要 Redux,那就是不需要它。"
Redux 的創造者 Dan Abramov 又補充了一句。
"只有遇到 React 實在解決不了的問題,你才需要 Redux 。"
打包react項目
把我們寫的react項目打包成起來,放在服務器上運行。
使用下面的命令進行打包:
npm run build
打包完成后在同目錄下回多一個build文件夾,該文件夾內部就是打包完畢的內容。接下來我們可以把它放到服務器上運行,可以放在node或者java編寫的服務器,也可以借助serve模塊快速開啟一台服務器。
node服務器和java服務器的部分之后再補充。
node服務器
java服務器
serve模塊
首先全局安裝serve模塊。
npm install serve -g
之后運行下面命令
serve build
該命令會把當前文件夾作為服務器的根目錄進行啟動。
之后可以在瀏覽器訪問3000端口,顯示正常:
想要關閉服務器的話直接ctrl+c即可!
遇到的問題
1.全局安裝serve后
參考(82條消息) npm : 無法加載文件 D:\nodejs\npm.ps1,因為在此系統上禁止運行腳本。_WebView-CSDN博客_nodejs 因為在此系統上禁止運行腳本
2.npm全局安裝的模塊組件都在C:\Users\Administrator.DESKTOP-VDATQMB\AppData\Roaming\npm這個目錄下,需要打開查看隱藏目錄,因為AppData是隱藏的。
拓展部分
lazy_load懶加載
定義
顧名思義,懶加載的意思就是我需要或者說我請求的時候再加載,如果我還沒有請求那就不要加載。最典型的應用就是用在路由組件上,如果我們沒有點擊路由,就先不加載路由對應的組件,當我們點擊路由了再進行加載。
如何判斷是否是懶加載呢?以我們做的路由案例為例。如果點擊路由之后network請求了相應的組件就是懶加載,說明是點擊的時候再請求,如果network沒有任何請求信息,則說明之前就已經加載完畢了,
未實現懶加載:
實現了懶加載:
實現方法
我們需要lazy函數和suspense組件
1.引入lazy函數和suspense組件
(1)使用路由表
①在路由表的js文件里引入lazy和Suspense,在路由表里將組件的引入用lazy函數包裝起來。
②在路由表的地方用Suspense包裹起來即可
其中Loading組件是我寫的用來實現Loading加載的小組件,引用了antd里的
當我把網速調成3G時,就會調用Suspense里的Loading組件:
嵌套路由同理,在路由表里用lazy,在outlet組件外部包裹Suspense
(2)不使用路由表
①引入lazy和Suspense,將引入方式用lazy函數包裝起來。
②用Suspense包裹起來並且指定一個回調組件,當加載過慢時就會調用這個組件。
2.如果不寫suspense則會報如下錯誤
這是因為如果懶加載了,並且網速很慢的情況下,在網絡請求組件返回之前需要給react一個組件或者標簽先顯示着,可以理解為loading
hooks
函數式組件的缺陷
1.函數式組件無法使用組件實例三大屬性中的ref和state,props因為可以給函數傳參數所以可以正常使用。
2.無法使用生命周期鈎子
修補措施
React16.8版本之后更新了hooks,修補了函數式組件的缺陷。
useState()
1.useState(),非常簡單
點按鈕數字+1的案例:
useEffect()
具體可以參考這篇:(100條消息) react中用useEffect模擬組件生命周期_君君yui的博客-CSDN博客_react useeffect 生命周期
2.useEffect可以讓函數式組件擁有聲明周期鈎子
import React,{useState,useEffect} from 'react';
export default function App(props) {
const [number,setNumber] = useState(0);
//number是state里的一個屬性,設為初值0,setNumber是用於改變number的函數
useEffect(()=>{console.log("我正在檢測所有的狀態,只要狀態改變我就會顯示")})
function addone(){
// setNumber(number+1); 這是第一種寫法
setNumber(()=>{
return number+1;
})//這是第二種寫法
}
return (
<div>
<h1>{number}</h1>
<button onClick={addone}>點我加一</button>
</div>
);
}
(1)如果不帶第二個數組參數,則默認檢測所有的狀態。
(2)帶上第二個數組,但是數組內部沒有任何東西,就相當於compoentDidMount()函數,組件第一次掛載時默認調用。
import React,{useState,useEffect} from 'react';
export default function App(props) {
const [number,setNumber] = useState(0);
//number是state里的一個屬性,設為初值0,setNumber是用於改變number的函數
useEffect(()=>{
console.log("App組件掛載上了,我默認被調用!")
},[])
function addone(){
// setNumber(number+1); 這是第一種寫法
setNumber(()=>{
return number+1;
})//這是第二種寫法
}
return (
<div>
<h1>{number}</h1>
<button onClick={addone}>點我加一</button>
</div>
);
}
(3)如果數組里有組件名稱,則表示監聽這個組件,組件一旦變化就會調用這個函數,類似於componentDidUpdate()。
(4)useEffect參數的函數體返回的函數就是組件被卸載時調用的函數。
import React,{useState,useEffect} from 'react';
import ReactDOM from "react-dom";
export default function App(props) {
const [number,setNumber] = useState(0);
//number是state里的一個屬性,設為初值0,setNumber是用於改變number的函數
useEffect(()=>{
console.log("我正在監聽number,它更新了");
return ()=>{
console.log("我是useEffect函數參數內部的返回" +
"函數的內容,組件被卸載了,所以我調用了")
}
},[number])
function addone(){
// setNumber(number+1); 這是第一種寫法
setNumber(()=>{
return number+1;
})//這是第二種寫法
}
function unmount(){
ReactDOM.unmountComponentAtNode(document.getElementById("root"))
}
return (
<div>
<h1 style={{marginLeft:'1000px'}}>{number}</h1>
<button style={{marginLeft:'1000px'}} onClick={addone}>點我加一</button>
<button onClick={unmount}>卸載組件</button>
</div>
);
}
當前組件卸載時調用return的函數,即window.clearInterval(timerId)
useEffect(() => {
let timerId = window.setInterval(() => {
console.log(Date.now())
}, 1000)
// 返回一個函數
// 模擬 componentWillUnmount 組件銷毀的時候 停止計時器
return () => {
window.clearInterval(timerId)
}
}, [])
useRef()
作用類似於類式組件的ref,比如我想用ref拿到input里面的取值
import React,{useState,useEffect,useRef} from 'react';
export default function App(props) {
const Myref=useRef()
function show(){
alert(Myref.current.value);
}
return (
<div>
<input type={"text"} value={"哈哈哈哈"} ref={Myref}/>
<button style={{marginLeft:'1000px'}} onClick={show}>點我顯示input的內容</button>
</div>
);
}
Fragment
組件里的語法要求必須被
也可以直接使用空標簽包裹,即<> xxxx</>
Context
用途
Context主要用於祖組件和孫組件進行通信。
用法
通過React.createContext來創建Context對象,之后使用Provider把需要傳遞值的子組件包裹起來,並且把需要傳遞的內容(可以是對象)放在value里,子孫組件要取值時,在需要取值的部分使用Consumer包裹起來,直接通過value就可以取到context傳遞的值。
import React from 'react';
import "./app.css"
//如果要通過Context直接給孫子組件傳值,則需要創建context變量
const MyContext = React.createContext();
const {Provider,Consumer} =MyContext
export default function App(props) {
const [username,setUsername]=React.useState("Jack");
return (
<div className={"grandcomponent"}>
<h1>我是App組件</h1>
<h1>我的用戶名是:{username}</h1>
{/*將要通過Context傳值的子組件用Provider包裹起來*/}
<Provider value={username}>
<Father_App username={username}/>
</Provider>
</div>
);
}
function Father_App(props) {
return (
<div className={"fathercomponent"}>
<h1>我是Father_App組件</h1>
{/* 父給子組件傳遞直接通過props傳值,兒子組件通過props.自定義屬性來取值*/}
<h1>我從APP得到的用戶名是:{props.username}</h1>
{/*注意這里Father_App沒有給Son_App傳值*/}
<Son_App/>
</div>
);
}
function Son_App(props){
return(
<div className={"soncomponent"}>
<h1>我是Son_App組件</h1>
{/*孫組件要從子組件的部分要用Consumer包裹起來,寫一個回調函數*/}
<Consumer>
{
value =>{
console.log(value)
return (<h1>我從App而不是FatherApp拿到的用戶名是:{value}</h1>)
// 這里return必須用()包裹,代表解析時應解析為js語句,
// 這樣h1標簽才能正常被渲染
}
}
</Consumer>
</div>
)
}
ErrorBundary
錯誤邊界,主要用於將錯誤局限於最小化。比如某個小組件需要接收后端服務器傳來的數組,但是后端傳來了一個對象,這時如果對對象調用map函數就會報錯,這個時候整個頁面都無法顯示。
注意錯誤邊界只能捕獲后代組件生命周期產生的錯誤,不能捕獲自己組件產生的錯誤和其他組件在合成事件、定時器中產生的錯誤。
未使用錯誤邊界
即不用錯誤邊界組件將代碼包裹起來。
使用錯誤邊界
由於函數式組件是趨勢,這里就介紹函數式組件的用法,但還是需要用類式組件來定義錯誤邊界組件,因為類式組件里有getDerivedStateFromError函數,該函數是生命周期函數,一旦后台組件報錯,就會觸發。
1.錯誤邊界組件MyErrorBoundary.jsx
import React, {Component} from 'react';
import './app.css'
export default class MyErrorBoundary extends Component {
state = {error: null,};
//生命周期函數,一旦后台組件報錯,就會觸發
static getDerivedStateFromError(error) {return { error: error };}
// componentDidCatch函數內部可以做錯誤上報的操作
componentDidCatch(error, info) {}
render() {
if (this.state.error) {
// 渲染出錯時的 UI
return(
<div className={"fathercomponent"}>
<p>出現錯誤了,請稍后再試!</p>
</div>
);
}
return this.props.children;
// 如果沒有出錯就正常渲染被MyErrorBoundary包裹的子組件
}
}
2.在需要進行錯誤邊界限制的父組件外部用該錯誤邊界包裹起來
(1)app.jsx
import React from 'react';
import Child from './child'
import './app.css'
import MyErrorBoundary from "./MyErrorBoundary";
export default function App(props) {
return (
<div className={"grandcomponent"}>
<h1>我是App組件</h1>
<MyErrorBoundary>
<Child/>
</MyErrorBoundary>
</div>
);
}
(2)會發生錯誤的子組件child.jsx
import React, {Fragment, useState} from 'react';
export default function Child(props) {
const [chucuo, setchucuo] = useState(1/0);
return (
<Fragment>
<h1>我是h2組件,我會出錯!{chucuo.map(()=>{
return "haha"
})}</h1>
</Fragment>
);
}
效果如下:
PS:A組件被B組件包裹時,B組件的props.child就是A組件,可以通過props的方式調用。
組件之間通信總結
額外自己補充
本單元主要寫一些自己學完react之后,編寫react程序時遇到的一些問題和注意點。
render函數在react18中不再被支持
出現這個錯誤說明ReactDOM.render()方法在react18中已經不支持了,但是是警告,仍然可以運行。
解決辦法:
把
import React from "react";
import ReactDOM from 'react-dom'
import App from './App'
ReactDOM.render(<App/>,document.getElementById('root'))
改成
import React from "react";
import App from "./App";
import { createRoot } from 'react-dom/client';
const container = document.getElementById('root');
const root = createRoot(container);
root.render(<App />)
重復刷新錯誤。Too many re-renders. React limits the number of renderers to prevent an infinite loop
可以具體參考這篇文章的第三種(100條消息) 3 種導致 React 無限循環的方式_小公雞卡哇伊呀~的博客-CSDN博客_react無限循環
這不是設置 event handler 的正確方法,你需要向 onClick 提供一個函數,而非函數執行的結果。 通過在設置 event handler 之前就執行函數,render 內部的狀態將被更新,這會導致無限循環。
場景:Todolist案例中的item組件,以下是正確寫法
export default function Item(props){
const [mouseIsEnter,setmouseIsEnter] = React.useState(false)
let handleMouse = (mouseIsEnter)=>{
return ()=>{
setmouseIsEnter(mouseIsEnter)
}
}
//錯誤寫法:
//let handleMouse = (mouseIsEnter)=>{
// setmouseIsEnter(mouseIsEnter)
//}
let handleDelete = (id)=>{
return () => {
if(window.confirm('確定刪除嗎?')){
props.deleteItem(id)
}
}
}
let handleCheck = (id)=>{
return (event) =>{
props.updateItem(id,event.target.checked)
}
}
const {id, name, done} =props
return (
<li
onMouseEnter={handleMouse(true)}
onMouseLeave={handleMouse(false)}
className={mouseIsEnter? 'active' : ''}
>
<label>
<input
type="checkbox"
checked={done}
onChange={handleCheck(id)} />
<span>{name}</span>
</label>
<button
className="btn btn-danger"
style={{ display: mouseIsEnter ? "block" : "none" }}
onClick={handleDelete(id)}
>刪除</button>
</li>
)
}
其中的handleMouse必須寫成高級函數,因為mouseIsEnter是state里的一個屬性,如果寫成普通的函數,那么我們都知道在jsx中onClick類似的屬性后面的函數名后加()之后,每次渲染頁面就會自動調用,handleMouse中會設置state中的mouseIsEnter屬性值,mouseIsEnter屬性值變化又會導致頁面重新渲染,從而導致handleMouse函數又被調用,一直往復....
這個時候就需要使用高級函數了,即函數返回一個函數。也可以這么寫:
...
let handleMouse2 = (mouseIsEnter)=>{
setmouseIsEnter(mouseIsEnter)
}
...
return (
<li
onMouseEnter={() => {//這里寫成函數形式就可以了
handleMouse2(true)
}}
onMouseLeave={handleMouse(false)}
className={mouseIsEnter? 'active' : ''}
>
...