前言
Vue和React都是目前最流行、生态最好的前端框架之一。框架本身没有优劣之分,只有适用之别,选择符合自身业务场景、团队基础的技术才是我们最主要的目的。
正文
设计思想
vue
vue的官网中说它是一款渐进式框架,采用自底向上增量开发的设计。这里我们需要明确一个概念,什么是渐进式框架。在声明式渲染(视图模板引擎)的基础上,我们可以通过添加组件系统(components)、客户端路由(vue-router)、大规模状态管理(vuex)来构建一个完整的框架。Vue从设计角度来讲,虽然能够涵盖所有这些内容,但是你并不需要一上手就把所有东西全用上,因为没有必要。无论从学习角度,还是实际情况,这都是可选的。声明式渲染和组件系统是Vue的核心库所包含内容,而客户端路由、状态管理、构建工具都有专门解决方案。这些解决方案相互独立,你可以在核心的基础上任意选用其他的部件,不一定要全部整合在一起。可以看到,所说的“渐进式”,其实就是Vue的使用方式,同时也体现了Vue的设计的理念。
react
react主张函数式编程,所以推崇纯组件,数据不可变,单向数据流,当然需要双向的地方也可以手动实现,比如借助 onChange 和 setState 来实现一个双向的数据流。React的三个最大特点是声明式、组件化和多端(native、node)渲染。
- React专注于View层,并可配合其它库(如Redux, Mobx等)行程MV*架构。
- React提出了虚拟dom的概念,极大的提高了页面渲染的性能,并能在Native或服务端进行渲染。
- 单向数据流,React的数据会从上到下的通过props传递,用户只用关心数据状态的更新。
- React采用声明式渲染,提高代码可读性和可维护性。
- 组件化,React中组件是一等公民。
编写语法
vue
vue推荐的做法是webpack+vue-loader的单文件组件格式,vue保留了html、css、js分离的写法,使得现有的前端开发者在开发的时候能保持原有的习惯,更接近常用的web开发方式,模板就是普通的html,样式直接使用css.
react
react是没有模板的,直接就是一个渲染函数,React推荐的做法是 JSX + inline style, 也就是把HTML和CSS全都写进JavaScript了,即'all in js'。JSX实际就是一套使用XML语法,用于让我们更简单地去描述树状结构的语法糖。在react中,所有的组件的渲染功能都依靠JSX。你可以在render()中编写类似XML的语法,它最终会被编译成原生JavaScript。
组件
vue
vue组件定义使用xx.vue文件来表示,vue组件将html、css、js组合到一起,形式如下:
// 模板(html)
<template>
<div>{{name}}</div>
</template>
// 数据管理(js)
<script>
export default {
name: 'CompoenentName',
data() {
return {
name: 'xx'
}
}
}
</script>
// 样式(css)
<style scoped>
</style>
react
react推荐使用jsx或者js文件来表示组件,react支持类组件和函数式组件2种形式,react使用{}包裹变量。
注意: 组件名称必须以大写字母开头。React 会将以小写字母开头的组件视为原生 DOM 标签。例如,
<div />
代表HTML的div标签,而<App />
则代表一个组件。
以下定义了react的类组件和函数式组件
// 类组件
import React from 'react';
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
msg: 'hello world'
};
}
render() {
rerurn(<div>{msg}</div>);
}
}
export default App;
// 函数式组件
import React from 'react';
function App() {
return (<div>hello world</div>);
}
export default App;
组件数据交互
组件数据交互是指父子组件、兄弟组件、跨层组件之间传递数据。 兄弟组件之间可以通过事件总线或者通过父组件传递数
vue
父子组件数据交互
vue中父组件通过props传递数据给子组件,子组件使用$emit触发自定义事件,父组件中监听子组件的自定义事件获取子组件传递来的数据。
// 子组件使用$emit传递自定义事件myEvent:
<template>
<div @click="changeName">{{name}}</div>
</template>
<script>
export default {
name: 'Child',
data() {
return {
name: 'xxx'
}
},
methods: {
changeName() {
this.name = 'new Name';
this.$emit('myEvent', this.name);
}
}
}
</script>
// 父组件使用@myEvent监听自定义事件,回调函数参数是子组件传回的数据:
<template>
<div>
<Child @myEvent="getName"></Child>
</div>
</template>
<script>
import Child from './Child';
export default {
components: {
Child
},
methods: {
getName(name) {
console.log(name)
}
}
}
</script>
跨组件数据交互
通过provide / inject在祖先组件向所有子孙后代注入一个依赖,不论组件层次有多深,并在起上下游关系成立的时间里始终生效。
// 上层组件设置provide
<script>
export default {
provide: { // 定义provide选项
message: 'This is a big news'
}
}
</script>
// 下层组件定义inject
<template>
<div>{{message}}</div>
</template>
<script>
export default {
name: 'Children',
inject: ['message']
}
</script>
react
父子组件数据交互
react中父组件使用props传递数据和回调函数给子组件,子组件通过props传下来的回调函数返回数据,父组件通过回调函数获取子组件传递上来的数据。
// 子组件
import React, { useState } from 'react';
function Children(props) {
const { myEvent } = props;
const changeName = () => {
myEvent('new name');
};
return <div onClick={changeName}>修改name</div>;
}
// 父组件
function Parent() {
const changeName = name => {
console.log(name);
};
return <Children myEvent={changeName}></Children>;
}
跨组件数据交互
Context 提供了一个无需为每层组件手动添加 props,就能在组件树间进行数据传递的方法。
// Context 可以让我们无须明确地传遍每一个组件,就能将值深入传递进组件树
import React from 'react'
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
function Toolbar() {
return (
<div>
<ThemedButton />
</div>
);
}
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
状态集管理工具
vue
vue使用vuex来进行状态管理。vuex引入State、Getter的概念对状态进行定义;使用Mutation和Action对状态进行变更;引入Module对状态进行模块化分割;引入插件对状态进行快照、记录、以及追踪等;提供了mapState、mapGetters、 mapActions、 mapMutations 辅助函数方便开发者在vm中处理store。具体构成关系如下:
- 将需要共享的状态挂载到state上:this.$store.state或者mapState辅助函数来调用。
- 通过getters来创建状态:通过this.$store.getters来调用。可以根据某一个状态派生出一个新状态,vuex也提供了mapGetters辅助函数来帮助我们在组件中使用getters里的状态。
- 使用mutations来更改state:通过
this.$store.commit
来调用。我们不能直接在组件中更改state,而是需要使用mutations来更改,mutations也是一个纯对象,里面包含很多更改state的方法,这些方法的形参接收到state,在函数体里更改,这时,组件用到的数据也会更改,实现响应式。vuex提供了mapMutations方法来帮助我们在组件中调用mutations 的方法。 - 使用actions来处理异步操作:
this.$store.dispatch
来调用。Actions类似于mutations,不同在于:Actions提交的是mutations,而不是直接变更状态。Actions可以包含任意异步操作。也就是说,如果有这样的需求:在一个异步操作处理之后,更改状态,我们在组件中应该先调用actions,来进行异步动作,然后由actions调用mutations来更改数据。在组件中通过this.$store.dispatch方法调用actions的方法,当然也可以使用mapMutations来辅助使用。
react
react使用Redux进行状态管理,它的出现主要是为解决react中组件之间的通信问题。建议把数据放入到redux中管理,目的就是方便数据统一,好管理。
redux的流程:
- 创建store: 从redux工具中取出createStore去生成一个store。
- 创建一个reducer,然后将其传入到createStore中辅助store的创建。 reducer是一个纯函数,接收当前状态和action,返回一个状态,返回什么,store的状态就是什么,需要注意的是,不能直接操作当前状态,而是需要返回一个新的状态。 想要给store创建默认状态其实就是给reducer一个参数创建默认值。
- 组件通过调用store.getState方法来使用store中的state,挂载在了自己的状态上。
- 组件产生用户操作,调用actionCreator的方法创建一个action,利用store.dispatch方法传递给reducer。
- reducer对action上的标示性信息做出判断后对新状态进行处理,然后返回新状态,这个时候store的数据就会发生改变, reducer返回什么状态,store.getState就可以获取什么状态。
- 我们可以在组件中,利用store.subscribe方法去订阅数据的变化,也就是可以传入一个函数,当数据变化的时候,传入的函数会执行,在这个函数中让组件去获取最新的状态。
React Hooks
Hooks作为react所特有的,vue不存在hooks这一概念。是因为react创建组件有2种方式:类组件和函数式组件。类组件可以定义状态的,也有生命周期等等,但是在函数式组件里面没有,于是react就推出了hooks。
useState
useState 是最基本的 API,它传入一个初始值,每次函数执行都能拿到新值。
import React, { useState } from 'react';
import ReactDOM from 'react-dom';
function Counter() {
const [count, setCount] = useState(0);
return (
<div>
<button onClick={() => setCount(count + 1)}>+</button>
<h1>{count}</h1>
<button onClick={() => setCount(count - 1)}>-</button>
</div>
)
}
ReactDOM.render(<Counter />, document.getElementById('root'));
useEffect
一个至关重要的 Hooks API,顾名思义,useEffect 是用于处理各种状态变化造成的副作用,也就是说只有在特定的时刻,才会执行的逻辑,可用于生命周期:componentDidMount/componentDidUpdate/componentWillUnMount。
import { useState, useEffect } from 'react';
import ReactDOM from 'react-dom';
function Example() {
const [count, setCount] = useState(0);
// => componentDidMount/componentDidUpdate
useEffect(() => {
// update
document.title = `You clicked ${count} times`;
// => componentWillUnMount
return function cleanup() {
document.title = 'app';
}
}, [count]);
return (
<div>
<p>You clicked {count} times</p>
<button onClick={() => setCount(count + 1)}>
Click me
</button>
</div>
);
}
ReactDOM.render(<Example />, document.getElementById('root'));
useRef
创建ref对象:类组件、React 元素用 React.createRef,函数组件使用 useRef。
useRef 返回一个可变的 ref 对象,其 current 属性被初始化为传入的参数(initialValue)。
const refContainer = useRef(initialValue);
forwardRef:因为函数组件没有实例,所以函数组件无法像类组件一样可以接收 ref 属性。forwardRef 可以在父组件中操作子组件的 ref 对象。forwardRef 可以将父组件中的 ref 对象转发到子组件中的 dom 元素上。子组件接受 props 和 ref 作为参数。
function Child(props,ref){
return (
<input type="text" ref={ref}/>
)
}
Child = React.forwardRef(Child);
function Parent(){
let [number,setNumber] = useState(0);
// 在使用类组件的时候,创建 ref 返回一个对象,该对象的 current 属性值为空
// 只有当它被赋给某个元素的 ref 属性时,才会有值
// 所以父组件(类组件)创建一个 ref 对象,然后传递给子组件(类组件),子组件内部有元素使用了
// 那么父组件就可以操作子组件中的某个元素
// 但是函数组件无法接收 ref 属性 <Child ref={xxx} /> 这样是不行的
// 所以就需要用到 forwardRef 进行转发
const inputRef = useRef();//{current:''}
function getFocus(){
inputRef.current.value = 'focus';
inputRef.current.focus();
}
return (
<>
<Child ref={inputRef}/>
<button onClick={()=>setNumber({number:number+1})}>+</button>
<button onClick={getFocus}>获得焦点</button>
</>
)
}
useContext
- 接收一个 context 对象(React.createContext 的返回值)并返回该 context 的当前值
- 当前的 context 值由上层组件中距离当前组件最近的 <MyContext.Provider> 的 value prop 决定
- 当组件上层最近的 <MyContext.Provider> 更新时,该 Hook 会触发重渲染,并使用最新传递给 MyContext provider 的 context value 值
- useContext(MyContext) 相当于 class 组件中的 static contextType = MyContext 或者 <MyContext.Consumer>
import { useState, useContext, createContext } from 'react';
import ReactDOM from 'react-dom';
// 1. 使用 createContext 创建上下文
const UserContext = new createContext();
// 2. 创建 Provider
const UserProvider = props => {
let [username, handleChangeUsername] = useState('');
return (
<UserContext.Provider value={{ username, handleChangeUsername }}>
{props.children}
</UserContext.Provider>
);
};
const Pannel = () => {
const { username, handleChangeUsername } = useContext(UserContext); // 3. 使用 Context
return (
<div>
<div>user: {username}</div>
<input onChange={e => handleChangeUsername(e.target.value)} />
</div>
);
};
const App = () => (
<div>
<UserProvider>
<Pannel />
</UserProvider>
</div>
);
ReactDOM.render(<App />, document.getElementById('root'));
useCallback
语法:
const memoizedCallback = useCallback(() => { doSomething(a, b); }, [a, b]);
返回值 memoizedCallback 是一个 memoized 回调。传递内联回调和一系列依赖项。useCallback 将返回一个回忆的memoized版本,该版本仅在其中一个依赖项发生更改时才会更改。
Foo.js
import React from 'react';
const Foo = ({ onClick }) => {
console.log('Foo:', 'render');
return <button onClick={onClick}>Foo 组件按钮</button>;
};
export default React.memo(Foo);
App.js
import React, { useCallback, useState } from 'react';
import Foo from './Foo';
function App() {
const [count, setCount] = useState(0);
const fooClick = useCallback(() => {
console.log('点击了 Foo 组件的按钮');
}, []);
return (
<div>
<p>{count}</p>
<Foo onClick={fooClick} />
<br />
<br />
<button onClick={() => setCount(count + 1)}>count increment</button>
</div>
);
}
export default App;
如果点击count increment按钮,Foo组件是不会重新渲染的。如果将useCallback或者React.memo,只要改变父组件的任何变量,Foo组件还是会重新渲染的。
useMemo
语法:
const memoizedValue = useMemo(() => computeExpensiveValue(a, b), [a, b]);
返回一个memoized值。 传递“创建”函数和依赖项数组。useMemo 只会在其中一个依赖项发生更改时重新计算 memoized 值。此优化有助于避免在每个渲染上进行昂贵的计算。
- 可以进行数据的缓存,类似于 Vue 的 computed,可以根据依赖变化自动重新计算
- 可以帮助我们优化子组件的渲染,比如这种场景:在 App 组件中有两个子组件 Foo 和 Bar,当 App 组件中传给 Foo 组件的 props 发生变化时,App 组件状态会改变,重新渲染。此时 Foo 组件 和 Bar 组件 也都会重新渲染。其实这种情况是比较浪费资源的,现在我们就可以使用 useMemo 进行优化,Foo 组件用到的 props 变化时,只有 Foo 组件进行 render,而 Bar 却不会重新渲染。
Foo.js
import React from 'react';
export default ({ text }) => {
console.log('Foo:', 'render');
return <div>Foo 组件:{ text }</div>
}
Bar.js
import React from 'react';
export default ({ text }) => {
console.log('Bar:', 'render');
return <div>Bar 组件:{ text }</div>
}
App.js
import React, { useState, useMemo } from 'react';
import Foo from './Foo';
import Bar from './Bar';
export default () => {
const [a, setA] = useState('foo');
const [b, setB] = useState('bar');
const foo = useMemo(() => <Foo text={ a } />, [a]);
const bar = useMemo(() => <Bar text={ b } />, [b]);
return (
<div>
{ foo }
{ bar }
<br />
<button onClick={ () => setA('修改后的 Foo') }>修改传给 Foo 的属性</button>
<button onClick={ () => setB('修改后的 Bar') }>修改传给 Bar 的属性</button>
</div>
)
}
此时我们点击不同的按钮,控制台都只会打印一条输出,改变 a 或者 b,A 和 B 组件都只有一个会重新渲染。useCallback(fn, deps) 相当于 useMemo(() => fn, deps)
自定义 Hook
自定义 Hook 更像是一种约定,而不是一种功能。如果函数的名字以 use 开头,并且调用了其他的 Hook,则就称其为一个自定义 Hook。
import React, { useEffect, useState } from 'react';
function useNumber(){
let [number,setNumber] = useState(0);
useEffect(()=>{
setInterval(()=>{
setNumber(number=>number+1);
},1000);
},[]);
return [number,setNumber];
}
HOC
HOC(全称Higher-order component高阶组件)是一种React的进阶使用方法,主要还是为了便于组件的复用。HOC就是一个方法,获取一个组件,返回一个更高级的组件。在React开发过程中,发现有很多情况下,组件需要被"增强",比如说给组件添加或者修改一些特定的props,一些权限的管理,或者一些其他的优化之类的。而如果这个功能是针对多个组件的,同时每一个组件都写一套相同的代码,明显显得不是很明智,所以就可以考虑使用HOC。
- 代码复用,代码模块化
- 增删改props
- 渲染劫持
1.可以通过对传入的props进行修改,或者添加新的props来达到增删改props的效果。比如你想要给wrappedComponent增加一个props,可以这么搞:
function control(wrappedComponent) {
return class Control extends React.Component {
render(){
let props = {
...this.props,
message: "You are under control"
};
return <wrappedComponent {...props} />
}
}
}
这样,你就可以在你的组件中使用message这个props
2.这里的渲染劫持并不是你能控制它渲染的细节,而是控制是否去渲染。由于细节属于组件内部的render方法控制,所以你无法控制渲染细节。比如,组件要在data没有加载完的时候,实现loading...,就可以这么写:
function loading(wrappedComponent) {
return class Loading extends React.Component {
render(){
if(!this.props.data) {
return <div>loading...</div>
}
return <wrappedComponent {...props} />
}
}
}
这个样子,在父级没有传入data的时候,这一块儿就只会显示loading...,不会显示组件的具体内容