设计
| react | vue | 说明 | |
|---|---|---|---|
| 渲染 | 构建用户界面的 js 库 | 渐进式框架 | react 侧重于 library,vue 侧重于 framework | 
| 编写方式 | setState 更新 state 的值来达到重新 render 视图 | 响应式数据渲染,修改了响应式数据对应的视图也进行渲染 | react 需要考虑何时 setState,何时 render;vue 只需要考虑修改数据 | 
| 编写方式 | jsx | template | react 是函数式,all in js;vue 区分 tempalte、script、style,提供语法糖,使用 vue-loader 编译 | 
组件通信
- React: 严格单向数据流- 向下: props
- 向上: props func
- 跨层级: context
 - 严格单向数据流,数据只能从父组件传递到子组件,子组件不能修改父组件传递过来的数据,只能通过回调函数的方式,让父组件修改数据,然后再传递给子组件。遵循万物皆可 - props,- onChange/setState()
- 向下: 
- Vue: 单向数据流- 向下: props down
- 向上: event up
- 跨层级: provide/inject,$attrs/$listeners
 - 还有各种获取组件实例 - (VueComponent),如:- $refs、- $parent、- $children;通过递归获取上级或下级组件,如:- findComponentUpward、- findComponentDownward;高阶组件:- provide/reject,- dispatch/broadcast
- 向下: 
| react | vue | 说明 | |
|---|---|---|---|
| 子组件数据传递 | props | props | 都是声明式 | 
| 组件状态机 | state | data | 管理组件的状态,react 使用 setState 更改,vue 直接赋值,新属性使用$set;vue 使用函数闭包特性,保证组件 data 的独立性,react 本就是函数 | 
生命周期
| 生命周期 | react | vue | 说明 | 
|---|---|---|---|
| 数据初始化 | constructor | created | |
| 挂载 | componentDidMount | mounted | dom 节点已经生成 | 
| 更新 | componentDidUpdate | updated | react:组件更新完毕后,react 只会在第一次初始化成功会进入 componentDidmount,之后每次重新渲染后都会进入这个生命周期,这里可以拿到 prevProps 和 prevState,即更新前的 props 和 state。 vue:在数据更改导致的虚拟 DOM 重新渲染和更新完毕之后被调用 | 
| 卸载 | componentWillUnmount | destroyed | 销毁事件 | 
事件处理
- React- React事件的命名采用小驼峰式(camelCase),而不是纯小写
- 使用 JSX语法时你需要传入一个函数作为事件处理函数,而不是一个字符串
- 不能通过返回 false的方式阻止默认行为。你必须显式的使用preventDefault
- 不能卸载非Element标签上,否则会当成props传下去
 
function Form() {
  function handleSubmit(e) {
    e.preventDefault()
    console.log('You clicked submit.')
  }
  return (
    <form onSubmit={handleSubmit}>
      <button type='submit'>Submit</button>
    </form>
  )
}
- Vue- 用在普通元素上时,只能监听原生 DOM事件。用在自定义元素组件上时,也可以监听子组件触发的自定义事件
 
- 用在普通元素上时,只能监听原生 
//原生事件
<form v-on:submit.prevent="onSubmit"></form>
//自定义事件
<my-component @my-event="handleThis(123, $event)"></my-component>
- vue 事件修饰符: - .stop- 调用 event.stopPropagation()。
- .prevent- 调用 event.preventDefault()。
- .capture- 添加事件侦听器时使用 capture 模式。
- .self- 只当事件是从侦听器绑定的元素本身触发时才触发回调。
- .once- 事件将只触发一次。
- .native- 监听组件根元素的原生事件。
- .sync- 语法糖,会扩展成一个更新父组件绑定值的 v-on 侦听器。
- .left- (2.2.0) 只当点击鼠标左键时触发。
- .right- (2.2.0) 只当点击鼠标右键时触发。
- .middle- (2.2.0) 只当点击鼠标中键时触发。
- .passive- (2.3.0) 以 { passive: true } 模式添加侦听器
- 事件修饰符的顺序:v-on:click.stop.prevent
 
class 和 style
- class - React:- className
 - render() { let className = 'menu'; if (this.props.isActive) { className += ' menu-active'; } return <span className={className}>Menu</span> }- Vue:- class
 - <div class="static" :class="{ active: isActive, 'text-danger': hasError }" ></div> <div :class="[{ active: isActive }, errorClass]"></div>
- style 
- React:- style
<div style={{ color: 'red', fontWeight: 'bold' }} />
在
react中,可以使用CSS Modules达到局部和模块功能,避免组件间css污染;注意,根据CSS Modules的官方规范,更推荐以驼峰式的命名方式定义类名,而非kebab-casing。
<h1 className={styles.appTitle}>
- Vue:- style
<div :style="[baseStyles, overridingStyles]"></div>
组件样式的时候你可以在
style标签上声明一个scoped来作为组件样式隔离标注,如:<style lang="sass" scoped></style>。最后打包时其实样式都加入一个hash的唯一值,避免组件间css污染
条件渲染
- React:- if、- switch、- 三元表达式、- &&,- return null、- return false表示不渲染
function UserGreeting(props) {
  return <h1>Welcome back!</h1>
}
function GuestGreeting(props) {
  return <h1>Please sign up.</h1>
}
function Greeting(props) {
  const isLoggedIn = props.isLoggedIn
  if (isLoggedIn) {
    return <UserGreeting />
  }
  return <GuestGreeting />
}
- Vue:- v-if、- v-else-if、- v-else、- v-show、- return ture表示被渲染
<template>
  <div v-if="type === 'A'">A</div>
  <div v-else-if="type === 'B'">B</div>
  <div v-else-if="type === 'C'">C</div>
  <div v-else>Not A/B/C</div>
  <div v-show="isShow">show</div>
</template>
列表渲染
- React:- map、- filter、- reduce、- forEach、- for in、- for of, 一个元素的- key最好是这个元素在列表中拥有的一个独一无二的字符串。通常,我们使用数据中的- id来作为元素的- key
<ul>
  {props.posts.map((post) => (
    <li key={post.id}>{post.title}</li>
  ))}
</ul>
- Vue:- v-for,- key是为 Vue 中的虚拟 DOM 提供一个唯一标识,以便它能够跟踪每个节点的身份,从而重用和重新排序现有元素
<ul>
  <li v-for="item in items" :key="item.id"></li>
</ul>
组件嵌套
- React:默认插槽
<div className={'FancyBorder FancyBorder-' + props.color}>{props.children}</div>
- React: 具名插槽
<div className="SplitPane">
  <div className="SplitPane-left">
    {props.left}
  </div>
  <div className="SplitPane-right">
    {props.right}
  </div>
</div>
<SplitPane left={<Contacts />} right={<Chat />} />
- Vue:默认插槽
<main>
  <slot></slot>
</main>
- Vue:具名插槽
<template>
  <div>
    <header>
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot>
    </main>
    <footer>
      <slot name="footer"></slot>
    </footer>
  </div>
</template>
<!-- 使用 -->
<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <p>A paragraph for the main content.</p>
  <p>And another one.</p>
  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>
获取 DOM
- React:- ref
class MyComponent extends React.Component {
  constructor(props) {
    super(props)
    this.myRef = React.createRef()
  }
  render() {
    return <div ref={this.myRef} />
  }
}
- Vue:- ref
<template>
  <div ref="myRef"></div>
</template>
<script>
export default {
  mounted() {
    console.log(this.$refs.myRef.offsetWidth)
  },
}
</script>
项目文档结构
- React:- Umi为例
├── config                   # umi 配置,包含路由,构建等配置
│   ├── config.ts            # 项目配置.umirc.ts优先级更高,使用此方式需要删除.umirc.ts
│   ├── routes.ts            # 路由配置
│   ├── defaultSettings.ts   # 系统配置
│   └── proxy.ts             # 代理配置
├── mock                     # 此目录下所有 js 和 ts 文件会被解析为 mock 文件
├── public                   # 此目录下所有文件会被copy 到输出路径,配置了hash也不会加
├── src
│   ├── assets               # 本地静态资源
│   ├── components           # 业务通用组件
│   ├── e2e                  # 集成测试用例
│   ├── layouts              # 约定式路由时的全局布局文件
│   ├── models               # 全局 dva model
│   ├── pages                # 所有路由组件存放在这里
│   │   └── document.ejs     # 约定如果这个文件存在,会作为默认模板
│   ├── services             # 后台接口服务
│   ├── utils                # 工具库
│   ├── locales              # 国际化资源
│   ├── global.less          # 全局样式
│   ├── global.ts            # 全局 JS
│   └── app.ts               # 运行时配置文件,比如修改路由、修改 render 方法等
├── README.md
└── package.json
- Vue:- Vue-cli为例
├── mock                       # 项目mock 模拟数据
├── public                     # 静态资源
│   └── index.html             # html模板
├── src                        # 源代码
│   ├── api                    # 所有请求
│   ├── assets                 # 主题 字体等静态资源
│   ├── components             # 全局公用组件
│   ├── directive              # 全局指令
│   ├── filters                # 全局 filter
│   ├── layout                 # 全局 layout
│   ├── router                 # 路由
│   ├── store                  # 全局 store管理
│   ├── utils                  # 全局公用方法
│   ├── views                  # views 所有页面
│   ├── App.vue                # 入口页面
│   └── main.js                # 入口文件 加载组件 初始化等
├── tests                      # 测试
├── vue.config.js              # vue-cli 配置如代理,压缩图片
└── package.json               # package.json
路由
- React:- react-router-dom
- 动态路由 & 路由传参 - history.push(/list?id=${id})
- history.push({pathname: '/list', query: {id}})
- history.push(/list/id=${id})
- history.push({pathname: '/list', params: {id}})
 - 获取 - props.match.query / props.match.params
- Vue:- vue-router
- 动态路由 & 路由传参 - this.$router.push({path: '/list', query: {id}})
- this.$router.push({path: '/list', params: {id}})
 - 获取 - this.$route.query / this.$route.params
嵌套路由
- React:- react-router-dom
{
  path: '/',
  component: '@/layouts/index',
  routes: [
    { path: '/list', component: 'list' },
    { path: '/admin', component: 'admin' },
  ],
}
<div style={{ padding: 20 }}>{ props.children }</div>
使用
props.children渲染子路由
- Vue:- vue-router
{
  path: '/',
  component: '@/layouts/index',
  children: [
    { path: '/list', component: 'list' },
    { path: '/admin', component: 'admin' },
  ],
}
<template>
  <div>
    <router-view></router-view>
  </div>
</template>
使用 vue 原生组件
<router-view/>组件渲染子路由
路由跳转
- React:- Umi为例
;<NavLink exact to='/profile' activeClassName='selected'>
  Profile
</NavLink>
history.push(`/list?id=${id}`)
- Vue:- vue-router为例
<router-link to="/about">About</router-link>
this.$router.push({path: '/list', query: {id}})
路由守卫(登录验证,特殊路由处理)
- React:- Umi为例
// 路由配置
{
  path: '/login',
  component: '@/pages/login',
  wrappers: ['@/wrappers/auth'],
}
// /wrappers/auth.js
import { Redirect } from 'umi'
export default (props) => {
  const isLogin = true
  if (isLogin) {
    return <div>{props.children}</div>
  } else {
    return <Redirect to="/login" />
  }
}
- React:- react-router-dom为例
import React from 'react'
import { BrowserRouter as Router, Route, Redirect } from 'react-router-dom'
const isAuthenticated = true
const PrivateRoute = ({ component: Component, ...rest }) => (
  <Route
    {...rest}
    render={(props) =>
      isAuthenticated === true ? (
        <Component {...props} />
      ) : (
        <Redirect to='/login' />
      )
    }
  />
)
const HomePage = () => {
  return (
    <div>
      <h1>Welcome to the home page!</h1>
    </div>
  )
}
const LoginPage = () => {
  return (
    <div>
      <h1>Please log in to continue.</h1>
    </div>
  )
}
const App = () => {
  return (
    <Router>
      <div>
        <PrivateRoute exact path='/' component={HomePage} />
        <Route path='/login' component={LoginPage} />
      </div>
    </Router>
  )
}
export default App
状态管理
| dva | vuex | 说明 | |
|---|---|---|---|
| 模块 | namespace | modules | 解决应用的所有状态会集中到一个比较大的对象,store 对象可能变得相当臃肿 | 
| 单一状态树 | state | state | 唯一数据源 | 
| 提交状态机 | reducer | mutations | 用于处理同步操作,唯一可以修改 state 的地方 | 
| 处理异步操作 | effects | action | 调用提交状态机更改状态树 | 
- React:- dva为例
// new model:models/products.js
export default {
  namespace: 'products',
  state: [],
  reducers: {
    'delete'(state, { payload: id }) {
      return state.filter(item => item.id !== id);
    },
  },
};
//connect model
export default connect(({ products }) => ({
  products,
}))(Products);
//dispatch model reduce
dispatch model reduce({
  type: 'products/delete',
  payload: id,
})
如有异步操作,如 ajax 请求,dispath model effects,然后 effects 调用 model reduce
- Vue:- vuex为例
// new module
const store = new Vuex.Store({
  state: {
    count: 1
  },
  mutations: {
    increment (state) {
      state.count++
    }
  },
  actions: {
    increment (context) {
      context.commit('increment')
    }
  }
})
//bind UI
<input v-model="$store.state[modelesName].name"/>
//commit module mutation
store.commit('increment')
如有异步操作,如
ajax请求,dispathmoduleactions,然后actions调用modulemutations
// 异步action
store.dispatch({
  type: 'incrementAsync',
  amount: 10,
})
