AndroidBlackCat 2019-11-04
因为没有明确的界定,这里不讨论正确与否,只表达个人对前端MV*架构模式理解看法,再比较React和Vue两种框架不同.
写完之后我知道这文章好水,特别是框架对比部分都是别人说烂的,而我也是打算把这作为长期文章来写,慢慢梳理深入,每次有新的理解就更新文章,我挺期待之后到了超过字数限制不得不写成系列文章的那一天.
2018/04/28 新增声明式渲染 && 生命周期对比 && 状态(State) OR 属性(Data) && Props组件通信
2018/05/02 新增状态管理
2018/06/07 新增mobx状态管理
2018/11/06 补充更新机制和状态管理对比
MVC全名是Model View Controller,把应用程序分成三部分分别是:
(这些简单的东西我就懒得特意画图了,直接百度图片找张清晰的拿来用的..)
(更多内容请自行查阅,本节到此为止了.)
还有一种情况: MVC允许在不改变视图外观的情况下改变视图对用户输入的响应方式,只要用不同种类的controller实例替换即可。例如改变URL触发hashChange事件,用户不经过View直接到达Controller最后再影响回View.
MVP全名是Model-View-Presenter,从经典的模式MVC演变而来,分两种情况:
Presenter占据绝对主导地位,掌控著Model和View,而后两者之间互不联系.
(这些简单的东西我就懒得特意画图了,直接百度图片找张清晰的拿来用的..)
(更多内容请自行查阅,本节到此为止了.)
Presenter依旧占据主导地位,但是会把一部分简单的视图逻辑(如双向绑定)交还给View和Model进行处理,自身负责其他复杂的视图逻辑.
MVC和MVP(Supervising Controller)区别:
1, 视图支持Presenter和View两种途径更新;
1, 模型与视图高度分离,我们可以修改视图而不影响模型;
2, 可以更高效地使用模型,因为所有的交互都发生在一个地方——Presenter内部;
3, 可以将一个Presenter用于多个视图,而不需要改变Presenter的逻辑;
4, 如果把逻辑放在Presenter中,就可以脱离用户接口来测试这些逻辑(单元测试);
1, 由于对视图的渲染放在了Presenter中,所以View和Presenter的交互会过于频繁并且难以维护;
MVVM全名是Model-View-ViewModel,本质上就是MVC的改进版,也可以说是MVP的改良版,把应用程序分成三部分分别是:
MVP和MVVM区别: 它使用 数据绑定(Data Binding)
、依赖属性(Dependency Property)
、命令(Command)
、路由事件(Routed Event)
来搞定与view层的交互, 当ViewModel对Model进行更新的时候,会通过数据绑定更新到View.
(这些简单的东西我就懒得特意画图了,直接百度图片找张清晰的拿来用的..)
(更多内容请自行查阅,本节到此为止了.)
两个框架是现在最热门的选择之一,它们既类似又不同.
React就是MVC里的V,只专注视图层,而Vue算是MVVM框架,双向绑定是特色之一.
我们先看看它们自己的官方介绍:
React is a JavaScript library for building user interfaces.
React是一个用于构建用户界面的Javascript库.
Vue (pronounced /vjuː/, like view) is a progressive framework for building user interfaces. Unlike other monolithic frameworks, Vue is designed from the ground up to be incrementally adoptable. The core library is focused on the view layer only, and is easy to pick up and integrate with other libraries or existing projects. On the other hand, Vue is also perfectly capable of powering sophisticated Single-Page Applications when used in combination with modern tooling and supporting libraries.
Vue.js (读音 /vjuː/,类似于 view) 是一套构建用户界面的渐进式框架。与其他重量级框架不同的是,Vue 采用自底向上增量开发的设计。Vue 的核心库只关注视图层,它不仅易于上手,还便于与第三方库或既有项目整合。另一方面,当与单文件组件和 Vue 生态系统支持的库结合使用时,Vue 也完全能够为复杂的单页应用程序提供驱动。
React 组件实现一个 render() 方法,它接收输入数据并返回显示的内容。此示例使用类似XML的语法,称为 JSX 。输入数据可以通过 this.props 传入组件,被 render() 访问。
class HelloMessage extends React.Component { render() { return ( <div> Hello {this.props.name} </div> ); } } ReactDOM.render( <HelloMessage name="Taylor" />, mountNode );
Vue.js 的核心是一个允许采用简洁的模板语法来声明式的将数据渲染进 DOM 的系统
<div id="app"> {{ message }} </div> // --------省略-------- var app = new Vue({ el: '#app', data: { message: 'Hello Vue!' } })
他是 JavaScrip 的一种扩展语法。 React 官方推荐使用这种语法来描述 UI 信息。JSX 可能会让你想起某种模板语言,但是它具有 JavaScrip 的全部能力,从本质上讲,JSX 只是为 React.createElement(component, props, ...children) 函数提供的语法糖。
JSX 对使用React 不是必须的。
Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。
在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,在应用状态改变时,Vue 能够智能地计算出重新渲染组件的最小代价并应用到 DOM 操作上。
事实上 Vue 也提供了渲染函数,甚至支持 JSX。然而,默认推荐的还是模板。
个人感觉两者其实上手速度都挺快,相比之下JSX除了修改部分属性名字跟普通HTML变化不算大,Templates额外添加很多自定义功能帮助开发者做更多的事,框架痕迹也比较重.
我们可以把组件区分为两类:一类是偏视图表现的 (presentational)推荐使用模板,一类则是偏逻辑的 (logical)推荐使用 JSX 或渲染函数。
//Jsx写法 <div className="list"> <ol> { todos.map(item =><li>{todo.text}</li>) } </ol> </div>
//Templates写法 <div class="list"> <ol> <li v-for="todo in todos"> {{ todo.text }} </li> </ol> </div>
React 中推荐通过 CSS-in-JS 的方案实现的 (比如 styled-components、glamorous 和 emotion),虽然在构建时将 CSS 提取到一个单独的样式表是支持的,但 bundle 里通常还是需要一个运行时程序来让这些样式生效。当你能够利用 JavaScript 灵活处理样式的同时,也需要权衡 bundle 的尺寸和运行时的开销
var styleObj = { color:"blue", fontSize:40, fontWeight:"normal" }; --------省略-------- <h1 style={styleObj} className="alert-text">Hello</h1>
Vue 设置样式的默认方法是单文件组件里类似 style 的标签。让你可以在同一个文件里完全控制 CSS,将其作为组件代码的一部分。
<style scoped> @media (min-width: 250px) { .list-container:hover { background: orange; } } </style>
这个可选 scoped
属性会自动添加一个唯一的属性 (比如 data-v-21e5b78) 为组件内 CSS 指定作用域,编译的时候 .list-container:hover 会被编译成类似 .list-container[data-v-21e5b78]:hover。
最后,Vue 的单文件组件里的样式设置是非常灵活的。通过 vue-loader,你可以使用任意预处理器、后处理器,甚至深度集成 CSS Modules——全部都在 <style> 标签内。
各有好坏吧,React侵入式做法不太喜欢,Vue组件式做法倒也还行,个人而言更倾向独立css样式外部引入易于管理.
State是私有的,并且由组件本身完全控制。
// 错误 this.state.comment = 'Hello'; // 正确 this.setState({comment: 'Hello'});
componentDidMount() { fetchPosts().then(response => { this.setState({ posts: response.posts }); }); fetchComments().then(response => { this.setState({ comments: response.comments }); }); }
// 错误 this.setState({ counter: this.state.counter + this.props.increment, }); // 正确 //另一种 setState() 的形式,它接受一个函数而不是一个对象。这个函数将接收前一个状态作为第一个参数,应用更新时的 props 作为第二个参数 this.setState((prevState, props) => ({ counter: prevState.counter + props.increment }));
当一个 Vue 实例被创建时,它向 Vue 的响应式系统中加入了其 data 对象中能找到的所有的属性。当这些属性的值发生改变时,视图将会产生“响应”,即匹配更新为新的值。
// 有效 data: { visitCount1: 0 } --------省略-------- // 触发任何视图的更新 vm.visitCount1 = 2 // 不会触发任何视图的更新 vm.visitCount2 = 2
React是属于单向控制,即只能是通过改变State从而改变视图,我们可以利用JS方法像表单等场景模拟双向绑定的效果,实际还是由State去触发视图更新
Vue是属于双向绑定,原理是通过 Object.defineProperty 监听劫持data对象的getter/setter属性来实现的
一个组件可以选择将数据向下传递,作为其子组件的 props(属性).
//父组件传递数据 <Child num={this.state.num} /> //--------省略-------- //子组件读取数据 <h2>It is {this.props.num}.</h2>
//父组件传递数据 <Child num={this.state.num} /> //--------省略-------- constructor(props) { super(props); this.state = { //初始化成state num: this.props.num, }; }
//传递修改函数 class Father extends Component { construtor(props) { super(props); this.state = { num: 1, }; } onChangeState(val) { this.setState(val); } render() { <Child num={this.state.num} onClicked={this.onChangeState.bind(this)} />; } } //调用修改函数添加入参 class Child extends Component { render() { <button onClicked={() => this.props.onClicked({num: 2})}> It is {this.props.num}. </button>; } }
//父组件传递数据 <child message="hello!"></child> //--------省略-------- //子组件要显式地用 props 选项声明它预期的数据 Vue.component('child', { // 声明 props props: ['message'], // 就像 data 一样,prop 也可以在模板中使用 // 同样也可以在 vm 实例中通过 this.message 来使用 template: '<span>{{ message }}</span>' })
//父组件传递数据 <child message="hello!"></child> //--------省略-------- //1, 定义一个局部变量,并用 prop 的值初始化它: props: ['message'], data: function () { return { msg: this.message } } template: '<span>{{ msg }}</span>' //2, 定义一个计算属性,处理 prop 的值并返回: props: ['message'], computed: { msg: function () { return this.message } } template: '<span>{{ msg }}</span>'
//父组件传递数据 <child v-bind:message="message" v-on:update:message="message = $event"></child> //--------可用.sync 修饰符替代-------- //后面传递的message是变量,非字符串 //<child :message.sync="message"></child> //--------省略-------- //子组件 props: ['message'], data: function () { return { msg: this.message } } watch: { //监听msg变化自动通信父组件更改 msg(val) { this.$emit('update:message', newMsg) }, },
React生命周期 | 作用 |
---|---|
getDefaultProps | 作用于组件类,只调用一次,返回对象用于设置默认的props,对于引用值,会在实例中共享 |
getInitialState | 作用于组件的实例,在实例创建时调用一次,用于初始化每个实例的state,此时可以访问this.props。 |
componentWillMount | 在完成首次渲染之前调用,此时仍可以修改组件的state |
render | 必选的方法,创建虚拟DOM,该方法具有特殊的规则: 1) 只能通过this.props和this.state访问数据; 2) 可以返回null、false或任何React组件; 3) 只能出现一个顶级组件(不能返回数组); 4) 不能改变组件的状态; 5) 不能修改DOM的输出; |
componentDidMount | 真实的DOM被渲染出来后调用,在该方法中可通过this.getDOMNode()访问到真实的DOM元素。此时已可以使用其他类库来操作这个DOM。在服务端中,该方法不会被调用。 |
componentWillReceiveProps | 组件接收到新的props时调用,并将其作为参数nextProps使用,此时可以更改组件props及state |
shouldComponentUpdate | 组件是否应当渲染新的props或state,返回false表示跳过后续的生命周期方法,通常不需要使用以避免出现bug。在出现应用的瓶颈时,可通过该方法进行适当的优化。在首次渲染期间或者调用了forceUpdate方法后,该方法不会被调用 |
componentWillUpdate | 接收到新的props或者state后,进行渲染之前调用,此时不允许更新props或state。 |
componentDidUpdate | 完成渲染新的props或者state后调用,此时可以访问到新的DOM元素。 |
componentWillUnmount | 组件被移除之前被调用,可以用于做一些清理工作,在componentDidMount方法中添加的所有任务都需要在该方法中撤销,比如创建的定时器或添加的事件监听器 |
componentDidCatch | 16.x新增,捕获全局异常来进行页面的友好提示 |
Vue生命周期 | 作用 |
---|---|
beforeCreate | 在实例初始化之后,数据观测 (data observer) 和 event/watcher 事件配置之前被调用 |
created | 在实例创建完成后被立即调用。在这一步,实例已完成以下的配置:数据观测 (data observer),属性和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$el 属性目前不可见 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用。该钩子在服务器端渲染期间不被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子。如果 root 实例挂载了一个文档内元素,当 mounted 被调用时 vm.$el 也在文档内. 注意 mounted 不会承诺所有的子组件也都一起被挂载。如果你希望等到整个视图都渲染完毕,可以用 vm.$nextTick 替换掉 mounted |
beforeUpdate | 数据更新时调用,发生在虚拟 DOM 重新渲染和打补丁之前。你可以在这个钩子中进一步地更改状态,这不会触发附加的重渲染过程。该钩子在服务器端渲染期间不被调用 |
updated | 由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子。当这个钩子被调用时,组件 DOM 已经更新,所以你现在可以执行依赖于 DOM 的操作。然而在大多数情况下,你应该避免在此期间更改状态。如果要相应状态改变,通常最好使用计算属性或 watcher 取而代之,注意 updated 不会承诺所有的子组件也都一起被重绘。如果你希望等到整个视图都重绘完毕,可以用 vm.$nextTick 替换掉 updated:该钩子在服务器端渲染期间不被调用 |
activated | keep-alive 组件激活时调用。该钩子在服务器端渲染期间不被调用 |
deactivated | keep-alive 组件停用时调用。该钩子在服务器端渲染期间不被调用 |
beforeDestroy | 实例销毁之前调用。在这一步,实例仍然完全可用。该钩子在服务器端渲染期间不被调用。 |
destroyed | Vue 实例销毁后调用。调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁 |
errorCaptured | 当捕获一个来自子孙组件的错误时被调用。此钩子会收到三个参数:错误对象、发生错误的组件实例以及一个包含错误来源信息的字符串。此钩子可以返回 false 以阻止该错误继续向上传播 |
在React里渲染机制是以组件单位更新的,也就是说当数据发生改变,当前视图包括其中的组件子组件和底下的子组件都会一起更新,这种违反性能的机制肯定是有问题的,所以React提供了生命周期shouldComponentUpdate方法让你决定当前组件是否更新,还有一个PureComponent方法会自动检测到state或者props发生变化时,才会调用render方法.但是只是浅比较,如果搭配ImmutableJs持久化数据据说性能大大的提升.除此之外还能节省大量的手动比较的代码和时间,
简单描述过程
比较前后两棵Dom树同层级的节点区别,非同层级节点包括所属子节点整个直接删除重新创建;
在 Vue 应用中,组件的依赖是在渲染过程中自动追踪的,所以系统能精确知晓哪个组件确实需要被重渲染,因为Vue是使用 Object.defineProperty对绑定属性进行数据劫持的,所以比起React组件式更新它能够精确接收到哪些组件才是需要渲染的.
这是React-Router3的模板写法,实际到了React-Router4的API和思想都有些大的差异
import React from 'react' import { render } from 'react-dom' // 首先我们需要导入一些组件... import { Router, Route, Link } from 'react-router' // 然后我们从应用中删除一堆代码和 // 增加一些 <Link> 元素... const App = React.createClass({ render() { return ( <div> <h1>App</h1> {/* 把 <a> 变成 <Link> */} <ul> <li><Link to="/about">About</Link></li> <li><Link to="/inbox">Inbox</Link></li> </ul> {/* 接著用 `this.props.children` 替换 `<Child>` router 会帮我们找到这个 children */} {this.props.children} </div> ) } }) // 最后,我们用一些 <Route> 来渲染 <Router>。 // 这些就是路由提供的我们想要的东西。 React.render(( <Router> <Route path="/" component={App}> <Route path="about" component={About} /> <Route path="inbox" component={Inbox} /> </Route> </Router> ), document.body)
Vue-Router3的模板写法
// 0. 如果使用模块化机制编程,导入Vue和VueRouter,要调用 Vue.use(VueRouter) // 1. 定义(路由)组件。 // 可以从其他文件 import 进来 const Foo = { template: '<div>foo</div>' } const Bar = { template: '<div>bar</div>' } // 2. 定义路由 // 每个路由应该映射一个组件。 其中"component" 可以是 // 通过 Vue.extend() 创建的组件构造器, // 或者,只是一个组件配置对象。 // 我们晚点再讨论嵌套路由。 const routes = [ { path: '/foo', component: Foo }, { path: '/bar', component: Bar } ] // 3. 创建 router 实例,然后传 `routes` 配置 // 你还可以传别的配置参数, 不过先这么简单著吧。 const router = new VueRouter({ routes // (缩写)相当于 routes: routes }) // 4. 创建和挂载根实例。 // 记得要通过 router 配置参数注入路由, // 从而让整个应用都有路由功能 const app = new Vue({ router }).$mount('#app') // 现在,应用已经启动了!
状态管理库有很多种,我只是举出我用过的例子,并不是必须的.下面只会展示最基本的代码,想跑完整流程还得看文档.
Redux 可以用这三个基本原则来描述:
Actions: 把数据从应用传到store的有效载荷。它是store数据的唯一来源.
/* * action 类型 */ export const ADD_TODO = 'ADD_TODO'; /* * action 创建函数 */ export function addTodo(text) { return { type: ADD_TODO, text } }
Reducers: 指定了应用状态的变化如何响应actions并发送到store的,记住actions只是描述了有事情发生了这一事实,并没有描述应用如何更新state。
import { combineReducers } from 'redux' import { ADD_TODO, } from './actions' function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ] default: return state } } const todoApp = combineReducers({ todos }) export default todoApp
Store: 就是把Actions和Reducers联系到一起的对象.
import { createStore } from 'redux' import todoApp from './reducers' let store = createStore(todoApp)
单向数据流
(这些简单的东西我就懒得特意画图了,直接百度图片找张清晰的拿来用的..)
进阶:
另一种实现方式,具体可看Mobx4.X状态管理入门
Vuex 是一个专为 Vue.js 应用程序开发的状态管理模式。它采用集中式存储管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。Vuex 也集成到 Vue 的官方调试工具 devtools extension,提供了诸如零配置的 time-travel 调试、状态快照导入导出等高级调试功能。
Mutation: 必须是同步函数,更改Vuex的store中的状态的唯一方法是提交mutatio
// mutation-types.js export const SOME_MUTATION = 'SOME_MUTATION' // store.js import Vuex from 'vuex' import { SOME_MUTATION } from './mutation-types' const store = new Vuex.Store({ state: { ... }, mutations: { // 我们可以使用 ES2015 风格的计算属性命名功能来使用一个常量作为函数名 [SOME_MUTATION](state) { // mutate state } } })
Action 类似于mutation,不同在于:
actions: { incrementAsync ({ commit }) { setTimeout(() => { commit('increment') }, 1000) } }
Getter: 从store中的state中派生出一些状态
getters: { // ... doneTodosCount: (state, getters) => { return getters.doneTodos.length } }
State: 包含了全部的应用层级状态。至此它便作为一个“唯一数据源 (SSOT)”而存在.
const app = new Vue({ el: '#app', // 把 store 对象提供给 “store” 选项,这可以把 store 的实例注入所有的子组件 store, components: { Counter }, template: ` <div class="app"> <counter></counter> </div> ` })
Module: Vuex 允许我们将 store 分割成模块(module)。每个模块拥有自己的 state、mutation、action、getter、甚至是嵌套子模块——从上至下进行同样方式的分割:
const moduleA = { state: { ... }, mutations: { ... }, actions: { ... }, getters: { ... } } const moduleB = { state: { ... }, mutations: { ... }, actions: { ... } } const store = new Vuex.Store({ modules: { a: moduleA, b: moduleB } }) store.state.a // -> moduleA 的状态 store.state.b // -> moduleB 的状态
单向数据流
(官网来的)
Vuex
Redux指定了应用状态的变化如何响应actions并发送到store的reducer,且必须为纯函数.Vuex实际上是使用mutation更新状态
(更多内容请自行查阅,本节到此为止了.)
React 官方提供了 create-react-app,诟病的地方比较多.
更多人选择自己搭建或者使用民间打包库.
Vue 提供了 Vue-cli 脚手架,能让你非常容易地构建项目,包含了 Webpack,Browserify,甚至 no build system,但是有些设置例如Scss预处理器等自定义配置需要自己搞,总的来说相当实用.
React正常来说需要搭配Jsx和Es6语法和构建环境;
Vue可以直接引入js库就能开发,而且内置的功能属性比React多得多