Vue中封装的数组方法有哪些,其如何实现页面更新
在Vue中,对响应式处理利用的是Object.defineProperty对数据进行拦截,而这个方法并不能监听到数组内部变化,数组长度变化,数组的截取变化等,所以需要对这些操作进行hack,让Vue能监听到其中的变化。 那Vue是如何实现让这些数组方法实现元素的实时更新的呢,下面是Vue中对这些方法的封装:
// 缓存数组原型
const arrayProto = Array.prototype;
// 实现 arrayMethods.__proto__ === Array.prototype
export const arrayMethods = Object.create(arrayProto);
// 需要进行功能拓展的方法
const methodsToPatch = [
"push",
"pop",
"shift",
"unshift",
"splice",
"sort",
"reverse"
];
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function(method) {
// 缓存原生数组方法
const original = arrayProto[method];
def(arrayMethods, method, function mutator(...args) {
// 执行并缓存原生数组功能
const result = original.apply(this, args);
// 响应式处理
const ob = this.__ob__;
let inserted;
switch (method) {
// push、unshift会新增索引,所以要手动observer
case "push":
case "unshift":
inserted = args;
break;
// splice方法,如果传入了第三个参数,也会有索引加入,也要手动observer。
case "splice":
inserted = args.slice(2);
break;
}
//
if (inserted) ob.observeArray(inserted);// 获取插入的值,并设置响应式监听
// notify change
ob.dep.notify();// 通知依赖更新
// 返回原生数组方法的执行结果
return result;
});
});
简单来说就是,重写了数组中的那些原生方法,首先获取到这个数组的__ob__,也就是它的Observer对象,如果有新的值,就调用observeArray继续对新的值观察变化(也就是通过target__proto__ == arrayMethods
来改变了数组实例的型),然后手动调用notify,通知渲染watcher,执行update。
Vue.js的template编译
简而言之,就是先转化成AST树,再得到的render函数返回VNode(Vue的虚拟DOM节点),详细步骤如下:
首先,通过compile编译器把template编译成AST语法树(abstract syntax tree 即 源代码的抽象语法结构的树状表现形式),compile是createCompiler的返回值,createCompiler是用以创建编译器的。另外compile还负责合并option。
然后,AST会经过generate(将AST语法树转化成render funtion字符串的过程)得到render函数,render的返回值是VNode,VNode是Vue的虚拟DOM节点,里面有(标签名、子节点、文本等等)
怎样理解 Vue 的单向数据流
数据总是从父组件传到子组件,子组件没有权利修改父组件传过来的数据,只能请求父组件对原始数据进行修改。这样会 防止从子组件意外改变父级组件的状态 ,从而导致你的应用的数据流向难以理解
注意 :在子组件直接用 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()
}
}
虚拟DOM真的比真实DOM性能好吗
- 首次渲染大量DOM时,由于多了一层虚拟DOM的计算,会比innerHTML插入慢。
- 正如它能保证性能下限,在真实DOM操作的时候进行针对性的优化时,还是更快的。
Vue 是如何实现数据双向绑定的
Vue
数据双向绑定主要是指:数据变化更新视图,视图变化更新数据,如下图所示:
- 输入框内容变化时,
Data
中的数据同步变化。即View => Data
的变化。 Data
中的数据变化时,文本节点的内容同步变化。即Data => View
的变化
Vue 主要通过以下 4 个步骤来实现数据双向绑定的
- 实现一个监听器 Observer :对数据对象进行遍历,包括子属性对象的属性,利用
Object.defineProperty()
对属性都加上setter
和getter
。这样的话,给这个对象的某个值赋值,就会触发setter
,那么就能监听到了数据变化 - 实现一个解析器 Compile :解析
Vue
模板指令,将模板中的变量都替换成数据,然后初始化渲染页面视图,并将每个指令对应的节点绑定更新函数,添加监听数据的订阅者,一旦数据有变动,收到通知,调用更新函数进行数据更新 - 实现一个订阅者 Watcher :
Watcher
订阅者是Observer
和Compile
之间通信的桥梁 ,主要的任务是订阅Observer
中的属性值变化的消息,当收到属性值变化的消息时,触发解析器Compile
中对应的更新函数 - 实现一个订阅器 Dep :订阅器采用 发布-订阅 设计模式,用来收集订阅者
Watcher
,对监听器Observer
和 订阅者Watcher
进行统一管理
Vue 数据双向绑定原理图
子组件可以直接改变父组件的数据么,说明原因
这是一个实践知识点,组件化开发过程中有个单项数据流原则
,不在子组件中修改父组件是个常识问题
思路
- 讲讲单项数据流原则,表明为何不能这么做
- 举几个常见场景的例子说说解决方案
- 结合实践讲讲如果需要修改父组件状态应该如何做
回答范例
- 所有的
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
,但是我们还是能够直接改内嵌的对象或属性
参考 前端进阶面试题详细解答
怎么实现路由懒加载呢
这是一道应用题。当打包应用时,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
代码分块
Vue.set的实现原理
- 给对应和数组本身都增加了
dep
属性 - 当给对象新增不存在的属性则触发对象依赖的
watcher
去更新 - 当修改数组索引时,我们调用数组本身的
splice
去更新数组(数组的响应式原理就是重新了splice
等方法,调用splice
就会触发视图更新)
基本使用
以下方法调用会改变原始数组:
push()
,pop()
,shift()
,unshift()
,splice()
,sort()
,reverse()
,Vue.set( target, key, value )
- 调用方法:
Vue.set(target, key, value )
target
:要更改的数据源(可以是对象或者数组)key
:要更改的具体数据value
:重新赋的值
<div id="app">{{user.name}} {{user.age}}</div>
<div id="app"></div>
<script>
// 1. 依赖收集的特点:给每个属性都增加一个dep属性,dep属性会进行收集,收集的是watcher
// 2. vue会给每个对象也增加一个dep属性
const vm = new Vue({
el: '#app',
data: { // vm._data
user: {name:'poetry'}
}
});
// 对象的话:调用defineReactive在user对象上定义一个age属性,增加到响应式数据中,触发对象本身的watcher,ob.dep.notify()更新
// 如果是数组 通过调用 splice方法,触发视图更新
vm.$set(vm.user, 'age', 20); // 不能给根属性添加,因为给根添加属性 性能消耗太大,需要做很多处理
// 修改肯定是同步的 -> 更新都是一步的 queuewatcher
</script>
相关源码
// src/core/observer/index.js 44
export class Observer { // new Observer(value)
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
constructor (value: any) {
this.value = value
this.dep = new Dep() // 给所有对象类型增加dep属性
}
}
// src/core/observer/index.js 201
export function set (target: Array<any> | Object, key: any, val: any): any {
// 1.是开发环境 target 没定义或者是基础类型则报错
if (process.env.NODE_ENV !== 'production' &&
(isUndef(target) || isPrimitive(target))
) {
warn(`Cannot set reactive property on undefined, null, or primitive value: ${(target: any)}`)
}
// 2.如果是数组 Vue.set(array,1,100); 调用我们重写的splice方法 (这样可以更新视图)
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// 利用数组的splice变异方法触发响应式
target.splice(key, 1, val)
return val
}
// 3.如果是对象本身的属性,则直接添加即可
if (key in target && !(key in Object.prototype)) {
target[key] = val // 直接修改属性值
return val
}
// 4.如果是Vue实例 或 根数据data时 报错,(更新_data 无意义)
const ob = (target: any).__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
// 5.如果不是响应式的也不需要将其定义成响应式属性
if (!ob) {
target[key] = val
return val
}
// 6.将属性定义成响应式的
defineReactive(ob.value, key, val)
// 通知视图更新
ob.dep.notify()
return val
}
我们阅读以上源码可知,vm.$set 的实现原理是:
- 如果目标是数组 ,直接使用数组的
splice
方法触发相应式; - 如果目标是对象 ,会先判读属性是否存在、对象是否是响应式,最终如果要对属性进行响应式处理,则是通过调用
defineReactive
方法进行响应式处理(defineReactive
方法就是Vue
在初始化对象时,给对象属性采用Object.defineProperty
动态添加getter
和setter
的功能所调用的方法)
你觉得vuex有什么缺点
分析
相较于redux
,vuex
已经相当简便好用了。但模块的使用比较繁琐,对ts
支持也不好。
体验
使用模块:用起来比较繁琐,使用模式也不统一,基本上得不到类型系统的任何支持
const store = createStore({
modules: {
a: moduleA
}
})
store.state.a // -> 要带上 moduleA 的key,内嵌模块的话会很长,不得不配合mapState使用
store.getters.c // -> moduleA里的getters,没有namespaced时又变成了全局的
store.getters['a/c'] // -> 有namespaced时要加path,使用模式又和state不一样
store.commit('d') // -> 没有namespaced时变成了全局的,能同时触发多个子模块中同名mutation
store.commit('a/d') // -> 有namespaced时要加path,配合mapMutations使用感觉也没简化
回答范例
vuex
利用响应式,使用起来已经相当方便快捷了。但是在使用过程中感觉模块化这一块做的过于复杂,用的时候容易出错,还要经常查看文档- 比如:访问
state
时要带上模块key
,内嵌模块的话会很长,不得不配合mapState
使用,加不加namespaced
区别也很大,getters
,mutations
,actions
这些默认是全局,加上之后必须用字符串类型的path来匹配,使用模式不统一,容易出错;对ts的支持也不友好,在使用模块时没有代码提示。 - 之前
Vue2
项目中用过vuex-module-decorators
的解决方案,虽然类型支持上有所改善,但又要学一套新东西,增加了学习成本。pinia
出现之后使用体验好了很多,Vue3 + pinia
会是更好的组合
原理
下面我们来看看vuex
中store.state.x.y
这种嵌套的路径是怎么搞出来的
首先是子模块安装过程:父模块状态
parentState
上面设置了子模块名称moduleName
,值为当前模块state
对象。放在上面的例子中相当于:store.state['x'] = moduleX.state
。此过程是递归的,那么store.state.x.y
安装时就是:store.state['x']['y'] = moduleY.state
//源码位置 https://github1s.com/vuejs/vuex/blob/HEAD/src/store-util.js#L102-L115
if (!isRoot && !hot) {
// 获取父模块state
const parentState = getNestedState(rootState, path.slice(0, -1))
// 获取子模块名称
const moduleName = path[path.length - 1]
store._withCommit(() => {
// 把子模块state设置到父模块上
parentState[moduleName] = module.state
})
}
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标签;如果页面是个复杂的内容,比如商品信息,可以添加点击事件,使用编程式导航 - 实际上内部两者调用的导航函数是一样的
vue-router 有哪几种导航守卫
- 全局守卫
- 路由独享守卫
- 路由组件内的守卫
全局守卫
vue-router全局有三个守卫
router.beforeEach
全局前置守卫 进入路由之前router.beforeResolve
全局解析守卫(2.5.0+) 在beforeRouteEnter
调用之后调用router.afterEach
全局后置钩子 进入路由之后
// main.js 入口文件
import router from './router'; // 引入路由
router.beforeEach((to, from, next) => {
next();
});
router.beforeResolve((to, from, next) => {
next();
});
router.afterEach((to, from) => {
console.log('afterEach 全局后置钩子');
});
路由独享守卫
如果你不想全局配置守卫的话,你可以为某些路由单独配置守卫
const router = new VueRouter({
routes: [
{
path: '/foo',
component: Foo,
beforeEnter: (to, from, next) => {
// 参数用法什么的都一样,调用顺序在全局前置守卫后面,所以不会被全局守卫覆盖
// ...
}
}
]
})
路由组件内的守卫
beforeRouteEnter
进入路由前, 在路由独享守卫后调用 不能 获取组件实例this
,组件实例还没被创建beforeRouteUpdate
(2.2
) 路由复用同一个组件时, 在当前路由改变,但是该组件被复用时调用 可以访问组件实例this
beforeRouteLeave
离开当前路由时, 导航离开该组件的对应路由时调用,可以访问组件实例this
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
为例,先来看看Vue
中的双向绑定流程是什么的
new Vue()
首先执行初始化,对data
执行响应化处理,这个过程发生Observe
中- 同时对模板执行编译,找到其中动态绑定的数据,从
data
中获取并初始化视图,这个过程发生在Compile
中 - 同时定义⼀个更新函数和
Watcher
,将来对应数据变化时Watcher
会调用更新函数 - 由于
data
的某个key
在⼀个视图中可能出现多次,所以每个key
都需要⼀个管家Dep
来管理多个Watcher
- 将来data中数据⼀旦发生变化,会首先找到对应的
Dep
,通知所有Watcher
执行更新函数
流程图如下:
先来一个构造函数:执行初始化,对data
执行响应化处理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 对data选项做响应式处理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 执行编译
new Compile(options.el, this);
}
}
对data
选项执行响应化具体操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
编译Compile
对每个元素节点的指令进行扫描跟解析,根据指令模板替换数据,以及绑定相应的更新函数
class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 获取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍历子元素
if (this.isElement(node)) { // 判断是否为节点
console.log("编译元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("编译插值⽂本" + node.textContent); // 判断是否为插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判断是否有子元素
this.compile(node); // 对子元素进行递归遍历
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依赖收集
视图中会用到data
中某key
,这称为依赖。同⼀个key
可能出现多次,每次都需要收集出来用⼀个Watcher
来维护它们,此过程称为依赖收集多个Watcher
需要⼀个Dep
来管理,需要更新时由Dep
统⼀通知
实现思路
defineReactive
时为每⼀个key
创建⼀个Dep
实例- 初始化视图时读取某个
key
,例如name1
,创建⼀个watcher1
- 由于触发
name1
的getter
方法,便将watcher1
添加到name1
对应的Dep
中 - 当
name1
更新,setter
触发时,便可通过对应Dep
通知其管理所有Watcher
更新
// 负责更新视图
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 创建实例时,把当前实例指定到Dep.target静态属性上
Dep.target = this
// 读一下key,触发get
vm[key]
// 置空
Dep.target = null
}
// 未来执行dom更新函数,由dep调用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
声明Dep
class Dep {
constructor() {
this.deps = []; // 依赖管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
创建watcher
时触发getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依赖收集,创建Dep
实例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher实例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep执行更新方法
},
});
}
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
创建或更新
Vue3.2 setup 语法糖汇总
提示:vue3.2
版本开始才能使用语法糖!
在 Vue3.0
中变量必须 return
出来, template
中才能使用;而在 Vue3.2
中只需要在 script
标签上加上 setup
属性,无需 return
, template
便可直接使用,非常的香啊!
1. 如何使用setup语法糖
只需在 script
标签上写上 setup
<template>
</template>
<script setup>
</script>
<style scoped lang="less">
</style>
2. data数据的使用
由于 setup
不需写 return
,所以直接声明数据即可
<script setup>
import {
ref,
reactive,
toRefs,
} from 'vue'
const data = reactive({
patternVisible: false,
debugVisible: false,
aboutExeVisible: false,
})
const content = ref('content')
//使用toRefs解构
const { patternVisible, debugVisible, aboutExeVisible } = toRefs(data)
</script>
3. method方法的使用
<template >
<button @click="onClickHelp">帮助</button>
</template>
<script setup>
import {reactive} from 'vue'
const data = reactive({
aboutExeVisible: false,
})
// 点击帮助
const onClickHelp = () => {
console.log(`帮助`)
data.aboutExeVisible = true
}
</script>
4. watchEffect的使用
<script setup>
import {
ref,
watchEffect,
} from 'vue'
let sum = ref(0)
watchEffect(()=>{
const x1 = sum.value
console.log('watchEffect所指定的回调执行了')
})
</script>
5. watch的使用
<script setup>
import {
reactive,
watch,
} from 'vue'
//数据
let sum = ref(0)
let msg = ref('hello')
let person = reactive({
name:'张三',
age:18,
job:{
j1:{
salary:20
}
}
})
// 两种监听格式
watch([sum,msg],(newValue,oldValue)=>{
console.log('sum或msg变了',newValue,oldValue)
},
{immediate:true}
)
watch(()=>person.job,(newValue,oldValue)=>{
console.log('person的job变化了',newValue,oldValue)
},{deep:true})
</script>
6. computed计算属性的使用
computed
计算属性有两种写法(简写和考虑读写的完整写法)
<script setup>
import {
reactive,
computed,
} from 'vue'
// 数据
let person = reactive({
firstName:'poetry',
lastName:'x'
})
// 计算属性简写
person.fullName = computed(()=>{
return person.firstName + '-' + person.lastName
})
// 完整写法
person.fullName = computed({
get(){
return person.firstName + '-' + person.lastName
},
set(value){
const nameArr = value.split('-')
person.firstName = nameArr[0]
person.lastName = nameArr[1]
}
})
</script>
7. props父子传值的使用
父组件代码如下(示例):
<template>
<child :name='name'/>
</template>
<script setup>
import {ref} from 'vue'
// 引入子组件
import child from './child.vue'
let name= ref('poetry')
</script>
子组件代码如下(示例):
<template>
<span>{{props.name}}</span>
</template>
<script setup>
import { defineProps } from 'vue'
// 声明props
const props = defineProps({
name: {
type: String,
default: 'poetries'
}
})
// 或者
//const props = defineProps(['name'])
</script>
8. emit子父传值的使用
父组件代码如下(示例):
<template>
<AdoutExe @aboutExeVisible="aboutExeHandleCancel" />
</template>
<script setup>
import { reactive } from 'vue'
// 导入子组件
import AdoutExe from '../components/AdoutExeCom'
const data = reactive({
aboutExeVisible: false,
})
// content组件ref
// 关于系统隐藏
const aboutExeHandleCancel = () => {
data.aboutExeVisible = false
}
</script>
子组件代码如下(示例):
<template>
<a-button @click="isOk">
确定
</a-button>
</template>
<script setup>
import { defineEmits } from 'vue';
// emit
const emit = defineEmits(['aboutExeVisible'])
/**
* 方法
*/
// 点击确定按钮
const isOk = () => {
emit('aboutExeVisible');
}
</script>
9. 获取子组件ref变量和defineExpose暴露
即vue2
中的获取子组件的ref
,直接在父组件中控制子组件方法和变量的方法
父组件代码如下(示例):
<template>
<button @click="onClickSetUp">点击</button>
<Content ref="content" />
</template>
<script setup>
import {ref} from 'vue'
// content组件ref
const content = ref('content')
// 点击设置
const onClickSetUp = ({ key }) => {
content.value.modelVisible = true
}
</script>
<style scoped lang="less">
</style>
子组件代码如下(示例):
<template>
<p>{{data }}</p>
</template>
<script setup>
import {
reactive,
toRefs
} from 'vue'
/**
* 数据部分
* */
const data = reactive({
modelVisible: false,
historyVisible: false,
reportVisible: false,
})
defineExpose({
...toRefs(data),
})
</script>
10. 路由useRoute和useRouter的使用
<script setup>
import { useRoute, useRouter } from 'vue-router'
// 声明
const route = useRoute()
const router = useRouter()
// 获取query
console.log(route.query)
// 获取params
console.log(route.params)
// 路由跳转
router.push({
path: `/index`
})
</script>
11. store仓库的使用
<script setup>
import { useStore } from 'vuex'
import { num } from '../store/index'
const store = useStore(num)
// 获取Vuex的state
console.log(store.state.number)
// 获取Vuex的getters
console.log(store.state.getNumber)
// 提交mutations
store.commit('fnName')
// 分发actions的方法
store.dispatch('fnName')
</script>
12. await的支持
setup
语法糖中可直接使用await
,不需要写async
,setup
会自动变成async setup
<script setup>
import api from '../api/Api'
const data = await Api.getData()
console.log(data)
</script>
13. provide 和 inject 祖孙传值
父组件代码如下(示例):
<template>
<AdoutExe />
</template>
<script setup>
import { ref,provide } from 'vue'
import AdoutExe from '@/components/AdoutExeCom'
let name = ref('py')
// 使用provide
provide('provideState', {
name,
changeName: () => {
name.value = 'poetries'
}
})
</script>
子组件代码如下(示例):
<script setup>
import { inject } from 'vue'
const provideState = inject('provideState')
provideState.changeName()
</script>
Vue组件渲染和更新过程
渲染组件时,会通过
Vue.extend
方法构建子组件的构造函数,并进行实例化。最终手动调用$mount()
进行挂载。更新组件时会进行patchVnode
流程,核心就是diff
算法
怎么监听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为什么没有类似于React中shouldComponentUpdate的生命周期
- 考点:
Vue
的变化侦测原理 - 前置知识: 依赖收集、虚拟
DOM
、响应式系统
根本原因是
Vue
与React
的变化侦测方式有所不同
- 当React知道发生变化后,会使用
Virtual Dom Diff
进行差异检测,但是很多组件实际上是肯定不会发生变化的,这个时候需要shouldComponentUpdate
进行手动操作来减少diff
,从而提高程序整体的性能 Vue
在一开始就知道那个组件发生了变化,不需要手动控制diff
,而组件内部采用的diff
方式实际上是可以引入类似于shouldComponentUpdate
相关生命周期的,但是通常合理大小的组件不会有过量的diff,手动优化的价值有限,因此目前Vue
并没有考虑引入shouldComponentUpdate
这种手动优化的生命周期
Class 与 Style 如何动态绑定
Class
可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:class="{ active: isActive, 'text-danger': hasError }"></div>
data: {
isActive: true,
hasError: false
}
数组语法:
<div v-bind:class="[isActive ? activeClass : '', errorClass]"></div>
data: {
activeClass: 'active',
errorClass: 'text-danger'
}
Style
也可以通过对象语法和数组语法进行动态绑定
对象语法:
<div v-bind:style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>
data: {
activeColor: 'red',
fontSize: 30
}
数组语法:
<div v-bind:style="[styleColor, styleSize]"></div>
data: {
styleColor: {
color: 'red'
},
styleSize:{
fontSize:'23px'
}
}
v-once的使用场景有哪些
分析
v-once
是Vue
中内置指令,很有用的API
,在优化方面经常会用到
体验
仅渲染元素和组件一次,并且跳过未来更新
<!-- single element -->
<span v-once>This will never change: {{msg}}</span>
<!-- the element have children -->
<div v-once>
<h1>comment</h1>
<p>{{msg}}</p>
</div>
<!-- component -->
<my-component v-once :comment="msg"></my-component>
<!-- `v-for` directive -->
<ul>
<li v-for="i in list" v-once>{{i}}</li>
</ul>
回答范例
v-once
是vue
的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新- 如果我们有一些元素或者组件在初始化渲染之后不再需要变化,这种情况下适合使用
v-once
,这样哪怕这些数据变化,vue
也会跳过更新,是一种代码优化手段 - 我们只需要作用的组件或元素上加上
v-once
即可 vue3.2
之后,又增加了v-memo
指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了- 编译器发现元素上面有
v-once
时,会将首次计算结果存入缓存对象,组件再次渲染时就会从缓存获取,从而避免再次计算
原理
下面例子使用了v-once
:
<script setup>
import { ref } from 'vue'
const msg = ref('Hello World!')
</script>
<template>
<h1 v-once>{{ msg }}</h1>
<input v-model="msg">
</template>
我们发现v-once
出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:
// ...
return (_ctx, _cache) => {
return (_openBlock(), _createElementBlock(_Fragment, null, [
// 从缓存获取vnode
_cache[0] || (
_setBlockTracking(-1),
_cache[0] = _createElementVNode("h1", null, [
_createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */)
]),
_setBlockTracking(1),
_cache[0]
),
// ...
标签:面试题,vue,const,边面,Vue,key,组件,data
From: https://www.cnblogs.com/bbxiaxia1998/p/16993874.html