無縫切換?從 Vue 到 React
本文主要針對 Vue 開發者或新手快速上手 React。
本文主要分析 Vue 和 React 在開發上的區別,幫助 Vue 開發者快速上手 React,同時也適用於前端新手入門 React。
單文件組件 VS class 組件 VS 函數組件
Vue: 單文件組件
<template>
<div>{{ greeting }} world</div>
</template>
<script>
export default {
data() {
return {
greeting: 'hello'
}
}
}
</script>
<style>
</style>
React: Class 組件
class Comp extends Component {
constructor(props) {
super(props);
this.state = {greeting: 'hello'};
}
render() {
return (
<div>
<div>{ greeting } world</div>
</div>
);
}
}
官方文檔 [1]
React: 函數組件(推薦)
在 Vue 單文件組件和 React 的 Class 組件中,我們的元素、數據變量等必須放到固定的位置,以一種固定的格式書寫,而在函數組件中書寫方式變得更簡單,我們可以像寫函數一樣寫組件。更重要的是,這樣就不用關心那些難理解的 this 了。
const Comp = () => {
const [greeting, setGreeting] = useState('hello');
return (
<div>
<div>{ greeting } world</div>
</div>
)
}
官方文檔 [2]
雙向綁定 VS 單向數據流
在 Vue 中我們使用 v-bind、v-modal 對數據進行綁定,無論是來自用戶操作導致的變更,還是在某個方法裏賦值都能夠直接更新數據,不需要手動進行 update 操作。
this.data.greeting = "Hello"
而在 React 裏需要調用 set 方法更新,當 React 感應到 set 觸發時會再次調用 render 對 dom 進行刷新,雖然有些麻煩但這種方式可以讓數據變化更加清晰易追尋。
this.state.greeting = "Hello" // 錯誤寫法
this.setState({greeting: 'Hello'}); // 正確寫法✅
setGreeting('Hello'); // 來自hooks的set寫法 後面會介紹
React 的大 buff:JSX
初次接觸 JSX 的開發者可能會感覺 JSX 結構混亂,因爲你可以在 dom 元素內部直接寫 js 邏輯,也可以在 js 邏輯裏寫 dom,這就像把 html 和 js 混在一起:
import getUserType from './getUserType'
const Comp = () => {
const [greeting, setGreeting] = useState('hello');
const Button = () => {
const userType = getUserType()
if(userType === 0) {
return <button>去購買</button>
}
if(userType === 1) {
return <button>去充值</button>
}
return null
}
return (
<div>
<div>{ greeting } world</div>
{Button()}
</div>
)
}
雖然元素和邏輯的邊界模糊了,但我們的組件會變得更加靈活,這樣能夠將一個組件分成不同的模塊,當需要修改是時我們只需關注對應的函數,不用擔心影響到其他部分,這對複雜的頁面結構非常有用。
Hooks
是什麼
上面我們在講數據流的時候有提到,處理數據的兩種方式
// 方式1
this.state = {greeting: 'Hello'}
this.setState({greeting: 'Hello'});
// 方式2
const [greeting, setGreeting] = useState('hello');
setGreeting('Hello');
其中第二種方式的 useState 就是 Hooks 中的一種,是比較常用的 Hook,除此之外還有 useEffect,useRef 等,每個都有着不同的功能。
爲什麼用
邏輯獨立
以數據更新爲例,簡單來講,如果不用 Hooks,每次更新數據都用 setSate,我們的代碼裏就會出現很多 setState 調用,setState 根據入參可以一次修改一個字段,也可以一次修改多個字段,想要知道某個數據在哪裏被做了怎樣的修改就會變的很麻煩,甚至可能不小心多寫一個字段修改了不該修改的數據。而用 Hooks 的 useState 的話,因爲它在定義時會對字段創建其專用的修改函數,所以只要有這個函數的調用,就代表這個字段被做了修改。
怎麼用
常用 Hooks(Hooks 只能在的函數組件內使用):
1、useState: 用於定義組件的 State,相當於 this.state=xxx 或者 Vue 裏的 data(){return xxx}
const [greeting, setGreeting] = useState('hello'); // greeting 默認 hello
// 點擊greeting變爲Hello1
<div onClick={setGreeting('Hello1')}>{greeting}</div>
2、useEffect:通過依賴變更觸發的鉤子函數 ,類似 Vue 的 watcher
// 當userId變化時調用refresh
useEffect(() => {
refresh();
}, [userId]);
// 進入頁面時會執行init, 退出時會執行destroy
useEffect(() => {
init();
return () => {
destroy()
}
}, []);
3、useRef: 返回 ref 對象,.current 可以獲取到其原生的元素本身
const el = useRef(null);
<div ref={el}></div>
// console.log(el.current.offsetHeight) 返回div的offsetHeight
狀態管理
是什麼?爲什麼用?
官方定義:“集中式存儲管理應用的所有組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化”。
舉個例子,頁面裏兩個組件需要展示 / 更新 userName,如果不使用狀態管理,我們可以用父子組件交互的方式把 userName 字段和 setUserName 函數作爲組件參數傳入兩個組件中,調用 setUserName 即觸發 page 更新 userName:
但當業務變得越來越複雜,就會陷入透傳地獄!
加入狀態管理後,不在涉及組件之間的參數傳遞,數據管理全部放在 Store 中管理,組件直接從 Store 中讀取數據,修改時調用 Store 的修改函數修改對應數據:
怎麼用
Vue:Vuex在 Vue 中,官方腳手架自帶 Vuex 爲我們注入好了 Store,常用的 state 負責定義數據,mutations 負責修改數據,actions 負責利用 mutations 做一些複雜異步操作(如接口請求)
// store.js
import { createStore } from 'vuex'
const store = createStore({
state: {
count: 0
},
mutations: {
setCount (state, value) {
state.count = value
}
},
actions: {
addon ({ commit, state }) {
const count = state.count
commit('set', count+1)
}
}
})
// index.js
import App from './vue'
import { createApp } from 'vue'
const app = createApp(App).mount('#app');
// 將 store 實例作爲插件安裝
app.use(store)
// index.vue
<template>
<div>{{ this.$store.state.count }} world</div>
</template>
<script>
export default {
methods: {
increment() {
this.$store.commit('setCount', 10)
this.$store.dispatch('setCount')
console.log(this.$store.state.count)
}
}
}
</script>
<style>
</style>
React:不止是 ReduxReact 本身並不帶狀態管理,狀態管理對於 React 更像是一種普通的第三方工具,工作中不同項目可能用了 Redux、mobx、rematch 等不同的狀態管理工具,不同工具寫法會有所區別,使用者要自己區分學習,除此之外一些腳手架會自帶狀態管理,寫法會簡單些,比如 Rax,爲方便理解接下來以 Rax 的寫法進行說明。
與上面所說的 Vuex 的 state、mutations、actions 對應,React 裏叫做 state、reducers、effects。state 負責定義數據,reducers 負責修改數據,effects 負責利用 reducers 做一些複雜異步操作,下面示例解釋的更清楚:
// src/pages/Dashboard/models/counter.ts
const delay = (time) => new Promise((resolve) => setTimeout(() => resolve(), time));
export default {
// 定義 model 的初始 state
state: {
count: 0
},
// 定義改變該模型狀態的純函數
reducers: {
increment(prevState) {
return { count: prevState.count + 1 };
},
},
effects: (dispatch) => ({
async incrementAsync() {
await delay(10);
dispatch.counter.increment();
},
}),
}
// src/pages/Dashboard/store.ts
import { createStore } from 'rax-app';
import counter from './models/counter';
const store = createStore({ counter });
export default function Dashboard() {
// 使用 counter 模型
const [counterState, counterDispatchers] = store.useModel('counter');
return (
<>
<span>{counterState.count}</span>
<button onClick={counterDispatchers.increment}>+</button>
<button onClick={counterDispatchers.incrementAsync}>+</button>
</>
);
}
React 代碼實戰:開發一個 TodoList
// index.jsx
import $i18n from '@alife/panda-i18n';
import React, { useCallback } from 'react';
import { connect } from 'react-redux';
import { Link } from '@ice/router';
import PropTypes from 'prop-types';
import { Form, Input } from 'cn-next';
import styles from './index.module.scss';
const FormItem = Form.Item;
const AddTodo = (props) => {
const { onAdd } = props;
const onSubmit = useCallback(
(values, errors) => {
if (!errors) {
onAdd(values.text);
}
},
[onAdd],
);
return (
<div x-class={[styles.add]}>
<Form className={styles.addForm} inline onSubmit={onSubmit}>
<FormItem
className={styles.addItem}
required
requiredMessage={$i18n.get({
id: 'EnterAToDoList.other',
dm: '請輸入待辦事項',
})}
>
<Input
name='text'
placeholder={$i18n.get({
id: 'EnterAToDoList.other',
dm: '請輸入待辦事項',
})}
/>
</FormItem>
<Form.Submit className={styles.addSubmit} onClick={onSubmit} validate>
{$i18n.get({ id: 'Add.other', dm: '添加' })}
</Form.Submit>
</Form>
</div>
);
};
AddTodo.propTypes = {
onAdd: PropTypes.func,
};
AddTodo.defaultProps = {
onAdd: () => {},
};
const Todos = (props) => {
const { list, createAsync } = props;
// 添加
const onAddTodo = useCallback(
async (text) => {
await createAsync(text);
},
[createAsync],
);
return (
<div className={styles.todos}>
<AddTodo onAdd={onAddTodo} />
<div className='mb-30'>
{list.map((item) => {
return (
<div key={item.text} className={styles.todo}>
<span>{item.text}</span>
</div>
);
})}
</div>
<div>
<Link to='/'>
{$i18n.get({ id: 'ReturnToHomePage.other', dm: '返回首頁' })}
</Link>
</div>
</div>
);
};
Todos.propTypes = {
list: PropTypes.arrayOf(PropTypes.shape({})).isRequired,
createAsync: PropTypes.func.isRequired,
};
const mapState = (state) => ({
list: state.todos.list,
});
const mapDispatch = (dispatch) => ({
createAsync: dispatch.todos.createAsync,
doneAsync: dispatch.todos.doneAsync,
undoneAsync: dispatch.todos.undoneAsync,
});
export default connect(mapState, mapDispatch)(Todos);
// todo.js
export const todos = {
state: {
list: [
{
text: 'Learn typescript',
done: true,
},
{
text: 'Try immer',
done: false,
},
],
},
reducers: {
create(state, text) {
state.list.push({ text, done: false });
return state;
},
done(state, idx) {
if (state.list[idx]) {
state.list[idx].done = true;
}
return state;
},
undone(state, idx) {
if (state.list[idx]) {
state.list[idx].done = false;
}
return state;
},
},
effects: (dispatch) => ({
async createAsync(payload) {
// 模擬異步操作
await new Promise((resolve) => setTimeout(resolve, 250));
dispatch.todos.create(payload);
},
async doneAsync(payload) {
// 模擬異步操作
await new Promise((resolve) => setTimeout(resolve, 250));
dispatch.todos.done(payload);
},
async undoneAsync(payload) {
// 模擬異步操作
await new Promise((resolve) => setTimeout(resolve, 250));
dispatch.todos.undone(payload);
},
}),
};
參考鏈接:
[1]https://zh-hans.reactjs.org/docs/components-and-props.html#function-and-class-components
[2]https://zh-hans.reactjs.org/docs/components-and-props.html#function-and-class-components
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/5jCoWQcoB9Ztfy55Q0TeWA