前端框架概述
前端框架发展史
石器时代
在谈前端框架发展史之前,先来简单回顾一下前端的发展历史
- 1990 年 第一个 Web 浏览器诞生了。这是前端这个技术的起点,代表这一年它出生了。后面的时间里,前端圈有很多里程碑事件
- 1994 年网景公司发布第一个商业浏览器 Navigator
- 1995 年网景工程师 Brendan Eich 用 10 天时间设计了 JavaScript,同年微软发布了 IE 浏览器,进而掀起了浏览器大战
- 2002 年 IE 在浏览器大战中赢得胜利,IE6 占有率超过 96%
而前端的发展历史,又非常直观地显示在你看到的前端网页的演变历史中。整个 90 年代,受限于网速,网页都是静态页,显示非常单一,前端的工作大部分都只是让美工来切切图和写写 HTML+CSS
再后来后端越来越复杂,开始分层。公司规模大了之后,就要分部门,职责明确,代码也从揉在一起发展到 Model、View 和 Controller,分别负责不同的功能。这就是后端 MVC 模式的盛行,可以在模板里写上要展现的数据。以前的代码都是所有内容写在一起,现在就会用 Model 负责数据。
后端渲染页面之前,会把数据库的数据显示在前端。这个时候除了写前端代码必备的 HTML、CSS 和简单的 JavaScript 动效,也开始用到了 JSP 和 Smarty:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>smarty test1</title>
</head>
<body>
它的名字叫{$name}
</body>
</html>上述代码写出来的页面,就可以直接显示后端数据库里的数据了,这也就是所谓的动态网页。动态页面使得前端本身的丰富程度大大提升。这一下子迎来了整个互联网开发的繁荣时期,但这种模式下的任何数据更新,都需要刷新整个页面,并且在带宽不足的年代,这样做会耗费不少加载网页的时间。
所以这个时代的网页主要还是以显示数据和简单的特效为主,比如当时众多的门户网站,也都没有太多的用户交互
直到 2004 年 Google 发布了 Gmail,用户可以在不刷新页面的情况下进行复杂的交互,之后 Ajax 逐渐成为网页开发的技术标准,也不断地被应用于各种网站。
Ajax 这个技术可以异步的获取数据并且刷新页面,从此前端不再受限于后端的模板,这也宣告了 Web2.0 时代正式到来。至此,前端工程师也正式作为一个独立工种出现。
铁器时代
在 Gmail 诞生后,虽然依然有浏览器的混战和兼容性问题,比如绑定事件不同的浏览器就要写不同的代码,但大家意识到前端也可以做出复杂应用。而 jQuery 的出现迅速风靡全球,在这之后,前端的具体开发不再被 JavaScript 的兼容性问题所困扰
那个时候 jQuery+Bootstrap 成为了前端开发领域的主流技术,前端代码内嵌在后端的项目中,写完直接发布,通篇都是如下的代码:
$('#alert-btn').on('click',function(){
$('#app .input').val('hi')
})写代码就是找到某个元素,进行 DOM 操作,特别像铁器时代的拼刺刀,随着前端项目规模的逐渐提升,前端也需要规模化的时候。
在 2009 年 AngularJS 和 Node.js 的诞生,也宣告前端工业革命的到来
工业时代
AngularJS 的诞生引领了前端 MVVM 模式的潮流;Node.js 的诞生,让前端有了入侵后端的能力,也加速了前端工程化的诞生。现在前端三大框架 Angular、React、Vue 的发展主线,也就是从这里开始的
所谓 MVVM,就是在前端的场景下,把 Controller 变成了 View-Model 层,作为 Model 和 View 的桥梁,Model 数据层和 View 视图层交给 View-Model 来同步
前端三大框架
在前端 MVVM 模式下,不同框架的目标都是一致的,就是利用数据驱动页面,但是怎么处理数据的变化,各个框架走出了不同的路线
这些框架要回答的核心问题就是,数据发生变化后,我们怎么去通知页面更新。各大框架在这个步骤上,各显神通:
Angular 1 就是最老套的脏检查。所谓的脏检查,指的是 Angular 1 在对数据变化的检查上,遵循每次用户交互时都检查一次数据是否变化,有变化就去更新 DOM 这一方法。这个方法看似简单粗暴,但算是数据驱动页面早期的实现,所以一经推出,就迅速占领了 MVVM 市场
后面 Angular 团队自断双臂,完全抛弃 Angular 1,搞了一个全新的框架还叫 Angular,引入了 TypeScript、RxJS 等新内容,虽然这些设计很优秀,但是不支持向前兼容,抛弃了老用户。这也是 Angular 这个优秀的框架现在在国内没有大面积推广的原因
而 Vue 1 的解决方案就是使用响应式,初始化的时候 Watcher 监听了数据的每个属性,这样数据发生变化的时候,就能精确地知道数据的哪个 key 变了,去针对性修改对应的 DOM 即可,这一过程可以按如下方式解构

在上图中,左边是实际的网页内容,我们在网页中使用 {{}} 渲染一个变量,Vue 1 就会在内容里保存一个监听器监控这个变量,我们称之为 Watcher,数据有变化,watcher 会收到通知去更新网页
通俗来说,如果把网页数据看成你管理的员工,普通数据就是那种每次你都需要找到他,告诉他要怎么做的人,响应式数据就是他本身有任何变化,都会主动给你发日报告诉你的积极员工
此外,Facebook 的 React 团队提出了不同于上面的 Angular、Vue 的的解决方案,他们设计了 React 框架,在页面初始化的时候,在浏览器 DOM 之上,搞了一个叫虚拟 DOM 的东西,也就是用一个 JavaScript 对象来描述整个 DOM 树。我们可以很方便的通过虚拟 DOM 计算出变化的数据,去进行精确的修改
<div id="app">
<p class="item">Item1</p>
<div class="item">Item2</div>
</div>在 React 中,这样一段 HTML 会被映射成一个 JavaScript 的对象进行描述。这个对象就像数据和实际 DOM 的一个缓存层,通过管理这个对象的变化,来减少对实际 DOM 的操作
这种形式不仅让性能有个很好的保障,我们还多了一个用 JSON 来描述网页的工具,并且让虚拟 DOM 这个技术脱离了 Web 的限制。因为积累了这么多优势,虚拟 DOM 在小程序,客户端等跨端领域大放异彩
虚拟 DOM 在运行的时候就是这么一个对象:
{
tag: "div",
attrs: {
id: "app"
},
children: [
{
tag: "p",
attrs: { className: "item" },
children: ["Item1"]
},
{
tag: "div",
attrs: { className: "item" },
children: ["Item2"]
}
]
}这个对象完整地描述了 DOM 的树形结构,这样数据有变化的时候,我们生成一份新的虚拟 DOM 数据,然后再对之前的虚拟 DOM 进行计算,算出需要修改的 DOM,再去页面进行操作
浏览器操作 DOM 一直都是性能杀手,而虚拟 DOM 的 Diff 的逻辑,又能够确保尽可能少的操作 DOM,这也是虚拟 DOM 驱动的框架性能一直比较优秀的原因之一
Vue 与 React 框架的对比
通过上面对前端三大框架的介绍,发现 Vue 和 React 在数据发生变化后,在通知页面更新的方式上有明显的不同。
在 Vue 框架下,如果数据变了,那框架会主动告诉你修改了哪些数据;而 React 的数据变化后,只能通过新老数据的计算 Diff 来得知数据的变化
这两个解决方案都解决了数据变化后,如何通知页面更新的问题,并且迅速地获得了很高的占有率,但是他们都碰到了性能的瓶颈:
- 对于 Vue 来说,它的一个核心就是“响应式”,也就是数据变化后,会主动通知我们。响应式数据新建 Watcher 监听,本身就比较损耗性能,项目大了之后每个数据都有一个 watcher 会影响性能
- 对于 React 的虚拟 DOM 的 Diff 计算逻辑来说,如果虚拟 DOM 树过于庞大,使得计算时间大于 16.6ms,那么就可能会造成性能的卡顿
为了解决这种性能瓶颈, Vue 和 React 走了不同的道路
React 为了突破性能瓶颈,借鉴了操作系统时间分片的概念,引入了 Fiber 架构。通俗来说,就是把整个虚拟 DOM 树微观化,变成链表,然后我们利用浏览器的空闲时间计算 Diff。一旦浏览器有需求,我们可以把没计算完的任务放在一旁,把主进程控制权还给浏览器,等待浏览器下次空闲
这种架构虽然没有减少运算量,但是巧妙地利用空闲实现计算,解决了卡顿的问题

在上图中,左侧是一个树形结构,树形结构的 Diff 很难中断;右侧是把树形结构改造成了链表,遍历严格地按照子元素 -> 兄弟元素 -> 父元素的逻辑,随时可以中断和恢复 Diff 的计算过程
为了方便你对计算 Diff 的理解,我们来看下面这张图

Vue 1 的问题在于响应式数据过多,这样会带来内存占用过多的问题。所以 Vue 2 大胆引入虚拟 DOM 来解决响应式数据过多的问题
这个解决方案使用虚拟 DOM 解决了响应式数据过多的内存占用问题,又良好地规避了 React 中虚拟 DOM 的问题,还通过虚拟 DOM 给 Vue 带来了跨端的能力
响应式数据是主动推送变化,虚拟 DOM 是被动计算数据的 Diff,一个推一个拉,它们看起来是两个方向的技术,但被 Vue 2 很好地融合在一起,采用的方式就是组件级别的划分
对于 Vue 2 来说,组件之间的变化,可以通过响应式来通知更新。组件内部的数据变化,则通过虚拟 DOM 去更新页面。这样就把响应式的监听器,控制在了组件级别,而虚拟 DOM 的量级,也控制在了组件的大小
除了响应式和虚拟 DOM 这个维度,Vue 和 React 还有一些理念和路线的不同,在模板的书写上,也走出了 template 和 JSX 两个路线
- React 的世界里只有 JSX,最终 JSX 都会在 Compiler 那一层,也就是工程化那里编译成 JS 来执行,所以 React 最终拥有了全部 JS 的动态性,这也导致了 React 的 API 一直很少,只有 state、hooks、Component 几个概念,主要都是 JavaScript 本身的语法和特性
- 而 Vue 的世界默认是 template,也就是语法是限定死的,比如 v-if 和 v-for 等语法。有了这些写法的规矩后,我们可以在上线前做很多优化。Vue 3 很优秀的一个点,就是在虚拟 DOM 的静态标记上做到了极致,让静态的部分越过虚拟 DOM 的计算,真正做到了按需更新,很好的提高了性能
- 在模板的书写上,除了 Vue 和 React 走出的 template 和 JSX 两个路线,还出现了 Svelte 这种框架,没有虚拟 DOM 的库,直接把模板编译成原生 DOM,几乎没有 Runtime,所有的逻辑都在 Compiler 层优化,算是另外一个极致
Vue 的 MVVM 模型
在了解了前端框架的发展历程和 Vue 的设计理念后,我们来深入理解 Vue 所采用的 MVVM 设计模式。
MVC 模式
MVVM 模式是在 MVC 模式基础上演变而来的,后端经典的 MVC 模式:

最早的 MVC 设计模式是出现在后端开发中,主要目的就是让视图层与数据层分离,职责更加清晰,方便开发等等,例如:Spring MVC 等等。
随着 Ajax 技术的流行,前后端分离开发越来越流行,前端需要处理复杂的视图与数据,迫使前端也急需一种设计模式来进行分层处理,所以 MVC 设计模式开始进入前端领域。
早期比较经典的前端 MVC 框架就是 backbone.js,但是前后端还是有很大差异的,所以对传统 MVC 做了一些改良:

backbone.js 存在的问题:
- 数据流混乱,尤其是多视图多数据场景下
- 控制层单薄,可有可无
MVVM 模式
于是 2009 年 Angular.js 横空出世,带来了全新的 MVVM 设计模式,让开发者眼前一亮,除了 M 和 V 层以外,就是这个 VM 层啦,即:viewModel 层。
MVVM 设计模式的核心思想就是不让 Model 和 View 这两层直接通信,而是通过 VM 层来连接:

MVVM 设计模式比 MVP 模式的优势:
- ViewModel 能够观察到数据的变化,并对视图对应的内容进行自动更新
- ViewModel 能够监听到视图的变化,并能够通知数据发生变化
虽然最早提出 MVVM 模式的是 Angular.js,但是 Vue 把 MVVM 设计模式发扬光大了,Vue 也成为了当下最主流的前端框架之一:

Vue 官网上的一段话:虽然没有完全遵循 MVVM 模型,但是 Vue 的设计也受到了它的启发。因此在文档中经常会使用 vm (ViewModel 的缩写) 这个变量名表示组件实例。
MVVM 模型中 M 和 V 不能直接操作,需要 VM 层加持。但 Vue 比较灵活,可以直接去操作原生 DOM,也就是直接去操作 V 层。主要是因为 vue 中有 ref 这个 API:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="/vue.global.js"></script>
</head>
<body>
<div id="app">
{{ message }}
<input type="text" v-model="message"> <!-- View -->
</div>
<script>
let app = Vue.createApp({
data() {
return {
message: 'hello world' /* Model */
}
}
})
let vm = app.mount('#app')
</script>
</body>
</html>