無縫切換?從 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、effectsstate 負責定義數據,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