01 | 宏观视角:从前端框架发展史聊聊为什么要学Vue 3?

你好,我是大圣。

今天,我们来聊一聊前端框架的发展历史。在熟悉这段历史之后,相信你能把握到 Vue 在前端框架中的地位。这样,你就会对 Vue 有一个更精准的定位,从而能够知道我们为什么要选择 Vue 框架,以及Vue的优势和它的真正价值在哪里。

同时,前端框架的发展历史可能也会让你感触很多。因为每一个上网的人,或多或少都会感觉到前端网页在这些年发生了很多的变化,这是一种切身的、直观的体会。我们都能感觉到网页在设计模式、渲染等等地方的变化,而这种种变化的背后,其实都可以放到前端框架的演变历史中来解释。

石器时代

谈前端框架发展史之前,我们先来简单回顾一下前端的发展历史吧。

  • 1990 年,第一个 Web 浏览器诞生了。这是前端这个技术的起点,代表这一年它出生了。后面的时间里,前端圈有很多里程碑事件。
  • 1994 年,网景公司发布第一个商业浏览器 Navigator。
  • 1995 年,网景工程师 Brendan Eich 用 10 天时间设计了 JavaScript,同年微软发布了 IE 浏览器,进而掀起了浏览器大战。
  • 2002年,IE在浏览器大战中赢得胜利,IE6占有率超过96% 。

而前端的发展历史,又非常直观地显示在你看到的前端网页的演变历史中。整个90年代,受限于网速,网页都是静态页,显示非常单一,前端的工作大部分都只是让美工来切切图和写写HTML+CSS。也因此,在90年代,前端还处在一种萌发期的状态,前端工程师这一工种也没有明确出现。

再后来,后端越来越复杂,开始分层。就像在小公司里,大家啥都干,但公司规模大了之后,就要分部门,职责明确,代码也从揉在一起发展到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的出现迅速风靡全球,一个$走天下,学会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开发模式和传统jQuery的开发模式的区别,这里你先留个印象就好。

前端三大框架

在前端MVVM模式下,不同框架的目标都是一致的,就是利用数据驱动页面,但是怎么处理数据的变化,各个框架走出了不同的路线。

这些框架要回答的核心问题就是,数据发生变化后,我们怎么去通知页面更新。各大框架在这个步骤上,各显神通:

Angular 1就是最老套的脏检查。所谓的脏检查,指的是Angular 1在对数据变化的检查上,遵循每次用户交互时都检查一次数据是否变化,有变化就去更新DOM这一方法。这个方法看似简单粗暴,但算是数据驱动页面早期的实现,所以一经推出,就迅速占领了MVVM市场。

后面Angular团队自断双臂,完全抛弃Angular 1,搞了一个全新的框架还叫Angular,引入了TypeScript、RxJS等新内容,虽然这些设计很优秀,但是不支持向前兼容,抛弃了老用户。这样做也伤了一大批Angular 1用户的心,包括我。这也是Angular这个优秀的框架现在在国内没有大面积推广的原因。

而Vue 1的解决方案,就是使用响应式,初始化的时候,Watcher监听了数据的每个属性,这样数据发生变化的时候,我们就能精确地知道数据的哪个key变了,去针对性修改对应的DOM即可,这一过程可以按如下方式解构:

在上图中,左边是实际的网页内容,我们在网页中使用{{}}渲染一个变量,Vue 1就会在内容里保存一个监听器监控这个变量,我们称之为Watcher,数据有变化,watcher会收到通知去更新网页。

通俗来说,如果把网页数据看成你管理的员工,普通数据就是那种每次你都需要找到他,告诉他要怎么做的人,响应式数据就是他本身有任何变化,都会主动给你发日报告诉你的积极员工。

此外,Facebook的React团队提出了不同于上面的Angular、Vue的的解决方案,他们设计了React框架,在页面初始化的时候,在浏览器DOM之上,搞了一个叫虚拟DOM的东西,也就是用一个JavaScript对象来描述整个DOM树。我们可以很方便的通过虚拟DOM计算出变化的数据,去进行精确的修改。

我们先看React中的一段代码:

<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的理解,我们来看下面这张图:

这个图里两个虚线之间是浏览器的一帧,高性能的动画要求是60fps,也就是1秒要渲染60次,每一帧的时间就是16.6毫秒,在这16.6毫秒里,浏览器自己的渲染更新任务执行后,会有一部分的空闲时间,这段时间我们就用来计算Diff。

等到下一帧任务来了,我们就把控制权还给浏览器,让它继续去更新和渲染,等待空闲时间再继续计算,这样就不会导致卡顿。

Vue 1 的问题在于响应式数据过多,这样会带来内存占用过多的问题。所以 Vue 2 大胆引入虚拟DOM来解决响应式数据过多的问题。

这个解决方案使用虚拟DOM解决了响应式数据过多的内存占用问题,又良好地规避了React中虚拟DOM的问题, 还通过虚拟DOM给 Vue 带来了跨端的能力。看到这个解决方案的时候,我真是一拍大腿,直呼“真牛!”。

响应式数据是主动推送变化,虚拟DOM是被动计算数据的Diff,一个推一个拉,它们看起来是两个方向的技术,但被 Vue 2 很好地融合在一起,采用的方式就是组件级别的划分。

对于 Vue 2 来说,组件之间的变化,可以通过响应式来通知更新。组件内部的数据变化,则通过虚拟DOM去更新页面。这样就把响应式的监听器,控制在了组件级别,而虚拟DOM的量级,也控制在了组件的大小。

这个方案也体现了 Vue 一直以来坚持的中庸的设计思想。

下图左边就是一个个的组件,组件内部是没有Watcher监听器的,而是通过虚拟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层优化,算是另外一个极致。

图片

总结

了解了前端MVVM框架发展的历史和方向后,相信你脑海里已经建立起了一个前端框架发展的地图,每个框架都在探索自己的路线。后面还会涌现出更多优秀的框架,我们到时候只需要把那个框架纳入到这个地图中去理解,这样你很快就明白这个框架做了什么,而这也是很多前端大神能够快速学习一个新框架的主要原因。

浏览器的诞生让我们可以方便地显示文本和图片的内容和样式;JavaScript的出现让网页动了起来;Gmail的发布,宣告前端也可以使用Ajax异步加载技术,来进行复杂网页的开发,前端工程师这个工种也正式出现了。

随着浏览器厂商的混战,各个浏览器都有自己的特色,jQuery框架的出现统一了写法,解决了那个时代最棘手的前端问题:兼容性,极大提高了开发者的效率。

随着Angular 1的诞生,我们多了一套开发模式,就是数据驱动页面。我们甚至不再需要使用jQuery去寻找DOM,而是只关注数据的来源和修改,这也就是现在我们所处的前端时代。我们所熟悉的Vue、React、Angular和Svelte等框架,都是在数据驱动页面这个场景下涌现的框架。

相信到这里,你已经大概明白前端的这些框架的风格和特点,以及 Vue 在这些框架中的地位,Vue 3 在Vue 2 的基础之上做了全面的升级,在性能、扩展性和包的大小上,Vue3都有质的飞跃。

我已经迫不及待地想聊聊 Vue 3 到底有哪些新特性,并且强烈推荐你来学习。

思考题

在你看来,Vue需不需要React的Fiber呢?

欢迎在留言区分享你的思考,畅所欲言。如果你觉得今天的内容有所帮助,也欢迎你分享给你身边的朋友,邀请他一起学习。

精选留言

  • 徐洲更

    2021-10-19 00:14:31

    光这一篇就觉得值回票价了! 之前都不知道怎么入手前端,看完这段历史才知道了学习方向。 迫不及待后面的内容了
    作者回复

    感谢夸奖,坚持学完肯定更有收获 嘿嘿

    2021-10-19 12:42:05

  • 洛一

    2021-10-19 13:14:06

    怎么加群
    作者回复

    你好,加群的链接在课程详情页中有哦,戳链接加入交流群吧

    2021-10-19 16:47:35

  • CondorHero

    2021-10-18 18:01:24

    Vue 不需要 React 的 Fiber,因为更新系统中的 Watcher 控制在组件级。
    作者回复

    你好,回答正确,不过原因并不是Watcher的级别,而是虚拟Dom控制在组件级,最早Vue3的提案其实是包含时间切片方案的,最后废弃的主要原因,是时间切片解决的的问题,Vue3基本碰不到
    1. Vue3把虚拟Dom控制在组件级别,组件之间使用响应式,这就让Vue3的虚拟Dom不会过于庞大
    2. Vue3虚拟Dom的静态标记和自动缓存功能,让静态的节点和属性可以直接绕过Diff逻辑,也大大减少了虚拟Dom的Diff事件
    3. 时间切片也会带来额外的系统复杂性

    所以引入时间切片对于Vue3来说投入产出比不太理想,在后来的讨论中,Vue3的时间切片方案就被废弃了

    希望能帮到你

    2021-10-18 23:03:52

  • 杨村长

    2021-10-19 14:02:41

    我来回答思考题:
    vue的diff被限制在组件级,这样每次变化影响的只是一颗子树,相对范围较小。所以fiber带来的收益似乎也会降低,fiber要改架构还可能破坏目前稳定的表现。因此不需要。
    作者回复

    村长威武

    2021-10-19 17:03:28

  • 天择

    2021-10-18 18:57:57

    请问老师,react的fiber是如何实现的,有浏览器接口吗?怎么知道渲染线程在空闲呢?
    作者回复

    浏览器提供了一个api叫requestIdleCallback, 会在空闲的时候通知你,https://developer.mozilla.org/zh-CN/docs/Web/API/Window/requestIdleCallback
    由于这个api兼容性问题,React自己实现了一个

    2021-10-18 22:58:35

  • xzq

    2021-10-26 23:24:55

    看了之后有两个点我和大圣老师的观点有点不一样:
    1. 引入虚拟dom的核心并不是为了通过diff得出需要更新的节点从而加快速度(有时候甚至会变慢)。而是给各种类型节点提供了一层向上的抽象, 这种抽象扩展了框架能够完成的功能,和简化了一些操作。在大多数情况下,操作最快的永远是直接操作Dom,这个svelte 好像就是这么做的。
    2. Vue2.x 中并不是仅仅只有组件级别的watcher , 每个组件中的响应式数据也有watcher,在对应的deps上。只不过组件内部数据的watcher一般只会通知到组件级别的watcher, 然后由组件级别的watcher通知外部做对应的操作。
    作者回复

    很赞的分享
    1. 其实让diff更快和支持跨端这两个都算是虚拟Dom diff带来的有点,到底哪个才是核心的也没必要区分, 而且虚拟Dom精确的来说并不是更快,而是复杂度上去之后性能不崩盘,svelte项目大了之后其实代码体积会变得比较大
    2。我想表达的就是只有一个渲染Watcher,如果组件内部配置了watch啥的其实也都会有

    2021-12-07 21:10:46

  • William

    2021-10-18 23:41:57

    树形结构的diff很难中断,请问有什么依据吗?
    作者回复

    树形结构的Diff用的是递归,递归本身就不太好中断 ,你可以脑补一下 如果两棵树做对比,再一个节点停下来了,你需要记录的是层级,别的节点的状态等

    2021-10-19 12:43:03

  • Geek_3beb80

    2021-10-21 12:16:46

    由于vue只在组件级别diff,单个组件过于庞大会影响diff效率,过小会增加wacher的数量,改如何取舍呢
    作者回复

    你在设计组件的时候主要考虑功能即可,Vue有点像自动挡,很多优化都帮你做了, 尽量不要拆的太碎,因为虚拟Dom 是做了很多静态优化的,只要按照功能模块拆即可

    2021-10-21 15:12:49

  • 刷子iNG

    2021-10-22 00:25:27

    感觉懂了,但是细想来还是不懂,给自己占坑,希望学完后能解决自己的疑问:
    1.vue1 的 watcher 指的是 Object.defineProperty 吗?数据变化之后在 setter 里面去更新对应的dom?

    2.当数据变更后,是怎么通知react更新虚拟 dom 的呢?不需要 watcher 吗?

    3.为什么vue2引入了虚拟dom就能解决响应式数据过的内存问题呢?vue 中的data不都是响应式的吗?当dom过多 虚拟dom就没有性能问题吗?

    4.什么叫组件的变化通过响应式的更新,内部数据的变化通过虚拟dom去更新,不都是数据驱动视图更新,两者的差异是怎么实现的呢? watcher 是什么?
    作者回复

    1. 可以这个理解,不过setter也有一些优化策略
    2. react不是直接修改数据,而是使用setState这种函数去操作,可以在函数内部去完成通知
    3. 引入虚拟Dom后,一个组件一个Watcher,组件内部虚拟Dom,组件的量级不会大到diff时间超过16。6ms
    5. 组件之间的数据更新,是通过响应式去通知,组件内部没有响应式的wathcer,而是通过虚拟Dom更新

    2021-10-22 16:01:58

  • Mserke

    2021-10-23 15:59:19

    请问 Vue 2 引入虚拟 DOM 给 Vue 带来了跨端的能力,这句话怎么理解呢?
    作者回复

    虚拟Dom带来的能力,就是我们可以使用JSON描述Vue项目,我们可以基于这个JSON,在小程序渲染,在app上渲染,这是Vue1没有的能力

    2021-10-24 21:05:06

  • 褚琛

    2021-10-22 11:47:39

    老师您好,我想问一下虚拟DOM树转换成链表结构,树结构是怎么转换成链表结构的呢?
    作者回复

    从代码角度来说,就是修改的指向,比如之前的节点代码是
    let vnode = {name:"div", children:[vnode1,vnode2]}
    用children来管理子元素,children中的vnode继续嵌套
    现在的代码就是
    let vnode = {name:"div", child:vnode1, slibling:vnode4, parent:vnode5}
    就变成了单个的指向

    2021-10-22 12:45:42

  • 嘿吼

    2021-10-19 17:39:26

    宝!好好休息,千万别累到自己!吃完饭就抓紧去写稿子吧
    作者回复

    知道了大兄弟,垫吧两口就去了

    2021-10-20 11:49:00

  • 🐬🐳🐟🐟🐳🐟🐳🐟🐟

    2021-10-23 10:43:20

    fiber出现 的本意是减小虚拟dom diff的成本,理论上来说,vue 将 dom diff 压缩到了组件级别,应该不需要 fiber,同时 fiber 也会带来一些 性能损耗。 但也可能存在单个组件比较庞大的情况,不过本着组件化的思想,开发人员应该也会对其进行合理拆分,所以我觉得基本fiber 大概率不会在 vue3 中出现。
    作者回复

    正解!

    2021-10-24 21:36:41

  • tequ1lAneio

    2021-10-20 22:07:46

    vue真正需要fiber的情况会非常极端,除非有超大面积的组件在同一时间需要同时进行更新。
    作者回复

    确实是因为这个才废弃了这个提案

    2021-10-20 23:34:15

  • Alias

    2021-10-19 15:53:49

    react和vue的主要区别:
    1 数据更新上:react 采用fiber架构,使用链表表示DOM结构可以在diff时随时中断和继续,利用requestIdleCallback在空闲时diff,防止数据量大diff时间长导致卡顿;vue采用响应式,一个组件对应一个观察者对象,数据变更触发dom diff,将dom diff控制在组件级别;
    2 模板不同:一个jsx 一个template
    作者回复

    第一部分说的不错,第二条的话,其实Vue也有jsx的

    2021-10-20 12:25:23

  • 柒月

    2021-10-21 15:00:12

    vue2转react,还是放不下vue3啊 哈哈哈
    作者回复

    看来还是心头爱呀 哈哈哈

    2021-10-22 16:03:59

  • leite

    2021-10-22 20:54:38

    还是不太明白react的runtime为什么多于vue, vue又为什么居中。希望作者老师能看到
    作者回复

    维度并不是单纯的多或者少,而是框架的侧重程度
    1. React在编译阶段只是把jsx转成了createElement,其他事情都是在运行时做的
    2. Vue 编译阶段做了很多优化,比如静态标记,所以Vue在编译的侧重点上就比React多, 但是runtime也有虚拟Dom, 相比于Svelte这种rumetime非常少, 编译阶段直接解析成dom操作的库,Vue就属于居中的位置了

    2021-10-24 21:03:55

  • 人生如戏

    2021-10-20 18:25:42

    老师,文中说的利用空闲时间做比对,那如果我修改了一个数据,要等到其他任务执行完再比对替换?这样数据更新不及时了吧?
    作者回复

    一帧是16.6ms,你感觉不到的,哪怕是没有Fiber,setState也不是同步的

    2021-10-20 23:58:22

  • BigSpinach

    2021-10-19 16:47:24

    老师,vue1的watcher机制理论上应该比虚拟dom对数据变化的响应更快,是一种空间换时间的形式,可以这么理解么?
    作者回复

    可以这么理解,因为是直接通知到Dom,但是项目大了之后负担业很重,对Vue1感兴趣的话,我建议你去看一个项目叫petite-vue, 是尤雨溪基于Vue3的响应式 做了一个类似Vue1的存在

    2021-10-20 11:50:13

  • William

    2021-10-18 23:38:42

    我认为虚拟dom并没有脱离web的限制,react native和小程序还是基于web技术
    作者回复

    用一个对象去描述页面,其实就是脱离了Web,React native算是首先跨端的尝试, 还有很多有意思的项目,还有把虚拟Dom渲染成命令行应用的
    只不过现在生态语言用的还是Node,以后肯定会越来越多

    2021-10-19 12:44:27