虛擬DOM
- JSX 涉及到 虛擬DOM ,簡單聊一下
定時器渲染問題
// 方法
function render() {//2. 創建react對象let el = (<div><h3>時間更新</h3><p>{ new Date().toLocaleTimeString()}</p></div>)//3. 渲染ReactDOM.render(el, document.getElementById('root'))
}//4. 開啟一個定時器, 每秒渲染一次
setInterval(() => {render()
}, 1000)
渲染模式 :
/*** 最早的更新模式* 1. 數據* 2. 模板* 3. 數據+模板 => 真實的DOM* 4. 數據變了 => 最簡單直接的方式 => 最新數據+模板 => 新的DOM* 5. 新的DOM 把 舊的DOM完全直接替換掉* 6. 顯示新的DOM** 缺點 : 完全替換, 性能不好** 1. 數據* 2. 模板* 3. 數據+模板 => 真實的DOM* 4. 數據變化了 => 最新的數據 + 模板 => 新的DOM* 5. 新的DOM 和 舊的DOM 進行一一比較, 找到需要更新的地方* 6. 只需要更新需要改變的地方即可** 優點 : 不再是全部替換掉,* 缺點 : DOM比較就有性能問題了 , 會有多余的DOM和屬性進行比較(打印一級屬性),有損性能* p 和 p對比 h3 和h3對比(多余的對比)** 1. 數據* 2. 模板* 3. 數據 + 模板 => 虛擬DOM (js對象) => 真實的DOM* 4. 數據發生改變(zs=>ls) => 最新的數據 + 模板 => 新的虛擬DOM* 5. 新的虛擬DOM 和 舊的虛擬DOM 通過 diff算法 進行比較* 6. 找到有差異的地方,(需要更新的地方)* 7. 更新一下就可以看到最新的DOM了*/
- 打印屬性 :
let root = document.querySelector('#root')let str = ''
let count = 0
for (let k in root) {str += k + ' 'count++
}
console.log(str, count)
- 查看圖片 : 演示對比找差異渲染
- 文字描述 :
這就是所謂的 Virtual DOM 算法。包括幾個步驟:- 1.用 JavaScript 對象結構表示 DOM 樹的結構;然后用這個樹構建一個真正的 DOM 樹,插到文檔當中
- 2.當狀態變更的時候,重新構造一棵新的對象樹。然后用新的樹和舊的樹進行比較,記錄兩棵樹差異
- 3.把 2 所記錄的差異應用到步驟 1 所構建的真正的 DOM 樹上,視圖就更新了
DIff 算法
React 中有兩種假定:
- 1 兩個不同類型的元素會產生不同的樹
- 2 開發者可以通過 key 屬性指定不同樹中沒有發生改變的子元素
Diff 算法的說明 - 1
- 如果兩棵樹的根元素類型不同,React 會銷毀舊樹,創建新樹
// 舊樹
<div><Counter />
</div>// 新樹
<span><Counter />
</span>執行過程: 刪除 div , 創建 span
Diff 算法的說明 - 2
- 對于類型相同的 React DOM 元素,React 會對比兩者的屬性是否相同,只更新不同的屬性
- 當處理完這個 DOM 節點,React 就會遞歸處理子節點。
// 舊
<div className="before" title="stuff"></div>
// 新
<div className="after" title="stuff"></div>
只更新:className 屬性// 舊
<div style={{color: 'red', fontWeight: 'bold'}}></div>
// 新
<div style={{color: 'green', fontWeight: 'bold'}}></div>
只更新:color屬性
Diff 算法的說明 - 3
- 1 當在子節點的后面添加一個節點,這時候兩棵樹的轉化工作執行的很好
// 舊
<ul><li>1</li><li>2</li>
</ul>// 新
<ul><li>1</li><li>2</li><li>3</li>
</ul>執行過程:
React會匹配新舊兩個<li>1</li>,匹配兩個<li>2</li>,然后添加 <li>3</li> tree
- 2 但是如果你在開始位置插入一個元素,那么問題就來了:
// 舊
<ul><li>1</li><li>2</li>
</ul>// 新
<ul><li>3</li> li3 插入最前面<li>1</li><li>2</li>
</ul>執行過程:
React將改變每一個子節點,而非保持 <li>1</li> 和 <li>2</li> 不變
key 屬性
為了解決以上問題,React 提供了一個 key 屬性。當子節點帶有 key 屬性,React 會通過 key 來匹配原始樹和后來的樹。
// 舊
<ul><li key="1">1</li><li key="2">2</li>
</ul>// 新
<ul><li key="3">3</li><li key="1">1</li><li key="2">2</li>
</ul>執行過程:現在 React 知道帶有key '3' 的元素是新的,對于 '1' 和 '2' 僅僅移動位置即可
補充說明:
-
key 屬性在 React 內部使用,但不會傳遞給你的組件
-
推薦:在
遍歷
數據時,推薦在組件中使用 key 屬性:<li key={item.id}>{item.name}</li>
-
注意:key 只需要保持與他的兄弟節點唯一即可,不需要全局唯一
-
注意:盡可能的減少數組 index 作為 key,數組中插入元素的等操作時,會使得效率底下 。 如果數組比較簡單, 開發中沒有刪除和移動操作,使用 index 也是可以的
組件
組件1 - 函數組件
基本使用
- 函數組件 : 使用函數創建的組件叫組函數組件
- 約定:
- 約定1 : 組件名稱必須是大寫字母開頭, 也就是函數名稱需要首字母大寫
- 約定2 : 函數組件必須有返回值
- 不渲染內容 : return null
- 渲染內容 : return JSX
- 約定3 : 只能有一個唯一的根元素
- 約定4 : 結構復雜, 使用() 包裹起來
- 使用 : 把
組件名
當成標簽名
一樣使用
// 函數組件
function Hello() {// return nullreturn <div>這是我的第一個函數組件</div>
}
// 渲染組件
ReactDOM.render(<Hello />, document.getElementById('root'))
傳參
- 傳參 :
//1. 頭標簽內演示
//2. age='30' age是字符串30age={30} age是number 30
ReactDOM.render(<Child name='zs' age={30}/>, document.getElementById('root'))
- 接收 :
- 函數的參數接收 props
funciton Hello (props) { ... }
- 讀取 :
{ props.name } 、 { props.age }
- 注意 :
- 傳過來的props 不能添加屬性
- 傳過來的props 不能修改屬性值
- 也可以直接解構 props里的屬性
funciton Hello ({ name, age }) { ... }
- 函數的參數接收 props
箭頭函數改造
const Child = () => (<div><div>這是一個div</div><div>這是一個div</div></div>
)const Hello = ()=> <div>這是哈哈的啊</div>
組件2 - 類組件
基本使用
- 類組件 : 使用 ES6 中class 創建的組件, 叫
類組件
- 約定 (同 函數組件)
- 其他約定1 : 類組件必須繼承自
React.Component 父類
, 然后,才可以使用父類中提供的屬性或方法 - 其他約定2 : 必須提供
render 方法
, 來指定要渲染的內容, render 方法必須有返回值
- 其他約定1 : 類組件必須繼承自
// 2 創建類組件
class Hello extends React.Component {// 鉤子 render() {// return nullreturn <div>這是我的第一個class組件</div>}
}
傳參
- 傳參 : 同函數組件一樣
ReactDOM.render(<Child name='zs'/>, document.getElementById('root'))
- 接收參數
// 類組件
class Child extends React.Component { constructor(props) { super(props)// 接收方式1console.log(props);}render () { // 接收方式2console.log(this.props);// 解構const { name, age } = this.propsreturn <div>哈哈{ this.props.name }</div>}
}
ES6 - class
介紹
* 類* es6 之前 創建對象 都是通過構造函數 實現的, 給原型添加方法, 給實例添加屬性* es6 之后, 給我們提供了一個字段 class * 通過class 創建對象 * class : 類 * - 類 : 一類對象, 對象的抽象 , 我們可以通過類創建對象* - 動物 人 * * - 對象: 具體的事物, 它有特征(屬性)和行為(方法)* - 狗/貓/鳥 張三/王春春*/
使用 class 創建對象
- 使用class 創建一個類 class Person { }
- 創建對象 let p = new Person()
- 添加屬性 在類里面的 constructor() { } 的里面添加屬性
- 添加方法 直接在類里面添加方法
class Person { constructor() { this.name = 'zs';this.age = 30}say () { console.log('說話了');}
}let p = new Person()
console.log(p);
p.say()
繼承
- 繼承 : 之前混入, 原型繼承… 都是對象繼承… (對象與對象之間,只要拿過來用就是繼承)
- class 繼承 : 是 類與類之間的繼承 extends
// 人
class Person {constructor() {this.maxAge= 120}
}let p = new Person()
console.log(p)// ---------------------------------------------
/*** 以后凡是 extends 繼承* 當前類里面的 constructor 里面一定要加上super()* 因為底層都是給我們加的super,所以我們才能夠繼承過來,* 如果我們直接寫 constructor,而不寫super,意外著,底層的constructor會被覆蓋掉*/
// 中國人
class Chinese extends Person {constructor() {super() // super 其實就是調用父類的constructorthis.name = 'zs'}
}let c = new Chinese()
console.log(c)
函數組件和類組件的小結
- 函數組件 : 函數創建組件
- 函數名首字母一定要大寫
- 把組件當成標簽使用
- 函數內部 通過 return jsx
- 類組件 : 類創建組件
- 首字母也要大寫
- 一定要繼承(extends) React.Component
- 類里面一定要有一個 render 函數
- render 函數里面通過 return jsx
- 類組件也是當成標簽一樣使用的
函數組件和類組件的區別?
- 函數組件 : 沒有狀態的, 沒有自己的數據
- 類組件 : 有狀態 , 有自己的數據
狀態 State 的簡單說明
state 的定義
- 方式1 : constructor 里面
constructor() { super()//設置狀態1this.state = {name : 'zs'}}
- 方法2 : 屬性初始化語法
// 設置狀態2
state = {name :'zhangsan '}
獲取 狀態 值 :
// 直接獲取<p> { this.state.name }</p>// 解構獲取const { name } = this.state<p> { name }</p>
修改 狀態 值
// 鉤子函數 - 組件掛載完全 render調用完后會調用
componentDidMount() {//1. 直接修改// 如果使用 this.state.name = '春春' , 這樣只會修改state里面的數據,但是無法更新視圖// this.state.name = '春春'//2. 使用 setState 修改// 1-修改數據 2-重新調用render, 更新視圖this.setState({name: '春春'})
}
安裝 React 插件 ==> 查看 state 的值
react-developer-tools.crx
- 安裝步驟 : 后綴crx 改為 zip, 解壓到當前文件, 安裝 拓展程序
使用 state 修改定時器
//2. 類組件
class Child extends React.Component {state = {time : new Date() # + }render() {return (<div><h3>我是h3</h3><p>{ this.state.time.toLocaleTimeString() }</p> # + </div>)}componentDidMount () { setInterval(() => {this.setState({ # + time: new Date() # + }) # + }, 1000);}
}
總結 :
- 函數組件
- 沒有狀態 ( 沒有自己的私有數據 )
- 木偶組件, 組件一旦寫好, 基本就不會改變
- 傳參 :
function Child( props ) { }
, props 只讀
- 類組件
- 有狀態 ( 有自己的私有數據 state )
- 智能組件, 狀態發生改變, 就會更新視圖
- 傳參 :
- this.props
- 優點
- 類組件 : 有狀態,有生命周期鉤子函數, 功能比較強大
- 函數組件 : 渲染更快
- 以后區分使用 ?
- 就看要不要狀態 , 要狀態(類組件) , 不要狀態 (函數組件)
- props 和 state 區別?
- state - 自己的私有數據 類似 vue 里的data
- props - 外界傳進來的 類似 vue 里面的props
事件處理
事件注冊
- 以前注冊事件
<button onclick='fn'>按鈕</button>
- react 中注冊事件
- 注冊事件屬性采用的是駝峰的 onClick=…
- 注冊事件的事件處理函數 , 寫為函數形式,不能為字符串
onClick={ this.fn }
, {} 可以拿到它的原始類型
// 注冊事件
<button onClick={ this.fn }>按鈕</button>// 事件處理函數
fn() {console.log('我被點擊了')
}
事件中 this 的處理
- 演示this問題
// 注冊
<button onClick={this.fn}>按鈕</button>
// 事件
fn () { console.log('點擊了');this.setState({name : 'ls'})
}
// 報錯 : Uncaught TypeError: Cannot read property 'setState' of undefined
// react 中的this=undefined 是需要處理的
- bind 和 call 的回憶使用
function f() {console.log(this)
}let obj = { name: 'zs' }bind 的使用
f.bind(obj) : f里面的this 指向了obj
綁定之后沒有打印,不是bind出了問題,而是bind和call,和apply()
call和apply 1-調用 2-指向
bind 1-指向 2-返回一個新函數
let newF = f.bind(obj)newF()
- 方式1 : bind
- 第一種constructor() { super()this.fn1 = this.fn1.bind(this)}- 第二種 : <button onClick={ this.fn1.bind(this) }>按鈕</button>
- 方式2 : 屬性初始化語法
// 注冊事件
return <button onClick={this.fn2}>按鈕</button>
// 屬性初始化語法
fn2 = () => { // 箭頭函數里面的this指向了外部的thisthis.setState({name : 'ls'})}
- 方式3 : 箭頭函數
<button onClick={ () => this.fn3() }>按鈕</button>// 箭頭函數里面的this 指向外部的this
// 誰調當前this所在的函數, this就執行誰
fn3(){...
}
- 總結
// 方法1 : bind (常用)
return <button onClick={ this.fn.bind(this) }>按鈕</button>
// 方法2 : 屬性初始化語法
return <button onClick={ this.fn1 }>按鈕</button>
// 方法3 : 箭頭函數
return <button onClick={ () => this.fn() }>按鈕</button>
- 三者使用場景
- 方式1和方式3 常用
- 如果函數體內的代碼比較少,比如就1~2行 => 方式3
- 大部分情況下 => 優先使用 方式2
- 如果涉及到傳參 => 方式3
點擊事件傳參 + 配合this處理
- 演示效果
return <button onClick={this.fn(123)}>按鈕</button>
不能這樣傳參,因為還沒有開始點擊,就已經開始調用了fn , 并且把參數傳過去了
- 處理this方式1 : bind
// 注冊事
return <button onClick={ this.fn.bind(this,123) }>按鈕</button>// 傳參
fn( num ) { console.log('點擊了',num);
}
- 處理this方式2 : 屬性初始化語法 – 不能傳參
- 處理this方式3 : 箭頭函數
// 注冊事
return <button onClick={ () => this.fn(123) }>按鈕</button>// 傳參
fn( num ) { console.log('點擊了',num);
}
獲取事件對象 + 配合this處理
// 方式1 : bind , `處理函數最后一個參數`就是 事件對象 e
return <button onClick={this.fn1.bind(this, 123, 456)}>按鈕</button>
// 方式2 : 屬性初始化語法 不傳參數 默認形參就是事件對象 e
return <button onClick={this.fn2}>按鈕</button>
// 方式3 : 箭頭函數 通過箭頭函數獲取e,再繼續傳
return <button onClick={e => this.fn3(e)}>按鈕</button>