前端框架发展史

石器时代

我们先来简单回顾一下前端的发展历史

  • 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,我们会写出如下这种代码:

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <meta charset="utf-8">
  5. <title>smarty test1</title>
  6. </head>
  7. <body>
  8. 它的名字叫{$name}
  9. </body>
  10. </html>


上述代码写出来的页面,就可以直接显示后端数据库里的数据了,这也就是所谓的动态网页。动态页面使得前端本身的丰富程度大大提升。这一下子迎来了整个互联网开发的繁荣时期,但这种模式下的任何数据更新,都需要刷新整个页面,并且在带宽不足的年代,这样做会耗费不少加载网页的时间。
所以这个时代的网页主要还是以显示数据和简单的特效为主,比如当时众多的门户网站,也都没有太多的用户交互,主要就是显示后端存储的新闻。
直到 2004 年,Google 发布了 Gmail,用户可以在不刷新页面的情况下进行复杂的交互,之后,Ajax 逐渐成为网页开发的技术标准,也不断地被应用于各种网站。Ajax 这个技术让我们可以异步的获取数据并且刷新页面,从此前端不再受限于后端的模板,这也宣告了 Web2.0 时代正式到来。至此,前端工程师也正式作为一个独立工种出现

铁器时代

在 Gmail 诞生后,虽然依然有浏览器的混战和兼容性问题,比如绑定事件不同的浏览器就要写不同的代码,但大家意识到前端也可以做出复杂应用。而 jQuery 的出现迅速风靡全球,一个 $ 走天下,学会 jQuery 就等同于学会了前端,算是前端车同轴的时代。在这之后,前端的具体开发不再被 JavaScript 的兼容性问题所困扰。
那个时候 jQuery+Bootstrap 一把梭,成为了前端开发领域的主流技术,前端代码内嵌在后端的项目中,写完直接发布,通篇都是如下的代码:

  1. $('#alert-btn').on('click',function(){
  2. $('#app .input').val('hi')
  3. })

那个时候写代码,就是找到某个元素,进行 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 模式下,不同框架的目标都是一致的,就是利用数据驱动页面,但是怎么处理数据的变化,各个框架走出了不同的路线。
这些框架要回答的核心问题就是,数据发生变化后,我们怎么去通知页面更新。各大框架在这个步骤上,各显神通:
Angular1 就是最老套的脏检查。所谓的脏检查,指的是 Angular1 在对数据变化的检查上,遵循每次用户交互时都检查一次数据是否变化,有变化就去更新 DOM 这一方法。这个方法看似简单粗暴,但算是数据驱动页面早期的实现,所以一经推出,就迅速占领了 MVVM 市场。
后面 Angular 团队自断双臂,完全抛弃 Angular1,搞了一个全新的框架还叫 Angular,引入了 TypeScript、RxJS 等新内容,虽然这些设计很优秀,但是不支持向前兼容,抛弃了老用户。这样做也伤了一大批 Angular1 用户的心,包括我。这也是 Angular 这个优秀的框架现在在国内没有大面积推广的原因。
而 Vue 1 的解决方案,就是使用响应式,初始化的时候,Watcher 监听了数据的每个属性,这样数据发生变化的时候,我们就能精确地知道数据的哪个 key 变了,去针对性修改对应的 DOM 即可,这一过程可以按如下方式解构:
在上图中,左边是实际的网页内容,我们在网页中使用{{}}渲染一个变量,Vue1 就会在内容里保存一个监听器监控这个变量,我们称之为 Watcher,数据有变化,watcher 会收到通知去更新网页。
通俗来说,如果把网页数据看成你管理的员工,普通数据就是那种每次你都需要找到他,告诉他要怎么做的人,响应式数据就是他本身有任何变化,都会主动给你发日报告诉你的积极员工。
此外,Facebook 的 React 团队提出了不同于上面的 Angular、Vue 的的解决方案,他们设计了 React 框架,在页面初始化的时候,在浏览器 DOM 之上,搞了一个叫虚拟 DOM 的东西,也就是用一个 JavaScript 对象来描述整个 DOM 树。我们可以很方便的通过虚拟 DOM 计算出变化的数据,去进行精确的修改。
我们先看 React 中的一段代码:

  1. <div id = "app">
  2. <p class = "item">Item1</p>
  3. <div class = "item">Item2</div>
  4. </div>

在 React 中,这样一段 HTML 会被映射成一个 JavaScript 的对象进行描述。这个对象就像数据和实际 DOM 的一个缓存层,通过管理这个对象的变化,来减少对实际 DOM 的操作。
这种形式不仅让性能有个很好的保障,我们还多了一个用 JSON 来描述网页的工具,并且让虚拟 DOM 这个技术脱离了 Web 的限制。因为积累了这么多优势,虚拟 Dom 在小程序,客户端等跨端领域大放异彩。image.png

虚拟 Dom 在运行的时候就是这么一个对象:

  1. {
  2. tag: "div",
  3. attrs: {
  4. id: "app"
  5. },
  6. children: [
  7. {
  8. tag: "p",
  9. attrs: { className: "item" },
  10. children: ["Item1"]
  11. },
  12. {
  13. tag: "div",
  14. attrs: { className: "item" },
  15. children: ["Item2"]
  16. }
  17. ]
  18. }

这个对象完整地描述了 DOM 的树形结构,这样数据有变化的时候,我们生成一份新的虚拟 DOM 数据,然后再对之前的虚拟 DOM 进行计算,算出需要修改的 DOM,再去页面进行操作。
由于浏览器操作 DOM 一直都是性能杀手,虚拟 DOM 的计算数据 Diff 的逻辑,能够确保尽可能少的操作 DOM,这也是虚拟 DOM 驱动的框架性能一直比较优秀的原因之一。
image.png

Vue 与 React 框架的对比

通过上面对前端三大框架的介绍,我们不难发现 Vue 和 Reat 在数据发生变化后,在通知页面更新的方式上有明显的不同,通俗的来说,就是:在 Vue 框架下,如果数据变了,那框架会主动告诉你修改了哪些数据;而 React 的数据变化后,我们只能通过新老数据的计算 Diff 来得知数据的变化
这两个解决方案都解决了数据变化后,如何通知页面更新的问题,并且迅速地获得了很高的占有率,但是他们都碰到了性能的瓶颈:
对于 Vue 来说,它的一个核心就是“响应式”,也就是数据后变化后,会主动通知我们。响应式数据新建 Watcher 监听,本身就比较损耗性能,项目大了之后每个数据都有一个 watcher 会影响性能。
对于 React 的虚拟 DOM 的 Diff 计算逻辑来说,如果虚拟 DOM 树过于庞大,使得计算时间大于 16.6ms 的时候,就可能会造成性能的卡顿。
为了解决这种性能瓶颈, Vue 和 React 走了不同的道路。
React 为了突破性能瓶颈,借鉴了操作系统时间分片的概念,引入了 Fiber 架构。通俗来说,就是把整个虚拟 DOM 树微观化,变成链表,然后我们利用浏览器的空闲时间计算 Diff。一旦浏览器有需求,我们可以把没计算完的任务放在一旁,把主进程控制权还给浏览器,等待浏览器下次空闲。
这种架构虽然没有减少运算量,但是巧妙地利用空闲实现计算,解决了卡顿的问题。你可以看一下我画的图解:
image.png
在上图中,左侧是一个树形结构,由于树形结构的 Diff 很难中断,右侧是把树形结构改造成了链表,遍历严格地按照子元素 -> 兄弟元素 -> 父元素的逻辑,随时可以中断和恢复 Diff 的计算过程。
为了方便你对计算 Diff 的理解,我们来看下面这张图:
image.png
这个图里两个虚线之间是浏览器的一帧,高性能的动画要求是 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 来更新,每个组件对应一个监听器,大大减小了监听器的数量。
image.png
除了响应式和虚拟 DOM 这个维度,Vue 和 React 还有一些理念和路线的不同,在模板的书写上,也走出了 template 和 JSX 两个路线。
image.png
React 的世界里只有 JSX,最终 JSX 都会在 Compiler 那一层,也就是工程化那里编译成 JS 来执行,所以 React 最终拥有了全部 JS 的动态性,这也导致了 React 的 API 一直很少,只有 state、hooks、Component 几个概念,主要都是 JavaScript 本身的语法和特性。
而 Vue 的世界默认是 template,也就是语法是限定死的,比如 v-if 和 v-for 等语法。有了这些写法的规矩后,我们可以在上线前做很多优化。Vue 3 很优秀的一个点,就是在虚拟 DOM 的静态标记上做到了极致,让静态的部分越过虚拟 DOM 的计算,真正做到了按需更新,很好的提高了性能。
image.png
在模板的书写上,除了 Vue 和 React 走出的 template 和 JSX 两个路线,还出现了 Svelte 这种框架,没有虚拟 DOM 的库,直接把模板编译成原生 DOM,几乎没有 Runtime,所有的逻辑都在 Compiler 层优化,算是另外一个极致。
image.png

总结

了解了前端 MVVM 框架发展的历史和方向后,相信你脑海里已经建立起一个前端框架发展的地图,每个框架都在探索自己的路线。后面还会涌现出更多优秀的框架,我们到时候只需要把那个框架纳入到这个地图中去理解,你很快就明白这个框架做了什么,这也是很多前端大神能够快速学习一个新框架的主要原因。
浏览器的诞生让我们可以方便地显示文本和图片的内容和样式;JavaScript 的出现让网页动起来;Gmail 的发布,宣告前端也可以使用 Ajax 异步加载技术,来开发复杂的网页,前端工程师这个工种正式出现。
随着浏览器厂商的混战,各个浏览器都有自己的特色,jQuery 框架的出现统一了写法,解决了那个时代最棘手的前端问题:兼容性,极大提高了开发者的效率。
随着 Angular1 的诞生,我们多了一套开发模式,就是数据驱动页面。我们甚至不再需要使用 jQuery 去寻找 DOM,而是只关注数据的来源和修改,也就是现在我们所处的前端时代。我们所熟悉的 Vue,React,Angular,Svelte 等框架,都是在数据驱动页面这个场景下涌现的框架。
相信到这里,你已经大概明白前端的这些框架的风格和特点,以及 Vue 在这些框架中的地位,Vue 3 在 Vue 2 的基础之上做了全面的升级,在性能、扩展性和包的大小上,Vue3 都有质的飞跃。
我已经迫不及待的想聊聊 Vue 3 到底有哪些新特性,并且强烈推荐你来学习。

思考题

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

上手:一个清单应用帮你入门Vue.js

我们的专栏课程会通过故事的形式展开。故事的主角小圣是一名刚入行的前端工程师,在校期间学了点 HTML、CSS 和 JavaScript,但是不太懂框架。我是他的经理,会手把手教他在 Vue.js 这个框架里打怪升级。
小圣在学习 Vue 的过程中碰到的各种问题,同样也可能是你会碰到的问题。所以,在我带着你一起解决小圣面临的问题的同时,你的很多问题也会迎刃而解。
今天是小圣第一天入职,他只知道团队的项目是用 Vue.js 开发的,但并不熟悉 Vue 的具体技术细节,所以我决定带他先做一个清单应用,先在整体上熟悉这个框架。
如果你已经很熟悉 Vue 开发了,可以直接粗略地把本讲过一遍,直奔下一讲。在那里,我会带你梳理 Vue 3 的新特性,相信这些新特性会让你对 Vue 3 产生新的期待。

任务分解

如下图所示,小圣要上手开发的应用大概长这个样子:它有一个输入框,供我们输入数据;下方有一个列表,显示着所有我们要做的事情。
在输入框输入内容后,敲下回车,下面就会新增一条数据。对于每个要做的事情,你还可以用复选框标记,标记后文字就会变灰,并带有一个删除的效果,表示这件事情已经做完了。
image.png
清单应用虽然看起来简单,不过麻雀虽小,五脏俱全。其实不管入门哪个框架,你都可以写一个清单,来上手体验一下。
不过,由于小圣只有简单的 jQuery 开发经验,他在学习 Vue 的时候,首先要做的就是思想的转变。为什么要这么说呢?下面我们来对比看看 jQuery 的开发思路和 Vue.js 的开发思路有什么不同,看完你就会明白,我为什么说小圣在学习 Vue 时,首先要做的是转变思路。
比如,我们想做一个输入框,里面输入的任何数据都会在页面上同步显示。
对于这样一个前端的功能,jQuery 开发的思路是:

  1. 先找到输入框,给输入框绑定输入事件;
  2. 输入的同时,我们获取输入的值;
  3. 再找到对应的 html 标签,更新标签的内容。

对应代码大概是这样的:

  1. <div>
  2. <h2 id="app"></h2>
  3. <input type="text" id="todo-input">
  4. </div>
  5. <script src="jquery.min.js"></script>
  6. <script>
  7. // 找到输入框,监听输入
  8. $('#todo-input').on('input',function(){
  9. let val = $(this).val() // 获取值
  10. $('#app').html(val) // 找到标签,修改内容
  11. })
  12. </script>

在实现我们想要的输入框的功能时,上述 jQuery 代码需要先找到输入框,然后持续监听输入,之后一直等待到输入值被获取,最后找到标签所在的前端页面位置,进行内容的修改。
上述的 jQuery 代码,其实是 jQuery 时代的开发逻辑的一个缩影。而 jQuery 时代的开发逻辑,就是我们先要找到目标元素,然后再进行对应的修改
学习 Vue.js,首先就要进行思想的升级,也就是说,不要再思考页面的元素怎么操作,而是要思考数据是怎么变化的。这就意味着,我们只需要操作数据,至于数据和页面的同步问题,Vue 会帮我们处理。实际上,Vue 让前端开发者能够专注数据本身的操作,而数据和页面的同步问题,则交由 Vue 来负责。这种机制正是 Vue 当初受到开发者青睐的一个重要原因。
对于同样的输入框需求,Vue 的开发思路是:我们需要一个数据,在输入框的代码和 h2 标签的代码内使用。我们只需要操作数据,然后交给 Vue 去管理数据和页面的同步就可以了。
在 Vue 框架下,如果你想要页面显示一个数据,就要先在代码的 data 里声明数据;在输入框的代码里,使用 v-model 来标记输入框和数据的同步;在 HTML 模板里,使用两个花括号标记,来显示数据,例如{{title}}。对应代码大概是这个样子:

  1. <div id="app">
  2. <h2>{{title}}</h2>
  3. <input type="text" v-model="title">
  4. </div>
  5. <script src="https://unpkg.com/vue@next"></script>
  6. <script>
  7. const App = {
  8. data() {
  9. return {
  10. title: "" // 定义一个数据
  11. }
  12. }
  13. }
  14. // 启动应用
  15. Vue.createApp(App).mount('#app')
  16. </script>

从这个例子中,你就可以看到 Vue 在开发思路上和 jQuery 的不同。而我们要做的,就是逐渐习惯 Vue 的这种开发模式。

清单页面的渲染

在前端页面,我们在输入框输入数据,然后输入框下方要有一个列表,显示我们所有输入的值。按照 Vue 的思考方式,如果我们想实现这个功能,那么我们需要一个数组,然后使用 v-for 这个语法来循环渲染。
先看代码:

  1. <div id="app">
  2. <h2>{{title}}</h2>
  3. <input type="text" v-model="title">
  4. <ul>
  5. <li v-for="todo in todos">{{todo}}</li>
  6. </ul>
  7. </div>
  8. <script src="https://unpkg.com/vue@next"></script>
  9. <script>
  10. const App = {
  11. data() {
  12. return {
  13. title: "", // 定义一个数据
  14. todos:['吃饭','睡觉']
  15. }
  16. }
  17. }
  18. // 启动应用
  19. Vue.createApp(App).mount('#app')
  20. </script>

看上述代码,在 data 中,我们再定义一个数据 todos,输入一个数组。为了方便调试,我们先放两个假数据,如果我们在标签里直接写{{todos}},就会看到显示的是一个数组,但这个不是我们想要的,我们需要的是显示一个列表。
在 Vue 中,只要是渲染列表,我们都是用 v-for 这个语法,而具体到上述代码对 v-for 语法的使用,也即:

  1. <li v-for="todo in todos">{{todo}}</li>

上面这行单独抽出来的代码的意思就是:我们循环遍历 todos 这个数据, 每一条遍历的结果叫 todo,然后把这个数据渲染出来,这样页面就能显示一个列表了。
image.png

处理用户交互

在上一步中,我们主要考虑的是:实现前端页面的一个输入框,以及能显示输入值的一个列表的功能。下一步,就是让用户敲回车的时候,能够让列表新增一条。采用 Vue 的思维,我们需要完成以下这几个步骤:

  1. 监听用户的输入。在监听中,如果判断到用户的输入是回车的时候,那就执行一个函数。
  2. 在执行的这个函数内部把 title 追加到 todos 最后面的位置,并且清空 title。

那么 Vue 如何实现这一功能呢?我们先看实现这一功能后的完整代码:

  1. <div id="app">
  2. <input type="text" v-model="title" @keydown.enter="addTodo">
  3. <ul>
  4. <li v-for="todo in todos">{{todo}}</li>
  5. </ul>
  6. </div>
  7. <script src="https://unpkg.com/vue@next"></script>
  8. <script>
  9. const App = {
  10. data() {
  11. return {
  12. title: "", // 定义一个数据
  13. todos:['吃饭','睡觉']
  14. }
  15. },
  16. methods:{
  17. addTodo(){
  18. this.todos.push(this.title)
  19. this.title = ""
  20. }
  21. }
  22. }
  23. // 启动应用
  24. Vue.createApp(App).mount('#app')
  25. </script>

对照上述代码,我们来看一下在 Vue 中,监听用户交互的方法。在 Vue 中,我们使用 @来标记用户的交互,@click 是点击,@keydown 是键盘敲下,所以就像上述代码展示的那样,如果只监听回车键,那么我们就用 @keydown.enter=“addTodo” 。
监听到用户的输入后,对于要执行的函数,我们新增一个 methods 配置。在函数内部,我们可以在 this 上直接读到 data 里的的数据,所以我们不需要考虑怎么找到标签,只需要进行如下这行潇洒的代码,就能让列表自动新增了一条, 这就是数据驱动页面的魅力。

  1. this.todos.push(this.title)

额外信息的显示

好了,我们现在既实现了一个输入框,以及输入数据后能够新增一条数据的列表的功能,也实现了用户在输入后的交互功能。
下一步,我们想实现标记清单中某一项是否完成的功能。但这却难住了小圣同学,因为从目前的代码设计上来看,我们的输入只能是字符串格式的内容。而我们想要实现的标记功能,却是把列表中的某一项,用灰色的字体背景和中划线来标记,以此表示这一条内容是已经完成的内容。
如果我们想实现这个功能,就需要对数据结构进行一下改造,把内容的数据类型,从简单的字符串类型改为对象。
那么数据结构要怎么改造呢?我们先直接看改造数据结构后的完整代码:

  1. <ul>
  2. <li v-for="todo in todos">
  3. <input type="checkbox" v-model="todo.done">
  4. <span :class="{done:todo.done}"> {{todo.title}}</span>
  5. </li>
  6. </ul>
  7. <script>
  8. const App = {
  9. data() {
  10. return {
  11. title: "", // 定义一个数据
  12. todos:[
  13. {title:'吃饭',done:false},
  14. {title:'睡觉',done:true}
  15. ]
  16. }
  17. },
  18. methods:{
  19. addTodo(){
  20. this.todos.push({
  21. title:this.title,
  22. done:false
  23. })
  24. this.title = ""
  25. }
  26. }
  27. }
  28. </script>
  29. <style>
  30. .done{
  31. color:gray;
  32. text-decoration: line-through;
  33. }
  34. </style>

进一步优化

完成前面的步骤以后,现在看起来一个清单应用最基本的功能模块、用户交互、复选框功能都已经实现了。但是为了进一步提升交互,小圣还想要增加两个功能,第一个功能是:在前端页面显示的列表的最下面,显示一下列表项目中没完成的项目的比例;第二个功能是:新增一个清理的按钮,用来删掉已经标记为完成的列表中的一条或多条数据。
那么,对于要增加的第一个功能,也即如何实现在前端页面的列表的最下方,显示一下列表项目中没有完成的项目的比例呢?小圣按照学到的知识,写出了下面的代码:

  1. <div>
  2. {{todos.filter(v=>!v.done).length}}
  3. /
  4. {{todos.length}}
  5. </div>

把这段代码增加到上一步最后的完整代码中,运行代码,从下图所示的前端页面运行时状态中,我们能看到,其中显示的未完成比例的数据也没问题。
image.png
不过,从上述代码实现的方式上看,代码看起来很丑且性能不好,而且需要二次计算的数据,这在我们开发的需求中很常见。此外,在模板里面写 JS,看起来代码也很乱。Vue 针对这种情况,设计了一个功能,也就是计算属性
我们看一下采用 Vue 的计算属性实现的,能够支持二次计算的上述功能的实现代码:

  1. <div>
  2. {{active}} / {{all}}
  3. </div>
  4. <script>
  5. computed:{
  6. active(){
  7. return this.todos.filter(v=>!v.done).length
  8. },
  9. all(){
  10. return this.todos.length
  11. }
  12. }
  13. </script>

从上面的代码中能看到,和之前采用往模板里写 JS 的办法相比,我们新增了一个属性 computed。computed 属性的配置,也即 active 和 all,都是函数。这两个函数返回的计算后的值,在模板里可以直接当做数据来用,这样把 JavaScript 的计算逻辑依然放在了 JavaScript 里,避免了过于臃肿的模板。
而且 computed 计算属性还内置了缓存功能,如果依赖数据没变化,多次使用计算属性会直接返回缓存结果,同我们直接写在模板里相比,性能也有了提升。
计算属性不仅可以用来返回数据,有些时候我们也需要修改计算属性,比如我让小圣新增一个全选的复选框,要求如下:
1
全选框在勾选与取消勾选两个状态之间的切换,会把所有清单内的数据都同步勾选。
2
清单内的项目如果全部选中或者取消,也会修改全选框的状态。
对于新增全选框的功能,需要满足上面的两个要求,所以全选框这个计算属性就有了修改的需求。这时候 computed 的配置就不能是函数了,要变成一个对象,分别实现 get 和 set 函数,get 就是之前的返回值,set 就是修改计算属性要执行的函数。
我们来看一下 computed 修改后的代码:

  1. <div>
  2. 全选<input type="checkbox" v-model="allDone">
  3. <span> {{active}} / {{all}} </span>
  4. </div>
  5. <script>
  6. computed:{
  7. active(){
  8. return this.todos.filter(v=>!v.done).length
  9. },
  10. all(){
  11. return this.todos.length
  12. },
  13. allDone: {
  14. get: function () {
  15. return this.active === 0
  16. },
  17. set: function (val) {
  18. this.todos.forEach(todo=>{
  19. todo.done = val
  20. });
  21. }
  22. }
  23. }
  24. </script>

和没有全选框时的 computed 属性的配置代码相比,上面的代码新增了一个 allDone 的计算属性,页面中直接使用 checbox 绑定。在 allDone 的 get 函数里,对于 allDone 会返回什么值,我们只需要判断计算属性 active 是不是 0 就可以。
而 set 函数做到的就是,我们在修改 allDone,也就是前端页面切换全选框的时候,直接遍历 todos,把里面的 done 字段直接和 allDone 同步即可。
实现新增一个全选的复选框后的效果是什么样呢?我们一起来看一下:
image.png

条件渲染

在上面一部分,我们增加了在前端页面的底部显示未完成比例,和增加全选框这两个功能。除此之外,我们还需要新增一个“清理”的按钮,点击之后把已完成的数据删除,功能需求很简单,但是有一个额外的要求,就是列表中没有标记为完成的某一项列表数据时,这个按钮是不显示的。
这种在特定条件下才显示,或者隐藏的需求也很常见,我们称之为条件渲染。在 Vue 中,我们使用 v-if 来实现条件渲染。
老规矩,我们还是先看代码:

  1. <button v-if="active<all" @click="clear">清理</button>
  2. <script>
  3. methods:{
  4. clear(){
  5. this.todos = this.todos.filter(v=>!v.done)
  6. }
  7. }
  8. </script>

通过上述代码,我们实现了增加一个清理按钮的功能。当 active 小于 all 的时候,我们显示清理按钮,也就是说,v-if 后面的值是 true 的时候,显示清理按钮,false 的时候不显示。“@”符号的作用,我们在前面讲到监听用户交互时,已经拿 @keydown 为例说明过了,这里代码中的 @click 的作用是绑定点击事件。
我们还可以用 v-else 配合 v-if,当 todos 是空的时候,显示一条“暂无数据”的信息,具体的实现代码如下:

  1. <ul v-if="todos.length">
  2. <li v-for="todo in todos">
  3. <input type="checkbox" v-model="todo.done">
  4. <span :class="{done:todo.done}"> {{todo.title}}</span>
  5. </li>
  6. </ul>
  7. <div v-else>
  8. 暂无数据
  9. </div>

当我们实现了清理按钮的功能,并且也实现了列表为空时,能够显示“暂无数据”的信息后,我们看下清单应用的最终效果。
image.png

这个需求并没有考虑美观性,小圣没写太多 CSS,主要专注在 JS 的交互逻辑上。小圣这个需求做完,晚上下班的时候跟我分享了一下学习 Vue 的心得,你也可以在评论区分享一下你对 Vue 的开发的心得,我们一起交流。

总结

我们来总结一下小圣今天都学到了什么吧。入职第一天,小圣首先扭转了之前使用 jQuery 时的开发思路,并且弄明白了 jQuery 和 Vue 开发思路的区别。从寻找 DOM 到数据驱动,这是前端开发的一次巨大的变革,也是小圣同学的第一个挑战。
其次就是对 Vue 的入门使用,我带你回顾一下今天做的这个清单应用:对于这个应用,首先我们要有输入框能输入文本,并且在输入框下方循环显示清单,我们用到了 v-model,v-for 这些功能。这些 v- 开头的属性都是 Vue 自带写法,我们通过{{}}包裹的形式显示数据。
然后我们想在用户输入完成后敲击回车新增一条数据,这就用到 @开头的几个属性,@keyup 和 @click 都是绑定对应的交互事件。最后,通过 computed,我们能对页面数据的显示进行优化。我们所需要关心的,就是数据的变化,这种思维方式会贯穿小圣的整个打怪升级之路。

思考题

下班前我给小圣布置一个作业,现在所有的操作状态一刷新就都没了,这个问题怎么解决呢?