「面试三板斧」之框架

2020-12-02 16:47:56 浏览数 (1)

背景

框架是前端面试中的常客。

尤其是 ReactVue

React 和 Vue 这两个极其优秀的前端类库,基本上占据了前端开发的半壁江山。

如果把这两个神仙框架放在一起比较一下, 一定会发现一些比较有意思的知识点。

掌握这些知识点, 并灵活运用, 或许可以成为面试中的闪光点。

今天, 我们就从以下六个方面进行比较:

  1. 数据绑定
  2. 组件化和数据流
  3. 数据状态管理
  4. 渲染和更新
  5. 社区
  6. 新版本

正文

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 方法,把 propsdispatch 注入给组件。

造成这些不同的 **本质原因**是 :

  1. Redux 提倡不可变性,而 Vuex 的数据是可变的,Redux 中 reducer 每次都会生成新的 state 以替代旧的 state,而 Vuex 是直接修改;
  2. 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 渲染的过程如下:

  1. new Vue,进行实例化
  2. 挂载 $mount 方法,通过自定义 Render 方法、template、el 等生成 Render 函数,准备渲染内容
  3. 通过 Watcher 进行依赖收集
  4. 当数据发生变化时,Render 函数执行生成 VNode 对象
  5. 通过 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 时是应该进行规避的。

我们只需要对比动态节点, 那如何理解动态结点和静态结点呢?

比如这样的内容:

代码语言:javascript复制
<template>
  <div>
    <p>1</p>
    <p>2</p>
    <p>{{ data.foo }}</p>
    <p>3</p>
    <p>4</p>
  </div>
</template>

对于以上代码,最理想的情况是只需要对比可能会发生变化的 p 标签。

再看这种情况:

代码语言:javascript复制
<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 容器中:

代码语言:javascript复制
const rootNode = document.getElementById('root');
ReactDOM.render(<App />, rootNode);

在 React 16 及之前版本中,React 会对大多数事件进行 document.addEventListener() 操作。

React v17 开始会通过调用 rootNode.addEventListener() 来代替。

更改事件委托结点的原因如下:

从技术上讲,始终可以在应用程序中嵌套不同版本的 React。但是,由于 React 事件系统的工作原理,这很难实现。

在 React 组件中,通常会内联编写事件处理:

代码语言:javascript复制
<button onClick={handleClick}>

与此代码等效的原生 DOM 操作如下:

代码语言:javascript复制
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 可以实现双向绑定 ?

等问题。

研究框架也不一定非要等到基础很扎实的时候。

因为我们在学习框架之时,也是对自己基础查漏补缺的很好时机。

总结

内容大概就这么多,比较轻松,我们重点做了框架的对比以及最新版本的特点。

我们每一个前端开发者, 都应该从框架中汲取养分。

谢谢大家。

如果觉得文章有帮助, 别忘了点赞哦~

0 人点赞