action 与 mutation 的区别
mutation
是同步更新,$watch
严格模式下会报错action
是异步操作,可以获取数据后调用mutation
提交最终数据
Vue路由hash模式和history模式
1. hash
模式
早期的前端路由的实现就是基于 location.hash
来实现的。其实现原理很简单,location.hash
的值就是 URL
中 #
后面的内容。比如下面这个网站,它的 location.hash
的值为 '#search'
https://interview2.poetries.top#search
hash 路由模式的实现主要是基于下面几个特性
URL
中hash
值只是客户端的一种状态,也就是说当向服务器端发出请求时,hash
部分不会被发送;hash
值的改变,都会在浏览器的访问历史中增加一个记录。因此我们能通过浏览器的回退、前进按钮控制hash
的切换;- 可以通过
a
标签,并设置href
属性,当用户点击这个标签后,URL
的hash
值会发生改变;或者使用JavaScript
来对loaction.hash
进行赋值,改变URL
的hash
值; - 我们可以使用
hashchange
事件来监听hash
值的变化,从而对页面进行跳转(渲染)
window.addEventListener("hashchange", funcRef, false);
每一次改变 hash
(window.location.hash
),都会在浏览器的访问历史中增加一个记录利用 hash
的以上特点,就可以来实现前端路由“更新视图但不重新请求页面”的功能了
特点 :兼容性好但是不美观
2. history
模式
history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
可以对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变更
window.history.pushState(null, null, path);
window.history.replaceState(null, null, path);
这两个方法有个共同的特点:当调用他们修改浏览器历史记录栈后,虽然当前 URL
改变了,但浏览器不会刷新页面,这就为单页应用前端路由“更新视图但不重新请求页面”提供了基础。
history 路由模式的实现主要基于存在下面几个特性:
pushState
和repalceState
两个API
来操作实现URL
的变化 ;- 我们可以使用
popstate
事件来监听url
的变化,从而对页面进行跳转(渲染); history.pushState()
或history.replaceState()
不会触发popstate
事件,这时我们需要手动触发页面跳转(渲染)。
特点 :虽然美观,但是刷新会出现 404
需要后端进行配置
为什么要用虚拟DOM
(1)保证性能下限,在不进行手动优化的情况下,提供过得去的性能 看一下页面渲染的流程:解析HTML -> 生成DOM -> 生成 CSSOM -> Layout -> Paint -> Compiler 下面对比一下修改DOM时真实DOM操作和Virtual DOM的过程,来看一下它们重排重绘的性能消耗∶
- 真实DOM∶ 生成HTML字符串+重建所有的DOM元素
- 虚拟DOM∶ 生成vNode+ DOMDiff+必要的dom更新
Virtual DOM的更新DOM的准备工作耗费更多的时间,也就是JS层面,相比于更多的DOM操作它的消费是极其便宜的。尤雨溪在社区论坛中说道∶ 框架给你的保证是,你不需要手动优化的情况下,依然可以给你提供过得去的性能。 (2)跨平台 Virtual DOM本质上是JavaScript的对象,它可以很方便的跨平台操作,比如服务端渲染、uniapp等。
用过pinia吗?有什么优点?
1. pinia是什么?
- 在
Vue3
中,可以使用传统的Vuex
来实现状态管理,也可以使用最新的pinia
来实现状态管理,我们来看看官网如何解释pinia
的:Pinia
是Vue
的存储库,它允许您跨组件/页面共享状态。- 实际上,
pinia
就是Vuex
的升级版,官网也说过,为了尊重原作者,所以取名pinia
,而没有取名Vuex
,所以大家可以直接将pinia
比作为Vue3
的Vuex
2. 为什么要使用pinia?
Vue2
和Vue3
都支持,这让我们同时使用Vue2
和Vue3
的小伙伴都能很快上手。pinia
中只有state
、getter
、action
,抛弃了Vuex
中的Mutation
,Vuex
中mutation
一直都不太受小伙伴们的待见,pinia
直接抛弃它了,这无疑减少了我们工作量。pinia
中action
支持同步和异步,Vuex
不支持- 良好的
Typescript
支持,毕竟我们Vue3
都推荐使用TS
来编写,这个时候使用pinia
就非常合适了 - 无需再创建各个模块嵌套了,
Vuex
中如果数据过多,我们通常分模块来进行管理,稍显麻烦,而pinia
中每个store
都是独立的,互相不影响。 - 体积非常小,只有
1KB
左右。 pinia
支持插件来扩展自身功能。- 支持服务端渲染
3. pinna使用
- 准备工作
我们这里搭建一个最新的Vue3 + TS + Vite
项目
npm create vite@latest my-vite-app --template vue-ts
pinia
基础使用
yarn add pinia
// main.ts
import { createApp } from "vue";
import App from "./App.vue";
import { createPinia } from "pinia";
const pinia = createPinia();
const app = createApp(App);
app.use(pinia);
app.mount("#app");
2.1 创建store
//sbinsrc/store/user.ts
import { defineStore } from 'pinia'
// 第一个参数是应用程序中 store 的唯一 id
export const useUsersStore = defineStore('users', {
// 其它配置项
})
创建store
很简单,调用pinia
中的defineStore
函数即可,该函数接收两个参数:
name
:一个字符串,必传项,该store
的唯一id
。options
:一个对象,store
的配置项,比如配置store
内的数据,修改数据的方法等等。
我们可以定义任意数量的store
,因为我们其实一个store
就是一个函数,这也是pinia
的好处之一,让我们的代码扁平化了,这和Vue3
的实现思想是一样的
2.2 使用store
<!-- src/App.vue -->
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
console.log(store);
</script>
2.3 添加state
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
});
2.4 读取state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
const name = ref<string>(store.name);
const age = ref<number>(store.age);
const sex = ref<string>(store.sex);
</script>
上段代码中我们直接通过store.age
等方式获取到了store
存储的值,但是大家有没有发现,这样比较繁琐,我们其实可以用解构的方式来获取值,使得代码更简洁一点
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store); // storeToRefs获取的值是响应式的
2.5 修改state
数据
<template>
<img alt="Vue logo" src="./assets/logo.png" />
<p>姓名:{{ name }}</p>
<p>年龄:{{ age }}</p>
<p>性别:{{ sex }}</p>
<button @click="changeName">更改姓名</button>
</template>
<script setup lang="ts">
import child from './child.vue';
import { useUsersStore, storeToRefs } from "../src/store/user";
const store = useUsersStore();
const { name, age, sex } = storeToRefs(store);
const changeName = () => {
store.name = "张三";
console.log(store);
};
</script>
2.6 重置state
- 有时候我们修改了
state
数据,想要将它还原,这个时候该怎么做呢?就比如用户填写了一部分表单,突然想重置为最初始的状态。 - 此时,我们直接调用
store
的$reset()
方法即可,继续使用我们的例子,添加一个重置按钮
<button @click="reset">重置store</button>
// 重置store
const reset = () => {
store.$reset();
};
当我们点击重置按钮时,store
中的数据会变为初始状态,页面也会更新
2.7 批量更改state
数据
如果我们一次性需要修改很多条数据的话,有更加简便的方法,使用store
的$patch
方法,修改app.vue
代码,添加一个批量更改数据的方法
<button @click="patchStore">批量修改数据</button>
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
- 有经验的小伙伴可能发现了,我们采用这种批量更改的方式似乎代价有一点大,假如我们
state
中有些字段无需更改,但是按照上段代码的写法,我们必须要将state中的所有字段例举出了。 - 为了解决该问题,
pinia
提供的$patch
方法还可以接收一个回调函数,它的用法有点像我们的数组循环回调函数了。
store.$patch((state) => {
state.items.push({ name: 'shoes', quantity: 1 })
state.hasChanged = true
})
2.8 直接替换整个state
pinia
提供了方法让我们直接替换整个state
对象,使用store
的$state
方法
store.$state = { counter: 666, name: '张三' }
上段代码会将我们提前声明的state
替换为新的对象,可能这种场景用得比较少
getters
属性
getters
是defineStore
参数配置项里面的另一个属性- 可以把
getter
想象成Vue
中的计算属性,它的作用就是返回一个新的结果,既然它和Vue
中的计算属性类似,那么它肯定也是会被缓存的,就和computed
一样
3.1 添加getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 10,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
},
})
上段代码中我们在配置项参数中添加了getter
属性,该属性对象中定义了一个getAddAge
方法,该方法会默认接收一个state
参数,也就是state
对象,然后该方法返回的是一个新的数据
3.2 使用getter
<template>
<p>新年龄:{{ store.getAddAge }}</p>
<button @click="patchStore">批量修改数据</button>
</template>
<script setup lang="ts">
import { useUsersStore } from "../src/store/user";
const store = useUsersStore();
// 批量修改数据
const patchStore = () => {
store.$patch({
name: "张三",
age: 100,
sex: "女",
});
};
</script>
上段代码中我们直接在标签上使用了store.gettAddAge
方法,这样可以保证响应式,其实我们state
中的name
等属性也可以以此种方式直接在标签上使用,也可以保持响应式
3.3 getter
中调用其它getter
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return state.age + 100;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
3.3 getter
传参
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
});
<p>新年龄:{{ store.getAddAge(1100) }}</p>
actions
属性
- 前面我们提到的
state
和getter
s属性都主要是数据层面的,并没有具体的业务逻辑代码,它们两个就和我们组件代码中的data
数据和computed
计算属性一样。 - 那么,如果我们有业务代码的话,最好就是卸载
actions
属性里面,该属性就和我们组件代码中的methods
相似,用来放置一些处理业务逻辑的方法。 actions
属性值同样是一个对象,该对象里面也是存储的各种各样的方法,包括同步方法和异步方法
4.1 添加actions
export const useUsersStore = defineStore("users", {
state: () => {
return {
name: "test",
age: 20,
sex: "男",
};
},
getters: {
getAddAge: (state) => {
return (num: number) => state.age + num;
},
getNameAndAge(): string {
return this.name + this.getAddAge; // 调用其它getter
},
},
actions: {
// 在实际场景中,该方法可以是任何逻辑,比如发送请求、存储token等等。大家把actions方法当作一个普通的方法即可,特殊之处在于该方法内部的this指向的是当前store
saveName(name: string) {
this.name = name;
},
},
});
4.2 使用actions
使用actions
中的方法也非常简单,比如我们在App.vue
中想要调用该方法
const saveName = () => {
store.saveName("poetries");
};
总结
pinia
的知识点很少,如果你有Vuex基础,那么学起来更是易如反掌
pinia无非就是以下3个大点:
state
getters
actions
ref和reactive异同
这是Vue3
数据响应式中非常重要的两个概念,跟我们写代码关系也很大
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
const obj = reactive({ count: 0 })
obj.count++
ref
接收内部值(inner value
)返回响应式Ref
对象,reactive
返回响应式代理对象- 从定义上看
ref
通常用于处理单值的响应式,reactive
用于处理对象类型的数据响应式 - 两者均是用于构造响应式数据,但是
ref
主要解决原始值的响应式问题 ref
返回的响应式数据在JS中使用需要加上.value
才能访问其值,在视图中使用会自动脱ref
,不需要.value
;ref
可以接收对象或数组等非原始值,但内部依然是reactive
实现响应式;reactive
内部如果接收Re
f对象会自动脱ref
;使用展开运算符(...
)展开reactive
返回的响应式对象会使其失去响应性,可以结合toRefs()
将值转换为Ref
对象之后再展开。reactive
内部使用Proxy
代理传入对象并拦截该对象各种操作,从而实现响应式。ref
内部封装一个RefImpl
类,并设置get value/set value
,拦截用户对值的访问,从而实现响应式
怎么监听vuex数据的变化
分析
vuex
数据状态是响应式的,所以状态变视图跟着变,但是有时还是需要知道数据状态变了从而做一些事情。- 既然状态都是响应式的,那自然可以
watch
,另外vuex
也提供了订阅的API:store.subscribe()
回答范例
- 我知道几种方法:
- 可以通过
watch
选项或者watch
方法监听状态 - 可以使用
vuex
提供的API:store.subscribe()
watch
选项方式,可以以字符串形式监听$store.state.xx
;subscribe
方式,可以调用store.subscribe(cb)
,回调函数接收mutation
对象和state
对象,这样可以进一步判断mutation.type
是否是期待的那个,从而进一步做后续处理。watch
方式简单好用,且能获取变化前后值,首选;subscribe
方法会被所有commit
行为触发,因此还需要判断mutation.type
,用起来略繁琐,一般用于vuex
插件中
实践
watch
方式
const app = createApp({
watch: {
'$store.state.counter'() {
console.log('counter change!');
}
}
})
subscribe
方式:
store.subscribe((mutation, state) => {
if (mutation.type === 'add') {
console.log('counter change in subscribe()!');
}
})
参考 前端进阶面试题详细解答
写过自定义指令吗?原理是什么
回答范例
-
Vue
有一组默认指令,比如v-model
或v-for
,同时Vue
也允许用户注册自定义指令来扩展Vue能力 -
自定义指令主要完成一些可复用低层级
DOM
操作 -
使用自定义指令分为定义、注册和使用三步:
-
定义自定义指令有两种方式:对象和函数形式,前者类似组件定义,有各种生命周期;后者只会在
mounte
d和updated
时执行 -
注册自定义指令类似组件,可以使用
app.directive()
全局注册,使用{directives:{xxx}}
局部注册 -
使用时在注册名称前加上
v-
即可,比如v-focus
- 我在项目中常用到一些自定义指令,例如:
-
复制粘贴
v-copy
-
长按
v-longpress
-
防抖
v-debounce
-
图片懒加载
v-lazy
-
按钮权限
v-premission
-
页面水印
v-waterMarker
-
拖拽指令
v-draggable
vue3
中指令定义发生了比较大的变化,主要是钩子的名称保持和组件一致,这样开发人员容易记忆,不易犯错。另外在v3.2
之后,可以在setup
中以一个小写v
开头方便的定义自定义指令,更简单了
基本使用
当Vue中的核心内置指令不能够满足我们的需求时,我们可以定制自定义的指令用来满足开发的需求
我们看到的v-
开头的行内属性,都是指令,不同的指令可以完成或实现不同的功能,对普通 DOM元素进行底层操作,这时候就会用到自定义指令。除了核心功能默认内置的指令 (v-model
和 v-show
),Vue
也允许注册自定义指令
// 指令使用的几种方式:
//会实例化一个指令,但这个指令没有参数
`v-xxx`
// -- 将值传到指令中
`v-xxx="value"`
// -- 将字符串传入到指令中,如`v-html="'<p>内容</p>'"`
`v-xxx="'string'"`
// -- 传参数(`arg`),如`v-bind:class="className"`
`v-xxx:arg="value"`
// -- 使用修饰符(`modifier`)
`v-xxx:arg.modifier="value"`
注册一个自定义指令有全局注册与局部注册
// 全局注册注册主要是用过Vue.directive方法进行注册
// Vue.directive第一个参数是指令的名字(不需要写上v-前缀),第二个参数可以是对象数据,也可以是一个指令函数
// 注册一个全局自定义指令 `v-focus`
Vue.directive('focus', {
// 当被绑定的元素插入到 DOM 中时……
inserted: function (el) {
// 聚焦元素
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
})
// 局部注册通过在组件options选项中设置directive属性
directives: {
focus: {
// 指令的定义
inserted: function (el) {
el.focus() // 页面加载完成之后自动让输入框获取到焦点的小功能
}
}
}
// 然后你可以在模板中任何元素上使用新的 v-focus property,如下:
<input v-focus />
钩子函数
bind
:只调用一次,指令第一次绑定到元素时调用。在这里可以进行一次性的初始化设置。inserted
:被绑定元素插入父节点时调用 (仅保证父节点存在,但不一定已被插入文档中)。update
:被绑定于元素所在的模板更新时调用,而无论绑定值是否变化。通过比较更新前后的绑定值,可以忽略不必要的模板更新。componentUpdated
:被绑定元素所在模板完成一次更新周期时调用。unbind
:只调用一次,指令与元素解绑时调用。
所有的钩子函数的参数都有以下:
el
:指令所绑定的元素,可以用来直接操作 DOMbinding
:一个对象,包含以下property
:name
:指令名,不包括v-
前缀。value
:指令的绑定值,例如:v-my-directive="1 + 1"
中,绑定值为2
。oldValue
:指令绑定的前一个值,仅在update
和componentUpdated
钩子中可用。无论值是否改变都可用。expression
:字符串形式的指令表达式。例如v-my-directive="1 + 1"
中,表达式为"1 + 1"
。arg
:传给指令的参数,可选。例如v-my-directive:foo
中,参数为"foo"
。modifiers
:一个包含修饰符的对象。例如:v-my-directive.foo.bar
中,修饰符对象为{ foo: true, bar: true }
vnode
:Vue
编译生成的虚拟节点oldVnode
:上一个虚拟节点,仅在update
和componentUpdated
钩子中可用
除了 el
之外,其它参数都应该是只读的,切勿进行修改。如果需要在钩子之间共享数据,建议通过元素的 dataset
来进行
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
<script>
Vue.directive('demo', function (el, binding) {
console.log(binding.value.color) // "white"
console.log(binding.value.text) // "hello!"
})
</script>
应用场景
使用自定义组件组件可以满足我们日常一些场景,这里给出几个自定义组件的案例:
- 防抖
// 1.设置v-throttle自定义指令
Vue.directive('throttle', {
bind: (el, binding) => {
let throttleTime = binding.value; // 防抖时间
if (!throttleTime) { // 用户若不设置防抖时间,则默认2s
throttleTime = 2000;
}
let cbFun;
el.addEventListener('click', event => {
if (!cbFun) { // 第一次执行
cbFun = setTimeout(() => {
cbFun = null;
}, throttleTime);
} else {
event && event.stopImmediatePropagation();
}
}, true);
},
});
// 2.为button标签设置v-throttle自定义指令
<button @click="sayHello" v-throttle>提交</button>
- 图片懒加载
设置一个v-lazy
自定义组件完成图片懒加载
const LazyLoad = {
// install方法
install(Vue,options){
// 代替图片的loading图
let defaultSrc = options.default;
Vue.directive('lazy',{
bind(el,binding){
LazyLoad.init(el,binding.value,defaultSrc);
},
inserted(el){
// 兼容处理
if('InterpObserver' in window){
LazyLoad.observe(el);
}else{
LazyLoad.listenerScroll(el);
}
},
})
},
// 初始化
init(el,val,def){
// src 储存真实src
el.setAttribute('src',val);
// 设置src为loading图
el.setAttribute('src',def);
},
// 利用InterpObserver监听el
observe(el){
let io = new InterpObserver(entries => {
let realSrc = el.dataset.src;
if(entries[0].isIntersecting){
if(realSrc){
el.src = realSrc;
el.removeAttribute('src');
}
}
});
io.observe(el);
},
// 监听scroll事件
listenerScroll(el){
let handler = LazyLoad.throttle(LazyLoad.load,300);
LazyLoad.load(el);
window.addEventListener('scroll',() => {
handler(el);
});
},
// 加载真实图片
load(el){
let windowHeight = document.documentElement.clientHeight
let elTop = el.getBoundingClientRect().top;
let elBtm = el.getBoundingClientRect().bottom;
let realSrc = el.dataset.src;
if(elTop - windowHeight<0&&elBtm > 0){
if(realSrc){
el.src = realSrc;
el.removeAttribute('src');
}
}
},
// 节流
throttle(fn,delay){
let timer;
let prevTime;
return function(...args){
let currTime = Date.now();
let context = this;
if(!prevTime) prevTime = currTime;
clearTimeout(timer);
if(currTime - prevTime > delay){
prevTime = currTime;
fn.apply(context,args);
clearTimeout(timer);
return;
}
timer = setTimeout(function(){
prevTime = Date.now();
timer = null;
fn.apply(context,args);
},delay);
}
}
}
export default LazyLoad;
- 一键 Copy的功能
import { Message } from 'ant-design-vue';
const vCopy = { //
/*
bind 钩子函数,第一次绑定时调用,可以在这里做初始化设置
el: 作用的 dom 对象
value: 传给指令的值,也就是我们要 copy 的值
*/
bind(el, { value }) {
el.$value = value; // 用一个全局属性来存传进来的值,因为这个值在别的钩子函数里还会用到
el.handler = () => {
if (!el.$value) {
// 值为空的时候,给出提示,我这里的提示是用的 ant-design-vue 的提示,你们随意
Message.warning('无复制内容');
return;
}
// 动态创建 textarea 标签
const textarea = document.createElement('textarea');
// 将该 textarea 设为 readonly 防止 iOS 下自动唤起键盘,同时将 textarea 移出可视区域
textarea.readOnly = 'readonly';
textarea.style.position = 'absolute';
textarea.style.left = '-9999px';
// 将要 copy 的值赋给 textarea 标签的 value 属性
textarea.value = el.$value;
// 将 textarea 插入到 body 中
document.body.appendChild(textarea);
// 选中值并复制
textarea.select();
// textarea.setSelectionRange(0, textarea.value.length);
const result = document.execCommand('Copy');
if (result) {
Message.success('复制成功');
}
document.body.removeChild(textarea);
};
// 绑定点击事件,就是所谓的一键 copy 啦
el.addEventListener('click', el.handler);
},
// 当传进来的值更新的时候触发
componentUpdated(el, { value }) {
el.$value = value;
},
// 指令与元素解绑的时候,移除事件绑定
unbind(el) {
el.removeEventListener('click', el.handler);
},
};
export default vCopy;
- 拖拽
<div ref="a" id="bg" v-drag></div>
directives: {
drag: {
bind() {},
inserted(el) {
el.onmousedown = (e) => {
let x = e.clientX - el.offsetLeft;
let y = e.clientY - el.offsetTop;
document.onmousemove = (e) => {
let xx = e.clientX - x + "px";
let yy = e.clientY - y + "px";
el.style.left = xx;
el.style.top = yy;
};
el.onmouseup = (e) => {
document.onmousemove = null;
};
};
},
},
}
原理
- 指令本质上是装饰器,是
vue
对HTML
元素的扩展,给HTML
元素增加自定义功能。vue
编译DOM
时,会找到指令对象,执行指令的相关方法。 - 自定义指令有五个生命周期(也叫钩子函数),分别是
bind
、inserted
、update
、componentUpdated
、unbind
原理
- 在生成
ast
语法树时,遇到指令会给当前元素添加directives
属性 - 通过
genDirectives
生成指令代码 - 在
patch
前将指令的钩子提取到cbs
中,在patch
过程中调用对应的钩子 - 当执行指令对应钩子函数时,调用对应指令定义的方法
说说Vue 3.0中Treeshaking特性?举例说明一下?
一、是什么
Tree shaking
是一种通过清除多余代码方式来优化项目打包体积的技术,专业术语叫 Dead code elimination
简单来讲,就是在保持代码运行结果不变的前提下,去除无用的代码
如果把代码打包比作制作蛋糕,传统的方式是把鸡蛋(带壳)全部丢进去搅拌,然后放入烤箱,最后把(没有用的)蛋壳全部挑选并剔除出去
而treeshaking
则是一开始就把有用的蛋白蛋黄(import)放入搅拌,最后直接作出蛋糕
也就是说 ,tree shaking
其实是找出使用的代码
在Vue2
中,无论我们使用什么功能,它们最终都会出现在生产代码中。主要原因是Vue
实例在项目中是单例的,捆绑程序无法检测到该对象的哪些属性在代码中被使用到
import Vue from 'vue'
Vue.nextTick(() => {})
而Vue3
源码引入tree shaking
特性,将全局 API 进行分块。如果您不使用其某些功能,它们将不会包含在您的基础包中
import { nextTick, observable } from 'vue'
nextTick(() => {})
二、如何做
Tree shaking
是基于ES6
模板语法(import
与exports
),主要是借助ES6
模块的静态编译思想,在编译时就能确定模块的依赖关系,以及输入和输出的变量
Tree shaking
无非就是做了两件事:
- 编译阶段利用
ES6 Module
判断哪些模块已经加载 - 判断那些模块和变量未被使用或者引用,进而删除对应代码
下面就来举个例子:
通过脚手架vue-cli
安装Vue2
与Vue3
项目
vue create vue-demo
Vue2 项目
组件中使用data
属性
<script>
export default {
data: () => ({
count: 1,
}),
};
</script>
对项目进行打包,体积如下图
为组件设置其他属性(compted
、watch
)
export default {
data: () => ({
question:"",
count: 1,
}),
computed: {
double: function () {
return this.count * 2;
},
},
watch: {
question: function (newQuestion, oldQuestion) {
this.answer = 'xxxx'
}
};
再一次打包,发现打包出来的体积并没有变化
Vue3 项目
组件中简单使用
import { reactive, defineComponent } from "vue";
export default defineComponent({
setup() {
const state = reactive({
count: 1,
});
return {
state,
};
},
});
将项目进行打包
在组件中引入computed
和watch
import { reactive, defineComponent, computed, watch } from "vue";
export default defineComponent({
setup() {
const state = reactive({
count: 1,
});
const double = computed(() => {
return state.count * 2;
});
watch(
() => state.count,
(count, preCount) => {
console.log(count);
console.log(preCount);
}
);
return {
state,
double,
};
},
});
再次对项目进行打包,可以看到在引入computer
和watch
之后,项目整体体积变大了
三、作用
通过Tree shaking
,Vue3
给我们带来的好处是:
- 减少程序体积(更小)
- 减少程序执行时间(更快)
- 便于将来对程序架构进行优化(更友好)
Vue-router 路由模式有几种
vue-router
有 3
种路由模式:hash
、history
、abstract
,对应的源码如下所示
switch (mode) {
case 'history':
this.history = new HTML5History(this, options.base)
break
case 'hash':
this.history = new HashHistory(this, options.base, this.fallback)
break
case 'abstract':
this.history = new AbstractHistory(this, options.base)
break
default:
if (process.env.NODE_ENV !== 'production') {
assert(false, `invalid mode: ${mode}`)
}
}
其中,3 种路由模式的说明如下:
hash
: 使用URL hash
值来作路由,支持所有浏览器history
: 依赖HTML5 History API
和服务器配置abstract
: 支持所有JavaScript
运行环境,如Node.js
服务器端。如果发现没有浏览器的API
,路由会自动强制进入这个模式.
vue和react的区别
=> 相同点:
1. 数据驱动页面,提供响应式的试图组件
2. 都有virtual DOM,组件化的开发,通过props参数进行父子之间组件传递数据,都实现了webComponents规范
3. 数据流动单向,都支持服务器的渲染SSR
4. 都有支持native的方法,react有React native, vue有wexx
=> 不同点:
1.数据绑定:Vue实现了双向的数据绑定,react数据流动是单向的
2.数据渲染:大规模的数据渲染,react更快
3.使用场景:React配合Redux架构适合大规模多人协作复杂项目,Vue适合小快的项目
4.开发风格:react推荐做法jsx + inline style把html和css都写在js了
vue是采用webpack + vue-loader单文件组件格式,html, js, css同一个文件
vue 中使用了哪些设计模式
1.工厂模式 - 传入参数即可创建实例
虚拟 DOM 根据参数的不同返回基础标签的 Vnode 和组件 Vnode
2.单例模式 - 整个程序有且仅有一个实例
vuex 和 vue-router 的插件注册方法 install 判断如果系统存在实例就直接返回掉
3.发布-订阅模式 (vue 事件机制)
4.观察者模式 (响应式数据原理)
5.装饰模式: (@装饰器的用法)
6.策略模式 策略模式指对象有某个行为,但是在不同的场景中,该行为有不同的实现方案-比如选项的合并策略
...其他模式欢迎补充
了解nextTick吗?
异步方法,异步渲染最后一步,与JS事件循环联系紧密。主要使用了宏任务微任务(setTimeout
、promise
那些),定义了一个异步方法,多次调用nextTick
会将方法存入队列,通过异步方法清空当前队列。
Vue模版编译原理知道吗,能简单说一下吗?
简单说,Vue的编译过程就是将template
转化为render
函数的过程。会经历以下阶段:
- 生成AST树
- 优化
- codegen
首先解析模版,生成AST语法树
(一种用JavaScript对象的形式来描述整个模板)。 使用大量的正则表达式对模板进行解析,遇到标签、文本的时候都会执行对应的钩子进行相关处理。
Vue的数据是响应式的,但其实模板中并不是所有的数据都是响应式的。有一些数据首次渲染后就不会再变化,对应的DOM也不会变化。那么优化过程就是深度遍历AST树,按照相关条件对树节点进行标记。这些被标记的节点(静态节点)我们就可以跳过对它们的比对
,对运行时的模板起到很大的优化作用。
编译的最后一步是将优化后的AST树转换为可执行的代码
。
Composition API 与 Options API 有什么不同
分析
Vue3
最重要更新之一就是Composition API
,它具有一些列优点,其中不少是针对Options API
暴露的一些问题量身打造。是Vue3
推荐的写法,因此掌握好Composition API
应用对掌握好Vue3
至关重要
What is Composition API?(opens new window)
Composition API
出现就是为了解决Options API导致相同功能代码分散的现象
体验
Composition API
能更好的组织代码,下面用composition api
可以提取为useCount()
,用于组合、复用
compositon api提供了以下几个函数:
setup
ref
reactive
watchEffect
watch
computed
toRefs
- 生命周期的
hooks
回答范例
Composition API
是一组API
,包括:Reactivity API
、生命周期钩子
、依赖注入
,使用户可以通过导入函数方式编写vue
组件。而Options API
则通过声明组件选项的对象形式编写组件Composition API
最主要作用是能够简洁、高效复用逻辑。解决了过去Options API
中mixins
的各种缺点;另外Composition API
具有更加敏捷的代码组织能力,很多用户喜欢Options API
,认为所有东西都有固定位置的选项放置代码,但是单个组件增长过大之后这反而成为限制,一个逻辑关注点分散在组件各处,形成代码碎片,维护时需要反复横跳,Composition API
则可以将它们有效组织在一起。最后Composition API
拥有更好的类型推断,对ts支持更友好,Options API
在设计之初并未考虑类型推断因素,虽然官方为此做了很多复杂的类型体操,确保用户可以在使用Options API
时获得类型推断,然而还是没办法用在mixins
和provide/inject
上Vue3
首推Composition API
,但是这会让我们在代码组织上多花点心思,因此在选择上,如果我们项目属于中低复杂度的场景,Options API
仍是一个好选择。对于那些大型,高扩展,强维护的项目上,Composition API
会获得更大收益
可能的追问
Composition API
能否和Options API
一起使用?
可以在同一个组件中使用两个script
标签,一个使用vue3,一个使用vue2写法,一起使用没有问题
<!-- vue3 -->
<script setup>
// vue3写法
</script>
<!-- 降级vue2 -->
<script>
export default {
data() {},
methods: {}
}
</script>
Vue生命周期钩子是如何实现的
vue
的生命周期钩子就是回调函数而已,当创建组件实例的过程中会调用对应的钩子方法- 内部会对钩子函数进行处理,将钩子函数维护成数组的形式
Vue
的生命周期钩子核心实现是利用发布订阅模式先把用户传入的的生命周期钩子订阅好(内部采用数组的方式存储)然后在创建组件实例的过程中会一次执行对应的钩子方法(发布)
<script>
// Vue.options 中会存放所有全局属性
// 会用自身的 + Vue.options 中的属性进行合并
// Vue.mixin({
// beforeCreate() {
// console.log('before 0')
// },
// })
debugger;
const vm = new Vue({
el: '#app',
beforeCreate: [
function() {
console.log('before 1')
},
function() {
console.log('before 2')
}
]
});
console.log(vm);
</script>
相关代码如下
export function callHook(vm, hook) {
// 依次执行生命周期对应的方法
const handlers = vm.$options[hook];
if (handlers) {
for (let i = 0; i < handlers.length; i++) {
handlers[i].call(vm); //生命周期里面的this指向当前实例
}
}
}
// 调用的时候
Vue.prototype._init = function (options) {
const vm = this;
vm.$options = mergeOptions(vm.constructor.options, options);
callHook(vm, "beforeCreate"); //初始化数据之前
// 初始化状态
initState(vm);
callHook(vm, "created"); //初始化数据之后
if (vm.$options.el) {
vm.$mount(vm.$options.el);
}
};
// 销毁实例实现
Vue.prototype.$destory = function() {
// 触发钩子
callHook(vm, 'beforeDestory')
// 自身及子节点
remove()
// 删除依赖
watcher.teardown()
// 删除监听
vm.$off()
// 触发钩子
callHook(vm, 'destoryed')
}
原理流程图
对keep-alive的理解,它是如何实现的,具体缓存的是什么?
如果需要在组件切换的时候,保存一些组件的状态防止多次渲染,就可以使用 keep-alive 组件包裹需要保存的组件。
(1)keep-alive
keep-alive有以下三个属性:
- include 字符串或正则表达式,只有名称匹配的组件会被匹配;
- exclude 字符串或正则表达式,任何名称匹配的组件都不会被缓存;
- max 数字,最多可以缓存多少组件实例。
注意:keep-alive 包裹动态组件时,会缓存不活动的组件实例。
主要流程
- 判断组件 name ,不在 include 或者在 exclude 中,直接返回 vnode,说明该组件不被缓存。
- 获取组件实例 key ,如果有获取实例的 key,否则重新生成。
- key生成规则,cid +"∶∶"+ tag ,仅靠cid是不够的,因为相同的构造函数可以注册为不同的本地组件。
- 如果缓存对象内存在,则直接从缓存对象中获取组件实例给 vnode ,不存在则添加到缓存对象中。 5.最大缓存数量,当缓存组件数量超过 max 值时,清除 keys 数组内第一个组件。
(2)keep-alive 的实现
const patternTypes: Array<Function> = [String, RegExp, Array] // 接收:字符串,正则,数组
export default {
name: 'keep-alive',
abstract: true, // 抽象组件,是一个抽象组件:它自身不会渲染一个 DOM 元素,也不会出现在父组件链中。
props: {
include: patternTypes, // 匹配的组件,缓存
exclude: patternTypes, // 不去匹配的组件,不缓存
max: [String, Number], // 缓存组件的最大实例数量, 由于缓存的是组件实例(vnode),数量过多的时候,会占用过多的内存,可以用max指定上限
},
created() {
// 用于初始化缓存虚拟DOM数组和vnode的key
this.cache = Object.create(null)
this.keys = []
},
destroyed() {
// 销毁缓存cache的组件实例
for (const key in this.cache) {
pruneCacheEntry(this.cache, key, this.keys)
}
},
mounted() {
// prune 削减精简[v.]
// 去监控include和exclude的改变,根据最新的include和exclude的内容,来实时削减缓存的组件的内容
this.$watch('include', (val) => {
pruneCache(this, (name) => matches(val, name))
})
this.$watch('exclude', (val) => {
pruneCache(this, (name) => !matches(val, name))
})
},
}
render函数:
- 会在 keep-alive 组件内部去写自己的内容,所以可以去获取默认 slot 的内容,然后根据这个去获取组件
- keep-alive 只对第一个组件有效,所以获取第一个子组件。
- 和 keep-alive 搭配使用的一般有:动态组件 和router-view
render () {
//
function getFirstComponentChild (children: ?Array<VNode>): ?VNode {
if (Array.isArray(children)) {
for (let i = 0; i < children.length; i++) {
const c = children[i]
if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
return c
}
}
}
}
const slot = this.$slots.default // 获取默认插槽
const vnode: VNode = getFirstComponentChild(slot)// 获取第一个子组件
const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions // 组件参数
if (componentOptions) { // 是否有组件参数
// check pattern
const name: ?string = getComponentName(componentOptions) // 获取组件名
const { include, exclude } = this
if (
// not included
(include && (!name || !matches(include, name))) ||
// excluded
(exclude && name && matches(exclude, name))
) {
// 如果不匹配当前组件的名字和include以及exclude
// 那么直接返回组件的实例
return vnode
}
const { cache, keys } = this
// 获取这个组件的key
const key: ?string = vnode.key == null
// same constructor may get registered as different local components
// so cid alone is not enough (#3269)
? componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : '')
: vnode.key
if (cache[key]) {
// LRU缓存策略执行
vnode.componentInstance = cache[key].componentInstance // 组件初次渲染的时候componentInstance为undefined
// make current key freshest
remove(keys, key)
keys.push(key)
// 根据LRU缓存策略执行,将key从原来的位置移除,然后将这个key值放到最后面
} else {
// 在缓存列表里面没有的话,则加入,同时判断当前加入之后,是否超过了max所设定的范围,如果是,则去除
// 使用时间间隔最长的一个
cache[key] = vnode
keys.push(key)
// prune oldest entry
if (this.max && keys.length > parseInt(this.max)) {
pruneCacheEntry(cache, keys[0], keys, this._vnode)
}
}
// 将组件的keepAlive属性设置为true
vnode.data.keepAlive = true // 作用:判断是否要执行组件的created、mounted生命周期函数
}
return vnode || (slot && slot[0])
}
keep-alive 具体是通过 cache 数组缓存所有组件的 vnode 实例。当 cache 内原有组件被使用时会将该组件 key 从 keys 数组中删除,然后 push 到 keys数组最后,以便清除最不常用组件。
实现步骤:
-
获取 keep-alive 下第一个子组件的实例对象,通过他去获取这个组件的组件名
-
通过当前组件名去匹配原来 include 和 exclude,判断当前组件是否需要缓存,不需要缓存,直接返回当前组件的实例vNode
-
需要缓存,判断他当前是否在缓存数组里面:
-
存在,则将他原来位置上的 key 给移除,同时将这个组件的 key 放到数组最后面(LRU)
-
不存在,将组件 key 放入数组,然后判断当前 key数组是否超过 max 所设置的范围,超过,那么削减未使用时间最长的一个组件的 key
- 最后将这个组件的 keepAlive 设置为 true
(3)keep-alive 本身的创建过程和 patch 过程
缓存渲染的时候,会根据 vnode.componentInstance(首次渲染 vnode.componentInstance 为 undefined) 和 keepAlive 属性判断不会执行组件的 created、mounted 等钩子函数,而是对缓存的组件执行 patch 过程∶ 直接把缓存的 DOM 对象直接插入到目标元素中,完成了数据更新的情况下的渲染过程。
首次渲染
- 组件的首次渲染∶判断组件的 abstract 属性,才往父组件里面挂载 DOM
// core/instance/lifecycle
function initLifecycle (vm: Component) {
const options = vm.$options
// locate first non-abstract parent
let parent = options.parent
if (parent && !options.abstract) { // 判断组件的abstract属性,才往父组件里面挂载DOM
while (parent.$options.abstract && parent.$parent) {
parent = parent.$parent
}
parent.$children.push(vm)
}
vm.$parent = parent
vm.$root = parent ? parent.$root : vm
vm.$children = []
vm.$refs = {}
vm._watcher = null
vm._inactive = null
vm._directInactive = false
vm._isMounted = false
vm._isDestroyed = false
vm._isBeingDestroyed = false
}
- 判断当前 keepAlive 和 componentInstance 是否存在来判断是否要执行组件 prepatch 还是执行创建 componentlnstance
// core/vdom/create-component
init (vnode: VNodeWithData, hydrating: boolean): ?boolean {
if (
vnode.componentInstance &&
!vnode.componentInstance._isDestroyed &&
vnode.data.keepAlive
) { // componentInstance在初次是undefined!!!
// kept-alive components, treat as a patch
const mountedNode: any = vnode // work around flow
componentVNodeHooks.prepatch(mountedNode, mountedNode) // prepatch函数执行的是组件更新的过程
} else {
const child = vnode.componentInstance = createComponentInstanceForVnode(
vnode,
activeInstance
)
child.$mount(hydrating ? vnode.elm : undefined, hydrating)
}
},
prepatch 操作就不会在执行组件的 mounted 和 created 生命周期函数,而是直接将 DOM 插入
(4)LRU (least recently used)缓存策略
LRU 缓存策略∶ 从内存中找出最久未使用的数据并置换新的数据。
LRU(Least rencently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是 "如果数据最近被访问过,那么将来被访问的几率也更高"。 最常见的实现是使用一个链表保存缓存数据,详细算法实现如下∶
- 新数据插入到链表头部
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部
- 链表满的时候,将链表尾部的数据丢弃。
vue-router中如何保护路由
分析
路由保护在应用开发过程中非常重要,几乎每个应用都要做各种路由权限管理,因此相当考察使用者基本功。
体验
全局守卫:
const router = createRouter({ ... })
router.beforeEach((to, from) => {
// ...
// 返回 false 以取消导航
return false
})
路由独享守卫:
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
组件内的守卫:
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
},
}
回答
vue-router
中保护路由的方法叫做路由守卫,主要用来通过跳转或取消的方式守卫导航。- 路由守卫有三个级别:
全局
、路由独享
、组件级
。影响范围由大到小,例如全局的router.beforeEach()
,可以注册一个全局前置守卫,每次路由导航都会经过这个守卫,因此在其内部可以加入控制逻辑决定用户是否可以导航到目标路由;在路由注册的时候可以加入单路由独享的守卫,例如beforeEnter
,守卫只在进入路由时触发,因此只会影响这个路由,控制更精确;我们还可以为路由组件添加守卫配置,例如beforeRouteEnter
,会在渲染该组件的对应路由被验证前调用,控制的范围更精确了。 - 用户的任何导航行为都会走
navigate
方法,内部有个guards
队列按顺序执行用户注册的守卫钩子函数,如果没有通过验证逻辑则会取消原有的导航。
原理
runGuardQueue(guards)
链式的执行用户在各级别注册的守卫钩子函数,通过则继续下一个级别的守卫,不通过进入catch
流程取消原本导航
// 源码
runGuardQueue(guards)
.then(() => {
// check global guards beforeEach
guards = []
for (const guard of beforeGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
.then(() => {
// check in components beforeRouteUpdate
guards = extractComponentsGuards(
updatingRecords,
'beforeRouteUpdate',
to,
from
)
for (const record of updatingRecords) {
record.updateGuards.forEach(guard => {
guards.push(guardToPromiseFn(guard, to, from))
})
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check the route beforeEnter
guards = []
for (const record of to.matched) {
// do not trigger beforeEnter on reused views
if (record.beforeEnter && !from.matched.includes(record)) {
if (isArray(record.beforeEnter)) {
for (const beforeEnter of record.beforeEnter)
guards.push(guardToPromiseFn(beforeEnter, to, from))
} else {
guards.push(guardToPromiseFn(record.beforeEnter, to, from))
}
}
}
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// NOTE: at this point to.matched is normalized and does not contain any () => Promise<Component>
// clear existing enterCallbacks, these are added by extractComponentsGuards
to.matched.forEach(record => (record.enterCallbacks = {}))
// check in-component beforeRouteEnter
guards = extractComponentsGuards(
enteringRecords,
'beforeRouteEnter',
to,
from
)
guards.push(canceledNavigationCheck)
// run the queue of per route beforeEnter guards
return runGuardQueue(guards)
})
.then(() => {
// check global guards beforeResolve
guards = []
for (const guard of beforeResolveGuards.list()) {
guards.push(guardToPromiseFn(guard, to, from))
}
guards.push(canceledNavigationCheck)
return runGuardQueue(guards)
})
// catch any navigation canceled
.catch(err =>
isNavigationFailure(err, ErrorTypes.NAVIGATION_CANCELLED)
? err
: Promise.reject(err)
)
$route
和$router
的区别
$route
是“路由信息对象”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。- 而
$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等
vue初始化页面闪动问题
使用vue开发时,在vue初始化之前,由于div是不归vue管的,所以我们写的代码在还没有解析的情况下会容易出现花屏现象,看到类似于{{message}}的字样,虽然一般情况下这个时间很短暂,但是还是有必要让解决这个问题的。
首先:在css里加上以下代码:
[v-cloak] { display: none;}
如果没有彻底解决问题,则在根元素加上style="display: none;" :style="{display: 'block'}"
双向数据绑定的原理
Vue.js 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。主要分为以下几个步骤:
- 需要observe的数据对象进行递归遍历,包括子属性对象的属性,都加上setter和getter这样的话,给这个对象的某个值赋值,就会触发setter,那么就能监听到了数据变化
- compile解析模板指令,将模板中的变量替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,更新视图
- Watcher订阅者是Observer和Compile之间通信的桥梁,主要做的事情是: ①在自身实例化时往属性订阅器(dep)里面添加自己 ②自身必须有一个update()方法 ③待属性变动dep.notice()通知时,能调用自身的update()方法,并触发Compile中绑定的回调,则功成身退。
- MVVM作为数据绑定的入口,整合Observer、Compile和Watcher三者,通过Observer来监听自己的model数据变化,通过Compile来解析编译模板指令,最终利用Watcher搭起Observer和Compile之间的通信桥梁,达到数据变化 -> 视图更新;视图交互变化(input) -> 数据model变更的双向绑定效果。