前言
认识vue
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。 全程是Vue.js或者Vuejs; 渐进式框架?表示我们可以在项目中一点点来引入和使用Vue ,而不一定需要全部使用Vue来开发整个项目。
目前Vue在前端处于什么地位?
搜索量: 对比react、Vue、Angular在Google搜索中,react被搜索次数最多;百度中Vue最多
npm下载量: react>Vue>Angular
github star 量: Vue>react>Angular
谁是最好的框架?
不会给出结论。 首先,这是一个敏感的话题,在很多地方都争论不休,就像很多人喜欢争论谁才是世界上最好的语言一样;其次,争论不休的话题是没有意义的; 但是,我们从现实的角度,分析一下,学习哪一门语言更容易找到工作?
找后端的工作:优先推荐Java、其次推荐Go、再次推荐Node(JavaScript),可能不推荐PHP、C#; 找前端的工作:优先推荐JavaScript(TypeScript)、其次Flutter、再次Android(Java、Kotlin)、iOS(OC、 Swift); 也有很多的其他方向:游戏开发、人工智能、算法工程师等等; 那么,就前端来说,学习了HTML、CSS、JavaScript,哪一个框架更容易找到工作?
如果去国外找工作,优先推荐React、其次是Vue和Angular,不推荐jQuery了;
如果在国内找工作,优先推荐、必须学习Vue,其次是React,其次是Angular,不推荐jQuery了;
学习vue2还是vue3?
《程序员》: Vue 3版本兼容2.x,对于想要学习Vue的开发者而言,时常在纠结是从Vue2开始学基础还是直接学Vue3,对此,你有着什么样的建议?
尤雨溪: 直接学Vue3就行了,基础概念是一模一样的。
Vue3带来的变化(源码、性能、新的API)
- 源码
源码通过monorepo的形式来管理源代码:
Mono:单个
Repo:repository仓库
主要是将许多项目的代码存储在同一个repository中;
这样做的目的是多个包本身相互独立,可以有自己的功能逻辑、单元测试等,同时又在同一个仓库下方便管理;
而且模块划分的更加清晰,可维护性、可扩展性更强;
源码使用TypeScript来进行重写:
在Vue2.x的时候,Vue使用Flow来进行类型检测;
在Vue3.x的时候,Vue的源码全部使用TypeScript来进行重构,并且Vue本身对TypeScript支持也更好了;
- 性能
使用Proxy进行数据劫持
在Vue2.x的时候,Vue2是使用Object.defineProperty来劫持数据的getter和setter方法的;
这种方式一致存在一个缺陷就是当给对象添加或者删除属性时,是无法劫持和监听的;
所以在Vue2.x的时候,不得不提供一些特殊的API,比如set或delete,事实上都是一些hack方法,也增加了开发者学习新的API的成本;
而在Vue3.x开始,Vue使用Proxy来实现数据的劫持,这个API的用法和相关的原理我也会在后续讲到;
删除了一些不必要的API:
移除了实例上的on,off 和 $once;
移除了一些特性:如filter、内联模板等;
包括编译方面的优化:
生成Block Tree、Slot编译优化、diff算法优化;
- 新的API
源码通过monorepo的形式来管理源代码:
Mono:单个
Repo:repository仓库
主要是将许多项目的代码存储在同一个repository中;
这样做的目的是多个包本身相互独立,可以有自己的功能逻辑、单元测试等,同时又在同一个仓库下方便管理;
而且模块划分的更加清晰,可维护性、可扩展性更强;
源码使用TypeScript来进行重写:
在Vue2.x的时候,Vue使用Flow来进行类型检测;
在Vue3.x的时候,Vue的源码全部使用TypeScript来进行重构,并且Vue本身对TypeScript支持也更好了;
系统学习Vue3+TypeScript思维导图
如何使用Vue?
Vue的本质,就是一个JavaScript的库:
刚开始我们不需要把它想象的非常复杂;
我们就把它理解成一个已经帮助我们封装好的库;
在项目中可以引入并且使用它即可。
那么安装和使用Vue这个JavaScript库有哪些方式呢?
方式一:在页面中通过CDN的方式来引入;
方式二:下载Vue的JavaScript文件,并且自己手动引入;
方式三:通过npm包管理工具安装使用它(webpack再讲);
方式四:直接通过Vue CLI创建项目,并且使用它;
1.Vue3的初体验
Vue3的引入-CDN
什么是CDN呢?
CDN称之为内容分发网络(Content Delivery Network或Content Distribution Network,缩写:CDN)它是指通过相互连接的网络系统,利用最靠近每个用户的服务器; 更快、更可靠地将音乐、图片、视频、应用程序及其他文件发送给用户;来提供高性能、可扩展性及低成本的网络内容传递给用户;
常用的CDN服务器可以大致分为两种: 自己的CDN服务器:需要购买自己的CDN服务器,目前阿里、腾讯、亚马逊、Google等都可以购买CDN服务器;开源的CDN服务器:国际上使用比较多的是unpkg、JSDelivr、cdnjs;
Vue的CDN引入:
Hello Vue案例的实现:
Vue3的引入-local
下载Vue的源码,可以直接打开CDN的链接:
打开链接,复制其中所有的代码;
创建一个新的文件,比如vue.js,将代码复制到其中;
通过script标签,引入刚才的文件:
你好啊,Vue3,案例的实现:
计数器案例-原生
如果我们希望实现一个计数器的案例:点击+1,那么内容会显示数字+1;点击-1,那么内容会显示数字-1;我们可以选择很多种方式来实现:在这里我们就对比原生和Vue的实现方式的不同。
计数器案例-Vue
声明式和命令式编程
原生开发和Vue开发的模式和特点,我们会发现是完全不同的,这里其实涉及到两种不同的编程范式: 命令式编程和声明式编程;命令式编程关注的是 “how to do”,声明式编程关注的是 “what to do”,由框架(机器)完成 “how”的过程;
在原生的实现过程中,我们是如何操作的呢? 我们每完成一个操作,都需要通过JavaScript编写一条代码,来给浏览器一个指令; 这样的编写代码的过程,我们称之为命令式编程; 在早期的原生JavaScript和jQuery开发的过程中,我们都是通过这种命令式的方式在编写代码的;
在Vue的实现过程中,我们是如何操作的呢? 我们会在createApp传入的对象中声明需要的内容,模板template、数据data、方法methods; 这样的编写代码的过程,我们称之为是声明式编程; 目前Vue、React、Angular的编程模式,我们称之为声明式编程;
MVVM模型
MVC和MVVM都是一种软件的体系结构
MVC是Model–View–Controller的简称,是在前期被使用非常框架的架构模式,比如iOS、前端;MVVM是Model-View-ViewModel的简称,是目前非常流行的架构模式;
通常情况下,我们也经常称Vue是一个MVVM的框架。
Vue官方其实有说明,Vue虽然并没有完全遵守MVVM的模型,但是整个设计是受到它的启发的。
MVC(原生)
MVVM(Vue)
templat、data、methods属性
在使用createApp的时候,我们传入了一个对象,接下来我们详细解析一下之前传入的属性分别代表什么含义。
- template属性:
表示的是Vue需要帮助我们渲染的模板信息: 目前我们看到它里面有很多的HTML标签,这些标签会替换掉我们挂载到的元素(比如id为app的div)的innerHTML;模板中有一些奇怪的语法,比如 {{}},比如 @click,这些都是模板特有的语法,我们会在后面讲到;但是这个模板的写法有点过于别扭了,并且IDE很有可能没有任何提示,阻碍我们编程的效率。 data属性是传入一个函数,并且该函数需要返回一个对象: 在Vue2.x的时候,也可以传入一个对象(虽然官方推荐是一个函数);在Vue3.x的时候,必须传入一个函数,否则就会直接在浏览器中报错; data中返回的对象会被Vue的响应式系统劫持,之后对该对象的修改或者访问都会在劫持中被处理: 所以我们在template中通过 {{counter}} 访问counter,可以从对象中获取到数据;所以我们修改counter的值时,template中的 {{counter}}也会发生改变;具体这种响应式的原理,我们后面会有专门的篇幅来讲解。 methods属性是一个对象,通常我们会在这个对象中定义很多的方法: 这些方法可以被绑定到 template 模板中;在该方法中,我们可以使用this关键字来直接访问到data中返回的对象的属性;对于有经验的同学,在这里我提一个问题,官方文档有这么一段描述: 当然,这里还可以定义很多其他的属性,我们会在后续进行讲解: 比如props、computed、watch、emits、setup等等;也包括很多的生命周期函数;不用着急,我们会一个个学习它们的。
如果想要学习Vue的源码,比如看createApp的实现过程,应该怎么办呢? 问题回顾: 我们在methods中要使用data返回对象中的数据: 事实上Vue的源码当中就是对methods中的所有函数进行了遍历,并且通过bind绑定了this: 我们在前面练习Vue的过程中,有些代码片段是需要经常写的,我们再VSCode中我们可以生成一个代码片段,方便我们快速生成。 React的开发模式: 如果我们希望把数据显示到模板(template)中,使用最多的语法是 “Mustache”语法 (双大括号) 的文本插值。 v-once用于指定元素或者组件只渲染一次: 默认情况下,如果我们展示的内容本身是html的,那么vue并不会对其进行特殊的解析。 v-pre用于跳过元素和它的子元素的编译过程,显示原始的Mustache标签: 这个指令保持在元素上直到关联组件实例结束编译。 前端讲的一系列指令,主要是将值插入到模板内容中。 v-bind用于绑定一个或多个属性值,或者向另一个组件传递props值(这个学到组件时再介绍); 在开发中,有时候我们的元素class也是动态的,比如: 对象语法: 我们可以传给 :class (v-bind:class 的简写) 一个对象,以动态地切换 class。 数组语法: 我们可以把一个数组传给 :class,以应用一个 class 列表; 我们可以利用v-bind:style来绑定一些CSS内联样式: 对象语法: 在某些情况下,我们属性的名称可能也不是固定的: 如果我们希望将一个对象的所有属性,绑定到元素上的所有属性,应该怎么做呢? 前面我们绑定了元素的内容和属性,在前端开发中另外一个非常重要的特性就是交互。 v-on的使用: 我们可以使用v-on来监听一下点击的事件: 当通过methods中定义方法,以供@click调用时,需要注意参数问题: v-on支持修饰符,修饰符相当于对事件进行了一些特殊的处理: 在某些情况下,我们需要根据当前的条件决定某些元素或组件是否渲染,这个时候我们就需要进行条件判断了。 v-if、v-else、v-else-if用于根据条件来渲染某一块的内容: 因为v-if是一个指令,所以必须将其添加到一个元素上: v-show和v-if的用法看起来是一致的,也是根据一个条件决定是否显示元素或者组件: 首先,在用法上的区别: 在真实开发中,我们往往会从服务器拿到一组数据,并且需要对其进行渲染。 v-for的基本格式是 “item in 数组(可用 of 代替 in)”: v-for也支持遍历对象,并且支持有一二三个参数: 类似于v-if,你可以使用 template 元素来循环渲染一段包含多个元素的内容: Vue 将被侦听的数组的变更方法进行了包裹,所以它们也将会触发视图更新。这些被包裹过的方法包括: 在使用v-for进行列表渲染时,我们通常会给元素或者组件绑定一个key属性。 我们先来解释一下VNode的概念: 如果我们不只是一个简单的div,而是有一大堆的元素,那么它们应该会形成一个VNode Tree 我们先来看一个案例:这个案例是当我点击按钮时会在中间插入一个f; 我们知道,在模板中可以直接通过插值语法显示一些data中的数据。 什么是计算属性呢? 我们来看三个案例: 缺点一:模板中存在大量的复杂逻辑,不便于维护(模板中表达式的初衷是用于简单的计算); 缺点一:我们事实上先显示的是一个结果,但是都变成了一种方法的调用; 注意:计算属性看起来像是一个函数,但是我们在使用的时候不需要加(),这个后面讲setter和getter时会讲到; 在上面的实现思路中,我们会发现计算属性和methods的实现看起来是差别是不大的,而且我们多次提到计算属性有缓存的。 这是什么原因呢? 计算属性在大多数情况下,只需要一个getter方法即可,所以我们会将计算属性直接写成一个函数。 你可能觉得很奇怪,Vue内部是如何对我们传入的是一个getter,还是说是一个包含setter和getter的对象进行处理的呢? 什么是侦听器呢? 举个栗子(例子): 我们先来看一个例子: 另外一个是Vue3文档中没有提到的,但是Vue2文档中有提到的是侦听对象的属性: 现在我们来做一个相对综合一点的练习:书籍购物车 index.html
```html
<!DOCTYPE html>
style.css 表单提交是开发中非常常见的功能,也是和用户交互的重要手段: 官方有说到,v-model的原理其实是背后有两个操作: 事实上v-model更加复杂 单个勾选框: 单选:只能选中一个值 目前我们在前面的案例中大部分的值都是在template中固定好的: 默认情况下,v-model在进行双向绑定时,绑定的是input事件,那么会在每次内容输入后就将最新的值和绑定的属性进行同步; message总是string类型,即使在我们设置type为number也是string类型; v-model也可以使用在组件上,Vue2版本和Vue3版本有一些区别。 人面对复杂问题的处理方式: 组件化也是类似的思想: 如果我们将一个页面中所有的处理逻辑全部放在一起,处理起来就会变得非常复杂,而且不利于后续的管理以及扩展;但如果,我们将一个页面拆分成一个个小的功能块,每个功能块完成属于自己这部分独立的功能,那么之后整个页面的管理和维护就变得非常容易了;如果我们将一个个功能块拆分后,就可以像搭建积木一下来搭建我们的项目; 现在可以说整个的大前端开发都是组件化的天下,无论从三大框架(Vue、React、Angular),还是跨平台方案的Flutter,甚至是移动端都在转向组件化开发,包括小程序的开发也是采用组件化开发的思想。 组件化是Vue、React、Angular的核心思想,也是我们后续课程的重点(包括以后实战项目): 如果我们现在有一部分内容(模板、逻辑等),我们希望将这部分内容抽取到一个独立的组件中去维护,这个时候 如何注册一个组件呢? 我们先来学习一下全局组件的注册: 当然,我们组件本身也可以有自己的代码逻辑: 比如自己的data、computed、methods等等 在通过app.component注册一个组件的时候,第一个参数是组件的名称,定义组件名的方式有两种: 全局组件往往是在应用程序一开始就会全局组件完成,那么就意味着如果某些组件我们并没有用到,也会一起被注册: 目前我们使用vue的过程都是在html文件中,通过template编写自己的模板、脚本逻辑、样式等。 在这个组件中我们可以获得非常多的特性: 如果我们想要使用这一的SFC的.vue文件,比较常见的是两种方式: 事实上随着前端的快速发展,目前前端的开发已经变的越来越复杂了: 事实上我们上面提到的所有脚手架都是依赖于webpack的: 我们先来看一下官方的解释: webpack的官方文档是https://webpack.js.org/ webpack的安装目前分为两个:webpack、webpack-cli 我们可以通过webpack进行打包,之后运行打包之后的代码 1. 对象的引用赋值 2. 对象的浅拷贝 3. 对象的深拷贝 ECMAScript包括两个不同类型的值:基本数据类型和引用数据类型。 Number、String 、Boolean、Null和Undefined。基本数据类型是按值访问的,因为可以直接操作保存在变量中的实际值。 上面,b获取的是a值得一份拷贝,虽然,两个变量的值相等,但是两个变量保存了两个不同的基本数据类型值。 也就是对象类型Object type,比如:Object 、Array 、Function 、Data等。 说明这两个引用数据类型指向了同一个堆内存对象。obj1赋值给onj2,实际上这个堆内存对象在栈内存的引用地址复制了一份给了obj2,但是实际上他们共同指向了同一个堆内存对象。实际上改变的是堆内存对象。 a 声明变量时不同的内存分配:
Vue提供了两种方式: 方式一:使用script标签,并且标记它的类型为 x-template; 方式二:使用任意标签(通常使用template标签,因为不会被浏览器渲染),设置id;template元素是一种用于保存客户端内容的机制,该内容再加载页面时不会被呈现,但随后可以在运行时使用JavaScript实例化;
方式一:使用script标签
方式二:使用template
《MDN》:HTML 元素 是一种用于保存客户端内容的机制,该内容在页面加载时不被渲染,但可以在运行时使用JavaScript进行实例化。
可以将一个模板视为正在被存储以供随后在文档中使用的一个内容片段。
虽然, 在加载页面的同时,解析器确实处理 元素的内容,这样做只是确保这些内容是有效的; 然而,元素的内容不会被渲染。
这个时候,在createApp的对象中,我们需要传入的template以 # 开头:
如果字符串是以 # 开始,那么它将被用作 querySelector,并且使用匹配元素的innerHTML作为模板字符串;
问题一:为什么不能使用箭头函数(官方文档有给出解释)?
问题二:不使用箭头函数的情况下,this到底指向的是什么?(可以作为一道面试题)
Vue的源码
第一步:在GitHub上搜索vue-next,下载源代码;
这里推荐通过 git clone 的方式下载;
第二步:安装Vue源码项目相关的依赖;
执行 yarn install
第三步:对项目执行打包操作
执行yarn dev(执行前修改脚本)
2.Vue3的基本指令
methods方法绑定this
问题一:为什么不能使用箭头函数(官方文档有给出解释)?
问题二:不使用箭头函数的情况下,this到底指向的是什么?(可以作为一道面试题)
那么这个this是必须有值的,并且应该可以通过this获取到data返回对象中的数据。
那么我们这个this能不能是window呢?
不可以是window,因为window中我们无法获取到data返回对象中的数据;
但是如果我们使用箭头函数,那么这个this就会是window了;
为什么是window呢?
这里涉及到箭头函数使用this的查找规则,它会在自己的上层作用于中来查找this;
最终刚好找到的是script作用于中的this,所以就是window;
this到底是如何查找和绑定的呢?
在我的公众号有另外一篇文章,专门详细的讲解了this的绑定规则;
前端面试之彻底搞懂this指向;
认真学习之后你绝对对this的绑定一清二楚;
VSCode代码片段
VSCode中的代码片段有固定的格式,所以我们一般会借助于一个在线工具来完成。
具体的步骤如下:
第一步,复制自己需要生成代码片段的代码;
第二步,https://snippet-generator.app/ 在该网站中生成代码片段;
第三步,在VSCode中配置代码片段;
模板语法
React使用的jsx,所以对应的代码都是编写的类似于js的一种语法;
之后通过Babel将jsx编译成 React.createElement 函数调用;
Vue也支持jsx的开发模式(后续有时间也会讲到):
但是大多数情况下,使用基于HTML的模板语法(里面的);
在模板中,允许开发者以声明式的方式将DOM和底层组件实例的数据绑定在一起;
在底层的实现中,Vue将模板编译成虚拟DOM渲染函数,这个我会在后续给大家讲到;
所以,对于学习Vue来说,学习模板语法是非常重要的。
Mustache双大括号语法
并且我们前端提到过,data返回的对象是有添加到Vue的响应式系统中;
当data中的数据发生改变时,对应的内容也会发生更新。
当然,Mustache中不仅仅可以是data中的属性,也可以是一个JavaScript的表达式。
v-once指令
当数据发生变化时,元素或者组件以及其所有的子元素将视为静态内容并且跳过;
该指令可以用于性能优化;
如果是子节点,也是只会渲染一次:
v-text指令
v-html指令
如果我们希望这个内容被Vue可以解析出来,那么可以使用 v-html 来展示;
v-pre指令
跳过不需要编译的节点,加快编译的速度;
v-cloak指令
和 CSS 规则如 [v-cloak] { display: none } 一起用时,这个指令可以隐藏未编译的 Mustache 标签直到组件实例准备完毕。3.v-bind和v-on
v-bind的绑定属性
但是,除了内容需要动态来决定外,某些属性我们也希望动态来绑定。
比如动态绑定a元素的href属性;
比如动态绑定img元素的src属性;
绑定属性我们使用v-bind:
缩写::
预期: any (with argument) | Object (without argument)
参数: attrOrProp (optional)
修饰符:
√.camel - 将 kebab-case attribute 名转换为 camelCase。
用法: 动态地绑定一个或多个 attribute,或一个组件 prop 到表达式。
绑定基本属性
在开发中,有哪些属性需要动态进行绑定呢?
还是有很多的,比如图片的链接src、网站的链接href、动态绑定一些类、样式等等。
v-bind有一个对应的语法糖,也就是简写方式。
在开发中,我们通常会使用语法糖的形式,因为这样更加简洁。
绑定class介绍
当数据为某个状态时,字体显示红色。
当数据另一个状态时,字体显示黑色。
绑定class有两种方式:
对象语法
数组语法
绑定style介绍
这次因为某些样式我们需要根据数据动态来决定;
比如某段文字的颜色,大小等等;
CSS property 名可以用驼峰式 (camelCase) 或短横线分隔 (kebab-case,记得用引号括起来) 来命名;
绑定class有两种方式:
对象语法
数组语法
数组语法: style 的数组语法可以将多个样式对象应用到同一个元素上;
动态绑定属性
前端我们无论绑定src、href、class、style,属性名称都是固定的;
如果属性名称不是固定的,我们可以使用 :[属性名]=“值” 的格式来定义;
这种绑定的方式,我们称之为动态绑定属性;
绑定一个对象
非常简单,我们可以直接使用 v-bind 绑定一个对象;
案例:info对象会被拆解成div的各个属性
v-on绑定事件
在前端开发中,我们需要经常和用户进行各种各样的交互:
这个时候,我们就必须监听用户发生的事件,比如点击、拖拽、键盘事件等等
在Vue中如何监听事件呢?使用v-on指令。
接下来我们来看一下v-on的用法:
缩写:@
预期: Function | Inline Statement | Object
参数: event
修饰符:
√.stop - 调用 event.stopPropagation()。
√.prevent - 调用 event.preventDefault()。
√.capture - 添加事件侦听器时使用 capture 模式。
√.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
√.{keyAlias} - 仅当事件是从特定键触发时才触发回调。
√.once - 只触发一次回调。
√.left - 只当点击鼠标左键时触发。
√.right - 只当点击鼠标右键时触发。
√.middle - 只当点击鼠标中键时触发。
√.passive - { passive: true } 模式添加侦听器
用法: 绑定事件监听
v-on:click可以写成@click,是它的语法糖写法:
当然,我们也可以绑定其他的事件:
如果我们希望一个元素绑定多个事件,这个时候可以传入一个对象:
情况一: 如果该方法不需要额外参数,那么方法后的()可以不添加。
但是注意: 如果方法本身中有一个参数,那么会默认将原生事件event参数传递进去
情况二: 如果需要同时传入某个参数,同时需要event时,可以通过$event传入事件。
.stop - 调用 event.stopPropagation()。
.prevent - 调用 event.preventDefault()。
.capture - 添加事件侦听器时使用 capture 模式。
.self - 只当事件是从侦听器绑定的元素本身触发时才触发回调。
.{keyAlias} - 仅当事件是从特定键触发时才触发回调。
.once - 只触发一次回调。
.left - 只当点击鼠标左键时触发。
.right - 只当点击鼠标右键时触发。
.middle - 只当点击鼠标中键时触发。
.passive - { passive: true } 模式添加侦听器
4.条件渲染
Vue提供了下面的指令来进行条件判断:
v-if
v-else
v-else-if
v-show
下面我们来对它们进行学习。
v-if、v-else、v-else-if
这些内容只有在条件为true时,才会被渲染出来;
这三个指令与JavaScript的条件语句if、else、else if类似;
v-if的渲染原理:
v-if是惰性的;
当条件为false时,其判断的内容完全不会被渲染或者会被销毁掉;
当条件为true时,才会真正渲染条件块中的内容;
template元素
但是如果我们希望切换的是多个元素呢?
此时我们渲染div,但是我们并不希望div这种元素被渲染;
这个时候,我们可以选择使用template;
template元素可以当做不可见的包裹元素,并且在v-if上使用,但是最终template不会被渲染出来:
有点类似于小程序中的block
v-show
v-show和v-if的区别
v-show是不支持template;
v-show不可以和v-else一起使用;
其次,本质的区别:
v-show元素无论是否需要显示到浏览器上,它的DOM实际都是有渲染的,只是通过CSS的display属性来进行切换;
v-if当条件为false时,其对应的原生压根不会被渲染到DOM中;
开发中如何进行选择呢?
如果我们的原生需要在显示和隐藏之间频繁的切换,那么使用v-show;
如果不会频繁的发生切换,那么使用v-if;
5.列表渲染
这个时候我们可以使用v-for来完成;
v-for类似于JavaScript的for循环,可以用于遍历一组数据;
v-for基本使用
数组通常是来自data或者prop,也可以是其他方式; item是我们给每项元素起的一个别名,这个别名可以自定来定义;
我们知道,在遍历一个数组的时候会经常需要拿到数组的索引:
如果我们需要索引,可以使用格式: “(item, index) in 数组”;
注意上面的顺序:数组元素项item是在前面的,索引项index是在后面的;
v-for支持的类型
一个参数: “value in object”;
二个参数: “(value, key) in object”;
三个参数: “(value, key, index) in object”;
v-for同时也支持数字的遍历:
每一个item都是一个数字;
template元素
我们使用template来对多个元素进行包裹,而不是使用div来完成;
数组更新检测
ppush()
ppop()
pshift()
punshift()
psplice()
psort()
preverse()
替换数组的方法
上面的方法会直接修改原来的数组,但是某些方法不会替换原来的数组,而是会生成新的数组,比如 filter()、 concat() 和 slice()。
v-for中的key是什么作用?
这个key属性有什么作用呢?我们先来看一下官方的解释: key属性主要用在Vue的虚拟DOM算法,在新旧nodes对比时辨识VNodes;
如果不使用key,Vue会使用一种最大限度减少动态元素并且尽可能的尝试就地修改/复用相同类型元素的算法;
而使用key时,它会基于key的变化重新排列元素顺序,并且会移除/销毁key不存在的元素;
官方的解释对于初学者来说并不好理解,比如下面的问题:
什么是新旧nodes,什么是VNode?
没有key的时候,如何尝试修改和复用的?
有key的时候,如何基于key重新排列的?
因为目前我们还没有比较完整的学习组件的概念,所以目前我们先理解HTML元素创建出来的VNode;
VNode的全称是Virtual Node,也就是虚拟节点;
事实上,无论是组件还是元素,它们最终在Vue中表示出来的都是一个个VNode;
VNode的本质是一个JavaScript的对象;
我们可以确定的是,这次更新对于ul和button是不需要进行更新,需要更新的是我们li的列表:
在Vue中,对于相同父元素的子元素节点并不会重新渲染整个列表;
因为对于列表中 a、b、c、d它们都是没有变化的;
在操作真实DOM的时候,我们只需要在中间插入一个f的li即可;
那么Vue中对于列表的更新究竟是如何操作的呢?
Vue事实上会对于有key和没有key会调用两个不同的方法;
有key,那么就使用 patchKeyedChildren方法;
没有key,那么久使用 patchUnkeyedChildren方法;
1. 没有key的操作(源码)
没有key的过程如下:
我们会发现上面的diff算法效率并不高:
c和d来说它们事实上并不需要有任何的改动;
但是因为我们的c被f所使用了,所有后续所有的内容都要一次进行改动,并且最后进行新增;
2. 有key执行操作(源码)
有key的diff算法如下
第一步的操作是从头开始进行遍历、比较:
a和b是一致的会继续进行比较;
c和f因为key不一致,所以就会break跳出循环;
第二步的操作是从尾部开始进行遍历、比较:
第三步是如果旧节点遍历完毕,但是依然有新的节点,那么就新增节点:
第四步是如果新的节点遍历完毕,但是依然有旧的节点,那么就移除旧节点:
第五步是最特色的情况,中间还有很多未知的或者乱序的节点(此时尽量利用旧节点):
所以我们可以发现,Vue在进行diff算法的时候,会尽量利用我们的key来进行优化操作:
在没有key的时候我们的效率是非常低效的;
在进行插入或者重置顺序的时候,保持相同的key可以让diff算法更加的高效;
6.计算属性
但是在某些情况,我们可能需要对数据进行一些转化后再显示,或者需要将多个数据结合起来进行显示;
比如我们需要对多个data数据进行运算、三元运算符来决定结果、数据进行某种转化后显示;
在模板中使用表达式,可以非常方便的实现,但是设计它们的初衷是用于简单的运算;
在模板中放入太多的逻辑会让模板过重和难以维护;
并且如果多个地方都使用到,那么会有大量重复的代码;
我们有没有什么方法可以将逻辑抽离出去呢?
可以,其中一种方式就是将逻辑抽取到一个method中,放到methods的options中;
但是,这种做法有一个直观的弊端,就是所有的data使用过程都会变成了一个方法的调用;
另外一种方式就是使用计算属性computed;
认识计算属性computed
官方并没有给出直接的概念解释;
而是说:对于任何包含响应式数据的复杂逻辑,你都应该使用计算属性;
计算属性将被混入到组件实例中。所有getter 和setter 的this 上下文自动地绑定为组件实例;
计算属性的用法:
选项: computed
类型: { [key: string]: Function | { get: Function, set: Function } }
那接下来我们通过案例来理解一下这个计算属性。
案例实现思路
案例一:我们有两个变量:firstName和lastName,希望它们拼接之后在界面上显示;
案例二:我们有一个分数:score
当score大于60的时候,在界面上显示及格;
当score小于60的时候,在界面上显示不及格;
案例三:我们有一个变量message,记录一段文字:比如Hello World
某些情况下我们是直接显示这段文字;
某些情况下我们需要对这段文字进行反转;
我们可以有三种实现思路:
思路一:在模板语法中直接使用表达式;
思路二:使用method对逻辑进行抽取;
思路三:使用计算属性computed;
缺点二:当有多次一样的逻辑时,存在重复的代码;
缺点三:多次使用的时候,很多运算也需要多次执行,没有缓存;
缺点二:多次使用方法的时候,没有缓存,也需要多次计算;
我们会发现无论是直观上,还是效果上计算属性都是更好的选择;
并且计算属性是有缓存的;
计算属性 VS methods
接下来我们来看一下同一个计算多次使用,计算属性和methods的差异:
计算属性的缓存
这是因为计算属性会基于它们的依赖关系进行缓存;
在数据不发生变化时,计算属性是不需要重新计算的;
但是如果依赖的数据发生变化,在使用时,计算属性依然会重新进行计算;
计算属性的setter和getter
但是,如果我们确实想设置计算属性的值呢?
这个时候我们也可以给计算属性设置一个setter的方法;
源码如何对setter和getter处理呢?
事实上非常的简单,Vue源码内部只是做了一个逻辑判断而已;
7.侦听器watch
认识侦听器watch
开发中我们在data返回的对象中定义了数据,这个数据通过插值语法等方式绑定到template中;
当数据变化时,template会自动进行更新来显示最新的数据;
但是在某些情况下,我们希望在代码逻辑中监听某个数据的变化,这个时候就需要用侦听器watch来完成了;
侦听器的用法如下:
选项: watch
类型: { [key: string]: string | Function | Object | Array}
侦听器案例
比如现在我们希望用户在input中输入一个问题;
每当用户输入了最新的内容,我们就获取到最新的内容,并且使用该问题去服务器查询答案;
那么,我们就需要实时的去获取最新的数据变化;
侦听器watch的配置选项
当我们点击按钮的时候会修改info.name的值;
这个时候我们使用watch来侦听info,可以侦听到吗?答案是不可以。
这是因为默认情况下,watch只是在侦听info的引用变化,对于内部属性的变化是不会做出响应的:
这个时候我们可以使用一个选项deep进行更深层的侦听;
注意前面我们说过watch里面侦听的属性对应的也可以是一个Object;
还有另外一个属性,是希望一开始的就会立即执行一次:
这个时候我们使用immediate选项;
这个时候无论后面数据是否有变化,侦听的函数都会有限执行一次;
还有另外一种方式就是使用$watch的API:
我们可以在created的生命周期(后续会讲到)中,使用this.$watchs 来侦听;
第一个参数是要侦听的源;
第二个参数是侦听的回调函数callback;
第三个参数是额外的其他选项,比如deep、immediate;
8.综合案例
案例说明:
1.在界面上以表格的形式,显示一些书籍的数据;
2.在底部显示书籍的总价格;
3.点击+或者-可以增加或减少书籍数量(如果为1,那么不能继续-);
4.点击移除按钮,可以将书籍移除(当所有的书籍移除完毕时,显示:购物车为空~);
序号
书籍名称
出版日期
价格
购买数量
操作
{{index + 1}}
{{book.name}}
{{book.date}}
{{formatPrice(book.price)}}
{{book.count}}
总价格: {{formatPrice(totalPrice)}}
购物车为空~
- **index.js**
```javascript
Vue.createApp({
template: "#my-app",
data() {
return {
books: [
{
id: 1,
name: '《算法导论》',
date: '2006-9',
price: 85.00,
count: 1
},
{
id: 2,
name: '《UNIX编程艺术》',
date: '2006-2',
price: 59.00,
count: 1
},
{
id: 3,
name: '《编程珠玑》',
date: '2008-10',
price: 39.00,
count: 1
},
{
id: 4,
name: '《代码大全》',
date: '2006-3',
price: 128.00,
count: 1
},
]
}
},
computed: {
// vue2: filter/map/reduce
totalPrice() {
let finalPrice = 0;
for (let book of this.books) {
finalPrice += book.count * book.price;
}
return finalPrice;
},
// Vue3不支持过滤器了, 推荐两种做法: 使用计算属性/使用全局的方法
filterBooks() {
return this.books.map(item => {
const newItem = Object.assign({}, item);
newItem.price = "¥" + item.price;
return newItem;
})
}
},
methods: {
increment(index) {
// 通过索引值获取到对象
this.books[index].count++
},
decrement(index) {
this.books[index].count--
},
removeBook(index) {
this.books.splice(index, 1);
},
formatPrice(price) {
return "¥" + price;
}
}
}).mount("#app");
table {
border: 1px solid #e9e9e9;
border-collapse: collapse;
border-spacing: 0;
}
th, td {
padding: 8px 16px;
border: 1px solid #e9e9e9;
text-align: left;
}
th {
background-color: #f7f7f7;
color: #5c6b77;
font-weight: 600;
}
.counter {
margin: 0 5px;
}
9.v-model使用
v-model的基本使用
比如用户在登录、注册时需要提交账号密码;
比如用户在检索、创建、更新信息时,需要提交一些数据;
这些都要求我们可以在代码逻辑中获取到用户提交的数据,我们通常会使用v-model指令来完成:
v-model指令可以在表单input、textarea以及select元素上创建双向数据绑定;
它会根据控件类型自动选取正确的方法来更新元素;
尽管有些神奇,但v-model 本质上不过是语法糖,它负责监听用户的输入事件来更新数据,并在某种极端场景下进行一些特殊处理;
v-model的原理
v-bind绑定value属性的值;
v-on绑定input事件监听到函数中,函数会获取最新的值赋值到绑定的属性中;v-model绑定textarea、checkbox、radio、select
v-model即为布尔值。
此时input的value并不影响v-model的值。
多个复选框:
当是多个复选框时,因为可以选中多个,所以对应的data中属性是一个数组。
当选中某一个时,就会将input的value添加到数组中。
v-model绑定的是一个值;
当我们选中option中的一个时,会将它对应的value赋值到fruit中;
多选:可以选中多个值
v-model绑定的是一个数组;
当选中多个值时,就会将选中的option对应的value添加到数组fruit中;
v-model的值绑定
比如gender的两个输入框值male、female;
比如hobbies的三个输入框值basketball、football、tennis;
在真实开发中,我们的数据可能是来自服务器的,那么我们就可以先将值请求下来,绑定到data返回的对象中,再通过v-bind来进行值的绑定,这个过程就是值绑定。
这里不再给出具体的做法,因为还是v-bind的使用过程。
v-model修饰符- lazy、number、trim
如果我们在v-model后跟上lazy修饰符,那么会将绑定的事件切换为change 事件,只有在提交时(比如回车)才会触发;
如果我们希望转换为数字类型,那么可以使用 .number 修饰符:
另外,在我们进行逻辑判断时,如果是一个string类型,在可以转化的情况下会进行隐式转换的:下面的score在进行判断的过程中会进行隐式转化的;
v-mode组件上使用
具体的使用方法,后面讲组件化开发再具体学习。
10.组件化开发
人处理问题的方式
任何一个人处理信息的逻辑能力都是有限的
所以,当面对一个非常复杂的问题时,我们不太可能一次性搞定一大堆的内容。
但是,我们人有一种天生的能力,就是将问题进行拆解。
如果将一个复杂的问题,拆分成很多个可以处理的小问题,再将其放在整体当中,你会发现大的问题也会迎刃而解。
认识组件化开发
组件化开发
所以,学习组件化最重要的是它的思想,每个框架或者平台可能实现方法不同,但是思想都是一样的。
我们需要通过组件化的思想来思考整个应用程序:
我们将一个完整的页面分成很多个组件;
每个组件都用于实现页面的一个功能块;
而每一个组件又可以进行细分;
而组件本身又可以在多个地方进行复用;
Vue的组件化
前面我们的createApp函数传入了一个对象App,这个对象其实本质上就是一个组件,也是我们应用程序的根组件;
组件化提供了一种抽象,让我们可以开发出一个个独立可复用的小组件来构造我们的应用;
任何的应用都会被抽象成一颗组件树;
接下来,我们来学习一下在Vue中如何注册一个组件,以及之后如何使用这个注册后的组件。
我们先从简单的开始谈起,比如下面的模板希望抽离到一个单独的组件:
注册组件分成两种:
全局组件: 在任何其他的组件中都可以使用的组件;
局部组件: 只有在注册的组件中才能使用的组件;
注册全局组件
全局组件需要使用我们全局创建的app来注册组件;
通过component方法传入组件名称、组件对象即可注册一个全局组件了;
之后,我们可以在App组件的template中直接使用这个全局组件:
方式一:使用kebab-case(短横线分割符)
当使用kebab-case (短横线分隔命名) 定义一个组件时,你也必须在引用这个自定义元素时使用kebab-case,例如
方式二:使用PascalCase(驼峰标识符)
当使用PascalCase (首字母大写命名) 定义一个组件时,你在引用这个自定义元素时两种命名法都可以使用。也就是说
注册局部组件
比如我们注册了三个全局组件:ComponentA、ComponentB、ComponentC;
在开发中我们只使用了ComponentA、ComponentB,如果ComponentC没有用到但是我们依然在全局进行 了注册,那么就意味着类似于webpack这种打包工具在打包我们的项目时,我们依然会对其进行打包;
这样最终打包出的JavaScript包就会有关于ComponentC的内容,用户在下载对应的JavaScript时也会增加包的大小;
所以在开发中我们通常使用组件的时候采用的都是局部注册:
局部注册是在我们需要使用到的组件中,通过components属性选项来进行注册;
比如之前的App组件中,我们有data、computed、methods等选项了,事实上还可以有一个components选项;
该components选项对应的是一个对象,对象中的键值对是组件的名称: 组件对象;
Vue的开发模式
但是随着项目越来越复杂,我们会采用组件化的方式来进行开发:
这就意味着每个组件都会有自己的模板、脚本逻辑、样式等;
当然我们依然可以把它们抽离到单独的js、css文件中,但是它们还是会分离开来;
也包括我们的script是在一个全局的作用域下,很容易出现命名冲突的问题;
并且我们的代码为了适配一些浏览器,必须使用ES5的语法;
在我们编写代码完成之后,依然需要通过工具对代码进行构建、代码;
所以在真实开发中,我们可以通过一个后缀名为.vue 的single-file components (单文件组件) 来解决,并且可以使用webpack或者vite或者rollup等构建工具来对其进行处理。
代码的高亮;
ES6、CommonJS的模块化能力;
组件作用域的CSS;
可以使用预处理器来构建更加丰富的组件,比如TypeScript、Babel、Less、Sass等;
方式一:使用Vue CLI来创建项目,项目会默认帮助我们配置好所有的配置选项,可以在其中直接使用.vue文件;
方式二:自己使用webpack或rollup或vite这类打包工具,对其进行打包处理;
我们最终,无论是后期我们做项目,还是在公司进行开发,通常都会采用Vue CLI的方式来完成。
但是在学习阶段,为了让大家理解Vue CLI打包项目的过程,我会接下来穿插讲解一部分webpack的知识,帮助大家更好的理解Vue CLI的原理以及其打包的过程。
认识webpack
比如开发过程中我们需要通过模块化的方式来开发;
比如也会使用一些高级的特性来加快我们的开发效率或者安全性,比如通过ES6+、TypeScript开发脚本逻辑,通过sass、less等方式来编写css样式代码;
比如开发过程中,我们还希望实时的监听文件的变化来并且反映到浏览器上,提高开发的效率;
比如开发完成后我们还需要将代码进行压缩、合并以及其他相关的优化;
等等….
但是对于很多的前端开发者来说,并不需要思考这些问题,日常的开发中根本就没有面临这些问题:
这是因为目前前端开发我们通常都会直接使用三大框架来开发:Vue、React、Angular;
但是事实上,这三大框架的创建过程我们都是借助于脚手架(CLI) 的; 事实上Vue-CLI、create-react-app、Angular-CLI都是基于webpack来帮助我们支持模块化、less、TypeScript、打包优化等的;
webpack is a static module bundler for modern JavaScript applications.
webpack是一个静态的模块化打包工具,为现代的JavaScript应用程序;
我们来对上面的解释进行拆解:
打包bundler: webpack可以将帮助我们进行打包,所以它是一个打包工具
静态的static: 这样表述的原因是我们最终可以将代码打包成最终的静态资源(部署到静态服务器);
模块化module: webpack默认支持各种模块化开发,ES Module、CommonJS、AMD等;
现代的modern: 我们前端说过,正是因为现代前端开发面临各种各样的问题,才催生了webpack的出现和发展;
webpack的中文官方文档是https://webpack.docschina.org/
DOCUMENTATION:文档详情,也是我们最关注的
Webpack的运行是依赖Node环境的,所以我们电脑上必须有Node环境
所以我们需要先安装Node.js,并且同时会安装npm;
我当前电脑上的node版本是v14.15.5,npm版本是6.14.11(你也可以使用nvm或者n来管理Node版本);
Node官方网站:https://nodejs.org/
那么它们是什么关系呢?
执行webpack命令,会执行node_modules下的.bin目录下的webpack;
webpack在执行时是依赖webpack-cli的,如果没有安装就会报错;
而webpack-cli中代码执行时,才是真正利用webpack进行编译和打包的过程;
所以在安装webpack时,我们需要同时安装webpack-cli(第三方的脚手架事实上是没有使用webpack-cli的,而是类似于自己的vue-service-cli的东西)npm install webpack webpack-cli –g # 全局安装
npm install webpack webpack-cli –D # 局部安装
在目录下直接执行webpack 命令
webpack
生成一个dist文件夹,里面存放一个main.js的文件,就是我们打包之后的文件:
这个文件中的代码被压缩和丑化了;
我们暂时不关心他是如何做到的,后续我讲webpack实现模块化原理时会再次讲到;
另外我们发现代码中依然存在ES6的语法,比如箭头函数、const等,这是因为默认情况下webpack并不清楚我们打包后的文件是否需要转成ES5之前的语法,后续我们需要通过babel来进行转换和设置;
我们发现是可以正常进行打包的,但是有一个问题,webpack是如何确定我们的入口的呢?
事实上,当我们运行webpack时,webpack会查找当前目录下的src/index.js作为入口;
所以,如果当前项目中没有存在src/index.js文件,那么会报错;
知识补充
对象的引用-浅拷贝-深拷贝
// 对象是引用类型
const info = {name: "why", age: 18};
const obj = info;
info.name = "kobe";
console.log(obj.name); //打印出kobe
const info = { name: "why", age: 18, friend: { name: "kobe" } };
const obj = Object.assign({}, info);
// 或者使用lodash函数库的浅拷贝方法
// const obj = _.clone(info);
info.name = "kobe";
console.log(obj.name); //打印出why
info.friend.name = "james";
console.log(obj.friend.name); //打印出james
const info = { name: "why", age: 18, friend: { name: "kobe" } };
const obj = JSON.parse(JSON.stringify(info));
// 或者使用lodash函数库方法
// const obj = _.cloneDeep(info);
info.friend.name = "james";
console.log(obj.friend.name); //打印出Kobe
JavaScript中基本数据类型和引用数据类型的区别
基本数据类型指的是简单的数据段,引用数据类型指的是有多个值构成的对象。
当我们把变量赋值给一个变量时,解析器首先要确认的就是这个值是基本类型值还是引用类型值。
var a = 10;
var b = a;
b= 20;
console.log(a); // 10值
b只是保存了a复制的一个副本。所以,b的改变,对a没有影响。
下图演示了这种基本数据类型赋值的过程:
javascript的引用数据类型是保存在堆内存中的对象。
与其他语言的不同是,你不可以直接访问堆内存空间中的位置和操作堆内存空间。只能操作对象在栈内存中的引用地址。 所以,引用类型数据在栈内存中保存的实际上是对象在堆内存中的引用地址。通过这个引用地址可以快速查找到保存中堆内存中的对象 var obj1 = new Object();
var obj2 = obj1;
obj2.name = "我有名字了";
console.log(obj1.name); // 我有名字了
下面我们来演示这个引用数据类型赋值过程:
1)原始值:存储在栈(stack)中的简单数据段,也就是说,它们的值直接存储在变量访问的位置。这是因为这些原始类型占据的空间是固定的,所以可将他们存储在较小的内存区域 – 栈中。这样存储便于迅速查寻变量的值。
2)引用值:存储在堆(heap)中的对象,也就是说,存储在变量处的值是一个指针(point),指向存储对象的内存地址。这是因为:引用值的大小会改变,所以不能把它放在栈中,否则会降低变量查寻的速度。相反,放在变量的栈空间中的值是该对象存储在堆中的地址。地址的大小是固定的,所以把它存储在栈中对变量性能无任何负面影响。
b 不同的内存分配机制也带来了不同的访问机制
1)在javascript中是不允许直接访问保存在堆内存中的对象的,所以在访问一个对象时,首先得到的是这个对象在堆内存中的地址,然后再按照这个地址去获得这个对象中的值,这就是传说中的按引用访问。
2)而原始类型的值则是可以直接访问到的。
c 复制变量时的不同
1)原始值: 在将一个保存着原始值的变量复制给另一个变量时,会将原始值的副本赋值给新变量,此后这两个变量是完全独立的,他们只是拥有相同的value而已。
2)引用值: 在将一个保存着对象内存地址的变量复制给另一个变量时,会把这个内存地址赋值给新变量,也就是说这两个变量都指向了堆内存中的同一个对象,他们中任何一个作出的改变都会反映在另一个身上。(这里要理解的一点就是,复制对象时并不会在堆内存中新生成一个一模一样的对象,只是多了一个保存指向这个对象指针的变量罢了)。多了一个指针
d 参数传递的不同(把实参复制给形参的过程)
首先我们应该明确一点:ECMAScript中所有函数的参数都是按值来传递的。
但是为什么涉及到原始类型与引用类型的值时仍然有区别呢?还不就是因为内存分配时的差别。
1)原始值: 只是把变量里的值传递给参数,之后参数和这个变量互不影响。
2)引用值: 对象变量它里面的值是这个对象在堆内存中的内存地址,这一点你要时刻铭记在心! 因此它传递的值也就是这个内存地址,这也就是为什么函数内部对这个参数的修改会体现在外部的原因了,因为它们都指向同一个对象。