vue的基本原理
当vue的实例创建时,vue会遍历data中的属性,用Object.defineProperty(vue3.0使用的是proxy)将他们转换为setter/getter,并在内部追踪相关依赖,在属性被访问和修改时通知变化。每个组件实例都有响应的watcher程序实例,它会在组件渲染的过程中把属性记录为依赖,之后当依赖项的setter被调用时,会通知watcher重新计算,从而使关联的组件更新。
双向绑定对的原理
Vue.js是采用数据劫持结合发布者-订阅者模式的方法,通过Object.defineProperty()来劫持各个属性的setter和getter,在数据发生变动时发布消息给订阅者,触发响应的监听回调。
-
实现一个监听器Observer,用来劫持并监听所有属性,如果有变动的,就通知订阅者。
-
实现一个订阅者Watcher,每一个Watcher都绑定一个更新函数,watcher可以收到属性的变化通知并执行相应的函数,从而更新视图。
-
实现一个解析器Compile,可以扫描和解析每个节点的相关指令(v-model,v-on等指令),如果节点存在v-model,v-on等指令,则解析器Compile初始化这类节点的模板数据,使之可以显示在视图上,然后初始化相应的订阅者(Watcher)。
// dep是消息订阅器Dep
使用 Object.defineProperty() 来进行数据劫持有什么缺点?
在对一些属性进行操作时,使用这种方法无法拦截,比如通过下标方式修改数组数据或者给对象新增属性,这都不能触发组件的重新渲染,因为 Object.defineProperty 不能拦截到这些操作。更精确的来说,对于数组而言,大部分操作都是拦截不到的,只是 Vue 内部通过重写函数的方式解决了这个问题。
在 Vue3.0 中已经不使用这种方式了,而是通过使用 Proxy 对对象进行代理,从而实现数据劫持。使用Proxy 的好处是它可以完美的监听到任何方式的数据改变,唯一的缺点是兼容性的问题,因为 Proxy 是 ES6 的语法。
MVVM、MVC、MVP的区别
MVVM,MVC,MVP是三种常见的软件架构设计模式,主要通过分离关注点的方式来组织代码结构,优化代码效率。
- MVVM分为Model,View和ViewModel三部分
- Model代表数据模型,数据和业务逻辑都在Model层中定义;
- View代表UI视图,负责数据的展示;
- ViewModel负责监听Model中数据的改变并且控制视图的更新,处理用户交互操作;
Model和View并无直接关联,而是通过ViewModel来进行联系的,Model和ViewModel之间有着双向数据绑定的联系。因此当Model中的数据改变时会触发View层的刷新,View中由于用户交互操作而改变的数据也会在Model中同步。 这种模式实现了 Model和View的数据自动同步,因此开发者只需要专注于数据的维护操作即可,而不需要自己操作DOM。
单项数据流
- 什么是单向数据流
单向数据流(Unidirectional data flow)方式使用一个上传数据流和一个下传数据流进行双向数据通信,两个数据流之间相互独立。单向数据流指只能从一个方向来修改状态。
2. Vue 中的单向数据流
对于 Vue 来说,组件之间的数据传递具有单向数据流这样的特性。
- 父组件总是通过 props 向子组件传递数据;
- 所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定;
- 父级 prop 的更新会向下流动到子组件中,但是反过来则不行;
- 这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解;
- 每次父级组件发生更新时,子组件中所有的 prop 都将会刷新为最新的值;
- 这意味着不应该在一个子组件内部改变 prop。如果这样做,Vue 会在浏览器的控制台中发出警告。
3. 单向数据流 - 优点
- 所有状态的改变可记录、可跟踪,源头易追溯;
- 所有的数据,具有唯一出口和入口,使得数据操作更直观更容易理解,可维护性强;
- 当数据变化时,页面会自动变化
- 当你需要修改状态,完全重新开始走一个修改的流程。这限制了状态修改的方式,让状态变得可预测,容易调试。
4. 单向数据流 - 缺点
- 页面渲染完成后,有新数据不能自动更新,需要手动整合新数据和模板重新渲染
- 代码量上升,数据流转过程变长,代码重复性变大
- 由于对应用状态独立管理的严格要求(单一的全局 store,如:Vuex),在处理局部状态较多的场景时(如用户输入交互较多的“富表单型”应用),会显得啰嗦及繁琐。
双向数据流
1. 什么是双向数据流?
在双向数据流中,Model(可以理解为状态的集合) 中可以修改自己或其他Model的状态, 用户的操作(如在输入框中输入内容)也可以修改状态。(双向数据流也可以叫双向数据绑定)
当我们在前端开发中采用 MV* 的模式时,M - model,指的是模型,也就是数据,V - view,指的是视图,也就是页面展现的部分。
将从服务器获取的数据进行“渲染”,展现到视图上。每当数据有变更时,我们会再次进行渲染,从而更新视图,使得视图与数据保持一致
页面也会通过用户的交互,产生状态、数据的变化,这个时候,我们则编写代码,将视图对数据的更新同步到数据
2. 双向数据流 - 优点
- 数据模型变化与更新,会自动同步到页面上,用户在页面的数据操作,也会自动同步到数据模型
- 无需进行和单向数据绑定的那些相关操作;
- 在表单交互较多的场景下,会简化大量业务无关的代码。
3. 双向数据流 - 缺点
- 无法追踪局部状态的变化;
- “暗箱操作”,增加了出错时 debug 的难度;
- 由于组件数据变化来源入口变得可能不止一个,数据流转方向易紊乱。
- 改变一个状态有可能会触发一连串的状态的变化,最后很难预测最终的状态是什么样的。使得代码变得很难调试
生命周期
Vue 实例有⼀个完整的⽣命周期,也就是从开始创建、初始化数据、编译模版、挂载Dom -> 渲染、更新 -> 渲染、卸载 等⼀系列过程,称这是Vue的⽣命周期。
- 生命周期包括beforeCreate,created,beforeMount,Mounted,beforeUpdate,Updated,beforeDestroy,destroyed
-
beforeCreate(创建前) :数据观测和初始化事件还未开始,此时 data 的响应式追踪、event/watcher 都还没有被设置,也就是说不能访问到data、computed、watch、methods上的方法和数据。
-
created(创建后) :实例创建完成,实例上配置的 options 包括 data、computed、watch、methods 等都配置完成,但是此时渲染得节点还未挂载到 DOM,所以不能访问到
$el属性。 -
beforeMount(挂载前) :在挂载开始之前被调用,相关的render函数首次被调用。实例已完成以下的配置:编译模板,把data里面的数据和模板生成html。此时还没有挂载html到页面上。
-
mounted(挂载后) :在el被新创建的 vm.$el 替换,并挂载到实例上去之后调用。实例已完成以下的配置:用上面编译好的html内容替换el属性指向的DOM对象。完成模板中的html渲染到html 页面中。此过程中进行ajax交互。
-
beforeUpdate(更新前) :响应式数据更新时调用,此时虽然响应式数据更新了,但是对应的真实 DOM 还没有被渲染。
-
updated(更新后) :在由于数据更改导致的虚拟DOM重新渲染和打补丁之后调用。此时 DOM 已经根据响应式数据的变化更新了。调用时,组件 DOM已经更新,所以可以执行依赖于DOM的操作。然而在大多数情况下,应该避免在此期间更改状态,因为这可能会导致更新无限循环。该钩子在服务器端渲染期间不被调用。
-
beforeDestroy(销毁前) :实例销毁之前调用。这一步,实例仍然完全可用,
this仍能获取到实例。 -
destroyed(销毁后) :实例销毁后调用,调用后,Vue 实例指示的所有东西都会解绑定,所有的事件监听器会被移除,所有的子实例也会被销毁。该钩子在服务端渲染期间不被调用。
keep-alive
keep-alive是 Vue 提供的一个内置组件,用来对组件进行缓存——在组件切换过程中将状态保留在内存中,防止重复渲染DOM。
keep-alive: keep-alive 独有的生命周期,分别为 activated 和 deactivated 。用 keep-alive 包裹的组件在切换时不会进行销毁,而是缓存到内存中并执行 deactivated 钩子函数,命中缓存渲染后会执行 activated 钩子函数。
如果为一个组件包裹了 keep-alive,那么它会多出两个生命周期:deactivated、activated。同时,beforeDestroy 和 destroyed 就不会再被触发了,因为组件不会被真正销毁。
父子组件嵌套下的生命周期
父组件created -> 子组件created -> 子组件mounted ->父组件mounted
常用的修饰符
- .stop
- .once
- .prevent
- sync (属性间的双向绑定)
表单修饰符
- .number
- .trim
watch和computed的区别
watch如何实现首次加载就触发监听
计算属性里面可不可以有异步逻辑
不可以 计算属性里面有return 所以不能有异步 immdeterly deep
vue项目实现首屏加载优化
- 服务器端渲染
- 骨架屏
- 图片懒加载
- 异步组件
- 路由懒加载
- webpack配置
- 减少http请求
- css合并
动态组件
根据某些条件渲染组件 component占位符 vue的内置组件component,是一个占位符的作用,用v-bing:is动态绑定组件名
vueX
1.页面刷新后store中的数据是否存在
页面刷新后strore中的数据会丢失。原因是在页面刷新时,vue的实例会重新加载,因此导致store也会重置。store是用来存储状态的,而不是用来存储数据的。
2.如何持久化
- 监听页面是否刷新,如果页面刷新了,将store对象存储到localStorage中。页面打开后,判断localStorage中是否存在store,有的话则直接赋值使用,没有的话则表示是第一次进入,取store中的默认值。
<template>
<div id="app">
<router-view />
</div>
</template>
<script>
export default {
name: 'App',
created() {
// 在页面加载时读取sessionStorage里的状态信息
if (sessionStorage.getItem('store')) {
this.$store.replaceState( Object.assign(
{}, this.$store.state,JSON.parse(sessionStorage.getItem('store')) ) )
}
// 页面刷新时,将vuex里的信息保存到sessionStorage
// 在页面刷新时先触发beforeunload事件
window.addEventListener('beforeunload', () =>{sessionStorage.setItem('store',JSON.stringify(this.$store.state)) }) },
}
</script>
- 使用插件
vuex-persistedstate
import Vue from 'vue'
import Vuex from 'vuex'
import createPersistedState from 'vuex-persistedstate'
Vue.use(Vuex)
const store = new Vuex.Store({
// 定义状态
state: {
isopen:'',
isThirdAuthDev:false,
id: '',
nickname: '',
fieldsbind:[],
bindList:{}
},
mutations: {
// 相当于编写set方法
setUser (state, data) {
state.nickname = data
},
setUserId (state, data) {
state.id = data
},
},
actions: {
// 写一些异步的操作
// setUserAysnc (context, LoginUser) {
// context.commit('setUser', LoginUser) // }
},
modules: {},
plugins: [
createPersistedState({
storage: window.localStorage,
reducer (state) {
return { id: state.id, // 持久化
nickname: state.nickname,
username: state.username,
}
}
})
]
})
vuex包括哪些方法
- state:统一定义管理公共数据 ($store.state.userInfo.name) MapState
- getters:相当于计算属性($store.getters.realName) MapGetters
- mutations:修改数据 ($store.commit('addAge',{nuber:1}))
- actions:里面写一些异步方法 ($store.dispatch('userInfo'))
- model:拆分模块
vue2和vue3的区别
- 生命周期 对于生命周期来说,整体上变化不大,只是大部分的生命周期函数名称加上"on",功能上是类似的。vue3在组合式API(Composition API)中使用生命周期钩子时需要钩子时需要先引入,而Vue2在选项API(Option API)中可以直接调用钩子函数
// vue3
<script setup>
import { onMounted } from 'vue';
// 使用前需引入生命周期钩子
onMounted(() => { // ... }); // 可将不同的逻辑拆开成多个onMounted,依然按顺序执行,不会被覆盖
onMounted(() => { // ... });
</script>
// vue2
<script>
export default { mounted() { // 直接调用生命周期钩子 // ... }, }
</script>
- 多根节点
在vue2中,模板中只能有一个根节点,但是vue3中支持多节点,也就是fragment。
// vue2中在template里存在多个根节点会报错
<template>
<header></header>
<main></main>
<footer></footer>
</template>
// 只能存在一个根节点,需要用一个<div>来包裹着
<template>
<div>
<header></header>
<main></main>
<footer></footer>
</div>
</template>
- Composition API
Vue2 是选项API(Options API),一个逻辑会散乱在文件不同位置(data、props、computed、watch、生命周期钩子等),导致代码的可读性变差。当需要修改某个逻辑时,需要上下来回跳转文件位置。
Vue3 组合式API(Composition API)则很好地解决了这个问题,可将同一逻辑的内容写到一起,增强了代码的可读性、内聚性,其还提供了较为完美的逻辑复用性方案。
- 异步组件(Suspense)
Vue3 提供 Suspense 组件,允许程序在等待异步组件加载完成前渲染兜底的内容,如 loading ,使用户的体验更平滑。使用它,需在模板中声明,并包括两个命名插槽:default 和 fallback。Suspense 确保加载完异步内容时显示默认插槽,并将 fallback 插槽用作加载状态。
<tempalte>
<suspense>
<template #default>
<List />
</template>
<template #fallback>
<div> Loading... </div>
</template>
</suspense>
</template>
在 List 组件(有可能是异步组件,也有可能是组件内部处理逻辑或查找操作过多导致加载过慢等)未加载完成前,显示 Loading...(即 fallback 插槽内容),加载完成时显示自身(即 default 插槽内容)。
- 响应式原理
Vue2 响应式原理基础是 Object.defineProperty;Vue3 响应式原理基础是 Proxy。
-
为什么vue3更改响应原理呢:vue2无法监听数组或对象新增删除的元素。
Vue2 相应解决方案:针对常用数组原型方法push、pop、shift、unshift、splice、sort、reverse进行了hack处理;提供Vue.set监听对象/数组新增属性。对象的新增/删除响应,还可以new个新对象,新增则合并新属性和旧对象;删除则将删除属性后的对象深拷贝给新对象。
Proxy 是 ES6 新特性,通过第2个参数 handler 拦截目标对象的行为。相较于 Object.defineProperty 提供语言全范围的响应能力,消除了局限性。 6.优化底层diff算法
vue2怎么实现深度监听
- 增加属性时使用set(this.data,”key”,value')**
let obj = {name:'amy'}
let arr = ['joy']
obj.age = 19
arr [1] = 'fff' //此时监听不到相应
this.$set(this.obj,'age',19);
this.$set(this.arr,1,'fff')
- watch增加deep的属性
watch:{
name:{
handler(newVuale,oldValue){
console.log(newVuale)
}
},
deep:true
}
组合式api的优势
- 逻辑统一写在一起,便于开发维护
vue2的v-modle在vue3中如何实现
- vue2中的v-modle是一个语法糖,实现数据双向绑定。vue中提供:mode属性来改变默认绑定的props和event。
<input v-modle="val">
//等价于
<input :value="val" @input="val=$event.target.value">
//在自定义组件中
如果作为子组件使用,父组件如何实现双向绑定?
可以在父组件调用子组件的v-model重写
//父组件
<component v-model="val">
<component :value="val" @input="val = arguments[0]"
//子组件
<input type="text" :value="val" @input="$emit('input',$event.target.value)">
model: {
prop: 'val',
event: 'input'
}
props: {
val: String
}
//**model的作用就是将父子组件之间,传入的值和触发的事件一一对应起来(所以prop要和props里面获取的值一样)。**
**但是我们也可以发现一个问题,由于传入的值和model中的prop必须是对应的,所以一次只能绑定一个v-model。**
- Vue3,重写了v-model,可以绑定多个v-model
移除了model
prop默认值改为了modelValue
event默认值改为了update:modelValue
**总结一下,就是可以通过v-model:xxx的方式,然后通过抛出的方法update:xxx来实现多个v-model的绑定 **
//父组件
<template>
<div>
<Child v-model:text="message" />
<div>绑定的值:{{message}}</div>
</div>
</template>
//子组件
<template>
<div>
<input
type="text"
:value="text"
@input="$emit('update:text', $event.target.value)"
>
</div>
</template>
<script>
export default {
//3.x 接收v-model冒号后面的值,相应的触发的方法改为update:text
props:['text']
}
</script>
//父组件
<template>
<div>
<Child
v-model="message1"
v-model:text="message2"
/>
<div>绑定的值1:{{message1}}</div>
<div>绑定的值2:{{message2}}</div>
</div>
</template>
//子组件
<template>
<div>
<input
type="text"
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)"
>
<input
type="text"
:value="text"
@input="$emit('update:text', $event.target.value)"
>
</div>
</template>
<script>
export default {
//v-model冒号后面不写值,默认就是modelValue
props:['modelValue','text']
}
</script>
父子组件之间如何通信
- props/$emit 父组件通过props将数据流向子组件,子组件触发函数通知父组件
- emit A组件通过emit发送数据 B组件通过on来接收数据 ($once移除事件)
- vuex
$refs在通信时有什么弊端
- 必须在模板渲染之后,不是响应式的,时不时配合
$nextTick
自定义组件的v-model是做什么的
是实现父子组件数据双向绑定的
vue3
1.ref和reactive的区别
- reactive参数一般接受对象或数组,是深层次的响应式。ref参数一般接收简单数据类型,若ref接收对象为参数,本质上会转变为reactive方法
- 在JS中访问ref的值需要手动添加
.value,访问reactive不需要 - 响应式的底层原理都是Proxy
vue2
- 如何获取事件对象 $event
- 父子组件之间的通信 props+refs
- $refs在通信时的弊端
必须在模板渲染之后,不是响应式的,时不时配合`$nextTick`
- vuex页面刷新state是否存在
- vuex持久化怎么实现
- .sync修饰符的作用 :实现属性的双向绑定
money.sync ="total"
等价于
:money = "total" v-on:update:money="total =$event"
7. 自定义组件中的v-modle?v-bind和v-on的结合快乐老家 8. vue3中的v-modle怎么实现 9. 嵌套组件的生命周期的顺序 10. 首屏的优化 11. diff算法 双端算法和简单算法 12. qiankun框架 主应用和子应用之间合同通信(登录信息如何同步) 13. webpack 性能问题 14. 组件懒加载如何实现
- 普通组件的加载
<template>
<div class="hello">
<One-com></One-com>
</div>
</template>
<script>
import One from './one'
export default {
components:{
"One-com":One
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
- ES 提出的import方法
<template>
<div class="hello">
<One-com></One-com>
</div>
</template>
<script>
export default {
components:{
"One-com": ()=>import("./one");
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
- 异步方法
- 路由懒加载
- 普通路由加载
<template>
<div class="hello">
<One-com></One-com>
</div>
</template>
<script>
export default {
components:{
"One-com":resolve=>(['./one'],resolve)
},
data () {
return {
msg: 'Welcome to Your Vue.js App'
}
}
}
</script>
import Vue from 'vue'
import Router from 'vue-router'
import HelloWorld from '@/components/HelloWorld'
Vue.use(Router)
export default new Router({
routes: [ {
path: '/',
name: 'HelloWorld',
component:HelloWorld
} ]
})
vue异步组件实现懒加载
- component:resolve=>(require(['需要加载的路由的地址']),resolve)
import Vue from 'vue'
import Router from 'vue-router'
/* 此处省去之前导入的HelloWorld模块 */
Vue.use(Router)
export default new Router({
routes: [ {
path: '/',
name: 'HelloWorld',
component: resolve=>(require(["@/components/HelloWorld"],resolve))
}]
})
ES 提出的import方法(------最常用------)
- 方法如下:
const HelloWorld = ()=>import('需要加载的模块地址')(不加 { } ,表示直接return)
import Vue from 'vue'
import Router from 'vue-router'
Vue.use(Router)
export default new Router({
routes: [ {
path: '/',
name: 'HelloWorld',
component: ()=>import("@/components/HelloWorld")
} ]
})
webpack提供的require.ensure()
{
path: '/home',
name: 'Home',
component: r => require.ensure([],() => r(require('@/components/HelloWorld')), 'home')
}
- vue2中的minix和vue3中的hooks
项目中的问题
- 如何实现登录
- 怎么判断用户登录 路由拦截 判断token是否失效
- 代码管理 git 分支管理流程
- 如何实现用户的鉴权
- 如何保证多端显示效果的一致性
- 封装组件 如何实现
路由传参的两种方式
-
query
query是通过 URL 中的查询参数传递数据的,比如/user?id=123&name=张三,这里的id和name就是查询参数。在路由中,可以通过this.$route.query来获取当前路由的查询参数,例如:
// 跳转到 /user 页面,并传递查询参数
this.$router.push({ path: '/user', query: { id: '123', name: '张三' } })
// 在 /user 页面中获取查询参数
console.log(this.$route.query.id) // '123'
console.log(this.$route.query.name) // '张三'
- params
params 是通过 URL 中的占位符传递数据的,比如 /user/123,这里的 123 就是占位符。在路由中,可以通过 this.$route.params 来获取当前路由的占位符参数,例如:
// 定义带有占位符的路由
{
path: '/user/:id',
component: User
}
// 在 User 组件中获取占位符参数
console.log(this.$route.params.id) // '123'
需要注意的是,query 传递的数据会被编码到 URL 中,因此适合传递一些简单的数据,比如查询条件;而 params 传递的数据则不会被编码,因此适合传递一些复杂的数据,比如对象或数组。
- props
另外,params 传递的数据还可以使用 props 属性来进行传递,这种方式需要在路由配置中设置 props: true,并在组件中定义 props 属性来接收参数。例如:
// 定义带有占位符的路由,并开启 props 属性
{
path: '/user/:id',
component: User,
props: true
}
// 在 User 组件中定义 props 属性
props: ['id']
插槽
插槽是父组件向子组件传递代码片段
-
默认插槽
当 slot 没有指定 name 属性值的时候一个默认显示插槽,一个组件内只有有一个匿名插槽。
-
具名插槽
带有具体名字的插槽,也就是带有 name 属性的 slot,一个组件可以出现多个具名插槽。
-
作用域插槽
默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。
实现原理:当子组件 vm 实例化时,获取到父组件传入的 slot 标签的内容,存放在vm.$slot中,默认插槽为vm.$slot.default,具名插槽为vm.$slot.xxx,xxx 为插槽名,当组件执行渲染函数时候,遇到 slot 标签,使用$slot中的内容进行替换,此时可以为插槽传递数据,若存在数据,则可称该插槽为作用域插槽。
// 父组件
<template>
<div id="father">
<son>
<!-- 下面的节点如果没有使用插槽,将不会显示 -->
// 旧版本中的写法
<!-- 默认插槽 -->
<div>0000000000</div>
<!-- 具名插槽 -->
<div slot="a">11111111111</div>
<div slot="b">2222222222</div>
<!-- 作用域插槽:获取子组件中内部定义一个数据`msg` -->
<div slot="c" slot-scope="slotProps">
{{slotProps.msg}}
</div>
// 新版本中的写法:v-slot:插槽名 或 #插槽名
<template v-slot:c="slotProps">
{{slotProps.msg}}
</template>
<!-- 也可以缩写为 #插槽名 -->
<template #c="slotProps"> // slotProps是绑定在 <slot> 元素上的 attribute 被称为插槽 prop
{{slotProps.msg}}
</template>
</son>
</div>
</template>
<script>
import son from "./son.vue";
export default {
name: father,
components: {
son
}
};
</script>
// 子组件
<template>
<div id="son">
<!-- 插槽,允许父组件里面的元素在此处插入并显示 -->
<!-- 默认插槽 -->
<slot></slot>
<!-- 具名插槽 -->
<slot name="a"></slot>
<slot name="b"></slot>
<!-- 作用域插槽 -->
<slot name="c" :msg="msg"></slot>
</div>
</template>
<script>
export default {
name: "son",
data () {
return {
msg: "子组件的数据"
}
}
};
</script>
- slot的作用
- 扩展组件能力,提高组件的复用性;
- 使用插槽可以将一些比较复杂的父传子的通信去掉,直接在父组件中完成后利用插槽显示到子组件中(这是由于父组件模板的内容在父组件作用域内编译,子组件模板的内容在子组件作用域内编译)。