是一个动态构建用户界面的渐进式JavaScript框架。用来创建单页应用的 web 应用框架。
优势:Vue 是一个轻量级框架,只关注图层,是一个构建数据的视图集合,大小只有十几KB。vue简单易学,而且通过 MVVM 思想实现了数据的双向绑定,让开发者不用再操作 dom 对象,有更多时间去思考业务逻辑。而且 vue是组件化的,通过组件,将单页应用中的各个模块拆分成单独的组件,提高了复用性。在更新视图的时候,还提供了虚拟节点,将新旧虚拟节点进行对比,然后更新视图。
Vue与React
-
相同点:
-
都有组件化思想
-
都是数据驱动视图
-
都支持服务端渲染
-
都有虚拟DOM
-
-
不同点:
-
数据流向不同。前者是双向数据流,后者是单向数据流。
-
数据变化的实现原理不同。前者使用的是可变的数据,后者使用的是不可变的数据。
-
diff算法不同。前者使用双指针,边对比,边更新 DOM。后者主要使用 diff 队列保存需要更新的一些 DOM,然后得到patch 树,在统一进行批量更新 DOM。
-
MVC与MVVM
MVC:
-
M(模型层):处理应用程序数据逻辑的部分(存数据、取数据)
-
V(视图层):处理数据显示的部分(页面展示、Dom操作)
-
C(控制层):处理用户交互的部分(控制模型层与视图层的关联)
MVVM:
-
M(模型层):处理数据与业务逻辑的部分
-
V(视图层):负责数据展示的部分
-
VM(视图模型层):负责从模型层监听数据的变化从而更新视图层,用来处理用户交互操作的部分
两者最大区别:MVVM 实现了模型层与视图层的自动同步,当数据发生变化时,不用手动操作DOM元素来改变视图层的显示,而是改变了数据对应的视图层自动更新
双向数据绑定(可以看我之前写的vue2双向数据绑定的源码解析https://blog.csdn.net/weixin_51642358/article/details/124878452)
vue 采用的是数据劫持结合发布者-订阅者模式的方式,通过 Object.defineProperty() (vue3 通过 Proxy进行劫持)来劫持各个属性的 getter 与 setter 方法,然后再数据变动的时候,发送消息给订阅者,触发相应的监听回调。主要步骤:
-
先使用 数据监听器Observe 对数据对象上的所有属性都添加上 getter 和 setter 方法。这样如果数据有发生变动的话,就能拿到最新值。
-
再使用 compile 进行模板的解析。将模板中的变量替换成数据,然后初始化渲染视图。并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图。
-
创建一个 订阅者 watcher ,在自身实例化时,往属性订阅器(dep)中添加自己,然后一旦属性变动,就会调用 dep.notice() 方法通知 对应的 watcher 调用 update() 方法进行更新,从而更新视图。
-
MVVM 作为数据绑定的入口,整合了 Observe、Compile、Watcher三者。通过 Observe 监听 模型层的数据变化,通过 Compile 来编译解析 模板指令,最后通过 Watcher 搭起 Observer 和 Compile 之间的通信桥梁,可以收到属性的变化通知并执行相应的函数,从而达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model 变更的双向绑定效果。
使⽤ Object.defineProperty() 来进⾏数据劫持有什么缺点
在对⼀些属性进⾏操作时,使⽤这种⽅法⽆法拦截,⽐如通过下标⽅式修改数组数据或者给对象新增属 性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来 说,对于数组⽽⾔,⼤部分操作都是拦截不到的,只是 Vue 内部通过重写函数的⽅式解决了这个问题。
Vue3.0 通过使⽤ Proxy 对对象进⾏代理,从⽽实现数据劫持。它可以完美的监听到任何方式的数据改变,唯⼀的缺点是兼容性的问题,因为 Proxy是 ES6 的语法。
computed 与 watch区别
-
前者支持缓存,只有依赖的数据发生变化时,才会重新计算。后者不支持缓存,只要数据发生变化,就会触发相应操作。
-
前者不支持异步,有异步就无法监听数据变化。后者支持异步
-
前者一个属性由另外的属性计算而来的话,这个属性也依赖与另外的属性。
-
前者的值会默认走缓存,计算属性是基于它们的响应式依赖进行缓存的,也就是基于data声明过,或者父组件传递过来的props中的数据进行计算的。
-
后者监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会出大其他操作,函数有两个的参数:
-
immediate:组件加载立即触发回调函数
-
deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
-
-
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch
插槽(slot)
是子组件的一个模板标签元素。
-
默认插槽:在 slot 没有指定 name 属性值时候,一个默认的显示插槽。
-
具名插槽:带有 name 属性的 slot, 一个组件可以有多个具名插槽。
-
作用域插槽:默认插槽、具名插槽的⼀个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不 同点是在⼦组件渲染作⽤域插槽时,可以将⼦组件内部的数据传递给⽗组件,让⽗组件根据⼦组件 的传递过来的数据决定如何渲染该插
常见的事件修饰符
-
.stop: 防止冒泡
-
.prevent: 阻止默认行为(链接跳转)
-
.once: 只会触发一次
-
.capture:进行事件捕捉(由外到内)
-
.self:只会触发自己范围内的事件,不包含子元素。
v-if和v-show的区别
-
控制手段不同。前者动态的向DOM树内增加或者删除DOM元素来控制元素的显示与隐藏。后者通过 css 中的 display属性来控制元素的显示与隐藏。
-
前者支持<template>标签。后者不支持。
-
编译条件不同。前者只有当第一次初始值为真的话,才会开始编译渲染。后者无论初始值,都会进行编译。
-
运行场景不同。前者适用于条件很少改变的情况。后者使用与频繁切换的情况。
-
开销不同。前者有更高的切换开销。后者有更高的初始渲染开销。
Vue2给对象添加新属性,界面不刷新
Vue2是通过 Object.defineProperty 来实现数据响应式的。(简单源码讲解:https://blog.csdn.net/weixin_51642358/article/details/124878452)在我们访问旧属性的时候,都会触发 getter 和 setter 方法,进而进行页面的刷新。但在我们添加新属性的时候,没有通过 Object.defineProperty 设置成响应式数据,所以也就无法触发事件属性的拦截,也就无法进行页面的刷新了。
Vue3是用 proxy 进行数据响应式的,直接动态添加新属性还是可以实现数据响应式。
解决方案:
-
Vue.set(target, propertyName/index, value ) 通过Vue.set() 向响应式对象中添加一个 property ,并确保这个新的 property 是响应式的,而且还会触发视图的更新。
-
Object.assign() 直接使用这个方法添加到对象的新属性还是不会触发更新。 需要创建一个新对象,然后合并原对象和混入对象的属性
v-model实现原理
v-model实际上是一个语法糖,它的实现主要包括属性绑定和事件监听两部分
-
当作用于表单元素上
-
动态绑定了 input 的 value 指向了 messgae 变量,并且在触发 input 事件的时候去动态把 message设置为当前DOM的value值
-
-
作用在组件上
-
在⾃定义组件中,v-model 默认会利⽤名为 value 的 prop和名为 input 的事件
-
本质是一个父子组件通信的语法糖,通过prop和$.emit实现。因此父组件 v-model 语法糖本质上可以修改为:
<child :value="message"@input="function(e){message = e}"></child>
-
data为什么是⼀个函数⽽不是对象
-
在根实例对象中,data可以是一个函数也可以是一个对象,因为根实例是单例的,不会造成数据污染
-
在组件实例对象中,data必须是一个函数,防止多个组件实例对象之间共用一个data,会产生数据污染。如果data是函数的话,initData 时会将其作为工厂函数都会返回全新的 data 对象。
nextTick
Vue 在更新 DOM 的时候是异步更新的。当数据发生变化的时候,nextTick 会开启一个异步更新队列,视图需要等待队列中所有数据变化完成后,在统一进行更新。(简单源码解析:https://blog.csdn.net/weixin_51642358/article/details/124878452)
Mixin
Mixin是面向对象程序设计语言中的类,通常作为功能模块使用。其他类可以直接访问Mixin类的方法而不用称为其子类。本质上就是一个 JS 对象,包含我们组件中任意功能选项,如 data、methods等。
我们只要将共用的功能以对象的方式传入 mixins
选项中,当组件使用 mixins
对象时所有mixins
对象的选项都将被混入该组件本身的选项中来。
注意事项
-
当组件存在与 Mixin 对象相同的选项时候,合并时组件选项会覆盖 Mixin 的选项
-
如果生命周期钩子有相同选项时,会合并成一个数组,然后先执行 mixin 的钩子,在执行组件的钩子
优点:增加代码的复用性
Vue 中 key 的原理
简而言之 ,key 就是每个虚拟DOM节点的 唯一ID,也是 diff 的一种优化策略,可以根据 key ,更准确、更快的找到对应的 虚拟 DOM 节点。
在虚拟节点中,key就是虚拟 DOM 对象的标识,当数据变化时,Vue 就会根据新数据生成一个新的虚拟DOM
,随后 Vue 会根据这个 key 进行 新旧虚拟DOM 的差异对比。
对比规则:
-
旧虚拟DOM 找到和新虚拟DOM相同的 key
-
若内容没有发生改变,直接使用之前的真实DOM
-
若内容发生变化,则生成一个新的真实DOM,然后替换掉页面中的真实DOM
-
-
旧虚拟DOM 没找到和新虚拟DOM相同的 key
-
直接创建新的真实DOM,渲染到页面上
-
diff算法
diff 算法是一种通过同层的树节点进行比较的高效算法。
特点:
-
比较只会在同层级进行,不会跨层级比较。
-
比较的过程中,循环从两边向中间进行比较。
当数据发生改变时,set方法会调用 Dep.notify 通知所有 订阅者 watcher,订阅者就会调用 patch 给真实的 DOM 打补丁,更新响应视图。
patch 函数前两个参数位为 oldVnode 和 Vnode ,主要做了四个判断:
-
没有新节点: 直接触发旧节点的 destory 钩子
-
没有旧节点: 说明是页面刚初始化,不需要比较,直接调用 createElm
-
通过 sameVnode 判断新旧节点是否一样
-
新旧节点不一样 :直接创建新节点,删除旧节点,不在进行深度比较
-
新旧节点一样 :直接调用 patchVnode 去处理这两个节点
-
找到对应的真实dom,称为el
-
如果都有文本节点且不相等,将el文本节点设置为Vnode的文本节点
-
如果oldVnode有子节点而VNode没有,则删除el子节点
-
如果oldVnode没有子节点而VNode有,则将VNode的子节点真实化后添加到el
-
如果两者都有子节点,则执行updateChildren函数比较子节点
-
-
常见的Vue性能优化
-
路由懒加载
-
keep-alive 缓存页面
-
使用 v-show 复用 DOM
-
v-for 遍历避免同时使用 v-if
-
长列表性能优化:单纯的展示,不做改变,就不需要做响应化。
-
图片懒加载
-
第三方组件按需引入
-
SSR
keep-alive
在动态组件切换的过程中,组件的实例都是重新创建的。keep-alive 包裹动态组件时,会缓存不活动的组件实例,就是缓存组件内部状态,避免重新渲染。
-
三个属性:
-
include:字符串或正则表达式,只有名称匹配的组件才会被缓存
-
exclude:字符串或正则表达式,任何名称匹配的组件都不会被缓存
-
max:数字,最多可以缓存多少组件实例
-
-
优点:
-
较少的CPU和内存的使⽤(由于同时打开的连接的减少了);
-
降低拥塞控制 (TCP连接减少了);
-
减少了后续请求的延迟(⽆需再进⾏握⼿);
-
报告错误⽆需关闭TCP连接
-
-
缺点
-
长时间的 TCP 连接,会导致系统资源无效占用,浪费系统资源。
-
生命周期
说⼀下Vue的⽣命周期
Vue实例从开始创建、初始化数据、编译模板、挂载DOM --> 渲染、更新 -->渲染、卸载 称为 Vue的一个完整的生命周期。
-
beforeCreate(创建前):
数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据
-
created(创建后) :
实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到 $el 属性。常用于异步数据获取
-
beforeMount(挂载前):
在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置∶ 编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
-
mounted(挂载后):
el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
-
beforeUpdate(更新前):
响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染
-
updated(更新后) :
在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
-
beforeDestroy(销毁前):
实例销毁之前调用。这一步,实例仍然完全可用,this 仍能获取到实例。可用于一些定时器或订阅的取消。
-
destroyed(销毁后):
实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
created和mounted的区别
-
created:在模板渲染成html前调⽤,即通常初始化某些属性值,然后再渲染成视图。
-
mounted:在模板渲染成html后调⽤,通常是初始化⻚⾯完成后,再对html的dom节点进⾏⼀些需要的操作。
在 created 请求异步数据的优点
-
能更快获取到服务端数据,减少⻚⾯加载时间,⽤户体验更好;
-
SSR不⽀持 beforeMount 、mounted 钩⼦函数,放在 created 中有助于⼀致性;
组件通信
父子组件通信
-
props: 父组件向子组件传递数据
-
子组件设置
props
属性,定义接收父组件传递过来的参数 -
父组件在使用子组件标签中通过字面量来传递值
//Father.vue
<Children name="jack" age=18/>
//Children.vue
props:{
// 字符串形式
name:String // 接收的类型参数
// 对象形式
age:{
type:Number, // 接收的类型为数值
defaule:18, // 默认值为18
require:true // age属性必须传递
}
} -
-
$emit: 子组件向父组件传递数据
-
子组件通过
$emit触发
自定义事件,$emit
第二个参数为传递的数值 -
父组件绑定监听器获取到子组件传递过来的参数
//Chilfen.vue
this.$emit('add', good)
//Father.vue
<Children @add="cartAdd($event)" /> -
-
ref:
-
父组件在使用子组件的时候设置
ref
-
父组件通过设置子组件
ref
来获取数据
//Father.vue
<Children ref="foo" />
this.$refs.foo // 获取子组件实例,通过子组件实例我们就能拿到对应的数据 -
-
EventBus
-
创建一个中央事件总线
EventBus
-
兄弟组件通过
$emit
触发自定义事件,$emit
第二个参数为传递的数值 -
另一个兄弟组件通过
$on
监听自定义事件
Bus.js
// 创建一个中央时间总线类
class Bus {
constructor() {
this.callbacks = {}; // 存放事件的名字
}
$on(name, fn) {
this.callbacks[name] = this.callbacks[name] || [];
this.callbacks[name].push(fn);
}
$emit(name, args) {
if (this.callbacks[name]) {
this.callbacks[name].forEach((cb) => cb(args));
}
}
}
// main.js
Vue.prototype.$bus = new Bus() // 将$bus挂载到vue实例的原型上
// 另一种方式
Vue.prototype.$bus = new Vue() // Vue已经实现了Bus的功能//Children1.vue
this.$bus.$emit('foo')
//Children2.vue
this.$bus.$on('foo', this.handle) -
-
$parent 或$ root
-
通过共同祖辈
$parent
或者$root
搭建通信桥连
//Children1.vue
this.$parent.on('add',this.add)
//Children2.vue
this.$parent.emit('add') -
祖孙与后代组件之间的通信
-
$attrs 与$ listeners
-
设置批量向下传属性
$attrs
和$listeners
-
包含了父级作用域中不作为
prop
被识别 (且获取) 的特性绑定 ( class 和 style 除外)。 -
可以通过
v-bind="$attrs"
传⼊内部组件
// child:并未在props中声明foo
<p>{{$attrs.foo}}</p>
// parent
<HelloWorld foo="foo"/>// 给Grandson隔代传值,communication/index.vue
<Child2 msg="chuanzhi" @some-event="onSomeEvent"></Child2>
// Child2做展开
<Grandson v-bind="$attrs" v-on="$listeners"></Grandson>
// Grandson使⽤
<div @click="$emit('some-event', 'msg from grandson')">
{{msg}}
</div> -
-
provide 与 inject
-
在祖先组件定义
provide
属性,返回传递的值 -
在后代组件通过
inject
接收组件传递过来的值
祖先组件
provide(){
return {
foo:'foo'
}
}后代组件
inject:['foo'] // 获取到祖先组件传递过来的值
-
非关系组件间之间的通信
-
vuex : 存储共享变量的容器
-
state
用来存放共享变量的地方 -
getter
,可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值 -
mutations
用来存放修改state
的方法。 -
actions
也是用来存放修改state的方法,不过action
是在mutations
的基础上进行,不能直接进行修改。常用来做一些异步操作
-
路由
路由的 hash 和 history 模式
-
hash模式: 开发中默认的模式, url中带着 #
-
原理: hash模式的主要原理就是onhashchange()事件
window.onhashchange = function(event){
console.log(event.oldURL, event.newURL);
let hash = location.hash.slice(1);
} -
使⽤onhashchange()事件的,在⻚⾯的hash值发⽣变化时,⽆需向后端发起请求,window就可以监听事件的改变,并按规则加载相应的代码。除此之外,hash值变化对应的URL都会被浏览器记录下来,这样浏览器就能实现⻚⾯的前进和后退。虽然是没有请求后端服务器,但是⻚⾯的hash值和对应的URL关联起来了。
-
history模式:history 模式的 URL中没有 #,他使用的是传统的路由分发模式,在用户输入一个URL时,服务器会接收这个请求,并解析这个URL,然后做出相应的逻辑处理。
-
特点: history 模式的 URL中没有 #,会好看一点。history模式需要后台配置支持。如果后台没有正确配置,访问时会返回404。
-
API: history api可以分为两大部分,切换历史状态和修改历史状态:
-
修改历史状态:包括了 HTML5 History Interface 中新增的 pushState() 和replaceState() 方法,这两个方法应用于浏览器的历史记录栈,提供了对历史记录进行修改的功能。只是当他们进行修改时,虽然修改了url,但浏览器不会立即向后端发送请求。如果要做到改变url但又不刷新页面的效果,就需要前端用上这两个API。
-
切换历史状态: 包括forward()、back()、go()三个方法,对应浏览器的前进,后退,跳转操作
-
-
-
对比:
-
调用history.pushState()相比于直接修改hash,存在以下优势:
-
pushState()设置的新URL可以是与当前URL同源的任意URL;而hash只可修改#后面的部分,因此只能设置与当前URL同文档的URL
-
pushState()设置的新URL可以与当前URL一模一样,这样也会把记录添加到栈中;而hash设置的新值必须与原来不一样才会触发动作将记录添加到栈中
-
pushState()通过stateObject参数可以添加任意类型的数据到记录中;而hash只可添加短字符串
-
虽然history模式丢弃了丑陋的#。但是,它也有自己的缺点,就是在刷新页面的时候,如果没有相应的路由或资源,就会刷出404来。 如果想要切换到history模式,前后端都要进行配置(后端配置比较复杂)。
-
$route 和$router 的区别
-
$route 是用来获取路由信息的。$ route是一个跳转的路由对象(路由信息对象)。
//常用的属性
$route.path 字符串,相当于当前页面的绝对路径。
$route.params 对象,包含路由中的动态片段和全匹配片段的键值对,不会拼接到路由的url后面
$route.query 对象,包含路由中查询参数的键值对。会拼接到路由url后面
$route.router 路由规则所属的路由器
$route.name 当前路由的名字,如果没有使用具体路径,则名字为空
-
$router 是用来操作路由的。 $router是VueRouter的一个实例,他包含了所有的路由,包括路由的跳转方法,钩子函数等,也包含一些子对象(例如history)
//常用的方法
this.$router.push()
this.$router.replace()
this.$router.go()
this.$router.forward()
this.$router.back()
路由跳转和链接跳转区别
-
使用链接跳转比较简单,但是刷新了页面。
-
使用路由跳转,不会刷新页面
params和query区别
-
query使用 path 引入,params使用 name 引入
-
query会在浏览器地址栏中显示参数,params则不显示
-
query 刷新不会丢失里面的数据,params 刷新会丢失里面的数据
vue导航守卫
-
全局守卫:router.beforeEach
-
任何路由跳转到另外一个路由的时候都会触发,而且是跳转前触发的。
-
const router = new VueRouter({ ... })
router.beforeEach((to, from, next) => {
// ...
})
-
to :要去的路由对象
-
from:要离开的路由对象
-
next:存放方法,判断是否能够跳转成功。
-
next() 能跳转成功
-
next(false) :跳转失败,中断跳转。
-
next({path:“/login”}) :跳转失败。跳转到指定的路径。
-
vuex