HTML 是一種方便開發者撰寫的語言,只是純文字
DOM 是瀏覽器渲染引擎中真正存在的物件節點
頁面初始化時,渲染引擎會依照 HTML 程式碼的內容產生出 DOM
<!DOCTYPE html>
<html>
<body>
<div>
<button>按鈕</button>
</div>
</body>
</html>
<!DOCTYPE html>
<html>
<body></body>
<script>
const div = document.createElement('div');
document.body.appendChild(div);
const button = document.createElement('button');
button.innerText('按鈕');
div.appendChild(button);
</script>
</html>
y = f(x)
componentInstance = componentClass(data)
React 本身並不是一個完整的前端框架,而是一個只處理 View 的函式庫,也就是負責 DOM 的產生與操作
React 是一個中間媒介,連結了 UI 純粹的定義層面與 DOM 的實際層面
基本上,React 本身只做以下兩件工作;
讓你定義 UI 的藍圖
幫你把這個 UI 渲染到到使用者的瀏覽器畫面上
React 捨棄了傳統的 HTML 開發方式,
改成完全由 JavaScript 來代管 DOM 的產生與操作,
實現 100% 純粹的 Client-Side Rendering。
前端 UI 程式碼本身,應該要足以完整的自我表達其擁有的行為與可能的顯示變化
UI 渲染無法避免邏輯,將 UI 的定義直接在 JavaScript 中進行,有助於提高 UI 的自我表達能力
class ProductItem extends React.Component {
handleButtonClick = () => {
alert(this.props.price);
}
render() {
return (
<div className="item">
<div className="title">{this.props.title}</div>
<div className="price">{this.props.price}</div>
<button onClick={this.handleButtonClick}>購買</button>
</div>
);
}
}
JSX 是 React 在使用的一種特殊 JavaScript 語法糖
能夠讓你以可讀性較高的語法來定義想要產生的 DOM 結構
語法長得很像 HTML,但本質上完全不是 HTML
瀏覽器看不懂,需要翻譯成原生的 JS 程式碼才能正常的在瀏覽器上執行
<div className="item">
<div className="title">{this.props.title}</div>
<div className="price">{this.props.price}</div>
<button onClick={this.handleButtonClick}>購買</button>
</div>
React.createElement("div", {"className": "item"},
React.createElement("div", {"className": "title"}, this.props.title),
React.createElement("div", {"className": "price"}, this.props.price),
React.createElement("button", {onClick: this.handleButtonClick}, "購買")
);
自定義元件藍圖 ( Component Class ),可以嵌套或拼裝
讓前端 UI 程式碼有更好的可組合性與可重用性
每個 Component 的第一層,只能有一個根節點元素
class ProductItem extends React.Component {
handleButtonClick = () => {
alert(this.props.price);
}
render() {
return (
<div className="item">
<div className="title">{this.props.title}</div>
<div className="price">{this.props.price}</div>
<button onClick={this.handleButtonClick}>購買</button>
</div>
);
}
}
class ProductList extends React.Component {
render() {
return (
<div>
{dataList.map(data => (
<ProductItem title={data.title} price={data.price}/>
))}
</div>
);
}
}
ReactDOM.render(<ProductList/>, document.getElementsById('root'));
Virtual DOM 是一份純資料的 Tree 物件,對應到實際的 DOM
Virtual DOM 為自定義 Component 提供了中介的虛擬層,讓開發者能描述 UI 樣貌與邏輯
我們透過定義 Component 來表達「UI 什麼情況該如何呈現」。而「要用什麼手段來達到這個畫面改變(如何操作 DOM)」 ,React 則會自動幫你做,而且絕大多數情況下都比你自己來要做的更好
Single Source of Truth
UI 元件要如何顯示,資料是唯一變因
只有因為資料改變才能導致 UI 元件跟著改變
把畫面全部洗掉,然後再依據最新資料重新畫,顯示結果通常一定是正確的
每次都重繪全部的實體 DOM 顯然是不可行,但是重繪 VDOM 則成本相對降低許多
當畫面需要改變時,根據最新的資料重繪出新的 VDOM Tree,
並與改變前的舊 VDOM Tree 進行全面式的比較與計算,
其中新舊差異的地方,才真的會在實際的 DOM 上發生操作改變
Component 外部(父 Component)傳遞給 Component 內部的靜態參數
抽象化出跟問題有關的參數,方便 Component 進行重用
Props 傳遞到 Component 內部後,應是不可變更的固定值
當 Component 外部改變傳遞進來的 Props 時,Component 內部會自動發起重繪
class ProductItem extends React.Component {
handleButtonClick = () => {
alert(this.props.price);
}
render() {
return (
<div className="item">
<div className="title">{this.props.title}</div>
<div className="price">{this.props.price}</div>
<button onClick={this.handleButtonClick}>購買</button>
</div>
);
}
}
class ProductList extends React.Component {
render() {
return (
<div>
{dataList.map(data => (
<ProductItem title={data.title} price={data.price}/>
))}
</div>
);
}
}
ReactDOM.render(<ProductList/>, document.getElementsById('root'));
propTypes
檢查傳入的 props 型別
defaultProps
該 props 外部沒有傳入時的預設值
class ProductItem extends React.Component {
static propTypes = {
title: React.PropTypes.string,
price: React.PropTypes.number
}
static defaultProps = {
title: '預設的商品標題',
price: 0
}
}
取得 component 的子節點內容
class SomeThing extends React.Component {
render() {
console.log(this.props.children);
return (
<div/>
);
}
}
<SomeThing>
<button id="btn1">按鈕1</button>
<button id="btn2">按鈕2</button>
</SomeThing>
Component 內部私有的動態狀態值
在內部使用 this.setState 方法進行修改
在內部調用修改後,Component 會自動發起畫面重繪
State 的預設值,對 this.state 物件來指定
class ProductList extends Component {
state = {
data: [{
title: '小熊軟糖',
price: 100
}, {
title: '盆栽',
price: 300
}, {
title: 'Macbook Pro',
price: 40000
}]
}
componentDidMount() {
setTimeout(() => {
this.setState({
data: [
...this.state.data, {
title: '新增商品',
price: 9999
}
]
});
}, 5000);
}
render() {
return (
<div>
{this.state.data.map((data, index) => (
<ProductItem
title={data.title}
price={data.price}
/>
))}
</div>
);
}
}
嚴格標籤閉合
支援原生 HTML 有的標籤以及自訂的 Component 標籤
與 HTML 重要的語法差異:class → className
使用 { } 來填入 JavaScript 的表達式(一個值)
<input type="text"/>
<br/>
<img src=""/>
class ProductList extends React.Component {
render() {
return (
<div>
{dataList.map(data => (
<ProductItem title={data.title} price={data.price}/>
))}
</div>
);
}
}
<div className="item">
<div className="title">{this.props.title}</div>
{(this.props.price > 500) && (
<div className="price">{this.props.price}</div>
)}
<button onClick={this.handleButtonClick}>購買</button>
</div>
<div className="item">
<div className="title">{this.props.title}</div>
{(this.props.price > 500) ? (
<div className="price">{this.props.price}</div>
) : (
<div>一個很便宜的商品</div>
)}
<button onClick={this.handleButtonClick}>購買</button>
</div>
使用「...object」來將物件解構並填入當作多個 Props
class SomeThing extends React.Component {
render() {
const propsObject = {
name: 'Zet',
gender: 'male'
}
return (
<div>
<Person {...propsObject}/>
</div>
);
}
}
使用 JavaScript Object 來撰寫,並填入 HTML 類型的 element 的 style props 當中
Property:名稱改用駝峰式命名
Value:數字的預設單位是 px,其他數字單位或非數字的值要使用字串來表示
const styles = {
fontSize: 20,
marginLeft: '20%'
}
<button style={styles}>按鈕</button>
class UncontrolledInputExample extends Component {
handleInputChange = (event) => {
console.log(event.target.value);
}
render() {
return (
<input
type="text"
defaultValue="hello"
onChange={this.handleInputChange}
/>
);
}
}
class ControlledInputExample extends Component {
state = {
inputText: 'hello'
}
handleInputChange = (event) => {
console.log(event.target.value);
this.setState({
inputText: event.target.value
});
}
render() {
return (
<input
type="text"
value={this.state.inputText}
onChange={this.handleInputChange}
/>
);
}
}
input 本身自己管理資料狀態,不與資料來源綁定
使用 defaultValue 或 defaultChecked 來設定預設值
class UncontrolledInputExample extends React.Component {
handleInputChange = (event) => {
console.log(event.target.value);
}
render() {
return (
<input
type="text"
defaultValue="hello"
onChange={this.handleInputChange}
/>
);
}
}
input 自己本身不存放資料,也不能改變自己的值,與指定的資料來源綁定
使用 value 或 checked 來指定綁定的資料
使用 onChange 來指定接收資料的函數
class ControlledInputExample extends Component {
state = {
inputText: 'hello'
}
handleInputChange = (event) => {
console.log(event.target.value);
this.setState({
inputText: event.target.value
});
}
render() {
return (
<input type="text" value={this.state.inputText} onChange={this.handleInputChange}/>
);
}
}
View
( React )
Store
Action
Reducer
pass
by
dispatch
畫面需要改變
產生 action
return
newState
資料變更
Server
Action 負責定義發生的事件種類與想要傳遞的參數,必須是一個純 Object
{
type: 'increment',
value: 1
}
Action 只是描述了有事情發生,並沒有指明要如何更新 State,而 Reducer 正是要負責做這件事
Reducer 是一個純函數,接收完整的舊的 State 和當前 Action,回傳完整的新的 State
你不應該直接修改傳入的舊 State,而是應該回傳一個更新資料後的新 State
若找不到符合的 Action Type,也一定要把舊的 State 照舊回傳
function reducer(state = 0, action) {
switch (action.type) {
case 'increment':
return state + action.value;
case 'decrement':
return state - action.value;
default:
return state;
}
}
Redux 的 createStore() 方法:傳入 Reducer 以建立一個 Store
Store 有以下幾個重要功能
存取 State 資料:使用 store.getState() 方法,可以取得 Store 裡目前最新的 State
store.dispatch() 方法:來發起一個 Action 以更新 State
store.subscribe() 方法:當 State 發生改變時,呼叫 Callback
const store = createStore(reducer);
store.subscribe(() => {
console.log(store.getState());
});
store.dispatch({
type: 'increment',
value: 1
});
用來建立 Store 的 Reducer Function 只能有一個,因此如果你因為程式碼可維護性而分開寫了很多個 Reducer 的話,可以使用 Redux 的 combindReducers 方法將其合併成一個大 Reducer
const rootReducer = combineReducers({
number: numberReducer,
todos: todosReducer
});
const store = createStore(rootReducer);
Smart Component(State Container)
向 Redux 存取資料並傳遞進 Dumb Component
Dumb Component (Display Component)
不知道外界發生什麼,只負責依據 Props 印出資料或調用 Callback
Smart Component | Dumb Component | |
---|---|---|
位於 | 外層 | 內層 |
能直接與 Redux 溝通 | 是 | 否 |
讀取資料 | 從 Redux 獲取 State | 從傳入的 Props 獲取資料 |
修改資料 | 透過 dispatch 方法 向 Redux 發送 Action |
從傳入的 Props 調用 Callback |
class CounterContainer extends Component {
dispatchIncrementNumber = (value) => {
this.props.dispatch(incrementNumber(value));
}
dispatchDecrementNumber = (value) => {
this.props.dispatch(decrementNumber(value));
}
render() {
return (
<Counter
value={this.props.number}
dispatchIncrementNumber={this.dispatchIncrementNumber}
dispatchDecrementNumber={this.dispatchDecrementNumber}
/>
);
}
}
export default connect(state => ({
number: state.number
}))(CounterContainer);
當你呼叫一個非同步 API,有兩個關鍵的時間點:你開始呼叫的的時候,以及當你收到回應 (或是失敗) 的時候。
所以一個請求的行為應該可以分為下列四種狀態:
尚未開始請求(status: null)
請求中,還沒得到結果(status: request)
得到結果並成功(status: success)
得到結果但失敗 (status: failure)
你可以將上述中會發生的三種情況(除掉預設本來就是還沒請請求的狀態),設計成對應的三種 Action
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
const initState = {
status: null,
data: [],
error: null
}
function postReducer(state = initState, action) {
switch (action.type) {
case FETCH_POSTS_REQUEST:
return {
...state,
status: 'request'
};
case FETCH_POSTS_SUCCESS:
return {
...state,
status: 'success',
data: action.reponse
}
case FETCH_POSTS_FAILURE:
return {
...state,
status: 'failure',
error: action.error
}
default:
return state;
}
}
使用Redux Thunk 來讓 dispatch 方法可以接收一個函數
function fetchPosts() {
return async (dispatch) => {
dispatch({
type: FETCH_POSTS_REQUEST
});
try {
const httpResponse = await fetch('/posts');
if (httpResponse.status != 200) {
throw new Error(`${httpResponse.status(httpResponse.statusText)}`);
}
dispatch({
type: FETCH_POSTS_SUCCESS,
response: await httpResponse.json()
});
} catch (error) {
console.error(error);
dispatch({
type: FETCH_POSTS_FAILURE,
error: error.message
});
}
}
}
dispatch
Redux
Thunk
Middleware
Reducer
action is a function
=> pass dispath and call it
action is a object