框架是前端面试中的常客。
尤其是 React
和 Vue
。
React 和 Vue 这两个极其优秀的前端类库,基本上占据了前端开发的半壁江山。
如果把这两个神仙框架放在一起比较一下, 一定会发现一些比较有意思的知识点。
掌握这些知识点, 并灵活运用, 或许可以成为面试中的闪光点。
今天, 我们就从以下六个方面进行比较:
- 数据绑定
- 组件化和数据流
- 数据状态管理
- 渲染和更新
- 社区
- 新版本
1. 数据绑定
数据绑定, 是两者一个比较大的区别。
Vue 在数据绑定上,采取了双向绑定
策略,依靠 Object.defineProperty。
Vue 3.0 已迁移到 Proxy 以及监听 DOM 事件实现。
这里稍微做一下延伸:
Proxy
& Object.defineProperty
两种方式的区别:
-
Object.defineProperty 不能监听数组的变化,需要进行数组方法的重写。
-
Object.defineProperty 必须遍历对象的每个属性,且对于嵌套结构需要深层遍历。
-
Proxy 的代理是针对整个对象的,而不是对象的某个属性,因此不同于 Object.defineProperty 的必须遍历对象每个属性,Proxy 只需要做一层代理就可以监听同级结构下的所有属性变化,当然对于深层结构,递归还是需要进行的。
-
Proxy 支持代理数组的变化
-
Proxy 的第二个参数除了 set 和 get 以外,可以有 13 种拦截方法,比起 Object.defineProperty() 更加强大,这里不再一一列举。
-
Proxy 性能将会被底层持续优化,而 Object.defineProperty 已经不再是优化重点。
而 React
并没有数据和视图之间的双向绑定,它的策略是局部刷新
。
2. 双向绑定策略
双向绑定, 简单来说数据改变,依赖对数据进行「 拦截 / 代理 」;
视图改变,依赖 DOM 事件(如 onInput、onChange 等)。
Vue 实例中的 data 和 模版展现是一条线,无论谁被修改,另一方也会发生变动。
需要说明的是:
双向绑定
和单向数据流
并没有直接关联。
双向绑定是指「 数据和视图 」之间的绑定关系
。
而单向数据流是指组件之间数据的传递
。
局部刷新策略
局部刷新, 通俗点说就是,当数据发生变化时,直接重新渲染组件,以得到最新的视图。
这种「无脑」刷新的做法看似粗暴,但是换来的简单直观,并且 React 本身在性能上也提供了一定保障。
3. 组件化和数据流
Vue 中组件不像 React 组件,它不是完全以组件功能和 UI 为维度划分的,而 Vue 组件本质是一个 Vue 实例。
每个 Vue 实例在创建时都需要经过:设置数据监听、编译模版、应用模版到 DOM,在更新时根据数据变化更新 DOM 的过程。
在这个过程中,类似 React 也提供了生命周期方法。
Vue 组件间通信或者说组件间数据流如同 React,也是单向的。
数据流向
也很类似:
props 实现父组件向下传递数据,events 实现子组件向上发送消息给父组件.
React 中是基于 props 的回调实现子组件向父组件传递数据(Vue 也支持)。
当然,这两种框架也分别通过 context 和 provider/connect 实现了跨层级通信
,它们的实现也是非常类似的。
4. 数据状态管理
对于较为复杂的数据状态,Redux 是 React 应用最常用
的解决方案。
这里需要说明的是:Redux 和视图无关,它只是提供了数据管理的流程
。
因此, 哪怕 你在 Vue
里使用 Redux
也是完全没有问题的。
当然,Vue 中更常用的是 Vuex
,其借鉴了 Redux,也具有和 Redux
相同的 Store
概念。
组件不允许直接修改
store state,而是需要 dispatch action
来通知 store
的变化。
但是这个过程不同于 Redux 的函数式思想,Vuex 改变 store 的方法支持提交一个 mutation
。
mutation
类似于事件发布订阅系统:
每个 mutation 都有一个字符串来表示事件类型(type)和一个回调函数(handler)以进行对应的修改。
另一个显著区别是:在 Vuex
中,store 是被直接注入
到组件实例
中的,因此用起来更加方便。
而 Redux
需要 connect
方法,把 props
和 dispatch
注入给组件。
造成这些不同的 **本质原因
**是 :
-
Redux 提倡不可变性,而 Vuex 的数据是可变的,Redux 中 reducer 每次都会生成新的 state 以替代旧的 state,而 Vuex 是直接修改;
-
Redux 在检测数据变化的时候,是通过浅比较的方式比较差异的,而 Vuex 其实和 Vue 的原理一样,是通过遍历数据的 getter / setter 来比较。
5. 渲染和更新
就像上面所提到的,React 和 Redux 倡导不可变性,更新需要维持不可变原则;
而 Vue 对数据进行了拦截/代理,因此它不要求不可变性,而允许开发者修改数据,以引起响应式更新。
React 更像 MVC 或者 MVVM 模式中的 view 层,但是搭配 Redux 等,它也是一个完整的 MVVM
类库。
Vue 直接是一个典型 MVVM
模式的体现,虽然它一直标榜自己也只是 View
层,但是毫无疑问它本身包含了对数据的操作。
比如,Vue 文档中经常会使用 VM(ViewModel 简称),这个变量名表示 Vue 实例,其命名让人想到 MVVM,这是 MVVM 模式的体现。
React 所有组件的渲染都依靠灵活而强大的 JSX
。
JSX
并不是一种模版语言,而是 JavaScript 表达式和函数调用的语法糖
。
在编译
之后,JSX 被转化为普通的 JavaScript
对象,用来表示虚拟 DOM
。
Vue templates
是典型的模版
,这相比于 JSX,表达更加自然
。
在底层实现上,Vue 模版被编译成 DOM 渲染函数,结合响应系统
,进行数据依赖的收集
。
Vue 渲染的过程如下:
-
new Vue,进行实例化
-
挂载 $mount 方法,通过自定义 Render 方法、template、el 等生成 Render 函数,准备渲染内容
-
通过 Watcher 进行依赖收集
-
当数据发生变化时,Render 函数执行生成 VNode 对象
-
通过 patch 方法,对比新旧 VNode 对象,通过 DOM Diff 算法,添加、修改、删除真正的 DOM 元素
当然 Vue 也可以支持 JSX。
关于更新性能的问题。
简单来说,在 React 应用中,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。
当然我们可以使用 PureComponent
,或是手动实现 shouldComponentUpdate
方法,来规避不必要的渲染。
在 Vue 应用中,组件的依赖是在渲染过程中自动追踪
的,因此系统能精确知晓
哪个组件需要被重渲染。
从理论上看,Vue 的渲染更新机制更加细粒度
,也更加精确
。
5. 社区
这两个框架都具有非常强大的社区,但是对于社区的理念
,Vue 和 React 稍有不同。
举个例子:路由系统的实现。
Vue 的路由库和状态管理库都是由官方维护的,并且与核心库是同步更新的。
而 React 把这件事情交给了社区,比如 React
应用中,需要引入 react-router
库来实现路由系统。
6. 新版本发布的思考
前不久,Vue 3.0 和 React 17.0 相继发布,都非常有特点。
Vue 3.0
Vue 3.0 推出了 Vite
以及 Hooks
。
除此之外,Vue 新版本还重构了虚拟 DOM, Vue 新版本将虚拟 DOM 的节点分为动态节点
和 静态节点
。
静态节点是指不会发生改变的节点,这些节点在进行 diff 时是应该进行规避的。
我们只需要对比动态节点, 那如何理解动态结点和静态结点呢?
比如这样的内容:
<template>
<div>
<p>1</p>
<p>2</p>
<p>{{ data.foo }}</p>
<p>3</p>
<p>4</p>
</div>
</template>
对于以上代码,最理想的情况是只需要对比可能会发生变化的 p 标签。
再看这种情况:
<template>
<div v-if="xxx">
<p>1</p>
<p>{{ data.foo }}</p>
<p>2</p>
</div>
</template>
最理想的情况是只需要对比 <div v-if="xxx">
以及 {{ data.foo }}
.
因为前者可能会根据判断条件消失 / 出现,后者直接取决于模版变量的值,都属于动态节点
。
这样一来,我们便可以根据模版,将动态节点切割为区块
,在进行 diff 操作时,递归进行
区块中的动态节点
比对即可。
因此,新的 diff 策略更新性能, 不再取决于模版整体节点数量
的多少,而和动态内容的数量正相关
。
Vue Hooks 和 React hooks 相比也非常有趣。
篇幅有限, 这里不再展开。
React v17
React 17 也做了一波更新。
在 React V17 中, React 不会再将事件处理添加到 document 上,而是将事件处理添加到渲染 React 树的根 DOM 容器中:
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);
在 React 16 及之前版本中,React 会对大多数事件进行 document.addEventListener() 操作。
React v17 开始会通过调用 rootNode.addEventListener() 来代替。
更改事件委托结点的原因如下:
从技术上讲,始终可以在应用程序中嵌套不同版本的 React。但是,由于 React 事件系统的工作原理,这很难实现。
在 React 组件中,通常会内联编写事件处理:
<button onClick={handleClick}>
与此代码等效的原生 DOM 操作如下:
myButton.addEventListener('click', handleClick);
但是,对大多数事件来说,React 实际上并不会将它们附加到 DOM 节点上。
相反,React 会直接在 document
节点上为每种事件类型附加一个处理器, 这被称为事件委托
。
除了在大型应用程序上具有性能优势外,它还使添加类似于 replaying events
这样的新特性变得更加容易。
自从其发布以来,React 一直自动进行事件委托。
当 document 上触发 DOM 事件时,React 会找出调用的组件,然后 React 事件会在组件中向上 “冒泡”。
但实际上,原生事件已经冒泡出了 document 级别,React 在其中安装了事件处理器。
但是,这就是逐步升级
的困难所在。
如果页面上有多个 React 版本,他们都将在顶层注册事件处理器。
这会破坏 e.stopPropagation():如果嵌套树结构中阻止了事件冒泡,但外部树依然能接收到它。
这会使不同版本 React 嵌套变得困难重重
。
这也是为什么要改变 React 底层附加事件方式的原因。
从框架再谈基础
从框架上来看,如果基础薄弱,你可能就不会明白:
-
为什么React 事件处理函数还需要手动绑定 this,而 React 生命周期函数中却不需要手动绑定 this ?
-
为什么 Vue 可以实现双向绑定 ?
等问题。
研究框架也不一定非要等到基础很扎实的时候。
因为我们在学习框架之时,也是对自己基础查漏补缺的很好时机。
总结内容大概就这么多,比较轻松,我们重点做了框架的对比以及最新版本的特点。
我们每一个前端开发者, 都应该从框架中汲取养分。
谢谢大家。