共计 27363 个字符,预计需要花费 69 分钟才能阅读完成。
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 应用
pinna 文档(opens new window)
- 筹备工作
咱们这里搭建一个最新的 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
很简略,调用 p inia
中的 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
:指令所绑定的元素,能够用来间接操作 DOM-
binding
:一个对象,蕴含以下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)
)
源码地位(opens new window)
$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 变更的双向绑定成果。