Vuejs基础
Vue的基本原理
Vuejs是依靠MVVM框架来实现的,也就是:Model、View、ViewModel
- View:视图层
- Model:数据层
- ViewModel:连接视图与数据的中间件
其实View和Model之间不能进行直接通信,但是可以通过ViewModel来进行通信。当数据发生改变的时候,ViewModel会监听数据发生了改变,并且通知View来进行页面的修改。当页面触发了事件,ViewModel会监听到事件,并通知model进行响应。
Vuejs数据驱动
vuejs在实例化的过程中,会对遍历传给实例化对象选项中的data 选项,遍历其所有属性并使用 Object.defineProperty 把这些属性全部转为 getter/setter。同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。当之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染。
双向绑定原理
new Vue()首先执行初始化,对data执行响应化处理,这个过程发生Observe中- 同时对模板执行编译,找到其中动态绑定的数据,从
data中获取并初始化视图,这个过程发生在Compile中 - 同时定义⼀个更新函数和
Watcher,将来对应数据变化时Watcher会调用更新函数 - 由于
data的某个key在⼀个视图中可能出现多次,所以每个key都需要⼀个管家Dep来管理多个Watcher - 将来data中数据⼀旦发生变化,会首先找到对应的
Dep,通知所有Watcher执行更新函数
Object.defineProperty() 来进行数据劫持有什么缺点?
因为Object.defineProperty()对于大部分数组的一些操作,不能拦截,vuejs在内部进行了重写函数。
vue3使用的是Proxy,然后能够对这些进行数据拦截
区别:proxy 的优势还在于监听的目标是整个对象而不是某个属性, Object.defineProperty 监听整个对象需要递归遍历,对性能也有影响,它监听的是属性
Computed和Watch的区别
Computed
- computed有缓存,优先走缓存,当依赖的数据发生改变的时候,才会重新进行
- computed是基于响应式来进行缓存的,对于那些ref、reactive,或者父组件传递过来的props中的数据进行计算的。
- 不支持异步
- 如果一个属性是由另一个属性计算而来的,这个属性依赖于另一个属性值,那么适合于computed
- 如果computed属性的属性值是函数,那么默认使用get方法,函数的返回值就是属性的属性值;在computed中,属性有一个get方法和一个set方法,当数据发生变化时,会调用set方法。
Watch
- 没有缓存
- 可以异步
- 接收两个参数,新值和旧值
- 监听数据必须是data中声明的或者父组件传递过来的props中的数据,当发生变化时,会触发其他操作,函数有两个的参数:
-
- immediate:组件加载立即触发回调函数
- deep:深度监听,发现数据内部的变化,在复杂数据类型中使用,例如数组中的对象发生变化。需要注意的是,deep无法监听到数组和对象内部的变化。
当想要执行异步或者昂贵的操作以响应不断的变化时,就需要使用watch。
运用的场景
- 当需要进行数据计算的时候,并且依赖其他数据的时候,应该使用computed
- 当数据的变化需要进行异步操作或者性能消耗很大的时候,就需要使用watch
Computed和Method的区别
- Computed是根据依赖的属性进行重新计算的
- Method是调用总是执行
Slot
slot为插槽,是Vue的内容分发机制,组件内部的模板引擎使用slot元素作为承载分发内容的出口。插槽slot是子组件的一个模板标签元素,而这一个标签元素是否显示,以及怎么显示是由父组件决定的。slot分为三类:默认插槽,具名插槽和作用域插槽。
- 作用域插槽:默认插槽、具名插槽的一个变体,可以是匿名插槽,也可以是具名插槽,该插槽的不同点是在子组件渲染作用域插槽时,可以将子组件内部的数据传递给父组件,让父组件根据子组件的传递过来的数据决定如何渲染该插槽。(这个准备在看看不会这里)
Vue3可以通过useSlot()来获取插槽名字
如何保存页面的当前的状态
分情况:
- 前组件会被卸载
- 前组件不会被卸载
前组件会被卸载
存储在localstorage/sessionstorage
子组件即将销毁的时候在localStorage/sessionStorage中把当前组件的state通过Json.stringify()储存下来
优点:简单、兼容性好,不需要额外的库或者工具,还有一些状态管理的库(pinia/vuex)其实都是根据这个来实现的
缺点:Json.stringfy自身的缺点,对于Date对象、Regexp对象来说会得到字符串,而不是原来的值
路由传值(看一下官网)
通过vue-router可以进行路由传值
将 props 传递给路由组件 | Vue Router (vuejs.org))
前组件不会被卸载
单页面渲染
也就是一个页面中含有许多子组件来构成的,由父组件来进行存储页面的状态
优点:代码量少,不需要考虑状态传递过程的错误
缺点:父组件维护成本增高,需要传递prop到B组件
keep-alive
当组件在keep-alive内被切换时,组件的activated、deactivated两个生命钩子会被执行,被包裹在keep-alive中的组建的状态会被保存
常见的事件修饰符
.stop、.prevent、self:只会触发自己范围之内的事件,不包含子组件,.once
v-if和v-show区别
-
v-if:动态地向DOM树中添加或者删除DOM元素;v-show:只是通过css地display来进行显示和隐藏元素
-
v-if:切换过程中会销毁dom元素及其内部的事件监听和子组件,v-show:只是简单的css切换
-
v-if:有更高的切换消耗,v-show:有更高地初始化消耗
-
v-show适合频繁切换,v-if适合运营条件不太会改变的时候
v-model作用
作用在表单元素
在表单上面绑定了 v-model,相当于input的value绑定了message值,@input动态地监听这个value的变化,并把这个值绑定了message,举个例子
<input v-model="message"></input>
=>等价于
<input :value="message" @input="message"></input>
作用在组件
本质上就是父子组件通信的一个语法糖,子组件通过props和emits来进行实现
例如
//父组件,这个pageTitle为响应式数据
<ChildComponent v-model="pageTitle" />
=>相当于
<ChildComponent :modelValue="pageTitle" @update:modelValue="pageTitle = $event" />
//子组件
<template>
<input :value="modelValue" @input="onChange" />
</template>
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
modelValue: {
type: XXX,
required: true
}
})
const emits = defineEmits(['update:modelValue'])
const onChange = () => {
emits('update:modelValue', 新值)
}
</script>
其实还可以进行修改这个默认的名称(modelValue),v-model:title进行修改
子组件就是这样的
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
title: {
type: XXX,
required: true
}
})
const emits = defineEmits(['update:title'])
</script>
多个v-model的使用
<Chile v-model:first="first" v-model:last="last" />
<script setup>
import { defineProps, defineEmits } from 'vue'
const props = defineProps({
first: {
type: String,
required: true
},
last: {
type: String,
required: true
},
})
const emits = defineEmits(['update:first','update:last'])
</script>
对keep-alive的理解
Vue3的通信
父子通信
- props(父传子)Props | Vue.js (vuejs.org))
- emits(子传父)子组件通知父组件触发一个事件,并且可以传值给父组件。(简称:子传父)
- expose/ref:子组件可以通过defineExpose来进行暴露出数据/函数,父组件通过在子组件上面添加ref来获取自组件的数据/函数
- 透传
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者
v-on事件监听器。最常见的例子就是class、style和id。
例子:
<!-- <MyButton> 的模板 -->
<button class="btn">click me</button>
<button class="btn large">click me</button>
单根节点
// Parent.vue
<template>
<Child msg="雷猴 世界!" name="鲨鱼辣椒" />
</template>
<script setup>
import { ref } from 'vue'
import Child from './components/Child.vue'
</script>
// Child.vue
<template>
<div>子组件:打开控制台看看</div>
</template>
多根节点:使用$attrs,来进行绑定
// Child.vue
<template>
<div :message="$attrs.msg">只绑定指定值</div>
<div v-bind="$attrs">全绑定</div>
</template>
- 插槽(默认插槽、具名插槽:子组件中的添加name、作用域插槽)
祖先传值
- provide/inject
遇到多层传值时,使用 props 和 emit 的方式会显得比较笨拙。这时就可以用 provide 和 inject 了。
provide 是在父组件里使用的,可以往下传值。
inject 是在子(后代)组件里使用的,可以网上取值。
无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
// Parent.vue
<template>
<Child></Child>
</template>
<script setup>
import { ref, provide, readonly } from 'vue'
import Child from './components/Child.vue'
const name = ref('猛虎下山')
const msg = ref('雷猴')
// 使用readonly可以让子组件无法直接修改,需要调用provide往下传的方法来修改
provide('name', readonly(name))
provide('msg', msg)
provide('changeName', (value) => {
name.value = value
})
</script>
// Child.vue
<template>
<div>
<div>msg: {{ msg }}</div>
<div>name: {{name}}</div>
<button @click="handleClick">修改</button>
</div>
</template>
<script setup>
import { inject } from 'vue'
const name = inject('name', 'hello') // 看看有没有值,没值的话就适用默认值(这里默认值是hello)
const msg = inject('msg')
const changeName = inject('changeName')
function handleClick() {
// 这样写不合适,因为vue里推荐使用单向数据流,当父级使用readonly后,这行代码是不会生效的。没使用之前才会生效。
// name.value = '雷猴'
// 正确的方式
changeName('虎躯一震')
// 因为 msg 没被 readonly 过,所以可以直接修改值
msg.value = '世界'
}
</script>
状态管理工具
- pinia
- vuex
pinia和vuex区别
Pinia 跟 Vuex 相比有以下优点:
- 调用时代码更简洁了。
- 对
TS更友好。 - 合并了
Vuex的Mutation和Action。天然的支持异步了。 - 天然分包。
其他的库
- mitt.js 官方示例
生命周期
路由
路由懒加载
- 箭头函数 + import
- 箭头函数 + require
const router = new Router({
routes:[
{
path:"/list",
component:resolve=>require([""],reslove)
}
]
})
路由的hash和history
hash模式
vue-router默认的路由模式:example.com/#/about 像这样的带一个#号的,这个#号在浏览器能看到,但是请求接口的时候不会携带。
Hash 模式基于浏览器的 window.location.hash 属性。当 URL 中的哈希部分发生变化时,Vue Router 会监听到这个变化,并相应地切换视图。
优点:
- 兼容性好
- 不需要服务器进行支持:刷新页面或直接访问某个路由时,服务器需要正确处理这个路由。
劣势:
- 不够美观
- SEO不友好:搜索引擎不会将哈希部分的内容作为独立的页面来处理。
开启方式
import { createRouter, createWebHashHistory } from 'vue-router'
// 省略...
// 创建路由 const router = createRouter({
history: createWebHashHistory(),
// routes: routes 的缩写
routes,
})
// 省略...
history模式
它是传统的路由分发模式,当输入一个url,服务器就会接受这个请求,并且进行解析这个url,然后做出相应的逻辑处理
原理:History 模式使用浏览器的 History API,通过修改浏览器的历史记录来实现前端路由的切换。在这种模式下,需要确保在任何路径下都返回同一个 HTML 文件,以便 Vue Router 能够正确处理路由。
优势:
- 美观
- SEO友好
劣势:
- 需要服务器支持:刷新页面或直接访问某个路由时,服务器需要正确处理这个路由。
- 兼容性差
开启方式
import { createRouter, createWebHistory } from 'vue-router'
// 省略...
// 创建路由
const router = createRouter({
history: createWebHistory(),
// routes: routes 的缩写
routes,
})
// 省略...
api:分为两大类修改历史状态和切换历史状态
- 修改历史状态:
history.pushState()和history.replaceState(),这两个方法应用于浏览器的历史记录站中,提供了对历史记录的修改,当以需要改变url但是不对页面进行刷新,可以用他们 - 切换历史状态:forword,back,go对应浏览器的前进,后退和跳转
监听hash的变化
setup(props, ctx: SetupContext) {
// 监听hash变化刷新页面,更换头部导航颜色
const hashChange = () => {
window.addEventListener(
'hashchange',
() => {
console.log('hash变化');
},
false
);
};
hashChange();
}
window.location.hash来读取#值(可读可写)
route和router
useRoute来获取路由的信息对象,path、fullPath、params、hash、query、name
useRouter是路由实例,包含对路由的跳转方法,push,replace
动态定义路由
const User = {
template: '<div>User</div>',
}
// 这些都会传递给 `createRouter`
const routes = [
// 动态字段以冒号开始
{ path: '/users/:id', component: User },
]
可以通过route.param来进行获取
监听路由的变化
const User = {
template: '...',
created() {
this.$watch(
() => this.$route.params,
(toParams, previousParams) => {
// 对路由变化做出响应...
}
)
},
}
捕获404和找不到路由
const routes = [
// 将匹配所有内容并将其放在 `$route.params.pathMatch` 下
{ path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound },
// 将匹配以 `/user-` 开头的所有内容,并将其放在 `$route.params.afterUser` 下
{ path: '/user-:afterUser(.*)', component: UserGeneric },
]
vue-router路由跳转和location.href区别
- location.href来跳转会刷新页面
- vue-router跳转,使用了diff算法,实现了按需加载,减少了dom的消耗
router里的路由守卫
- 全局路由守卫:router.beforeEach、router.afterEach(它们对于分析、更改页面标题、声明页面等辅助功能以及许多其他事情都很有用。)、router.beforeResolve(解析守卫刚好会在导航被确认之前、所有组件内守卫和异步路由组件被解析之后调用。)
router.beforeResolve(async to => {
if (to.meta.requiresCamera) {
try {
await askForCameraPermission()
} catch (error) {
if (error instanceof NotAllowedError) {
// ... 处理错误,然后取消导航
return false
} else {
// 意料之外的错误,取消导航并把错误传给全局处理器
throw error
}
}
}
})
- 路由独享守卫:beforeEnter
const routes = [
{
path: '/users/:id',
component: UserDetails,
beforeEnter: (to, from) => {
// reject the navigation
return false
},
},
]
beforeEnter 守卫 只在进入路由时触发,不会在 params、query 或 hash 改变时触发。例如,从 /users/2 进入到 /users/3 或者从 /users/2#info 进入到 /users/2#projects。它们只有在 从一个不同的 路由导航时,才会被触发。
- 组件内的导航:
beforeRouteEnterbeforeRouteUpdatebeforeRouteLeave
const UserDetails = {
template: `...`,
beforeRouteEnter(to, from) {
// 在渲染该组件的对应路由被验证前调用
// 不能获取组件实例 `this` !
// 因为当守卫执行时,组件实例还没被创建!
},
beforeRouteUpdate(to, from) {
// 在当前路由改变,但是该组件被复用时调用
// 举例来说,对于一个带有动态参数的路径 `/users/:id`,在 `/users/1` 和 `/users/2` 之间跳转的时候,
// 由于会渲染同样的 `UserDetails` 组件,因此组件实例会被复用。而这个钩子就会在这个情况下被调用。
// 因为在这种情况发生的时候,组件已经挂载好了,导航守卫可以访问组件实例 `this`
},
beforeRouteLeave(to, from) {
// 在导航离开渲染该组件的对应路由时调用
// 与 `beforeRouteUpdate` 一样,它可以访问组件实例 `this`
},
}
beforeRouteEnter可以传递一个回调拿到实例
beforeRouteEnter (to, from, next) {
next(vm => {
// 通过 `vm` 访问组件实例
})
}
这个 离开守卫 通常用来预防用户在还未保存修改前突然离开。该导航可以通过返回 false 来取消。
beforeRouteLeave (to, from) {
const answer = window.confirm('Do you really want to leave? you have unsaved changes!')
if (!answer) return false
}
使用的是组合式api的话,你可以通过 onBeforeRouteUpdate 和 onBeforeRouteLeave 分别添加 update 和 leave 守卫。
完整的导航解析流程
- 导航被触发。
- 在失活的组件里调用
beforeRouteLeave守卫。 - 调用全局的
beforeEach守卫。 - 在重用的组件里调用
beforeRouteUpdate守卫(2.2+)。 - 在路由配置里调用
beforeEnter。 - 解析异步路由组件。
- 在被激活的组件里调用
beforeRouteEnter。 - 调用全局的
beforeResolve守卫(2.5+)。 - 导航被确认。
- 调用全局的
afterEach钩子。 - 触发 DOM 更新。
- 调用
beforeRouteEnter守卫中传给next的回调函数,创建好的组件实例会作为回调函数的参数传入。
pinia
这一块准备后面写pinia源码,敬请期待
Vue3
相较于vue2的优化
- 使用proxy来代替Object.defineProperty
- 不仅仅能够监测属性,还能监测对对象,因为proxy本身就是代理的对象本身。
- 作用域插槽,vue2导致作用域插槽变了之后,父组件也会相应的重新渲染。vue3把作用域插槽改为函数模式,这样就只会下个子组件的渲染。
- 对象式的组件声明方式:
- vue2是通过声明的方式传入一系列的option,和typescript的结合需要通过一些装饰器来做
- vue3修改了组件的声明方式,改为了类式的写法,使得typescript的结合更加容易
- 基于tree shaking优化,提供了更多的内置功能。
defineProperty和proxy区别
- defineProperty是对属性的监测,proxy是对整个对象的代理
- defineProerty:对于添加或删除属性的时候,Vue是无法进行检测到的,因为添加或者删除的对象没有在初始化进行响应式处理,只能通过$set来调用
Object.defineProerty()处理 - 无法监听到数组下标和长度的变化
- 但这些proxy可以进行解决。
Composition API和 React Hook的区别
React Hook
因为React Hook是根据useState的调用顺序来确定下一次重新渲染的state是来源于哪个useState,出现了下面的限制
- 不能在循环、条件、嵌套函数中调用Hook
- 必须确保总是在你的react函数顶层调用Hook
- useEffect和useMemo需要手动的确定依赖
Composition API
它的设计思想是借鉴的react的
- 声明在setup函数里面的,一次组件实例化只会调用一次setup,而react hook每次渲染都需要重新调用Hook。
- composition api的调用不需要考虑顺序,可以在循环、条件、嵌套函数中使用
- 响应式系统自动实现依赖收集,进而组件的性能优化是由vue内部来进行维护的,react hook必须手动传入依赖,而且保证依赖顺序,否则可能会因为依赖的不正确使得组件性能下降。