共计 26145 个字符,预计需要花费 66 分钟才能阅读完成。
什么是递归组件?举个例子阐明下?
剖析
递归组件咱们用的比拟少,然而在 Tree
、Menu
这类组件中会被用到。
体验
组件通过组件名称援用它本人,这种状况就是递归组件
<template>
<li>
<div> {{model.name}}</div>
<ul v-show="isOpen" v-if="isFolder">
<!-- 留神这里:组件递归渲染了它本人 -->
<TreeItem
class="item"
v-for="model in model.children"
:model="model">
</TreeItem>
</ul>
</li>
<script>
export default {
name: 'TreeItem',
// ...
}
</script>
答复范例
- 如果某个组件通过组件名称援用它本人,这种状况就是递归组件。
- 理论开发中相似
Tree
、Menu
这类组件,它们的节点往往蕴含子节点,子节点构造和父节点往往是雷同的。这类组件的数据往往也是树形构造,这种都是应用递归组件的典型场景。 - 应用递归组件时,因为咱们并未也不能在组件外部导入它本人,所以设置组件
name
属性,用来查找组件定义,如果应用SFC
,则能够通过SFC
文件名推断。组件外部通常也要有递归完结条件,比方model.children
这样的判断。 - 查看生成渲染函数可知,递归组件查找时会传递一个布尔值给
resolveComponent
,这样理论获取的组件就是以后组件自身
原理
递归组件编译后果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent(
name: string,
maybeSelfReference?: boolean
): ConcreteComponent | string {return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name
}
resolveAsset
中最终返回的是组件本身:
if (!res && maybeSelfReference) {
// fallback to implicit self-reference
return Component
}
说说你对 slot 的了解?slot 应用场景有哪些
一、slot 是什么
在 HTML 中 slot
元素,作为 Web Components
技术套件的一部分,是 Web 组件内的一个占位符
该占位符能够在前期应用本人的标记语言填充
举个栗子
<template id="element-details-template">
<slot name="element-name">Slot template</slot>
</template>
<element-details>
<span slot="element-name">1</span>
</element-details>
<element-details>
<span slot="element-name">2</span>
</element-details>
template
不会展现到页面中,须要用先获取它的援用,而后增加到 DOM
中,
customElements.define('element-details',
class extends HTMLElement {constructor() {super();
const template = document
.getElementById('element-details-template')
.content;
const shadowRoot = this.attachShadow({mode: 'open'})
.appendChild(template.cloneNode(true));
}
})
在 Vue
中的概念也是如此
Slot
艺名插槽,花名“占坑”,咱们能够了解为 solt
在组件模板中占好了地位,当应用该组件标签时候,组件标签外面的内容就会主动填坑(替换组件模板中 slot
地位),作为承载散发内容的进口
二、应用场景
通过插槽能够让用户能够拓展组件,去更好地复用组件和对其做定制化解决
如果父组件在应用到一个复用组件的时候,获取这个组件在不同的中央有大量的更改,如果去重写组件是一件不明智的事件
通过 slot
插槽向组件外部指定地位传递内容,实现这个复用组件在不同场景的利用
比方布局组件、表格列、下拉选、弹框显示内容等
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
,拦挡用户对值的拜访,从而实现响应式
为什么要应用异步组件
- 节俭打包出的后果,异步组件离开打包,采纳
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
}
Watch 中的 deep:true 是如何实现的
当用户指定了
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()}
Vue-router 除了 router-link 怎么实现跳转
申明式导航
<router-link to="/about">Go to About</router-link>
编程式导航
// literal string path
router.push('/users/1')
// object with path
router.push({path: '/users/1'})
// named route with params to let the router build the url
router.push({name: 'user', params: { username: 'test'} })
答复范例
vue-router
导航有两种形式:申明式导航和编程形式导航- 申明式导航形式应用
router-link
组件,增加to
属性导航;编程形式导航更加灵便,可传递调用router.push()
,并传递path
字符串或者RouteLocationRaw
对象,指定path
、name
、params
等信息 - 如果页面中简略示意跳转链接,应用
router-link
最快捷,会渲染一个 a 标签;如果页面是个简单的内容,比方商品信息,能够增加点击事件,应用编程式导航 - 实际上外部两者调用的导航函数是一样的
参考 前端进阶面试题具体解答
怎么实现路由懒加载呢
这是一道应用题。当打包利用时,JavaScript 包会变得十分大,影响页面加载。如果咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访时才加载对应组件,这样就会更加高效
// 将
// import UserDetails from './views/UserDetails'
// 替换为
const UserDetails = () => import('./views/UserDetails')
const router = createRouter({
// ...
routes: [{path: '/users/:id', component: UserDetails}],
})
答复范例
- 当打包构建利用时,JavaScript 包会变得十分大,影响页面加载。利用路由懒加载咱们能把不同路由对应的组件宰割成不同的代码块,而后当路由被拜访的时候才加载对应组件,这样会更加高效,是一种优化伎俩
- 一般来说,对所有的 路由都应用动静导入 是个好主见
- 给
component
选项配置一个返回Promise
组件的函数就能够定义懒加载路由。例如:{path: '/users/:id', component: () => import('./views/UserDetails') }
- 联合正文
() => import(/* webpackChunkName: "group-user" */ './UserDetails.vue')
能够做webpack
代码分块
组件中写 name 属性的益处
能够标识组件的具体名称不便调试和查找对应属性
// 源码地位 src/core/global-api/extend.js
// enable recursive self-lookup
if (name) {Sub.options.components[name] = Sub // 记录本人 在组件中递归本人 -> jsx
}
Vue 组件为什么只能有一个根元素
vue3
中没有问题
Vue.createApp({
components: {
comp: {
template: `
<div>root1</div>
<div>root2</div>
`
}
}
}).mount('#app')
vue2
中组件的确只能有一个根,但vue3
中组件曾经能够多根节点了。- 之所以须要这样是因为
vdom
是一颗单根树形构造,patch
办法在遍历的时候从根节点开始遍历,它要求只有一个根节点。组件也会转换为一个vdom
vue3
中之所以能够写多个根节点,是因为引入了Fragment
的概念,这是一个形象的节点,如果发现组件是多根的,就创立一个Fragment
节点,把多个根节点作为它的children
。未来patch
的时候,如果发现是一个Fragment
节点,则间接遍历children
创立或更新
Vue 修饰符有哪些
vue 中修饰符分为以下五种
- 表单修饰符
- 事件修饰符
- 鼠标按键修饰符
- 键值修饰符
v-bind
修饰符
1. 表单修饰符
在咱们填写表单的时候用得最多的是 input
标签,指令用得最多的是v-model
对于表单的修饰符有如下:
lazy
在咱们填完信息,光标来到标签的时候,才会将值赋予给 value
,也就是在change
事件之后再进行信息同步
<input type="text" v-model.lazy="value">
<p>{{value}}</p>
trim
主动过滤用户输出的首空格字符,而两头的空格不会过滤
<input type="text" v-model.trim="value">
number
主动将用户的输出值转为数值类型,但如果这个值无奈被 parseFloat
解析,则会返回原来的值
<input v-model.number="age" type="number">
2. 事件修饰符
事件修饰符是对事件捕捉以及指标进行了解决,有如下修饰符
.stop
阻止了事件冒泡,相当于调用了event.stopPropagation
办法
<div @click="shout(2)">
<button @click.stop="shout(1)">ok</button>
</div>
// 只输入 1
.prevent
阻止了事件的默认行为,相当于调用了event.preventDefault
办法
<form v-on:submit.prevent="onSubmit"></form>
.capture
应用事件捕捉模式,使事件触发从蕴含这个元素的顶层开始往下触发
<div @click.capture="shout(1)">
obj1
<div @click.capture="shout(2)">
obj2
<div @click="shout(3)">
obj3
<div @click="shout(4)">
obj4
</div>
</div>
</div>
</div>
// 输入构造: 1 2 4 3
.self
只当在event.target
是以后元素本身时触发处理函数
<div v-on:click.self="doThat">...</div>
应用修饰符时,程序很重要;相应的代码会以同样的程序产生。因而,用
v-on:click.prevent.self
会阻止所有的点击,而v-on:click.self.prevent
只会阻止对元素本身的点击
.once
绑定了事件当前只能触发一次,第二次就不会触发
<button @click.once="shout(1)">ok</button>
.passive
通知浏览器你不想阻止事件的默认行为
在挪动端,当咱们在监听元素滚动事件的时候,会始终触发 onscroll
事件会让咱们的网页变卡,因而咱们应用这个修饰符的时候,相当于给 onscroll
事件整了一个 .lazy
修饰符
<!-- 滚动事件的默认行为 (即滚动行为) 将会立刻触发 -->
<!-- 而不会期待 `onScroll` 实现 -->
<!-- 这其中蕴含 `event.preventDefault()` 的状况 -->
<div v-on:scroll.passive="onScroll">...</div>
- 不要把
.passive
和.prevent
一起应用, 因为.prevent
将会被疏忽,同时浏览器可能会向你展现一个正告。passive
会通知浏览器你不想阻止事件的默认行为
native
让组件变成像html
内置标签那样监听根元素的原生事件,否则组件上应用v-on
只会监听自定义事件
<my-component v-on:click.native="doSomething"></my-component>
<!-- 应用.native 修饰符来操作一般 HTML 标签是会令事件生效的 -->
3. 鼠标按钮修饰符
鼠标按钮修饰符针对的就是左键、右键、中键点击,有如下:
.left
左键点击.right
右键点击.middle
中键点击
<button @click.left="shout(1)">ok</button>
<button @click.right="shout(1)">ok</button>
<button @click.middle="shout(1)">ok</button>
4. 键盘事件的修饰符
键盘修饰符是用来润饰键盘事件(onkeyup
,onkeydown
)的,有如下:
keyCode
存在很多,但 vue 为咱们提供了别名,分为以下两种:
- 一般键(
enter
、tab
、delete
、space
、esc
、up
、down
、left
、right
…) - 零碎润饰键(
ctrl
、alt
、meta
、shift
…)
<!-- 只有按键为 keyCode 的时候才触发 -->
<input type="text" @keyup.keyCode="shout()">
还能够通过以下形式自定义一些全局的键盘码别名
Vue.config.keyCodes.f2 = 113
5. v-bind 修饰符
v-bind
修饰符次要是为属性进行操作,用来别离有如下:
- async 能对
props
进行一个双向绑定
// 父组件
<comp :myMessage.sync="bar"></comp>
// 子组件
this.$emit('update:myMessage',params);
以上这种办法相当于以下的简写
// 父亲组件
<comp :myMessage="bar" @update:myMessage="func"></comp>
func(e){this.bar = e;}
// 子组件 js
func2(){this.$emit('update:myMessage',params);
}
应用 async
须要留神以下两点:
- 应用
sync
的时候,子组件传递的事件名格局必须为update:value
,其中value
必须与子组件中props
中申明的名称完全一致 - 留神带有
.sync
修饰符的v-bind
不能和表达式一起应用 - prop 设置自定义标签属性,防止裸露数据,避免净化 HTML 构造
<input id="uid" title="title1" value="1" :index.prop="index">
- camel 将命名变为驼峰命名法,如将
view-Box
属性名转换为viewBox
<svg :viewBox="viewBox"></svg>
利用场景
依据每一个修饰符的性能,咱们能够失去以下修饰符的利用场景:
.stop
:阻止事件冒泡.native
:绑定原生事件.once
:事件只执行一次.self
:将事件绑定在本身身上,相当于阻止事件冒泡.prevent
:阻止默认事件.caption
:用于事件捕捉.once
:只触发一次.keyCode
:监听特定键盘按下.right
:右键
Vue 组件之间通信形式有哪些
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 + $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 + $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
- 实用场景: 简单关系的组件数据传递
- Vuex 作用相当于一个用来存储共享变量的容器
state
用来寄存共享变量的中央getter
,能够减少一个getter
派生状态,(相当于store
中的计算属性),用来取得共享变量的值mutations
用来寄存批改state
的办法。actions
也是用来寄存批改 state 的办法,不过action
是在mutations
的根底上进行。罕用来做一些异步操作
小结
- 父子关系的组件数据传递抉择
props
与$emit
进行传递,也可抉择ref
- 兄弟关系的组件数据传递可抉择
$bus
,其次能够抉择$parent
进行传递 - 先人与后辈组件数据传递可抉择
attrs
与listeners
或者Provide
与Inject
- 简单关系的组件数据传递能够通过
vuex
寄存共享的变量
双向绑定的原理是什么
咱们都晓得 Vue
是数据双向绑定的框架,双向绑定由三个重要局部形成
- 数据层(Model):利用的数据及业务逻辑
- 视图层(View):利用的展现成果,各类 UI 组件
- 业务逻辑层(ViewModel):框架封装的外围,它负责将数据与视图关联起来
而下面的这个分层的架构计划,能够用一个专业术语进行称说:MVVM
这里的管制层的外围性能便是“数据双向绑定”。天然,咱们只需弄懂它是什么,便能够进一步理解数据绑定的原理
了解 ViewModel
它的主要职责就是:
- 数据变动后更新视图
- 视图变动后更新数据
当然,它还有两个次要局部组成
- 监听器(
Observer
):对所有数据的属性进行监听 - 解析器(
Compiler
):对每个元素节点的指令进行扫描跟解析, 依据指令模板替换数据, 以及绑定相应的更新函数
写过自定义指令吗?原理是什么
答复范例
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 采纳异步渲染
Vue 是组件级更新,如果不采纳异步更新,那么每次更新数据都会对以后组件进行从新渲染,所以为了性能,Vue 会在本轮数据更新后,在异步更新视图。核心思想
nextTick
源码相干
dep.notify()
告诉watcher
进行更新,subs[i].update
顺次调用watcher
的update
,queueWatcher
将watcher
去重放入队列,nextTick
(flushSchedulerQueue
)在下一tick
中刷新watcher
队列(异步)
update () { /* istanbul ignore else */
if (this.lazy) {this.dirty = true}
else if (this.sync) {this.run()
}
else {queueWatcher(this); // 当数据发生变化时会将 watcher 放到一个队列中批量更新
}
}
export function queueWatcher (watcher: Watcher) {
const id = watcher.id // 会对雷同的 watcher 进行过滤
if (has[id] == null) {has[id] = true
if (!flushing) {queue.push(watcher)
} else {
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {i--}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
if (process.env.NODE_ENV !== 'production' && !config.async) {flushSchedulerQueue()
return
}
nextTick(flushSchedulerQueue) // 调用 nextTick 办法 批量的进行更新
}
}
}
vue-router 路由钩子函数是什么 执行程序是什么
路由钩子的执行流程, 钩子函数品种有:
全局守卫
、路由守卫
、组件守卫
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave
守卫。 - 调用全局的
beforeEach
守卫。 - 在重用的组件里调用
beforeRouteUpdate
守卫 (2.2+
)。 - 在路由配置里调用
beforeEnter
。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter
。 - 调用全局的
beforeResolve
守卫 (2.5+
)。 - 导航被确认。
- 调用全局的
afterEach
钩子。 - 触发
DOM
更新。 - 调用
beforeRouteEnter
守卫中传给next
的回调函数,创立好的组件实例会作为回调函数的参数传入
子组件能够间接扭转父组件的数据吗?
子组件不能够间接扭转父组件的数据。这样做次要是为了保护父子组件的单向数据流。每次父级组件产生更新时,子组件中所有的 prop 都将会刷新为最新的值。如果这样做了,Vue 会在浏览器的控制台中收回正告。
Vue 提倡单向数据流,即父级 props 的更新会流向子组件,然而反过来则不行。这是为了避免意外的扭转父组件状态,使得利用的数据流变得难以了解,导致数据流凌乱。如果毁坏了单向数据流,当利用简单时,debug 的老本会十分高。
只能通过 $emit
派发一个自定义事件,父组件接管到后,由父组件批改。
Vue.extend 作用和原理
官网解释:
Vue.extend
应用根底Vue
结构器,创立一个“子类”。参数是一个蕴含组件选项的对象。
其实就是一个子类结构器 是 Vue
组件的外围 api
实现思路就是应用原型继承的办法返回了 Vue 的子类 并且利用 mergeOptions
把传入组件的 options
和父类的 options
进行了合并
extend
是结构一个组件的语法器。而后这个组件你能够作用到Vue.component
这个全局注册办法里还能够在任意vue
模板里应用组件。也能够作用到vue
实例或者某个组件中的components
属性中并在外部应用apple
组件。Vue.component
你能够创立,也能够取组件。
相干代码如下
export default function initExtend(Vue) {
let cid = 0; // 组件的惟一标识
// 创立子类继承 Vue 父类 便于属性扩大
Vue.extend = function (extendOptions) {
// 创立子类的构造函数 并且调用初始化办法
const Sub = function VueComponent(options) {this._init(options); // 调用 Vue 初始化办法
};
Sub.cid = cid++;
Sub.prototype = Object.create(this.prototype); // 子类原型指向父类
Sub.prototype.constructor = Sub; //constructor 指向本人
Sub.options = mergeOptions(this.options, extendOptions); // 合并本人的 options 和父类的 options
return Sub;
};
}
谈一谈对 Vue 组件化的了解
- 组件化开发能大幅提高开发效率、测试性、复用性等
- 罕用的组件化技术:属性、自定义事件、插槽
- 升高更新频率,只从新渲染变动的组件
- 组件的特点:高内聚、低耦合、单向数据流
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-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)