四、Vue2
1、Vue 的基本原理
当一个 Vue 实例创建时,Vue 会遍历 data 中的属性,用 Object.defineProperty 将它们转为 getter/setter,并且在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有相应的 watcher 程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的 setter 被调用时,会通知 watcher 重新计算,从而使它关联的组件得以更新
2、响应式原理
数据劫持
利用 Object.defineProperty 劫持对象的访问器,在属性发生变化时我们可以获取变化,从而进行下一步操作
发布者模式和订阅者模式
在软件架构中,发布订阅是一种消息范式,发布者不会将消息直接发送给订阅者,而是将发布的消息分为不同的类别,无需了解哪些订阅者是否存在。同样的,订阅者可以表达对一个或多个类别的兴趣,只接收感兴趣的消息,无需了解哪些发布者是否存在 发布者和订阅者都不知道对方的存在,发布者只需发送消息到订阅器里,订阅者只管接收自己订阅的内容
响应式原理
vue 的响应式原理就是采用数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty()来劫持各个属性的 getter 和 setter,在数据变动时发布消息给订阅者,触发相应的监听回调。只要分为以下几个步骤:
- 需要给Observe(被劫持的数据对象)的数据对象进行递归遍历,包括子属性对象的属性,都加上 getter、setter 这样的属性。修改这个对象的某个属性,就会触发 setter,那么就能监听到数据的变化
- Compile(Vue 的编译器)解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加侦听数据的订阅者,当数据发生变化时会收到通知,更新视图
- Watcher(订阅者)是Observe和Compile之间的桥梁,主要做的事情有:
- 在自身实例化时Dep(用于收集订阅者的订阅器)里添加自己
- 自身必须拥有一个**update()**方法
- 待属性变动dep.notice() 通知时,能调用自身的update() 方法,并触发Compile中绑定的回调,则功成身退
- MVVM 作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据 model 变更的双向绑定效果
Object.defineProperty(obj, prop, descriptor)
- obj - 要定义的对象
- prop - 要定义或修改的属性名称或 Symbol
- descriptor - 要定义或修改的属性的描述符(配置对象)
- get 属性的 getter 函数,如果没有 getter,则为
undefined
。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入this
对象(由于继承关系,这里的this
并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值 - **默认为 [
undefined
]**set 属性的 setter 函数,如果没有 setter,则为undefined
。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的this
对象。 **默认为 [undefined
]**
缺点:在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty() 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题
- 返回值:被传递给函数的对象 obj
3、MVVM
MVVM(Model-View-ViewModel)模式是一种基于前端开发的架构模式,其核心是提供对 View 和 Model 的双向数据绑定,即当 View 或 Model 其中任意一个发生变化时,都会通过 ViewModel 引起另外一个的改变。
- MVVM 的优点
- 双向绑定。数据层和视图层中任何一层发生变化,都会通过 ViewModel 使另一层变化,时刻保持数据和视图的一致性;
- 通过数据驱动视图更新,不再手动操作 DOM 元素,提高性能;
- MVVM 的缺点
- 由于双向绑定的技术,产生 bug 后无法确定问题出现在数据层还是视图层;
- 数据层过大会占用更多的内存资源,影响性能;
4、常用指令
指令 | 作用 |
---|---|
v-on | 缩写为@,绑定事件 |
v-bind | 简写为:,动态绑定 |
v-slot | 简写为#,组件插槽 |
v-for | 循环对象或数组元素,同时生成 DOM |
v-show | 显示内容与否 |
v-if | 显示与隐藏,决定当前 DOM 元素是否渲染 |
v-else | 必须和 v-if 连用 不能单独使用 否则报错 |
v-text | 解析文本 |
v-html | 解析 html 标签 |
5、动态绑定 class 与 style
class | style | |
---|---|---|
绑定对象 | :class="{ className: isActive }" 或 :class="classNameObject" | :style="{color: '#ffffff'}" 或 :style="styleObject" |
绑定数组 | :class="['active', 'is-success', { 'is-disabled': isDisabled }]" | :style="[styleObject1, styleObject2, styleObject3, ...]" |
6、常见修饰符
v-on
修饰符 | 作用 |
---|---|
.stop | event.stopPropagation(),阻止单击事件继续传播(阻止默认事件) |
.prevent | event.preventDefault(),提交事件不再重载页面(阻止默认行为) |
.native | 监听组件根元素的原生事件 |
.once | 点击事件将只会触发一次 |
v-model
修饰符 | 作用 |
---|---|
.lazy | 取代 input 监听 change 事件 |
.number | 输入值转为数值类型 |
.trim | 输入首尾空格过滤 |
7、v-if 与 v-show 的区别
原理
- v-if 会调用 addIfCondition 方法根据条件渲染,为 false 时在生成 vnode 的时候会忽略对应节点,render 的时候就不会渲染
- 添加 v-show 指令的元素一定会渲染,只是通过修改 display 属性的值来决定是否显示
切换
- v-if 切换时,DOM 元素会重复生成和销毁,会执行生命周期钩子
- v-show 切换时不会执行生命周期钩子
应用场景
需要频繁切换 DOM 时,使用 v-show;反之则使用 v-if
8、为什么避免 v-for 和 v-if 在一起使用?
Vue 处理指令时,v-for 比 v-if 具有更高的***优先级***,存在性能问题。 如果你有 5 个元素被 v-for 循环,,v-if 也会分别执行 5 次
9、v-for 循环为什么一定要绑定 key
提升 vue 渲染性能
- key 的作用主要是为了更高效的更新虚拟 DOM,因为它可以非常精确的找到相同节点,因此 patch 过程会非常高效
- Vue 在 patch 过程中会判断两个节点是不是相同节点时,key 是一个必要条件。比如渲染列表时,如果不写 key,Vue 在比较的时候,就可能会导致频繁更新元素,使整个 patch 过程比较低效,影响性能
- 应该避免使用数组下标作为 key,因为 key 值不是唯一的话可能会导致上面图中表示的 bug,使 Vue 无法区分它他,还有比如在使用相同标签元素过渡切换的时候,就会导致只替换其内部属性而不会触发过渡效果
- Vue 判断两个节点是否相同时主要判断两者的元素类型和 key 等,如果不设置 key,就可能永远认为这两个是相同节点,只能去做更新操作,就造成大量不必要的 DOM 更新操作,明显是不可取的
10、为什么不建议用 index 索引作为 key?
使用 index 作为 key 和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多无意义的额外工作
11、v-model
v-model 用于实现视图层与数据层的双向绑定,数据层变化时可以驱动视图层更新,当视图层变化时会改变数据。v-model 本质上是一个语法糖,默认情况下相当于:value
和@input
的结合。
v-model 通常使用在表单项上,但也能使用在自定义组件上,表示对某个值的输入和输出控制。使用 v-model 可以减少大量繁琐的事件处理代码,提高开发效率。
12、v-model 与.sync 的对比
共同点:都是语法糖,都可以实现父子组件中数据的双向通信
不同点:
v-model | .sync |
---|---|
父组件中使用 v-model 传递数据时,子组件通过@input 触发事件 | 父组件中传递数据时,子组件通过@update:xxx 触发事件 |
一个组件只能绑定一个 v-model vue2 | 一个组件可以多个属性用.sync 修饰符,可以同时"双向绑定多个“prop” |
v-model 针对更多的是最终操作结果,是双向绑定的结果,是 value,是一种 change 操作 | .sync 针对更多的是各种各样的状态,是状态的互相传递,是 status,是一种 update 操作 |
13、computed 与 watch 的区别
computed
- 支持缓存,只有依赖的数据发生了变化,才会重新计算
- 不支持异步,当 computed 中有异步操作时,无法监听数据的变化
- computed 的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,即 data 中声明的数据或 props 传递的数据
- 如果一个属性是由其他属性计算而来,这个属性依赖其它属性,一般会使用 computed
- 如果 computed 属性的属性值是函数,那么默认使用 get 方法,函数的返回值就是属性的属性值;在 computed 中,属性有一个 get 方法和一个 set 方法,当数据发生变化时,会调用 set 方法
watch
- 不支持缓存,数据变化会执行相应操作
- 支持异步监听
- 监听的函数接收两个参数,第一个参数为更新后的值,第二个参数为更新前的值‘
- 当一个属性发生变化时,就需要执行相应的操作
- 监听数据必须是 data 中声明的或者父组件传递过来的 props 中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
- immediate:默认为 false,为 true 时组件加载会立即触发
- deep:默认为 false,为 true 时开启深度监听。需要注意的是,deep 无法监听到数组和对象内部的变化
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用 watch
总结
computed
计算属性 : 依赖其它属性值,并且 computed 的值有缓存,只有它依赖的属性值发生改变,下一次获取 computed 的值时才会重新计算 computed 的值。watch
侦听器 : 更多的是观察的作用,无缓存性,类似于某些数据的监听回调,每当监听的数据变化时都会执行回调进行后续操作。
14、组件通信
组件通信指的是组件通过某一种方式来传递信息以达到某个目的的过程。每个组件都是独立的,在开发中我们就是通过组件通信使各个组件的功能联动起来
props/$emit
父组件通过 props 向子组件传递数据,子组件通过$emit 事件和父组件通信
- props 只能是父组件向子组件传递数据,使得父子组件直接形成一个向下的单向数据流。子组件不能直接修改 props 数据,只能通知父组件来修改
- $emit 绑定一个自定义事件,可以将参数传递给父组件;父组件中则通过
v-on
注册监听事件同时接收参数
provide/inject
这种方式是通过依赖注入的方式实现组件的(可跨级)通信。依赖注入所提供的属性是非响应式的。
- provide:用来发送数据或方法
- inject:用来接收数据或方法
ref/$refs
在父组件中通过 ref 可以获取子组件实例,通过实例来访问子组件的属性和方法
$parent/$children
- $parent可以获取上一级父组件实例,$root 来访问根组件的实例
- $children 可以让组件访问所有子组件的实例,但是不能保证顺序,访问的数据也不是响应式的
- 在根组件#app 上拿$parent得到的是new Vue()的实例,在这实例上再拿$parent 得到的是 undefined,而在最底层的子组件拿$children 是个空数组
- $children 的值是数组,而$parent 是个对象
$attrs/$listeners
inheritAttrs:默认值为 true,继承所有的父组件属性(除 props 之外的所有属性),为 false 表示只继承 class 属性
- $attrs:继承所有的父组件属性(除了 prop 传递的属性、class 和 style)
- $listeners:该属性是一个对象,里面包含了作用在这个组件上的所有监听器,可以配合
v-on="$listeners"
将所有的事件监听器指向这个组件的某个特定的子元素。(相当于子组件继承父组件的事件)
eventBus 事件总线
eventBus 事件总线适用于父子组件、非父子组件等之间的通信。这种组件通信方式会造成后期维护困难。vue3 中移除了事件总线,取而代之的是插件,使用方式并无变化。
总结
子组件不能直接修改父组件传递的数据,这样做是维护父子组件之间形成的单向数据流。如果子组件随意更改父组件传递的数据,会导致数据流混乱,提高开发和维护成本
15、生命周期
生命周期指的是 Vue 组件从创建到销毁经历的一系列的过程
- 创建前后
- beforeCreate - 组件创建之前,无法获取 data 中的数据
- created - 组件创建完成后,可以获取数据
- 渲染前后
- beforeMount - 组件挂载到 DOM 之前
- mounted - 组件挂载完毕,可以获取 DOM 节点
- 更新前后
- beforeUpdate - 响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染
- updated - 在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM 已经更新,所以可以执行依赖于 DOM 的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用
- 销毁前后
- beforeDestroy - 实例销毁之前调用。这一步,实例仍然完全可用,
this
仍能获取到实例 - destroyed - 实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁
父子组件的生命周期执行顺序
- 加载渲染过程
- 父 beforeCreate - 父 created - 父 beforeMount - 子 beforeCreate - 子 created - 子 beforeMount - 子 mounted - 父 mounted
- 更新过程
- 父 beforeUpdate - 子 beforeUpdate - 子 updated - 父 updated
- 销毁过程
- 父 beforeDestroy - 子 beforeDestroy - 子 destroyed - 父 destroyed
created 和 mounted 的区别
- created:在模板渲染成 html 前调用,即通常初始化某些属性值,然后再渲染成视图
- mounted:在模板渲染成 html 后调用,通常是初始化页面完成后,再对 html 的 dom 节点进行一些需要的操作
一般在哪个生命周期请求异步数据
我们可以在钩子函数created
、beforeMount
、mounted
中进行调用,因为在这三个钩子函数中,data 已经创建,可以将服务端端返回的数据进行赋值
推荐在 created 钩子函数中调用异步请求,因为在 created 钩子函数中调用异步请求有以下优点:
- 能更快获取到服务端数据,减少页面加载时间,用户体验更好
- SSR 不支持 beforeMount 、mounted 钩子函数,放在 created 中有助于一致性
16、keep-alive
keep-alive 用于缓存组件。在进行动态组件切换的时候对组件内部数据进行缓存,而不是走销毁流程。keep-alive 是一个抽象组件,它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。 当组件在
<keep-alive>
内被切换,它的activated
和deactivated
这两个生命周期钩子函数将会被对应执行
包含的参数:
- include - 名称匹配的组件会被缓存 --> include 的值为组件的 name
- exclude - 任何名称匹配的组件都不会被缓存
- max - 决定最多可以缓存多少组件
activated | deactivated |
---|---|
在 keep-alive 组件激活时调用 | 在 keep-alive 组件停用时调用 |
该钩子函数在服务器端渲染期间不被调用 | 该钩子在服务器端渲染期间不被调用 |
设置缓存后的钩子调用情况:
- 第一次进入:beforeRouterEnter ->created->…->activated->…->deactivated> beforeRouteLeave
- 后续进入时:beforeRouterEnter ->activated->deactivated> beforeRouteLeave
17、slot 插槽
slot 是 Vue 的内容分发机制,组件内部的模板引擎使用 slot 元素作为承载分发内容的出口。插槽 slot 是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的
- 默认插槽
子组件用
<slot>
标签来确定渲染的位置,标签里面可以放DOM
结构,当父组件使用的时候没有往插槽传入内容,标签内DOM
结构就会显示在页面。父组件在使用的时候,直接在子组件的标签内写入内容即可
- 具名插槽
子组件用
name
属性来表示插槽的名字,不传为默认插槽。父组件中在使用时在默认插槽的基础上加上slot
属性,值为子组件插槽name
属性值
- 作用域插槽
子组件在作用域上绑定属性来将子组件的信息传给父组件使用,这些属性会被挂在父组件
v-slot
接受的对象上父组件中在使用时通过v-slot:
(简写:#)获取子组件的信息,在内容中使用
小结:
v-slot
属性只能在<template>
上使用,但在只有默认插槽时可以在组件标签上使用- 默认插槽名为
default
,可以省略 default 直接写v-slot
。缩写为#
时不能不写参数,写成#default
- 可以通过解构获取
v-slot={user}
,还可以重命名v-slot="{user: newName}"
和定义默认值v-slot="{user = '默认值'}"
18、为什么 data 是一个函数而不是一个对象
保证每个组件内数据的独立性,防止出现变量污染。对象为引用类型,当复用组件时,由于数据对象都指向同一个 data 对象,当在一个组件中修改 data 时,其他重用的组件中的 data 会同时被修改;而使用返回对象的函数,由于每次返回的都是一个新对象(Object 的实例),引用地址不同,则不会出现这个问题。
19、Vue-Router
对前端路由的理解
前端路由的核心,就在于改变视图的同时不会向后端发出请求;而是加载路由对应的组件。Vue-Router 就是将组件映射到路由, 然后渲染出来的
- 拦截用户的刷新操作,避免服务端盲目响应、返回不符合预期的资源内容。把刷新这个动作完全放到前端逻辑里消化调
- 感知 URL 的变化,根据这些变化使用 js 生成不同的内容
什么是 Vue-Router,有哪些组件
Vue-Router 是 Vue 官方的路由管理器。它和 Vue.js 的核心深度集成,路径和组件的映射关系使得构建 SPA(Single Page Application,单页面应用)变得易如反掌
- router-link - 实质上最终会渲染成 a 链接
- router-view - 子级路由显示
- keep-alive - 包裹组件缓存
$route和$router
- $route 是路由信息对象,包含了 path、params、hash、query、fullPath、matched、name 等路由信息参数
- $router 是路由的实例对象,包含了路由的跳转方法、钩子函数等内容
路由开发的优缺点
优点:
- 整体不刷新页面,用户体验更好
- 数据传递简单,开发效率高
缺点:
- 学习成本高
- 首次加载缓慢,不利于 seo
使用方式
- 新建 index.js 路由入口文件
- 创建路由规则
- 创建路由对象
- 将路由对象挂载到 Vue.use()中
- 将路由对象挂载到 Vue 实例上
Hash 模式
基于浏览器的 hashchange 事件,当 url 发生变化时,通过
window.location.hash
获取地址上的 hash 值,并通过 Router 类,配置 routes 对象设置与 hash 值对应的组件内容
优点:
- hash 值会出现在 url 中,但是不会被包含在 http 请求中,因此 hash 值改变不会重新加载页面
- hash 改变会触发 hashchange 事件,能控制浏览器的前进后退
- 兼容性好
缺点:
- 地址中携带#,不美观
- 只可修改#后面的部分,因此只能设置与当前 URL 同文档的 URL
- hash 有体积限制,故只可以添加短字符串
- 设置的新值必须与原来不同才会触发 hashchange 事件,并将记录添加到栈中
- 每次 URL 的改变不属于一次 http 请求,所以不利于 seo 优化
History 模式
基于 H5 新增的 pushState()和 replaceState()两个 api,以及浏览器的 popstate 事件,地址变化时,通过
window.location.pathname
找到对应的组件,并通过构造 Router 类,配置 routes 对象设置 pathname 值与对应的组件内容
优点:
- 没有#,相对美观
- pushState() 设置的新 URL 可以是与当前 URL 同源的任意 URL
- pushState() 设置的新 URL 可以与当前 URL 一模一样,这样也会把记录添加到栈中
- pushState() 通过 stateObject 参数可以添加任意类型的数据到记录中
- pushState() 可额外设置 title 属性供后续使用
- 浏览器的进后退能触发浏览器的 popstate 事件,获取 window.location.pathname 来控制页面的变化
缺点:
- URL 的改变属于 http 请求,借助 history.pushState 实现页面的无刷新跳转,因此会重新请求服务器。所以前端的 URL 必须和实际向后端发起请求的 URL 一致。如果用户输入的 URL 回车或者浏览器刷新或者分享出去某个页面路径,用户点击后,URL 与后端配置的页面请求 URL 不一致,则匹配不到任何静态资源,就会返回 404 页面。所以需要后台配置支持,覆盖所有情况的候选资源,如果 URL 匹配不到任何静态资源,则应该返回 app 依赖的页面或者应用首页
- 兼容性差,特定浏览器支持
路由 hash 模式和 history 模式的区别
hash
模式是一种把前端路由的路径用#
拼接在真实url
后面的模式。当#
后面的路径发生变化时,浏览器并不会重新发起请求,而是会触发onhashchange
事件。 hash 模式的特点:
hash
变化会触发网页跳转,即浏览器的前进和后退hash
可以改变url
,但是不会触发页面重新加载(hash 的改变是记录在window.history
中),即不会刷新页面。也就是说,所有页面的跳转都是在客户端进行操作。因此,这并不算是一次http
请求,所以这种模式不利于SEO
优化。hash
只能修改#
后面的部分,所以只能跳转到与当前url
同文档的url
hash
通过触发hashchange
事件,来监听hash
的改变,借此实现无刷新跳转的功能hash
永远不会提交到server
端(可以理解为只在前端自生自灭)
history API
是 H5
提供的新特性,允许开发者直接更改前端路由,即更新浏览器 URL
地址而不重新发起请求
- 新的
url
可以是与当前url
同源的任意url
,也可以是与当前url
一样的地址,但是这样会导致的一个问题是,会把重复的这一次操作记录到栈当中 - 通过
history.state
,添加任意类型的数据到记录中 - 可以额外设置
title
属性,以便后续使用 - 通过
pushState
、replaceState
来实现无刷新跳转的功能,需要后端配合
history 模式下的 404 问题
- URL 的改变属于 http 请求,借助 history.pushState 实现页面的无刷新跳转,因此会重新请求服务器
- 所以前端的 URL 必须和实际向后端发起请求的 URL 一致
编程式导航
方式 | 作用 |
---|---|
$router.push() | 跳转到指定的 url,并在 history 中添加记录,点击回退返回到上一个页面 |
$router.replace() | 跳转到指定的 url,但是 history 中不会添加记录,点击回退到上上个页面 |
$router.go(n) | 向前或者后跳转 n 个页面,n 可以是正数也可以是负数 |
$router.back() | 后退、回到上一页 |
$router.forward() | 前进、回到下一页 |
路由传参的方式
- query 传参
query 传参需要使用 path 来引入,页面跳转后参数将会出现在 url 中;也可以直降将参数以?xxx=xx&xxx=xx 的形式拼接在路由地址中
this.$router.push({ path: "/example", query: { id: "1" } }); // 传递query参数
this.$route.query.id; // 获取query参数
以 query 的方式传参时,刷新页面不会导致参数丢失
- params 传参
params 传参需要使用 name 来引入,页面跳转后参数不会出现在 url 中;在 4.1.4 版本开始,在未定义动态路由的情况下,将不能直接使用编程式导航传递 params 参数,目的是解决刷新页面参数丢失的问题
传参的方式总结
- this.$router.push(path)
- this.$router.push({path, query})
- this.$router.push({name, params}) // 注意版本更新
- this.$router.push({name, query})