时间:2023-3-14 作者:悬浮的青春 分类: javascript
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 组件间通信是面试常考的知识点之一,这题有点类似于开放题,你回答出越多方法当然越加分,表明你对 Vue 掌握的越熟练。 Vue 组件间通信只要指以下 3 类通信 :
父子组件通信
、隔代组件通信
、兄弟组件通信
,下面我们分别介绍每种通信方式且会说明此种方法可适用于哪类组件间通信
组件传参的各种方式
组件通信常用方式有以下几种
props / $emit
适用 父子组件通信
prop
传递的,子组件传递数据给父组件是通过$emit
触发事件来做到的ref
与 $parent / $children(vue3废弃)
适用 父子组件通信
ref
:如果在普通的 DOM
元素上使用,引用指向的就是 DOM
元素;如果用在子组件上,引用就指向组件实例$parent / $children
:访问访问父组件的属性或方法 / 访问子组件的属性或方法EventBus ($emit / $on)
适用于 父子、隔代、兄弟组件通信
Vue
实例作为中央事件总线(事件中心),用它来触发事件和监听事件,从而实现任何组件间的通信,包括父子、隔代、兄弟组件$attrs / $listeners(vue3废弃)
适用于 隔代组件通信
$attrs
:包含了父作用域中不被 prop
所识别 (且获取) 的特性绑定 ( class
和 style
除外 )。当一个组件没有声明任何 prop
时,这里会包含所有父作用域的绑定 ( class
和 style
除外 ),并且可以通过 v-bind="$attrs"
传入内部组件。通常配合 inheritAttrs
选项一起使用$listeners
:包含了父作用域中的 (不含 .native
修饰器的) v-on
事件监听器。它可以通过 v-on="$listeners"
传入内部组件provide / inject
适用于 隔代组件通信
provider
来提供变量,然后在子孙组件中通过 inject
来注入变量。 provide / inject
API 主要解决了跨级组件间的通信问题, 不过它的使用场景,主要是子组件获取上级组件的状态 ,跨级组件间建立了一种主动提供与依赖注入的关系$root
适用于 隔代组件通信 访问根组件中的属性或方法,是根组件,不是父组件。$root
只对根组件有用Vuex
适用于 父子、隔代、兄弟组件通信
Vuex
是一个专为 Vue.js
应用程序开发的状态管理模式。每一个 Vuex
应用的核心就是 store
(仓库)。“store” 基本上就是一个容器,它包含着你的应用中大部分的状态 ( state
)Vuex
的状态存储是响应式的。当 Vue
组件从 store
中读取状态的时候,若 store
中的状态发生变化,那么相应的组件也会相应地得到高效更新。store
中的状态的唯一途径就是显式地提交 (commit
) mutation
。这样使得我们可以方便地跟踪每一个状态的变化。根据组件之间关系讨论组件通信最为清晰有效
props
/$emit
/$parent
/ref
$parent
/eventbus
/vuex
eventbus
/vuex
/provide+inject
/$attrs + $listeners
/$root
下面演示组件之间通讯三种情况: 父传子、子传父、兄弟组件之间的通讯
1. 父子组件通信
使用
props
,父组件可以使用props
向子组件传递数据。
父组件vue
模板father.vue
:
<template>
<child :msg="message"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
data () {
return {
message: 'father message';
}
}
}
</script>
子组件vue
模板child.vue
:
<template>
<div>{{msg}}</div>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
}
}
</script>
回调函数(callBack)
父传子:将父组件里定义的method
作为props
传入子组件
// 父组件Parent.vue:
<Child :changeMsgFn="changeMessage">
methods: {
changeMessage(){
this.message = 'test'
}
}
// 子组件Child.vue:
<button @click="changeMsgFn">
props:['changeMsgFn']
子组件向父组件通信
父组件向子组件传递事件方法,子组件通过
$emit
触发事件,回调给父组件
父组件vue
模板father.vue
:
<template>
<child @msgFunc="func"></child>
</template>
<script>
import child from './child.vue';
export default {
components: {
child
},
methods: {
func (msg) {
console.log(msg);
}
}
}
</script>
子组件vue
模板child.vue
:
<template>
<button @click="handleClick">点我</button>
</template>
<script>
export default {
props: {
msg: {
type: String,
required: true
}
},
methods () {
handleClick () {
//........
this.$emit('msgFunc');
}
}
}
</script>
2. provide / inject 跨级访问祖先组件的数据
父组件通过使用provide(){return{}}
提供需要传递的数据
export default {
data() {
return {
title: '我是父组件',
name: 'poetry'
}
},
methods: {
say() {
alert(1)
}
},
// provide属性 能够为后面的后代组件/嵌套的组件提供所需要的变量和方法
provide() {
return {
message: '我是祖先组件提供的数据',
name: this.name, // 传递属性
say: this.say
}
}
}
子组件通过使用inject:[“参数1”,”参数2”,…]
接收父组件传递的参数
<template>
<p>曾孙组件</p>
<p>{{message}}</p>
</template>
<script>
export default {
// inject 注入/接收祖先组件传递的所需要的数据即可
//接收到的数据 变量 跟data里面的变量一样 可以直接绑定到页面 {{}}
inject: [ "message","say"],
mounted() {
this.say();
},
};
</script>
3. parent+parent + parent+children 获取父组件实例和子组件实例的集合
this.$parent
可以直接访问该组件的父实例或组件this.$children
访问它所有的子组件;需要注意 $children
并不保证顺序,也不是响应式的<!-- parent.vue -->
<template>
<div>
<child1></child1>
<child2></child2>
<button @click="clickChild">$children方式获取子组件值</button>
</div>
</template>
<script>
import child1 from './child1'
import child2 from './child2'
export default {
data(){
return {
total: 108
}
},
components: {
child1,
child2
},
methods: {
funa(e){
console.log("index",e)
},
clickChild(){
console.log(this.$children[0].msg);
console.log(this.$children[1].msg);
}
}
}
</script>
<!-- child1.vue -->
<template>
<div>
<button @click="parentClick">点击访问父组件</button>
</div>
</template>
<script>
export default {
data(){
return {
msg:"child1"
}
},
methods: {
// 访问父组件数据
parentClick(){
this.$parent.funa("xx")
console.log(this.$parent.total);
}
}
}
</script>
<!-- child2.vue -->
<template>
<div>
child2
</div>
</template>
<script>
export default {
data(){
return {
msg: 'child2'
}
}
}
</script>
4. attrs+attrs + attrs+listeners多级组件通信
$attrs
包含了从父组件传过来的所有props
属性
// 父组件Parent.vue:
<Child :name="name" :age="age"/>
// 子组件Child.vue:
<GrandChild v-bind="$attrs" />
// 孙子组件GrandChild
<p>姓名:{{$attrs.name}}</p>
<p>年龄:{{$attrs.age}}</p>
$listeners
包含了父组件监听的所有事件
// 父组件Parent.vue:
<Child :name="name" :age="age" @changeNameFn="changeName"/>
// 子组件Child.vue:
<button @click="$listeners.changeNameFn"></button>
5. ref 父子组件通信
// 父组件Parent.vue:
<Child ref="childComp"/>
<button @click="changeName"></button>
changeName(){
console.log(this.$refs.childComp.age);
this.$refs.childComp.changeAge()
}
// 子组件Child.vue:
data(){
return{
age:20
}
},
methods(){
changeAge(){
this.age=15
}
}
6. 非父子, 兄弟组件之间通信
vue2
中废弃了broadcast
广播和分发事件的方法。父子组件中可以用props
和$emit()
。如何实现非父子组件间的通信,可以通过实例一个vue
实例Bus
作为媒介,要相互通信的兄弟组件之中,都引入Bus
,然后通过分别调用Bus事件触发和监听来实现通信和参数传递。Bus.js
可以是这样:
// 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的功能
<template>
<button @click="toBus">子组件传给兄弟组件</button>
</template>
<script>
export default{
methods: {
toBus () {
this.$bus.$emit('foo', '来自兄弟组件')
}
}
}
</script>
另一个组件也在钩子函数中监听on
事件
export default {
data() {
return {
message: ''
}
},
mounted() {
this.$bus.$on('foo', (msg) => {
this.message = msg
})
}
}
7. $root 访问根组件中的属性或方法
$root
只对根组件有用var vm = new Vue({
el: "#app",
data() {
return {
rootInfo:"我是根元素的属性"
}
},
methods: {
alerts() {
alert(111)
}
},
components: {
com1: {
data() {
return {
info: "组件1"
}
},
template: "<p>{{ info }} <com2></com2></p>",
components: {
com2: {
template: "<p>我是组件1的子组件</p>",
created() {
this.$root.alerts()// 根组件方法
console.log(this.$root.rootInfo)// 我是根元素的属性
}
}
}
}
}
});
8. vuex
state
用来存放共享变量的地方getter
,可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值mutations
用来存放修改state
的方法。actions
也是用来存放修改state的方法,不过action
是在mutations
的基础上进行。常用来做一些异步操作小结
props
与 $emit
进行传递,也可选择ref
$bus
,其次可以选择$parent
进行传递attrs
与listeners
或者 Provide
与 Inject
vuex
存放共享的变量1. 组件是什么
组件就是把图形、非图形的各种逻辑均抽象为一个统一的概念(组件)来实现开发的模式,在Vue中每一个.vue文件都可以视为一个组件
组件的优势
2. 插件是什么
插件通常用来为 Vue
添加全局功能。插件的功能范围没有严格的限制——一般有下面几种:
vue-custom-element
vue-touch
vue-router
Vue
实例方法,通过把它们添加到 Vue.prototype
上实现。API
,同时提供上面提到的一个或多个功能。如vue-router
3. 两者的区别
两者的区别主要表现在以下几个方面:
3.1 编写形式
编写组件
编写一个组件,可以有很多方式,我们最常见的就是vue单文件的这种格式,每一个.vue
文件我们都可以看成是一个组件
vue文件标准格式
<template>
</template>
<script>
export default{
...
}
</script>
<style>
</style>
我们还可以通过template
属性来编写一个组件,如果组件内容多,我们可以在外部定义template
组件内容,如果组件内容并不多,我们可直接写在template
属性上
<template id="testComponent"> // 组件显示的内容
<div>component!</div>
</template>
Vue.component('componentA',{
template: '#testComponent'
template: `<div>component</div>` // 组件内容少可以通过这种形式
})
编写插件
vue
插件的实现应该暴露一个 install
方法。这个方法的第一个参数是 Vue
构造器,第二个参数是一个可选的选项对象
MyPlugin.install = function (Vue, options) {
// 1. 添加全局方法或 property
Vue.myGlobalMethod = function () {
// 逻辑...
}
// 2. 添加全局资源
Vue.directive('my-directive', {
bind (el, binding, vnode, oldVnode) {
// 逻辑...
}
...
})
// 3. 注入组件选项
Vue.mixin({
created: function () {
// 逻辑...
}
...
})
// 4. 添加实例方法
Vue.prototype.$myMethod = function (methodOptions) {
// 逻辑...
}
}
3.2 注册形式
组件注册
vue组件注册主要分为全局注册与局部注册
全局注册通过Vue.component
方法,第一个参数为组件的名称,第二个参数为传入的配置项
Vue.component('my-component-name', { /* ... */ })
局部注册只需在用到的地方通过components
属性注册一个组件
const component1 = {...} // 定义一个组件
export default {
components:{
component1 // 局部注册
}
}
插件注册
插件的注册通过Vue.use()
的方式进行注册(安装),第一个参数为插件的名字,第二个参数是可选择的配置项
Vue.use(插件名字,{ /* ... */} )
注意的是:
注册插件的时候,需要在调用 new Vue()
启动应用之前完成
Vue.use
会自动阻止多次注册相同插件,只会注册一次
4. 使用场景
App.vue
简单来说,插件就是指对Vue
的功能的增强或补充
当用户指定了
watch
中的deep属性为true
时,如果当前监控的值是数组类型。会对对象中的每一项进行求值,此时会将当前watcher
存入到对应属性的依赖中,这样数组中对象发生变化时也会通知数据更新
源码相关
get () {
pushTarget(this) // 先将当前依赖放到 Dep.target上
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
if (this.deep) { // 如果需要深度监控
traverse(value) // 会对对象中的每一项取值,取值时会执行对应的get方法
}popTarget()
}
DOM
操作是非常昂贵的,因此我们需要尽量地减少DOM
操作。这就需要找出本次DOM
必须更新的节点来更新,其他的不更新,这个找出的过程,就需要应用diff算法
vue
的diff
算法是平级比较,不考虑跨级比较的情况。内部采用深度递归的方式+双指针(头尾都加指针)
的方式进行比较。
简单来说,Diff算法有以下过程
key
和tag
标签名判断)children
没有子节点,将旧的子节点移除)diff
)Diff
两个树的时间复杂度是O(n^3)
,但实际情况下我们很少会进行跨层级的移动DOM
,所以Vue
将Diff
进行了优化,从O(n^3) -> O(n)
,只有当新旧children
都为多个子节点时才需要用核心的Diff
算法进行同层级比较。Vue2
的核心Diff
算法采用了双端比较
的算法,同时从新旧children
的两端开始进行比较,借助key
值找到可复用的节点,再进行相关操作。相比React
的Diff
算法,同样情况下可以减少移动节点次数,减少不必要的性能损耗,更加的优雅VNode
时就确定其类型,以及在mount/patch
的过程中采用位运算来判断一个VNode
的类型,在这个基础之上再配合核心的Diff
算法,使得性能上较Vue2.x
有了提升vue3中采用最长递增子序列来实现
diff
优化
回答范例
思路
diff
算法是干什么的vue3
中的优化回答范例
Vue
中的diff
算法称为patching
算法,它由Snabbdo
m修改而来,虚拟DOM
要想转化为真实DOM
就需要通过patch
方法转换Vue1.x
视图中每个依赖均有更新函数对应,可以做到精准更新,因此并不需要虚拟DOM
和patching
算法支持,但是这样粒度过细导致Vue1.x
无法承载较大应用;Vue 2.x
中为了降低Watcher
粒度,每个组件只有一个Watcher
与之对应,此时就需要引入patching
算法才能精确找到发生变化的地方并高效更新vue
中diff
执行的时刻是组件内响应式数据变更触发实例执行其更新函数时,更新函数会再次执行render
函数获得最新的虚拟DOM
,然后执行patc
h函数,并传入新旧两次虚拟DOM,通过比对两者找到变化的地方,最后将其转化为对应的DOM
操作patch
过程是一个递归过程,遵循深度优先、同层比较的策略;以vue3
的patch
为例vue3
中引入的更新策略:静态节点标记等vdom中diff算法的简易实现
以下代码只是帮助大家理解diff
算法的原理和流程
vdom
转化为真实dom
:const createElement = (vnode) => {
let tag = vnode.tag;
let attrs = vnode.attrs || {};
let children = vnode.children || [];
if(!tag) {
return null;
}
//创建元素
let elem = document.createElement(tag);
//属性
let attrName;
for (attrName in attrs) {
if(attrs.hasOwnProperty(attrName)) {
elem.setAttribute(attrName, attrs[attrName]);
}
}
//子元素
children.forEach(childVnode => {
//给elem添加子元素
elem.appendChild(createElement(childVnode));
})
//返回真实的dom元素
return elem;
}
diff
算法做更新操作function updateChildren(vnode, newVnode) {
let children = vnode.children || [];
let newChildren = newVnode.children || [];
children.forEach((childVnode, index) => {
let newChildVNode = newChildren[index];
if(childVnode.tag === newChildVNode.tag) {
//深层次对比, 递归过程
updateChildren(childVnode, newChildVNode);
} else {
//替换
replaceNode(childVnode, newChildVNode);
}
})
}
参考 前端进阶面试题详细解答
jsonp
的方式进行加载,有效解决文件过大的问题。import()
语法,可以实现文件的分割加载。components:{
AddCustomerSchedule:(resolve)=>import("../components/AddCustomer") // require([])
}
原理
export function ( Ctor: Class<Component> | Function | Object | void, data: ?VNodeData, context: Component, children: ?Array<VNode>, tag?: string ): VNode | Array<VNode> | void {
// async component
let asyncFactory
if (isUndef(Ctor.cid)) {
asyncFactory = Ctor
Ctor = resolveAsyncComponent(asyncFactory, baseCtor) // 默认调用此函数时返回 undefiend
// 第二次渲染时Ctor不为undefined
if (Ctor === undefined) {
return createAsyncPlaceholder( // 渲染占位符 空虚拟节点
asyncFactory,
data,
context,
children,
tag
)
}
}
}
function resolveAsyncComponent ( factory: Function, baseCtor: Class<Component> ): Class<Component> | void {
if (isDef(factory.resolved)) {
// 3.在次渲染时可以拿到获取的最新组件
return factory.resolved
}
const resolve = once((res: Object | Class<Component>) => {
factory.resolved = ensureCtor(res, baseCtor)
if (!sync) {
forceRender(true) //2. 强制更新视图重新渲染
} else {
owners.length = 0
}
})
const reject = once(reason => {
if (isDef(factory.errorComp)) {
factory.error = true forceRender(true)
}
})
const res = factory(resolve, reject)// 1.将resolve方法和reject方法传入,用户调用 resolve方法后
sync = false
return factory.resolved
}
综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可
思路
prettier
,eslint
husky
,lint-staged`svg-loader
,vueuse
,nprogress
回答范例
0
创建一个项目我大致会做以下事情:项目构建、引入必要插件、代码规范、提交规范、常用库和组件vue3
项目我会用vite
或者create-vue
创建项目vue-router
、状态管理vuex/pinia
、ui
库我比较喜欢element-plu
s和antd-vue
、http
工具我会选axios
vueuse
,nprogress
,图标可以使用vite-svg-loader
prettier
和eslint
即可husky
,lint-staged
,commitlint
.vscode
:用来放项目中的 vscode
配置plugins
:用来放 vite