Vuex
是一个专为Vue.js
应用程序开发的状态管理模式。vuex
就是一个仓库,仓库里放了很多对象。其中state
就是数据源存放地,对应于一般 vue 对象里面的data
里面存放的数据是响应式的,vue
组件从store
读取数据,若是store
中的数据发生改变,依赖这相数据的组件也会发生更新它通过mapState
把全局的state
和getters
映射到当前组件的computed
计算属性
vuex
一般用于中大型 web
单页应用中对应用的状态进行管理,对于一些组件间关系较为简单的小型应用,使用 vuex
的必要性不是很大,因为完全可以用组件 prop
属性或者事件来完成父子组件之间的通信,vuex
更多地用于解决跨组件通信以及作为数据中心集中式存储数据。Vuex
解决非父子组件之间通信问题 vuex
是通过将 state
作为数据中心、各个组件共享 state
实现跨组件通信的,此时的数据完全独立于组件,因此将组件间共享的数据置于 State
中能有效解决多层级组件嵌套的跨组件通信问题
vuex
的State
在单页应用的开发中本身具有一个“数据库”的作用,可以将组件中用到的数据存储在State
中,并在Action
中封装数据读写的逻辑。这时候存在一个问题,一般什么样的数据会放在State
中呢? 目前主要有两种数据会使用vuex
进行管理:
包括以下几个模块
state
:Vuex
使用单一状态树,即每个应用将仅仅包含一个store
实例。里面存放的数据是响应式的,vue
组件从 store
读取数据,若是 store
中的数据发生改变,依赖这相数据的组件也会发生更新。它通过 mapState
把全局的 state
和 getters
映射到当前组件的 computed
计算属性mutations
:更改Vuex
的store
中的状态的唯一方法是提交mutation
getters
:getter
可以对 state
进行计算操作,它就是 store
的计算属性虽然在组件内也可以做计算属性,但是 getters
可以在多给件之间复用如果一个状态只在一个组件内使用,是可以不用 getters
action
:action
类似于 muation
, 不同在于:action
提交的是 mutation
,而不是直接变更状态action
可以包含任意异步操作modules
:面对复杂的应用程序,当管理的状态比较多时;我们需要将vuex
的store
对象分割成模块(modules
)
modules
:项目特别复杂的时候,可以让每一个模块拥有自己的state
、mutation
、action
、getters
,使得结构非常清晰,方便管理
回答范例
思路
回答范例
Vuex
是一个专为 Vue.js
应用开发的 状态管理模式 + 库 。它采用集中式存储,管理应用的所有组件的状态,并以相应的规则保证状态以一种可预测的方式发生变化。vuex
存在的必要性,它和react
生态中的redux
之类是一个概念Vuex
解决状态管理的同时引入了不少概念:例如state
、mutation
、action
等,是否需要引入还需要根据应用的实际情况衡量一下:如果不打算开发大型单页应用,使用 Vuex
反而是繁琐冗余的,一个简单的 store
模式就足够了。但是,如果要构建一个中大型单页应用,Vuex
基本是标配。vuex
过程中感受到一些等可能的追问
vuex
有什么缺点吗?你在开发过程中有遇到什么问题吗?vuex
中的state
会重新变为初始状态。解决方案-插件 vuex-persistedstate
action
和mutation
的区别是什么?为什么要区分它们?
action
中处理异步,mutation
不可以mutation
做原子操作action
可以整合多个mutation
的集合mutation
是同步更新数据(内部会进行是否为异步方式更新数据的检测)$watch
严格模式下会报错action
异步操作,可以获取数据后调佣mutation
提交最终数据
Action
,Action再触发
Mutation`。Mutation
:专注于修改State
,理论上是修改State
的唯一途径。Action
:业务代码、异步请求Mutation
:必须同步执行。Action
:可以异步,但不能直接操作State
当用户指定了
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()
}
Vue3.0 性能提升体现在哪些方面
API
,基于Proxy
实现,初始化时间和内存占用均大幅改进;静态标记pachFlag
(diff
算法增加了一个静态标记,只对比有标记的dom
元素)、事件增加缓存
、静态提升
(对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用)等,可以有效跳过大量diff
过程;tree-shaking
,因此整体体积更小,加载更快ssr
渲染以字符串方式渲染一、编译阶段
试想一下,一个组件结构如下图
<template>
<div id="content">
<p class="text">静态文本</p>
<p class="text">静态文本</p>
<p class="text">{ message }</p>
<p class="text">静态文本</p>
...
<p class="text">静态文本</p>
</div>
</template>
可以看到,组件内部只有一个动态节点,剩余一堆都是静态节点,所以这里很多 diff
和遍历其实都是不需要的,造成性能浪费
因此,Vue3在编译阶段,做了进一步优化。主要有如下:
diff
算法优化SSR
优化1. diff 算法优化
Vue 2x
中的虚拟 dom
是进行全量的对比。Vue 3x
中新增了静态标记(PatchFlag
):在与上次虚拟结点进行对比的时候,值对比 带有 patch flag
的节点,并且可以通过 flag
的信息得知当前节点要对比的具体内容化Vue2.x的diff算法
vue2.x
的diff
算法叫做全量比较
,顾名思义,就是当数据改变的时候,会从头到尾的进行vDom
对比,即使有些内容是永恒固定不变的
Vue3.0的diff算法
vue3.0
的diff
算法有个叫静态标记(PatchFlag
)的小玩意,啥是静态标记呢?简单点说,就是如果你的内容会变,我会给你一个flag
,下次数据更新的时候我直接来对比你,我就不对比那些没有标记的了
已经标记静态节点的p
标签在diff
过程中则不会比较,把性能进一步提高
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
//上面这个1就是静态标记
]))
}
关于静态类型枚举如下
TEXT = 1 // 动态文本节点
CLASS=1<<1,1 // 2//动态class
STYLE=1<<2,// 4 //动态style
PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
FULLPR0PS=1<<4,// 16 //具有动态key属性,当key改变时,需要进行完整的diff比较。
HYDRATE_ EVENTS = 1 << 5,// 32 //带有监听事件的节点
STABLE FRAGMENT = 1 << 6, // 64 //一个不会改变子节点顺序的fragment
KEYED_ FRAGMENT = 1 << 7, // 128 //带有key属性的fragment 或部分子字节有key
UNKEYED FRAGMENT = 1<< 8, // 256 //子节点没有key 的fragment
NEED PATCH = 1 << 9, // 512 //一个节点只会进行非props比较
DYNAMIC_SLOTS = 1 << 10 // 1024 // 动态slot
HOISTED = -1 // 静态节点
// 指示在diff算法中退出优化模式
BALL = -2
2. hoistStatic 静态提升
Vue 2x
: 无论元素是否参与更新,每次都会重新创建。Vue 3x
: 对不参与更新的元素,会做静态提升,只会被创建一次,之后会在每次渲染时候被不停的复用。这样就免去了重复的创建节点,大型应用会受益于这个改动,免去了重复的创建操作,优化了运行时候的内存占用<p>HelloWorld</p>
<p>HelloWorld</p>
<p>{ message }</p>
开启静态提升前
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, "'HelloWorld'"),
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
开启静态提升后编译结果
const _hoisted_1 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
const _hoisted_2 = /*#__PURE__*/_createVNode("p", null, "'HelloWorld'", -1 /* HOISTED */)
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_hoisted_1,
_hoisted_2,
_createVNode("p", null, _toDisplayString(_ctx.msg), 1 /* TEXT */)
]))
}
可以看到开启了静态提升后,直接将那两个内容为helloworld
的p
标签声明在外面了,直接就拿来用了。同时 _hoisted_1
和_hoisted_2
被打上了 PatchFlag
,静态标记值为 -1
,特殊标志是负整数表示永远不会用于 Diff
3. cacheHandlers 事件监听缓存
<div>
<button @click = 'onClick'>点我</button>
</div>
开启事件侦听器缓存之前:
export const render = /*#__PURE__*/_withId(function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", { onClick: _ctx.onClick }, "点我", 8 /* PROPS */, ["onClick"])
// PROPS=1<<3,// 8 //动态属性,但不包含类名和样式
]))
})
这里有一个8
,表示着这个节点有了静态标记,有静态标记就会进行diff
算法对比差异,所以会浪费时间
开启事件侦听器缓存之后:
export function render(_ctx, _cache, $props, $setup, $data, $options) {
return (_openBlock(), _createBlock("div", null, [
_createVNode("button", {
onClick: _cache[1] || (_cache[1] = (...args) => (_ctx.onClick(...args)))
}, "点我")
]))
}
上述发现开启了缓存后,没有了静态标记。也就是说下次diff
算法的时候直接使用
4. SSR优化
当静态内容大到一定量级时候,会用createStaticVNode
方法在客户端去生成一个static node
,这些静态node
,会被直接innerHtml
,就不需要创建对象,然后根据对象渲染
<div>
<div>
<span>你好</span>
</div>
... // 很多个静态属性
<div>
<span>{{ message }}</span>
</div>
</div>
编译后
import { mergeProps as _mergeProps } from "vue"
import { ssrRenderAttrs as _ssrRenderAttrs, ssrInterpolate as _ssrInterpolate } from "@vue/server-renderer"
export function ssrRender(_ctx, _push, _parent, _attrs, $props, $setup, $data, $options) {
const _cssVars = { style: { color: _ctx.color }}
_push(`<div${
_ssrRenderAttrs(_mergeProps(_attrs, _cssVars))
}><div><span>你好</span>...<div><span>你好</span><div><span>${
_ssrInterpolate(_ctx.message)
}</span></div></div>`)
}
二、源码体积
相比Vue2
,Vue3
整体体积变小了,除了移出一些不常用的API
,再重要的是Tree shanking
任何一个函数,如ref
、reactive
、computed
等,仅仅在用到的时候才打包,没用到的模块都被摇掉,打包的整体体积变小
import { computed, defineComponent, ref } from 'vue';
export default defineComponent({
setup(props, context) {
const age = ref(18)
let state = reactive({
name: 'test'
})
const readOnlyAge = computed(() => age.value++) // 19
return {
age,
state,
readOnlyAge
}
}
});
三、响应式系统
vue2
中采用 defineProperty
来劫持整个对象,然后进行深度遍历所有属性,给每个属性添加getter
和setter
,实现响应式
vue3
采用proxy
重写了响应式系统,因为proxy
可以对整个对象进行监听,所以不需要深度遍历
length
属性分析
递归组件我们用的比较少,但是在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
}
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解
注意 :在子组件直接用 v-model
绑定父组件传过来的 prop
这样是不规范的写法 开发环境会报警告
如果实在要改变父组件的 prop
值,可以在 data
里面定义一个变量 并用 prop
的值初始化它 之后用$emit
通知父组件去修改
有两种常见的试图改变一个 prop 的情形 :
prop
用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop
数据来使用。 在这种情况下,最好定义一个本地的 data
属性并将这个 prop
用作其初始值props: ['initialCounter'],
data: function () {
return {
counter: this.initialCounter
}
}
prop
以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性props: ['size'],
computed: {
normalizedSize: function () {
return this.size.trim().toLowerCase()
}
}
使用index 作为 key和没写基本上没区别,因为不管数组的顺序怎么颠倒,index 都是 0, 1, 2...这样排列,导致 Vue 会复用错误的旧子节点,做很多额外的工作。
参考 前端进阶面试题详细解答
缓存组件使用keep-alive
组件,这是一个非常常见且有用的优化手段,vue3
中keep-alive
有比较大的更新,能说的点比较多
思路
keep-alive
,它的作用与用法router
和transition
activated
或者beforeRouteEnter
回答范例
keep-alive
组件,keep-alive
是vue
内置组件,keep-alive
包裹动态组件component
时,会缓存不活动的组件实例,而不是销毁它们,这样在组件切换过程中将状态保留在内存中,防止重复渲染DOM
<keep-alive>
<component :is="view"></component>
</keep-alive>
include
和exclude
可以明确指定缓存哪些组件或排除缓存指定组件。vue3
中结合vue-router
时变化较大,之前是keep-alive
包裹router-view
,现在需要反过来用router-view
包裹keep-alive
<router-view v-slot="{ Component }">
<keep-alive>
<component :is="Component"></component>
</keep-alive>
</router-view>
beforeRouteEnter
:在有vue-router的
项目,每次进入路由的时候,都会执行beforeRouteEnter
beforeRouteEnter(to, from, next){
next(vm=>{
console.log(vm)
// 每次进入路由执行
vm.getData() // 获取数据
})
},
actived
:在keep-alive
缓存的组件被激活的时候,都会执行actived
钩子activated(){
this.getData() // 获取数据
},
keep-alive
是一个通用组件,它内部定义了一个map
,缓存创建过的组件实例,它返回的渲染函数内部会查找内嵌的component
组件对应组件的vnode
,如果该组件在map
中存在就直接返回它。由于component
的is
属性是个响应式数据,因此只要它变化,keep-alive
的render
函数就会重新执行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
的功能的增强或补充
分析
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>
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
}
这是一个实践知识点,组件化开发过程中有个单项数据流原则
,不在子组件中修改父组件是个常识问题
思路
回答范例
prop
都使得其父子之间形成了一个单向下行绑定:父级 prop
的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。另外,每次父级组件发生变更时,子组件中所有的 prop
都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop
。如果你这样做了,Vue
会在浏览器控制台中发出警告const props = defineProps(['foo'])
// ❌ 下面行为会被警告, props是只读的!
props.foo = 'bar'
这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data
,并将这个 prop
用作其初始值:
const props = defineProps(['initialCounter'])
const counter = ref(props.initialCounter)
这个 prop 以一种原始的值传入且需要进行转换。 在这种情况下,最好使用这个 prop
的值来定义一个计算属性:
const props = defineProps(['size'])
// prop变化,计算属性自动更新
const normalizedSize = computed(() => props.size.trim().toLowerCase())
emit
一个事件让父组件去做这个变更。注意虽然我们不能直接修改一个传入的对象或者数组类型的prop
,但是我们还是能够直接改内嵌的对象或属性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
state
用来存放共享变量的地方getter
,可以增加一个getter
派生状态,(相当于store
中的计算属性),用来获得共享变量的值mutations
用来存放修改state
的方法。actions
也是用来存放修改state的方法,不过action
是在mutations
的基础上进行。常用来做一些异步操作小结
props
与 $emit
进行传递,也可选择ref
$bus
,其次可以选择$parent
进行传递attrs
与listeners
或者 Provide
与 Inject
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()!');
}
})
// 在src目录下新建一个directive文件,在此文件夹下新建一个index.js文件夹,接着输入如下内容
const directives = (app) => {
//这里是给元素取得名字,虽然是focus,但是实际引用的时候必须以v开头
app.directive('focus',{
//这里的el就是获取的元素
mounted(el) {
el.focus()
}
})
}
//默认导出 directives
export default directives
// 在全局注册directive
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import store from './store'
import directives from './directives'
const app = createApp(App)
directives(app)
app.use(store).use(router).mount('#app')
<!-- 在你需要的页面进行自定义指令的使用 -->
<template>
<div class="container">
<div class="content">
<input type="text" v-focus>
内容
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
// const vMove:Directive = () =>{
// }
</script>
在
vue3.2 setup
语法糖模式下,自定义指令变得及其简单
<input type="text" v-model="value" v-focus>
<script setup>
//直接写,但是必须是v开头
const vFocus = {
mounted(el) {
// 获取input,并调用其focus()方法
el.focus()
}
}
</script>
<!-- demo 进去页面自动获取焦点,然后让盒子的颜色根据你input框输入的内容变色,并且作防抖处理 -->
<template>
<div class="container">
<div class="content" v-move="{ background: value }">
内容
<input type="text" v-model="value" v-focus @keyup="see">
</div>
</div>
</template>
<script setup>
import { reactive, ref } from 'vue'
const value = ref('')
const vFocus = {
mounted(el) {
// 获取input,并调用其focus()方法
el.focus()
}
}
let timer = null
const vMove = (el, binding) => {
if (timer !== null) {
clearTimeout(timer)
}
timer = setTimeout(() => {
el.style.background = binding.value.background
console.log(el);
}, 1000);
}
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 100%;
display: flex;
justify-content: center;
align-items: center;
.content {
border-top: 5px solid black;
width: 200px;
height: 200px;
cursor: pointer;
border-left: 1px solid #ccc;
border-right: 1px solid #ccc;
border-bottom: 1px solid #ccc;
}
}
</style>
data
、 Store
)的联系;实现时,主要如下
data
, 使用 Object.defineProperty
把这些属性全部转为 getter/setter
。computed
, 遍历 computed
里的每个属性,每个 computed
属性都是一个 watch
实例。每个属性提供的函数作为属性的 getter
,使用 Object.defineProperty
转化。Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性重新计算。computed
计算属性嵌套其他 computed
计算属性时,先进行其他的依赖收集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);
}
})
}
</details>
Vue 不允许在已经创建的实例上动态添加新的响应式属性
若想实现数据与视图同步更新,可采取下面三种解决方案:
Vue.set()
Object.assign()
$forcecUpdated()
Vue.set( target, propertyName/index, value )
参数
{Object | Array} target
{string | number} propertyName/index
{any} value
返回值:设置的值
通过Vue.set
向响应式对象中添加一个property
,并确保这个新 property
同样是响应式的,且触发视图更新
关于Vue.set
源码(省略了很多与本节不相关的代码)
源码位置:src\core\observer\index.js
function set (target: Array<any> | Object, key: any, val: any): any {
...
defineReactive(ob.value, key, val)
ob.dep.notify()
return val
}
这里无非再次调用defineReactive
方法,实现新增属性的响应式
关于defineReactive
方法,内部还是通过Object.defineProperty
实现属性拦截
大致代码如下:
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
console.log(`set ${key}:${newVal}`);
val = newVal
}
}
})
}
直接使用Object.assign()
添加到对象的新属性不会触发更新
应创建一个新的对象,合并原对象和混入对象的属性
this.someObject = Object.assign({},this.someObject,{newProperty1:1,newProperty2:2 ...})
如果你发现你自己需要在 Vue
中做一次强制更新,99.9% 的情况,是你在某个地方做错了事
$forceUpdate
迫使Vue
实例重新渲染
PS:仅仅影响实例本身和插入插槽内容的子组件,而不是所有子组件。
Vue.set()
Object.assign()
创建新对象$forceUpdate()
进行强制刷新 (不建议)PS:vue3
是用过proxy
实现数据响应式的,直接动态添加新属性仍可以实现数据响应式
v-show
隐藏则是为该元素添加css--display:none
,dom
元素依旧还在。v-if
显示隐藏是将dom
元素整个添加或删除v-if
切换有一个局部编译/卸载的过程,切换过程中合适地销毁和重建内部的事件监听和子组件;v-show
只是简单的基于css
切换v-if
是真正的条件渲染,它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和重建。只有渲染条件为假时,并不做操作,直到为真才渲染v-show
由false
变为true
的时候不会触发组件的生命周期v-if
由false
变为true
的时候,触发组件的beforeCreate
、create
、beforeMount
、mounted
钩子,由true
变为false
的时候触发组件的beforeDestory
、destoryed
方法v-if
有更高的切换消耗;v-show
有更高的初始渲染消耗v-show与v-if的使用场景
v-if
与 v-show
都能控制dom
元素在页面的显示v-if
相比 v-show
开销更大的(直接操作dom节
点增加与删除)v-if
较好v-show与v-if原理分析
v-show
原理不管初始条件是什么,元素总是会被渲染
我们看一下在vue中是如何实现的
代码很好理解,有transition
就执行transition
,没有就直接设置display
属性
// https://github.com/vuejs/vue-next/blob/3cd30c5245da0733f9eb6f29d220f39c46518162/packages/runtime-dom/src/directives/vShow.ts
export const vShow: ObjectDirective<VShowElement> = {
beforeMount(el, { value }, { transition }) {
el._vod = el.style.display === 'none' ? '' : el.style.display
if (transition && value) {
transition.beforeEnter(el)
} else {
setDisplay(el, value)
}
},
mounted(el, { value }, { transition }) {
if (transition && value) {
transition.enter(el)
}
},
updated(el, { value, oldValue }, { transition }) {
// ...
},
beforeUnmount(el, { value }) {
setDisplay(el, value)
}
}
v-if
原理v-if
在实现上比v-show
要复杂的多,因为还有else
else-if
等条件需要处理,这里我们也只摘抄源码中处理 v-if
的一小部分
返回一个node
节点,render
函数通过表达式的值来决定是否生成DOM
// https://github.com/vuejs/vue-next/blob/cdc9f336fd/packages/compiler-core/src/transforms/vIf.ts
export const transformIf = createStructuralDirectiveTransform(
/^(if|else|else-if)$/,
(node, dir, context) => {
return processIf(node, dir, context, (ifNode, branch, isRoot) => {
// ...
return () => {
if (isRoot) {
ifNode.codegenNode = createCodegenNodeForBranch(
branch,
key,
context
) as IfConditionalExpression
} else {
// attach this branch's codegen node to the v-if root.
const parentCondition = getParentCondition(ifNode.codegenNode!)
parentCondition.alternate = createCodegenNodeForBranch(
branch,
key + ifNode.branches.length - 1,
context
)
}
}
})
}
)
history
这个对象在html5
的时候新加入两个api
history.pushState()
和history.repalceState()
这两个API
可以在不进行刷新的情况下,操作浏览器的历史纪录。唯一不同的是,前者是新增一个历史记录,后者是直接替换当前的历史记录。
从参数上来说:
window.history.pushState(state,title,url)
//state:需要保存的数据,这个数据在触发popstate事件时,可以在event.state里获取
//title:标题,基本没用,一般传null
//url:设定新的历史纪录的url。新的url与当前url的origin必须是一样的,否则会抛出错误。url可以时绝对路径,也可以是相对路径。
//如 当前url是 https://www.baidu.com/a/,执行history.pushState(null, null, './qq/'),则变成 https://www.baidu.com/a/qq/,
//执行history.pushState(null, null, '/qq/'),则变成 https://www.baidu.com/qq/
window.history.replaceState(state,title,url)
//与pushState 基本相同,但她是修改当前历史纪录,而 pushState 是创建新的历史纪录
另外还有:
window.history.back()
后退window.history.forward()
前进window.history.go(1)
前进或者后退几步从触发事件的监听上来说:
pushState()
和replaceState()
不能被popstate
事件所监听综合实践类题目,考查实战能力。没有什么绝对的正确答案,把平时工作的重点有条理的描述一下即可
思路
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
插件的 plugin
配置public
:用来放一些诸如 页头icon
之类的公共文件,会被打包到dist
根目录下src
:用来放项目代码文件api
:用来放http
的一些接口配置assets
:用来放一些 CSS
之类的静态资源components
:用来放项目通用组件layout
:用来放项目的布局router
:用来放项目的路由配置store
:用来放状态管理Pinia
的配置utils
:用来放项目中的工具方法类views
:用来放项目的页面文件原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。