最近维护一个前端项目,重新把之前学习的一些前端框架梳理了一下,主要是React、redux、dva相关的概念。这里参考官网文档和别人的总结,记录一下自己的心得理解
1.React框架
1.1React 特性
声明式
React 使创建交互式 UI 变得轻而易举。为应用的每一个状态设计简洁的视图,当数据改变时 React 能有效地更新并正确地渲染组件。什么是声明式呢?声明式主要区分于命令式,常举的一个例子就是把一个数字数组 整体乘2。命令式的思路就是遍历-->取值-->计算-->添加到新数组,即遍历整个数组,取出每个元素,乘以二,然后把翻倍后的值放入新数组,每次都要操作这个双倍数组,直到计算完所有元素。
// 命令式关注如何做(how)
var numbers = [1,2,3,4,5]
var doubled = []
for(var i = 0; i < numbers.length; i++) {
var newNumber = numbers[i] * 2
doubled.push(newNumber)
}
console.log(doubled) //=> [2,4,6,8,10]
而声明式则通过隐藏遍历的细节,让方法的意图更加明显。使用map函数来完成数组映射,map 函数所作的事情是将直接遍历整个数组的过程归纳抽离出来,让我们专注于描述我们想要的是什么(what)。
// 声明式关注做什么(what)
var numbers = [1,2,3,4,5]
var doubled = numbers.map(function(n) {
return n * 2
})
console.log(doubled) //=> [2,4,6,8,10]
之前动态页面最流行的方案是使用模板引擎,后端把变量传进来,模板里使用少量支持的语法来完成复杂的页面交互。而React里的页面渲染方式则变得很简单。下面是一个react组件的例子,我们想显示一个动态变化的时间,不用再像以前一样,在全局里写一个js定时器,每秒的回调方法写:找到h2的标签,更新他的html content。而是直接声明自己要展现的诉求--“一个变化的时间”,而怎么样的手段去更新,则是框架帮我们做好了,我们只需要在组件内部维护它的state变量即可。
class Clock extends Component {
render() {
return (
北京时间: { this.state.date.toLocaleTimeString() }
);
}
}
组件化
在React里可以创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。
1.2关键概念
JSX
通过JSX实现js 和html标签的相互嵌套,下面是一个简单的例子
const name = 'Josh Perez';
const element = Hello, {name}
;
- 对于js表达式里来说,HTML标签是一种特殊的变量,实现标签/组件 的动态组合、组织
比如我们使用 Javascript 中的 map()
方法来遍历 numbers
数组。将数组中的每个元素变成 标签,最后我们将得到的数组赋值给
listItems
,这样就很优雅的得到了一组html标签
const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
{number}
);
又或者通过三目运算符,来实现不同的case下展示不同的内容。如下,登录时和非登录下展示不同的文案。
const isLoggedIn = this.state.isLoggedIn;
return (
The user is {isLoggedIn ? 'currently' : 'not'} logged in.
);
综上,利用js来处理逻辑,来精简html的组织结构。
- 对于HTML标签来说,内部可以通过{}引入变量,实现标签的内容会随着context里的state而变动,依然用上面时间动态变化的例子来说明
class Clock extends Component {
render() {
return (
北京时间: { this.state.date.toLocaleTimeString() }
);
}
}
组件化与传参
组件化一是让一些设计元素可以复用,而是能让各个组件的状态实现自治,简化编码之间的耦合度react有如下两种方式创建组件,定义组件最简单的方式就是编写 JavaScript 函数:
function Welcome(props) {
return Hello, {props.name}
;
}
该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。你同时还可以使用 ES6 的 class 来定义组件:
class Welcome extends React.Component {
render() {
return Hello, {this.props.name}
;
}
}
上述两个组件在 React 里是等效的。
组件是如何使用呢?如下例子中定义了函数组件Welcome,来展示对用户欢迎的内容,那组件App就像操作普通的html标签一样使用它即可,其中的【name="Sara"】即作为welcome的属性【props】传入
function Welcome(props) {
return Hello, {props.name}
;
}
function App() {
return (
);
}
ReactDOM.render(
,
document.getElementById('root')
);
生命周期
如何编写一个有状态的组件呢?
- 创建一个继承于 React.Component的class。
- 添加一个空的 render() 方法。
- 在 render() 方法之中使用JSX来写要展示出来的html元素,并return出来。其中不考虑其中会变动的元素,权当做静态页面来处理
- 对上述render方法进行修改,将动态变量【扣出来】,用变量进行替代。同时实现组件的onClick之类的交互响应方法,在响应中触发变量进行变化
- 理清变量是来源于props 和state,重新对上一步中的变量进行替换
下面是上面动态时间的完整例子
class Clock extends React.Component {
constructor(props) {
super(props);
this.state = {date: new Date()};
}
componentDidMount() {
this.timerID = setInterval(
() => this.tick(),
1000
);
}
componentWillUnmount() {
clearInterval(this.timerID);
}
tick() {
this.setState({
date: new Date()
});
}
render() {
return (
Hello, world!
It is {this.state.date.toLocaleTimeString()}.
);
}
}
ReactDOM.render(
,
document.getElementById('root')
);
React 提供了一些切点来让我们实现自己的一些动作,这个概念比较类似移动端比如 UIViewController 的didLoad之类的方法。比如上述例子中我们将定时器放在挂载的方法中fire,这样就触发了state的定时变化,然后我们只需要在render中声明这一点就行。为了理解我们的props 如何注入,state变化怎么引起页面的重新渲染,需要对React的生命周期有一定认识。React的生命周期从广义上分为三个阶段:挂载、渲染、卸载,如下图所示
结合生命周期快速概括一下发生了什么和这些方法的调用顺序:
- 当
被传给ReactDOM.render()
的时候,React 会调用Clock
组件的构造函数。因为Clock
需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化this.state
。我们会在之后更新 state。 - 之后 React 会调用组件的
render()
方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配Clock
渲染的输出。 - 当
Clock
的输出被插入到 DOM 中后,React 就会调用ComponentDidMount()
生命周期方法。在这个方法中,Clock
组件向浏览器请求设置一个计时器来每秒调用一次组件的tick()
方法。 - 浏览器每秒都会调用一次
tick()
方法。 在这方法之中,Clock
组件会通过调用setState()
来计划进行一次 UI 更新。得益于setState()
的调用,React 能够知道 state 已经改变了,然后会重新调用render()
方法来确定页面上该显示什么。这一次,render()
方法中的this.state.date
就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。 - 一旦
Clock
组件从 DOM 中被移除,React 就会调用componentWillUnmount()
生命周期方法,这样计时器就停止了。
state 更新的几个注意事项
- 不要直接修改 State
- State 的更新可能是异步的
- State 的更新会被合并
- state数据通过props向下流动的
解释如下:只能通过setState实现对state的更新
// Wrong
this.state.comment = 'Hello';
而是应该使用 setState()
:
// Correct
this.setState({comment: 'Hello'});
构造函数是唯一可以给 this.state
赋值的地方:出于性能考虑,React 可能会把多个 setState()
调用合并成一个调用。因为 this.props
和 this.state
可能会异步更新,所以你不要依赖他们的值来更新下一个状态。state可以作为子组件的props进而往下传递,一个组件的状态只能被父组件影响,而不会被子组件影响。
整体流程
初始化的渲染流程分为 3 步。因为react的JSX语法需要被编译成浏览器能解释的html、js文件,所以在整体的流程中还要加一个编译的步骤。第一步,开发者使用 JSX 语法写 React,babel
会将 JSX 编译为浏览器能识别的 React JS 语法。这一步,一般配合 webpack
在本地进行。第二步,执行 ReactDOM.render
函数,渲染出虚拟DOM。第三步,react 将虚拟DOM,渲染成真实的DOM。页面更新的流程同样也是 3 步。第一步,当页面需要更新时,通过声明式的方法,调用 setState
告诉 react。第二步,react 自动调用组件的 render 方法,渲染出虚拟 DOM。第三步,react 会通过 diffing
算法,对比当前虚拟 DOM 和需要更新的虚拟 DOM 有什么区别。然后重新渲染区别部分的真实 DOM。
2.Redux框架
React框架解决了展示层的耦合问题,但是依然遗留了若干问题:
- model层的耦合问题,比如子组件的state跟父组件甚至更远的祖先组件有关系,需要一层层以props的形式传递,代码上难以维护
- state修改缺乏入口,导致状态变化难以跟踪,复杂状态下bug难以定位
redux 针对这些问题提出了解决方案
2.1核心概念
action
Action 是把数据从应用传到 store 的有效载荷。它是 store 数据的唯一来源。一般来说你会通过 store.dispatch()
将 action 传到 store。添加新 todo 任务的 action 是这样的:
const ADD_TODO = 'ADD_TODO'
{
type: ADD_TODO,
text: 'Build my first Redux app'
}
Action 本质上是 JavaScript 普通对象。我们约定,action 内必须使用一个字符串类型的 type
字段来表示将要执行的动作。多数情况下,type
会被定义成字符串常量。当应用规模越来越大时,建议使用单独的模块或文件来存放 action。
可以理解成action 是物流系统的包裹,我们通过type这个字段标识了,这个包裹发往何处,需要谁来处理。
reducer
reducer 就是一个纯函数,接收旧的 state 和 action,返回新的 state。
(previousState, action) => newState
之所以将这样的函数称之为reducer,是因为这种函数与被传入 Array.prototype.reduce(reducer, ?initialValue)
里的回调函数属于相同的类型。保持 reducer 纯净非常重要。永远不要在 reducer 里做这些操作:
- 修改传入参数;
- 执行有副作用的操作,如 API 请求和路由跳转;
- 调用非纯函数,如
Date.now()
或Math.random()
。
下面是一个reducer的例子,reducer根据参数里action 的type进行处理,如果是这个reducer负责的action,则会对state进行处理,否则直接返回state
function todoApp(state = initialState, action) {
switch (action.type) {
case SET_VISIBILITY_FILTER:
return Object.assign({}, state, {
visibilityFilter: action.filter
})
default:
return state
}
}
store
解决了包裹和收件人的问题,还有个问题,就是需要构建一个物流系统的核心,寄件人知道打哪个客服预约寄件,物流公司负责包裹的保存及配送,包裹到达时提醒收件人取件。
Store 就是把它们联系到一起的对象。Store 有以下职责:
- 维持应用的 state;
- 提供
getState()
方法获取 state; - 提供
dispatch(action)
方法更新 state; - 通过
subscribe(listener)
注册监听器; - 通过
subscribe(listener)
返回的函数注销监听器。
store 在应用中只有一个,他的初始化可以直接将reducer传进去。
let store = createStore(todoApp)
dispatch
这个就是action产生时,调用方所发起的动作了。
下面是整体的一个调用过程
- 调用
store.dispatch(action)
。
Action 就是一个描述“发生了什么”的普通对象。比如:
{ type: 'LIKE_ARTICLE', articleId: 42 }
{ type: 'FETCH_USER_SUCCESS', response: { id: 3, name: 'Mary' } }
{ type: 'ADD_TODO', text: 'Read the Redux docs.' }
你可以在任何地方调用 store.dispatch(action)
,包括组件中、XHR 回调中、甚至定时器中。
2.Redux store 调用传入的 reducer 函数。
Store 会把两个参数传入 reducer: 当前的 state 树和 action。例如,在这个 todo 应用中,根 reducer 可能接收这样的数据:
// 当前应用的 state(todos 列表和选中的过滤器)
let previousState = {
visibleTodoFilter: 'SHOW_ALL',
todos: [
{
text: 'Read the docs.',
complete: false
}
]
}
// 将要执行的 action(添加一个 todo)
let action = {
type: 'ADD_TODO',
text: 'Understand the flow.'
}
// reducer 返回处理后的应用状态
let nextState = todoApp(previousState, action)
3.根 reducer 应该把多个子 reducer 输出合并成一个单一的 state 树。
根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()
辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。app
会负责调用两个 reducer:
let nextTodos = todos(state.todos, action)
let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
- 然后会把两个结果集合并成一个 state 树:
return {
todos: nextTodos,
visibleTodoFilter: nextVisibleTodoFilter
}
4.Redux store 保存了根 reducer 返回的完整 state 树。
这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener)
的监听器都将被调用;监听器里可以调用 store.getState()
获得当前 state。
现在可以应用新的 state 来更新 UI,通过调用 component.setState(newState)
来更新。
2.2使用
再回到开头说明的React使用中的几个问题:
- redux使用dispatch 来分发消息,使用统一的入口,解决了state直接赋值引起的状态不可控问题;
- redux统一维护state,将model集中到一起管理,各个组件只需要编写对应的reducer,并订阅state的变化即可,解决了state跨组件通信的问题。
那现在我们的编码工作就主要集中在,
- 编写声明式的view层的展示组件
- 创建reducer 处理action,在有变化的时候调用dispatch
- 订阅state变化,当统一state中当前组件关注的state分支发生变动时,转换成当前组件的state、props,触发重新渲染。
理论上是这样没错,但 React Redux 库提供了 connect()
方法,减少了订阅state、state变化判定 等过程中的样板代码,这个方法做了性能优化来避免很多不必要的重复渲染。
connect 作为连接普通组件到store的桥梁,需要知道当前组件对state树的那些分叉感兴趣,以及又会dispatch那些属性,所以该方法需要mapStateToProps 、mapDispatchToProps
这两个方法作为参数。mapStateToProps
这个函数来指定如何把当前 Redux store state 映射到展示组件的 props 中。下面是官方文档的一个例子:
//state的结构如下:
{
visibilityFilter: 'SHOW_ALL',
todos: [
{
text: 'Consider using Redux',
completed: true,
},
{
text: 'Keep all state in a single tree',
completed: false
}
]
}
//组件对过滤之后的todos感兴趣,需要对state处理一下
const getVisibleTodos = (todos, filter) => {
switch (filter) {
case 'SHOW_COMPLETED':
return todos.filter(t => t.completed)
case 'SHOW_ACTIVE':
return todos.filter(t => !t.completed)
case 'SHOW_ALL':
default:
return todos
}
}
const mapStateToProps = state => {
return {
todos: getVisibleTodos(state.todos, state.visibilityFilter)
}
}
除了读取 state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps()
方法接收 dispatch()
方法并返回期望注入到展示组件的 props 中的回调方法。例如,我们希望 VisibleTodoList
向 TodoList
组件中注入一个叫 onTodoClick
的 props ,还希望 onTodoClick
能分发 TOGGLE_TODO
这个 action:
const mapDispatchToProps = dispatch => {
return {
onTodoClick: id => {
dispatch(toggleTodo(id))
}
}
}
最后,使用 connect()
创建 VisibleTodoList
,并传入这两个函数。
import { connect } from 'react-redux'
const VisibleTodoList = connect(
mapStateToProps,
mapDispatchToProps
)(TodoList)
export default VisibleTodoList
3.dva框架
dva 是基于现有应用架构 (redux + react-router + redux-saga 等)的一层轻量封装,没有引入任何新概念。
dva 是 framework,不是 library,类似 emberjs,会很明确地告诉你每个部件应该怎么写,这对于团队而言,会更可控。另外,除了 react 和 react-dom 是 peerDependencies 以外,dva 封装了所有其他依赖。
他最核心的是提供了 app.model
方法,用于把 reducer, initialState, action, saga 封装到一起,比如:
app.model({
namespace: 'products',
state: {
list: [],
loading: false,
},
subscriptions: [
function(dispatch) {
dispatch({type: 'products/query'});
},
],
effects: {
['products/query']: function*() {
yield call(delay(800));
yield put({
type: 'products/query/success',
payload: ['ant-tool', 'roof'],
});
},
},
reducers: {
['products/query'](state) {
return { ...state, loading: true, };
},
['products/query/success'](state, { payload }) {
return { ...state, loading: false, list: payload };
},
},
});
在有 dva 之前,我们通常会创建 sagas/products.js
, reducers/products.js
和 actions/products.js
,然后在这些文件之间来回切换。
介绍下这些 model 的 key :
- namespace - 对应 reducer 在 combine 到 rootReducer 时的 key 值
- state - 对应 reducer 的 initialState
- effects - 对应 saga,并简化了使用
- reducers
定义完model之后,使用connect将model 和组件串起来,这里跟redux有点不同,只需要传入state-->props对应的namespace,就可以了,如下所示。dva 在redux基础上将state从同一管理的state再次剥离开来,让单个model来维护,同时使用namspace,作为相互调用的路由。
// export default Products;
export default connect(({ products }) => ({
products,
}))(Products);
4.ant-design介绍
antd
是基于 Ant Design 设计体系的 React UI 组件库,主要用于研发企业级中后台产品。
✨ 特性#
- 提炼自企业级中后台产品的交互语言和视觉风格。
- 开箱即用的高质量 React 组件。
- 使用 TypeScript 开发,提供完整的类型定义文件。
- ⚙️ 全链路开发和设计工具体系。
- 数十个国际化语言支持。
- 深入每个细节的主题定制能力。
ant-design 给我们提供了很多搭建企业应用所需要的组件素材,让我们也能很轻松的创建复杂元素。详细的组件可以从 https://3x.ant.design/docs/react/introduce-cn了解