前端框架学习小记

最近维护一个前端项目,重新把之前学习的一些前端框架梳理了一下,主要是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 (
          <div>
              <h2>北京时间: { this.state.date.toLocaleTimeString() }</h2>
          </div>
      );
  }
}

组件化

在React里可以创建拥有各自状态的组件,再由这些组件构成更加复杂的 UI。组件逻辑使用 JavaScript 编写而非模版,因此你可以轻松地在应用中传递数据,并使得状态与 DOM 分离。

1.2关键概念

JSX

通过JSX实现js 和html标签的相互嵌套,下面是一个简单的例子

const name = 'Josh Perez';
const element = <h1>Hello, {name}</h1>;
  • 对于js表达式里来说,HTML标签是一种特殊的变量,实现标签/组件 的动态组合、组织

比如我们使用 Javascript 中的 map() 方法来遍历 numbers 数组。将数组中的每个元素变成 <li> 标签,最后我们将得到的数组赋值给 listItems ,这样就很优雅的得到了一组html标签

const numbers = [1, 2, 3, 4, 5];
const listItems = numbers.map((number) =>
  <li>{number}</li>
);

又或者通过三目运算符,来实现不同的case下展示不同的内容。如下,登录时和非登录下展示不同的文案。

const isLoggedIn = this.state.isLoggedIn;
  return (
    <div>
      The user is <b>{isLoggedIn ? 'currently' : 'not'}</b> logged in.
    </div>
  );

综上,利用js来处理逻辑,来精简html的组织结构。

  • 对于HTML标签来说,内部可以通过{}引入变量,实现标签的内容会随着context里的state而变动,依然用上面时间动态变化的例子来说明
class Clock extends Component {
  render() {
      return (
          <div>
              <h2>北京时间: { this.state.date.toLocaleTimeString() }</h2>
          </div>
      );
  }
}

组件化与传参

组件化一是让一些设计元素可以复用,而是能让各个组件的状态实现自治,简化编码之间的耦合度react有如下两种方式创建组件,定义组件最简单的方式就是编写 JavaScript 函数:

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}

该函数是一个有效的 React 组件,因为它接收唯一带有数据的 “props”(代表属性)对象与并返回一个 React 元素。这类组件被称为“函数组件”,因为它本质上就是 JavaScript 函数。你同时还可以使用 ES6 的 class 来定义组件:

class Welcome extends React.Component {
  render() {
    return <h1>Hello, {this.props.name}</h1>;
  }
}

上述两个组件在 React 里是等效的。

组件是如何使用呢?如下例子中定义了函数组件Welcome,来展示对用户欢迎的内容,那组件App就像操作普通的html标签一样使用它即可,其中的【name="Sara"】即作为welcome的属性【props】传入

function Welcome(props) {
  return <h1>Hello, {props.name}</h1>;
}
function App() {
  return (
    <div>
      <Welcome name="Sara" />
      <Welcome name="Cahal" />
      <Welcome name="Edite" />
    </div>
  );
}
ReactDOM.render(
  <App />,
  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 (
      <div>
        <h1>Hello, world!</h1>
        <h2>It is {this.state.date.toLocaleTimeString()}.</h2>
      </div>
    );
  }
}
ReactDOM.render(
  <Clock />,
  document.getElementById('root')
);

React 提供了一些切点来让我们实现自己的一些动作,这个概念比较类似移动端比如 UIViewController 的didLoad之类的方法。比如上述例子中我们将定时器放在挂载的方法中fire,这样就触发了state的定时变化,然后我们只需要在render中声明这一点就行。为了理解我们的props 如何注入,state变化怎么引起页面的重新渲染,需要对React的生命周期有一定认识。React的生命周期从广义上分为三个阶段:挂载、渲染、卸载,如下图所示


屏幕快照 2020-03-20 下午10.44.52.png

结合生命周期快速概括一下发生了什么和这些方法的调用顺序:

  1. <Clock /> 被传给 ReactDOM.render()的时候,React 会调用 Clock 组件的构造函数。因为 Clock 需要显示当前的时间,所以它会用一个包含当前时间的对象来初始化 this.state。我们会在之后更新 state。
  2. 之后 React 会调用组件的 render() 方法。这就是 React 确定该在页面上展示什么的方式。然后 React 更新 DOM 来匹配 Clock 渲染的输出。
  3. Clock 的输出被插入到 DOM 中后,React 就会调用 ComponentDidMount() 生命周期方法。在这个方法中,Clock 组件向浏览器请求设置一个计时器来每秒调用一次组件的 tick() 方法。
  4. 浏览器每秒都会调用一次 tick() 方法。 在这方法之中,Clock 组件会通过调用 setState() 来计划进行一次 UI 更新。得益于 setState() 的调用,React 能够知道 state 已经改变了,然后会重新调用 render() 方法来确定页面上该显示什么。这一次,render() 方法中的 this.state.date 就不一样了,如此以来就会渲染输出更新过的时间。React 也会相应的更新 DOM。
  5. 一旦 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.propsthis.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。

1583859953704-88f9be29-875b-4014-97ae-92f23d5de9b5.png

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。<br />可以理解成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

解决了包裹和收件人的问题,还有个问题,就是需要构建一个物流系统的核心,寄件人知道打哪个客服预约寄件,物流公司负责包裹的保存及配送,包裹到达时提醒收件人取件。<br />Store 就是把它们联系到一起的对象。Store 有以下职责:

store 在应用中只有一个,他的初始化可以直接将reducer传进去。

let store = createStore(todoApp)

dispatch

这个就是action产生时,调用方所发起的动作了。<br />下面是整体的一个调用过程

  1. 调用 store.dispatch(action)。<br />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 回调中、甚至定时器中。<br />2.Redux store 调用传入的 reducer 函数。<br />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 树。<br />根 reducer 的结构完全由你决定。Redux 原生提供combineReducers()辅助函数,来把根 reducer 拆分成多个函数,用于分别处理 state 树的一个分支。<br />app 会负责调用两个 reducer:

let nextTodos = todos(state.todos, action)
 let nextVisibleTodoFilter = visibleTodoFilter(state.visibleTodoFilter, action)
  1. 然后会把两个结果集合并成一个 state 树:
return {
   todos: nextTodos,
   visibleTodoFilter: nextVisibleTodoFilter
 }

4.Redux store 保存了根 reducer 返回的完整 state 树。<br />这个新的树就是应用的下一个 state!所有订阅 store.subscribe(listener) 的监听器都将被调用;监听器里可以调用 store.getState() 获得当前 state。<br />现在可以应用新的 state 来更新 UI,通过调用 component.setState(newState) 来更新。<br />

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变化判定 等过程中的样板代码,这个方法做了性能优化来避免很多不必要的重复渲染。<br />connect 作为连接普通组件到store的桥梁,需要知道当前组件对state树的那些分叉感兴趣,以及又会dispatch那些属性,所以该方法需要mapStateToProps 、mapDispatchToProps这两个方法作为参数。<br />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)
  }
}

<br /><br />除了读取 state,容器组件还能分发 action。类似的方式,可以定义 mapDispatchToProps() 方法接收 dispatch() 方法并返回期望注入到展示组件的 props 中的回调方法。例如,我们希望 VisibleTodoListTodoList 组件中注入一个叫 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 等)的一层轻量封装,没有引入任何新概念。<br />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.jsactions/products.js,然后在这些文件之间来回切换。<br />介绍下这些 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 组件库,主要用于研发企业级中后台产品。
<a name="1d7ae939"></a>

✨ 特性#

  • 🌈 提炼自企业级中后台产品的交互语言和视觉风格。<br />
  • 📦 开箱即用的高质量 React 组件。<br />
  • 🛡 使用 TypeScript 开发,提供完整的类型定义文件。<br />
  • ⚙️ 全链路开发和设计工具体系。<br />
  • 🌍 数十个国际化语言支持。<br />
  • 🎨 深入每个细节的主题定制能力。<br />

ant-design 给我们提供了很多搭建企业应用所需要的组件素材,让我们也能很轻松的创建复杂元素。详细的组件可以从 https://3x.ant.design/docs/react/introduce-cn了解

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容