BING
Surmon's digital garden
OG

Vue 和 React 的异同

7,098 characters, 18 min read2018/09/03 PM8,452 views

真正的学习通常发生在实践的过程中,为了了解而去进行的学习往往容易脱离真实环境,不够彻底。

新工作新业务的驱动,导致我需要重新学习 React 和 TypeScript,所以,这是我对 Vue 和 React 异同点的一次总结,我希望不仅仅局限于使用层面。

叙述结构按照组件化进行描述,即:异同点会在同一个话题进行总结。

#数据流

#Vue

Vue 是基于 Object.defineproperty 及观察者模式实现的双向绑定,包括对所有 “类 DOM 原生元素” 的数据流包装,各种双向控件的 v-model 就是一种表现。 前端框架里 Directive 的实现最早来自于 angular.js,但在以 angular.js 为代表的 MVC 时代,Controller 往往才是主角,相当于 Vue 中的组件的 Script 部分。

至于数据流的单双向问题,更多取决于开发者的业务实现,在 Vue2.0 之后,官方提倡优先以单向数据流为设计原则。

由于基于 Object.defineproperty 所以 Vue.js 有条件在核心就支持 computed 等相关数据动态计算功能,而 React 中是不存在的。

#React

React 本身不存在整循环的数据绑定,它更专注于 State > View 的最优更新这件事。

所以,任何事件产生的数据 Input 需通过我们自组织的业务逻辑来完成 State 的更新,继而 Diff 计算后进行 View 更新; 并且,React 依然提供了允许我们手动管理 View 更新与否的能力,而在 Vue 中这一点是被内部自动优化的。

#小结

Vue 靠 Object.defineproperty 对象的拦截知道何时更新视图,React 靠 setState 的代理模式来做到同样的事。 假设丢弃或不使用 Vue 中 v-model 类似的 API,你会发现单事件流来说和 React 其实没什么区别。

Vue 的组件系统中,我们往往假设数据是可变的,所以也往往都是直接改变数据,正因为如此,我们可以得到最细粒度更新的效果。

React 提倡数据不可变,每一次我们应该创建一个新的 State 覆盖之前的,至于最终更新所需的资源靠 Diff 算法来优化。

两种拦截模式背后进行了怎样复杂的计算?

Vue 在不同环境下会降级为不同的异步 API 作为一个数据捕获周期,如 Promise.resolve().then MutationObserver setTimeout

React 根据版本不同,有稍微不同的更新策略,大致是维护一个状态队列,将所有状态的更改推进队列,按照不同的事务类型进行不同周期的批量更新。

So

  • React 中的 state 等价于 Vue 中的 data
  • React 中的 setState 等价于 Vue 中的 直接赋值
  • React 中的 setState.callback 等价于 Vue 中的 $nextTick
  • React 中的 render 等价于 Vue 中的 render/template

更详细可以参考官方文档。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
// Vue 中的双向绑定 export default { name: 'some-component', template: `<input name="count" v-model="count" />`, data() { return { count: 1 } } } // Vue 中实现单向数据流 export default { name: 'some-component', template: `<input name="count" :value="count" @input="handleChange" />`, data() { return { count: 1 } }, methods: { handleChange(event) { this.count = event.target.value } } } // React 中实现单向数据流 class SomeComponent extends React.Component { constructor() { this.state = { count: 1 } } handleChange(event) { this.setState({ count: event.target.value }) } render() { return ( <div> <input value={this.state.count} onChange={this.handleChange} /> </div> ) } }

#事件流

#Vue

按照我的理解,Vue 对于原生 DOM 元素的原生事件都有进行一层包装,简单说就是将所有 dispatchEvent 转化为基于中介者模式的 $emit, 最终导致了事件传播形式的统一,开发者只需要关注框架的事件派发标准就可以。

并且,Vue 中对于各 DOM 元素绑定的事件会被转换为真实 DOM 的事件绑定,顺序为:

TemplateEvent > DomEvent + Decorate > CompoentEvent

各种修饰符,就是在 “包装转化的这一层” 对事件进行了不同程度的:冒泡处理、拦截处理等等...

比如一个例子:

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
<!-- 事件捕获 + 停止传播 --> <div class="box" @click.capture.stop="handleClick"></div> <!-- 转换后的伪代码 --> <script> box.addEventListener( "click", (event) => { event.stopPropagation(); handleClick(event); }, false ); </script>

#React

在 React 中,为了兼顾浏览器兼容性、事件统一机制的高性能,React 内置了一套 SyntheticEvent 用于统一事件管理的系统。

简单说,从头到尾,React 只为最顶级元素绑定了一大套事件,我们为某某元素绑定的事件都相当于在 React 的 SyntheticEvent 里 “注册”; 当事件发生时,我们注册的事件会相应地被触发。

我们为 “类原生 DOM” 组件/元素 绑定的事件并不会真正地转化为真正的 Dom 事件,而是被挂载在 SyntheticEvent 系统的不同周期内进行触发和处理; 这些挂载的事件中得到的 Event 对象是经过 SyntheticEvent 包装后的复合对象,由原生事件产生,由 SyntheticEvent 加工得到的。

如果一定要得到原生事件属性,可通过 nativeEvent 访问得到;SyntheticEvent 支持大部分 Dom 原生事件,但不是全部; 如果需要支持 SyntheticEvent 之外的事件需要在 React 的生命周期中挂载和卸载事件,也就是原生的 window.addEventListener

由于 SyntheticEvent 是共享的。意味着在调用事件回调之后,SyntheticEvent 对象将会被重用,并且所有属性会被置空。这是出于性能因素考虑的。 因此,您无法以异步方式访问事件。

#小结

从使用层面看,这俩也没啥区别,hahaha~

          
  • 1
  • 2
  • 3
  • 4
  • 5
<!-- Vue 中事件捕获 + 停止传播 --> <div class="box" @click.capture.stop="handleClick"></div> <!-- React 中事件捕获 + 停止传播 --> <div class="box" onClickCapture={event => event.stopPropagation() && handleClick()}></div>

#组件通信

两者的通信机制基本类似。

  • “父 > 子”:数据不可变,单向数据流
  • “子 > 父”:事件派发(Vue),回调派发(React)

复杂的跨组件通信,两者都有对应的 MessageBus/Store 的解决方案。

#Vue

Vue 内置了基于中介者模式事件派发机制,使用 $emit$on 作为事件额派发器和接收器。

#React

在组件通信里,React 的事件系统并不能为其进行服务,所以往往是将 父组件的事件处理 以 props 的形式传递给子组件直接调用。

#小结

这两者的区别更多体现在事件传递,感觉 React 回调传递会导致组件间不能彻底解耦,当你为宿主组件编写处理逻辑时还需要了解子组件的 API、内部逻辑。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
<!-- Vue 父子通信 --> <template> <div class="child" @click="$emit('click-children', 'i am child 1 event data')" /> </template> <template> <div class="parent"> <children @click-children="handleChildrenClick"></children> </div> </template> <!-- React 父子通信 --> <script> function Child(props) { return <div className="child" onClick={props.onChildClick}></div>; } class Parent extends React.Component { constructor(props) { super(props); } onChildClick(event) { console.log("onChildClick", event); } render() { return <Child onChildClick={this.onChildClick} />; } } </script>

#Diff 算法

没仔细研究过,研究过也没看懂,所以,不知道。

#模板语法

#Vue

数据 IO 的类型采用不同前缀体现,当然本质上也都是内置的 Directive。 Directive 提供了大部分 View 层所需的展示逻辑功能。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
<!-- 数据传递: --> <some-compnent :attrdata="someData" /> <some-compnent strattr="someStr" /> <!-- 事件绑定: --> <some-compnent @event.decorate="eventHandle" /> <!-- 指令绑定: --> <some-compnent v-some-directive:name="directiveAttr" /> <!-- 渲染逻辑: --> <a-compnent v-if="isTrue" /> <b-compnent v-else /> <list-item-component v-for="item in list" :key="item.id" >{{ item.name }}</list-item-component > <!-- 插值实例: --> <div class="component-a"> <p>这是一个 A 组件</p> <slot></slot> </div> <component-a> <p>我是给 A 组件的内容</p> </component-a> <!-- output --> <div class="component-a"> <p>这是一个 A 组件</p> <p>我是给 A 组件的内容</p> </div>

#React

数据 IO 的类型统一采用花括号 {} 或属性值 "" 体现。 默认并不存在 Directive,View 层所需的展示逻辑功能都需要自行业务编码。

          
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45
  • 46
<!-- 数据传递: --> <Compoent data="{someData}" title="stringTitle" /> <!-- 事件绑定: --> <Compoent onEvent="{eventHandle}" /> <!-- 渲染逻辑:三目运算输出值、三目运算输出不同组件、输出循环列表 --> <script> render() { return ( <div>User {isLoggedIn ? 'username logged in' : 'not login'}</div> { isLoggedIn ? ( <LogoutButton onClick={this.handleLogoutClick} /> ) : ( <LoginButton onClick={this.handleLoginClick} /> ) } <ul> { numbers.map((number) => <li key={number.toString()}>{number}</li> )} </ul> ) } </script> <!-- 插值逻辑: --> <script> function Box(props) { return ( <div className="box"> <div className="box-inner">{props.children}</div> </div> ); } function App() { return ( <Box> <div className="login-box"> <p>登陆弹窗</p> </div> </Box> ); } </script>

#CSS 方案

#Vue

Vue 使用 loader 来抽离和打包 CSS,使用 CSS3 属性选择器实现作用域隔离, 但 CSS3 隔离属性在编译期生成,所以会有一些不可逆的弊端,比如无法根据动态组件结构来实现动态的 deep scope。

如 Angular 就有大概 4-5 种方案来实现作用域隔离。

#React

貌似 React 的 CSS 作用域方案都通过 All in js 实现。

其他待续...


2019-07-29 补充:

就 template 来说:

  • Vue == declarative programming
  • React == imperative programming

设计结果背后的设计目的

  • Vue 需要 DSL,且需要关注 DSL,DSL 是其灵魂,实际上 Vue 的 DSL 很简单,且满足并约束了大部分开发场景,弱化了开发者的自由度,使开发者仅需关注业务(组件)单元的关系。
  • React 没有 DSL,React 只有 JSX,JSX 的本质就是 JS,开发者需要关注「使用 JS 渲染数据」的最优解,同时需要关注额外的数据单元组织,抽象能力对 React 项目的组织有很大影响。

在 Vue 中,绝大部分抽象在 Vue 被表现为 filter、directive、event... 在 React 中,大部分抽象可能都会是函数,而对函数的设计和运用就至关重要了。

完。

Creative Commons BY-NC 4.0 https://surmon.me/article/116
4 / 4 comments
Guest
Join the discussion...
  • laughing66
    Laughing66🇨🇳CNHangzhouMac OSChrome

    怎么能像你一样优秀,大佬

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome

      reply:

      少夸我,多编码

  • Jooger
    Jooger🇹🇼TWTaichungMac OSChrome

    不是 all in js?

    • Surmon
      Surmon🇨🇳CNShanghaiMac OSChrome

      reply:

      我瓦特了我辣鸡我改