«

基础知识整理-4-Vue

codeyx 发布于 阅读:54 笔记


不应该使用箭头函数来定义 method 函数。理由是箭头函数绑定了父级作用域的上下文,所有 this 将不会按照期望指向组件实例,将是 undefined

Mustache 插值语法,支持表达式,三元运算符

指令:
v-once 内容只渲染一次,数据修改后也不会重新渲染包括子元素

v-text 将数据绑定到标签上展示,和 Mustache 语法没啥区别

v-html 会解析 html

v-pre 用于跳过元素和他子元素的编译过程,显示原始的 Mustache 标签,跳过不需要编译的节点,加快编译速度。

v-cloak 中文翻译“斗篷”,搭配 CSS 属性选择器和 display:none 使用,当 JS 代码加载完成后,v-clock 会消失,这样元素就会显示出来,防止用户看到插值表达式

v-memo 接收一个数组,指定元素可以响应式,没指定的不响应,比 v-once 更灵活。

v-bind 绑定属性
v-on 绑定事件,它有许多修饰符:
.stop 阻止冒泡
.prevent 阻止默认行为
.capture 添加事件监听时监听捕获阶段
.self 当事件是从被监听元素自身触发时才回调
.{keyAlias} 仅当事件是从特定按键触发时回调
.once 只触发一次
.left 点击鼠标左键触发
.right 点击鼠标右键触发
.middle 点击鼠标中键触发
.passive 以 {passive:true}的模式添加监听 (提升滚动性能)
想获取 event 对象,并有其他传参时,在最后一位实参传$event

<template>标签可以用来在上面写指令,它并不会渲染到页面中,类似小程序中的block

v-show 不可写在<template>上,不可和v-else使用
v-if 是不会渲染元素,v-show 是样式 display:none

v-for 还可以遍历对象(val,key,index) in obj
v-for 中的 in 和 of 的效果是完全一样的,key 属性主要是用于虚拟 DOM 算法,在新旧 nodes 对比时辨识 VNodes

computed 计算属性 任何包含响应式数据的复杂逻辑都应该使用计算属性
对比方法,它是有缓存的,基于他的依赖关系进行缓存,当数据不变化时,计算属性不重新计算
计算属性有 getter 和 setter,但基本不会使用

watch 里可以完成异步操作,计算属性不可以
watch 深度监听,第一次渲染时就立即执行 xxx:{ handler(newV,oldV){},deep:true,immediate:true }
“obj.key”:function(newV, oldV){} // 一种监听对象内属性的写法
$watch // 在created生命周期函数里使用this.$watch(想监听的属性,(newV,oldV)=>{},{deep:true,immediate:true})

Vue.toRow(代理对象) 可拿到原始对象

Vue3 把过滤器删除了,可以用方法替代

v-model 针对表单的双向绑定,本质上是 v-bind 和@input 的语法糖,但实际它底层还是很复杂
v-model 的修饰符.lazy 就会将 input 事件改为 change ,输入完内容后失去焦点或者按回车后才会触发
.number 修饰符 自动将内容转换为数值
.trim 修饰符 去除首尾空格

注册全局组件:app.component(组件名称, 组件),即使不用也会被打包,增大打包后 js 文件体积
局部组件 components

npm install @vue/cli -g 全局安装 vue cli (脚手架)

vue create xxx 创建 xxx 项目

.browserslistrc 是用来控制兼容的浏览器,它会自动去 caniuse 查询
.jsconfig.json 是给 vs code 用的,给予更好的代码提示
比如配置了路径别名,但 vs code 不知道,就可以在此文件配置

.vue 文件在脚手架中被 vue-loader 解析,但直接写 template 中的代码需要引入包含编译器的 vue 版本

npm init vue@latest 自动安装 create-vue 并使用 vite 创建项目

父传子 props 子组件接收 props 可用数组、对象(可设默认值、类型、required) 注意:如果类型是对象/数组,默认值必须用函数返回值。否则会被共享一个对象
子组件没有接收的元素会被添加到子元素的根节点,如果有内部的节点想拿到就通过 bind $attrs.xxx的方式拿到
如果不希望组件的根拿到attribute,可在组件中设置inheritAttrs:false,如果有多根节点,会报警告,必须手动绑定在某个根节点上 v-bind=“$attrs”
当子组件的最外层不是一个根节点时,他不知道要哪个元素绑定样式,就会不显示

子传父 this.$emit(触发的事件名称,参数),子组件通过事件传递信息。
在子组件中设置 emits 属性,可为数组也可对象,对象时可以传一个方法,在内部验证

插槽 slot 抽取共性、预留不同
当有多个插槽时需要用到具名插槽,<slot name=“xxx”></slot> 父元素<template v-slot:xxx> 没有名字的默认是 default
动态插槽名 v-slot:[变量]
简写 # 插槽名 代替 v-slot

渲染作用域
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的
作用域插槽
在父组件中拿到子组件的数据。
子组件 <slot :data=“data”></slot> 父组件 #xxx=”xxx.data” 这是 vue 特有功能,react 中没有
如果没有名字,默认插槽的传值就直接 v-slot=“xxx”【独占默认插槽的简写】

Provide 和 Inject 适用于孙子或更深层级 子组件通过数组的形式接收就可以
将 data 中的数据提供到 provide,provide 的定义形式类似 data 也是函数,搭配 computed 函数 可以做到动态的变化

全局事件总线
Vue3 从实例中移除了$on、$off、$once 方法,所以我们如果想继续使用,需要通过第三方库,例如 mitt 或 tiny-emitter

生命周期
created 里组件实例已创建,可以发送网络请求、事件监听等
mounted 里可使用 DOM
unmounted 里面可以坐回收操作比如取消事件监听

$refs 拿DOM
$parent 获取当前组件的父组件 $root 获取根组件 Vue3中移除了$children

动态组件 <component :is=“xxx”>

<keep-alive></keep-alive> 包裹动态组件,切换时可以缓存组件状态,也能提升一些性能
属性:include=“aaa,bbb” 只缓存这两个组件,这里的名称要对应在组件定义时 name 属性
exclude 排除、max 最多缓存多少组件实例
此时监控组件的进入和离开可用生命周期函数 activated 和 deactivated

webpack 的分包处理规则
自己编写的业务代码在 app.js 第三方的在 chunk-vendors 里面
当业务写的很多时,app.xxx.js 就会很大,导致首屏加载时间过长
import()方法导入的文件可以分包,这个方法返回的是个 promise
组件分包,通过 defineAsyncComponent 用异步的形式导入组件,会形成一个单独的 js 文件
defineAsyncComponent 接受两种类型的参数:
类型一:工厂函数,该工厂函数需要返回一个 Promise 对象;
类型二:接受一个对象类型,对异步函数进行配置

这也就是异步组件,因为有路由懒加载,所以用的不多

当你修改了响应式状态时,DOM 会被自动更新。但是需要注意的是,DOM 更新不是同步的。Vue 会在“next tick”更新周期中缓冲所有状态的修改,以确保不管你进行了多少次状态修改,每个组件都只会被更新一次。
要等待 DOM 更新完成后再执行额外的代码,可以使用 nextTick() 全局 API:

import { nextTick } from "vue";

async function increment() {
  count.value++;
  await nextTick();
  // 现在 DOM 已经更新了
}

reactive 的局限性之 3 对解构操作不友好
当我们将响应式对象的原始类型属性解构为本地变量时,或者将该属性传递给函数时,我们将丢失响应性连接:

const state = reactive({ count: 0 });

// 当解构时,count 已经与 state.count 断开连接
let { count } = state;
// 不会影响原始的 state
count++;

// 该函数接收到的是一个普通的数字
// 并且无法追踪 state.count 的变化
// 我们必须传入整个对象以保持响应性
callSomeFunction(state.count);

组件的 v-model ,和在表单中一样,做了两件事。一是 props 传值,相当于:modelValue=“xxx” 子组件要接收,二是监听事件@update:modelValue=“xxx = $event”
需要在 emits:[]里声明 , 参数会被赋值给 xxx
自定义名称(不使用 modelValue 这个名字)
v-model:myname=“xxx” 这样就可以绑定多个属性了

Mixin Vue2 中比较常用,3 中用的少了
组件和组件之间有时候会存在相同的代码逻辑,对相同的代码逻辑进行抽取
组件属性 mixins:[xxx] 完成混入,如果冲突自身优先,生命周期挨个执行,方法都会执行,对象 key 相同时优先组件自身
组件自己的 data 比 mixin 的 data 优先级高如果是生命周期函数,会先执行 mixin 的再执行组件自己的全局的 mixin 性能不好修改优先级策略

CompositionAPI 对比 OptionsAPI
原来操作一个数据 是分散的,methods、watch、等等分散到各个配置项
新的可以将一个数据和操作数据的各种方法抽离,抽离成一个方法,使用更灵活

setup 函数有两个参数,props,context。
无 this 文档说没创建,但看源码应该是没绑定
conetxt 中包含三个属性:attrs、slots、emit

ref 定义响应式数据,基本类型,也可以复杂类型,从网络中获取的数据一般用 ref
reactive 用于定义复杂类型数据,一般用于本地数据,聚合的数据,组织在一起有作用

父传子对象,子可以修改父组件的数据,并且修改的是同一个对象,这样是不好的,不符合单向数据流的规范,应该通过发送事件让父去修改。
readonly 是为了避免有的人不懂单向数据流,readonly(响应式数据) 包裹后就不可修改了
父组件修改它被包裹前的响应式对象即可【本质劫持了 proxy 的 set 方法】

isProxy 检查对象是否由 reactive 或 readonly 创建的 proxy
isReactive 检查对象是否由 reactive 创建的响应式代理,如果该代理是 readonly 创建的,但包裹了 reactive 创建的另一个代理也会返回 true
isReadonly 检查对象是否由 readonly 创建的只读代理
toRaw 返回 reactive 或 readonly 代理的原始对象
shallowReactive 浅层响应式
shallowReadonly 浅层只读
toRefs reactive 响应式复杂数据结构后不再拥有响应式,如果希望每个结构出来的值是响应式,可以在结构的时候用 toRefs()包裹对象,这样结构出来的值全是 ref 的响应式。
如果想单独结构出响应式的值,toRef(对象,“键”)
如果对象里没有一个属性,toRefs 不能用了,要用 toRef(),如果没有这个 age 属性,他就会给返回一个空的来,下面再再修改这个空的值就是响应式的,不会报错
isref 判断是否 ref 对象 shallowRef 创建浅层 ref

computed() 方法 在 setup 中使用计算属性
通过 ref 方法拿 DOM,绑定在元素上的 ref 名称和 setup 函数里定义的 ref 名称一致,在 onMounted 里面就可以拿到了

无 onBeforeCreate/onCreated,以前在这两个生命周期函数里做的事可直接在 setup 里用

Provide 函数和 Inject 函数
声明 provide(“name”,xxx) 后代组件中使用 inject(“name”),共享 ref 响应式数据也是可以同步改变的

watch 和 watchEffect
watch(xxx,(newV, oldV)=>{}) 监听响应式对象,对象属性改变会被监听到。也可传函数返回对象。也可传数组同时监听多个
watchEffect 会自动执行传入的函数,自动收集依赖,当依赖的变量变化时自动执行
停止监听 保存 watchEffect 的返回值,返回值() 调用 即可停止监听

在 setup 语法糖中 props
const props = defineProps({})
const emit = defineEmits([])
setup 语法糖中默认不暴露组件的方法,如果父组件想拿到子组件的 DOM 然后使用它的方法
defineExpose({想暴露出去的属性/方法}) 父组件就可以使用实例的属性/方法

路由主要维护的是一个映射表
history 接口是 HTML5 新增的,它有 6 种模式改变 URL 而不刷新页面:
replaceState 替换原来的路径
pushState 使用新的路径
popState 路径的回退
go 向前或向后改变路径
forward 向前改变路径
back 向后改变路径

useRoute() 拿到当前活跃的路由对象 route 对象默认是响应式的

懒加载 ()=>import(xxxx) 打包时就会进行分包处理,引入时可以配置魔法注释,webpack 在打包时会识别,让组件在打包后有名字,webpackChunkName(webpack3 开始有的功能)

动态路由 /user:id 在 route 的 params 里拿数据
跳转路由时可以在 query 属性里传递参数,会在地址栏中以 query string 的方式传递(查询字符串) $route.query 可以拿到
404 路由 path 设为 /:pathMatch(.),最后面的*,加上会按/解析为数组
.push 和 replace,push 可以回退

动态添加路由
管理后台里可以根据权限生成菜单,一些人偷懒可能只是隐藏菜单的显示,但其实已注册
router.addRoute(parentName, {path,component}) 相同 name 后添加的会覆盖
删除路由:router.removeRoute(‘name’)
另外 addRoute 会返回一个值,它是一个函数,调用它就会删除这个路由
router.hasRoute() 检测某个路由是否存在
router.getRoutes()获取所有路由信息

路由导航守卫
全局前置守卫 router.beforeEach()

Vuex 状态管理
State、Getters、Mutations、Actions、Modules 5 个核心概念
状态是响应式的,在 setup 函数中通过 useStore 来获取 store,修改 store 里的状态需要通过
提交【commit】Mutation 修改(Pinia 里已经取消)便于 vue-devtools 跟踪状态
Vuex 使用单一状态树,用一个对象就包含了全部的应用层级的状态(SSOT,单一数据源),和 module 不冲突
Pinia 可以创建多个 store 实例
在计算属性映射状态,使用…mapState([xxx,aaa,bbb]) 如果与当前组件的 data 属性名称冲突,可传入对象{newName:state=>state.name} 但在 Vue3 的 CompositionAPI 中使用不方便。所以 3 推荐使用 Pinia
getters 可以对状态进行一些处理 然后返回,使用时$store.getters.xxx(类似计算属性)
mapGetters 映射 getters
更改状态的唯一方法是提交 mutation,里面不可有异步操作,否则追踪不到数据变化
mapMutations 在 methods 里面解构,直接使用
actions
它不直接改变状态而是提交 mutation
它有一个非常重要的参数“context”,和 store 实例有相同方法的对象,里面有 commit 方法、state 属性、getters 属性,但它并不是 store 对象
通过 dispatch 派发,它也有辅助函数 mapActions

有一种设计方案将一些网络请求放到 vuex 中管理,然后在分模块。
异步函数自动返回 promise,所以在派发 action 的时候,如果是个异步请求的方法就可以通过 then 方法拿到 action 的执行结果

modules 拿数据的时候要从模块名上拿,派发和提交时还是直接提交即可
默认情况模块内部的 action 和 mutation 和 getter 是注册在全局命名空间中,如果希望使模块拥有自己的命名空间需添加 namespaced:true

Pinia
让它用起来更像组合式 API
与 TS 一起使用时具有可靠的类型推断支持
相比 vuex,mutations 不再存在,它经常被认为非常冗长,当初带来了 devtools 集成,但现在不是问题。更友好地 TS 支持,不再有 modules 的嵌套结构,可以灵活的使用每一个 store,他们通过扁平化的方式相互使用。不再有命名空间的概念,不需要记住他们复杂的关系
它有三个核心概念,state、getters、actions 相当于组件的 data、computed、methods
defineStore 定义 Store
结构出来的时候无响应式,可以用 toRefs 包裹或者 pinia 提供的 storeToRefs
可直接修改 state,store 的$reset()可以重置所有状态为初始值,$state 替换 state 为新对象、$patch({})一次修改多个状态
action 里面可以直接用 this 给 state 赋值

axios
多种请求方式:
axios(config)
axios.request(config)
axios.get(url [,config])
axios.post(url [,data [,config]])
axios.delete(url [,config])
axios.head(url [,config])
axios.put(url [,data[,config]])
axios.patch(url [,data[,config]])
axios.all 可以放入多个请求数组,返回结果是个数组可以用 axios.spread 将数组展开
URL 查询对象 params:{id:1}
查询对象序列化函数 paramsSerializer:function(params){ }
request body data:{key:’abc’}

vue 项目

1.创建项目 npm init vue@latest 2.项目配置 配置项目 icon、配置项目标题、配置 jsconfig.json 3.项目目录结构划分
src / assets 存放图片、css、font 等静态资源
src / components 存放一些通用的组件
src / hooks 抽取的公共代码逻辑
src / mock 模拟一些数据
src / router 路由
src / service 网络请求 request 文件夹里封装 axios modules 文件夹里是各个模块的请求
src / stores 状态管理
src / utils 工具
src / views 页面
4.css 样式重置
normalize.css 和 reset.css 都要用 5.配置路由、状态管理
使用 pinia 创建 index.js 使用 createPinia 在 main 中 use。再创建文件夹 modules,里面是各个 store 6.创建页面 views 里创建页面,每个页面一个文件夹,可以起名或者 index.vue,但多个 index 可能不便于查找,名称使用大驼峰或者小写+杠

在 webpack 中[动态]加载本地图片用 require
在 vite 中需要合成一个 URL,new URL(‘../../assets/img/xxx’,import.meta.url) // 获取当前文件所处的路径,拼接 url

修改第三方 UI 库的类时,因为作用域不同,导致选择器无法找到。可以通过 vue 的 deep 方法穿透 :deep(.leiming){xxx} 找到子组件的类

当一个组件中网络请求比较多,而且它还要给多个子组件传值的时候,就可以将这些网络请求抽取到 store 里面,以前用 vuex 的时候还要抽到 module 里面,比较复杂,但 pinia 很方便

前端时间处理库-Day.js 与 Moment.js

tree shaking 摇树 将没用到的东西摇下来减少包的体积

如何触发拉到底部自动加载数据的方法:
document.documentElement.scrollTop 获取已滚动的值
document.documentElement.clientHeight 获取文档高度
document.documentElement.scrollHeight 获取滚动条总共能够滚动的长度
当 scrollHeight <= scrollTop + clientHeight(当前文档的高度) 时,说明拉到底了
将此功能抽取到 hooks,监听频次比较高,可以用节流
组件卸载时记得移除监听

reactive 解构后无响应式,ref 可以
watch 只监听响应式数据
不能监听:数组通过索引修改数组元素、对象属性增加/删除、计算属性

防抖:一直触发,最后一个才会触发,连续点击一个,只有最后一次有效
节流:一直触发,每隔一段时间可以触发一次
除了面试手写,真正项目用第三方提供的,如:underscore、lodash
throttle 节流

pxtovw
vite/webpack -> postcss -> plugins -> postcss-px-to-viewport 打包的时候就将 px 转 vw
npm i postcss-px-to-viewport -D // 过时了 用 8 版本

vite 使用 rollup 打包的

Vue 高级语法
通常情况下,需要对 DOM 元素进行底层操作时可以用到自定义指令
局部指令:组件内通过 directives 选项
全局指令:app 的 directive 方法
自定义指令(为了一个功能更好的复用)它里面有生命周期函数,beforeMount mounted beforeUpdate updated beforeUnmount unmounted 定义一个全局指令
做一个为输入框自动获取焦点的功能

directives:{
  focus:{
    mounted(el){
      el.focus()
       }
  }
}

指令的生命周期
created 在绑定元素的 attribute 或事件监听器被应用之前调用
beforeMount 当指令第一次绑定到元素并且挂在父组件之前调用
mounted 在绑定元素的父组件被挂载后调用
beforeUpdate 在更新包含组件的 VNode 之前调用
updated 在包含组件的 VNode 及其子组件的 VNode 更新后调用
beforeUnmount 在卸载绑定元素的父组件之前调用
unmounted 当指令与元素解除绑定且父组件已卸载
v-xxx:mmm="vvv” mmm 是可以传参的

内置组件
teleport 传送门,类似 react 的 Portals
它有两个属性:to 要移动到的目标元素,可使用选择器,disabled 是否禁用 teleport 的功能

suspense
实验性特性
加载一些异步组件时(比如懒加载)由于网络原因可能需要等待,就可以先试用一个替代组件展示,加载完成后再替换回来,比如展示 loading

<suspense>
  <template #default> 要加载的组件 </template>
  <template #fallback> <loading />组件 </template>
</suspense>

Vue 插件
app.use()就是在安装插件,里面只能传对象/函数
传入的对象里面必须有个 install 方法,接收参数 app。use 方法就是执行传入的 install 方法
它很强大,能够添加全局方法、全局资源 指令、过滤器、过渡等,全局 mixin 添加一些组件选项等等

h 函数
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML,然后一些特殊场景,你真的需要 JavaScript 的完全编程的能力,这个时候你可以使用渲染函数,它比模板更接近编译器
Vue 在生成真实的 DOM 前,会将我们的节点转换成 VNode,而 VNode 组合在一起形成一棵树结构,就是虚拟 DOM
事实上之前编写的 template 中的 HTML 最终也是使用渲染函数生成对应的 VNode
如果想充分利用 JS 的编程能力,可以自己编写 createVNode 函数,生成对应 VNode
那么怎么做?
使用 h 函数,h 函数是用于创建 VNode 的一个函数
其实更准确的命名是 createVNode,但是为了简便在 Vue 中就是 h 函数,Vue 中这两个函数是一样的,都存在
它接收 3 个参数,标签名、属性、内容
“div”, {class:”xxx”},[ 子元素 ]
一个组件要有 template 标签,如果没有就要有个 render 函数,render 函数返回 h 函数创建的节点
render 函数是一个组件的选项,通过 optionsAPI 的方式写

export default{
    render(){
        return h(“div”, {}, [])
    }
}

在 CompositionAPI 中使用

setup(){
    xxx
    return ()=> h()
}

在语法糖中 需要在 template 里面用一下<render>标签
语法糖中定义 redner 函数返回函数 返回 h 方法

jsx 语法
如果想在项目中使用 jsx,需要添加对 jsx 的支持,通常使用 babel 转换,对于 Vue 来说只需要在 babel 中配置对应插件即可。(template 使用 vue-loader 转换的)
webpack 环境:
npm i @vue/babel-plugin-jsx -D
在 babel.config.js 配置

module.exports = {
    presets:[‘@vue/cli-plugin-babel/preset’],
    plugins:[“@vue/babel-plugin-jsx”]
}

如果是 vite 环境需要安装
npm i @vitejs/plugin-vue-jsx -D

然后就可以写 jsx 了
render 函数中返回 jsx 即可 不需要 h 函数了

render(){
    return( <div>aaa</div> )
}

好处就是能用一些方法了,比如 map 啥的,以前在 template 里只能用 v-for
<script lang="jsx”>
这里面插入 js 代码时使用{}不要双大括号

过渡动画
react 本身没有提供任何动画相关 API,所以在 react 中要使用第三方库 react-transition-group
Vue 中提供了内置 API
它并不是写好动画,只是在合适的时间帮你添加进对应的类
设置进入动画
.v-enter-from{初始状态 比如 透明度为 0}
.v-enter-active { 进入过程中,用来写动画的地方 transition xxx xxx }
.v-enter-to { 结束状态 比如 透明度为 1 }
然后用<transition></transition>标签将元素包裹
离开动画是-leave-
可以对元素、组件添加进入/离开过渡,条件渲染 if/show、动态组件
给组件加,默认加到组件的根节点上
transition 标签增加 name 属性 就可以不用 v-了 可自定义
也可以使用关键帧动画,直接-active 里面设置 animation 即可 不需要在定义 form 和 to 了
mode="in-out/out-in" 设置先后,appear 首次出现也有动画

<transition-group> 来包裹列表,该列表中添加、删除数据也有动画,默认情况下它不会渲染一个元素的包裹器,可以添加属性 tag="div” 让一个 div 包裹这些列表,不能使用 mode 属性,里面的循环必须加 key,动画会被应用到所有列表元素中
.v-move

响应式原理
什么是响应式?
数据改变页面自动刷新(或者说数据改变,其他一段代码自动重新执行)
vue2/3 响应式原理
Object.defineProperty 通过getter和setter来拦截

DevOps 开发模式
是 Development 和 Operations 两个词的结合,将开发和运维结合起来
伴随 DevOps 一起出现的两个词就是持续集成 CI、持续交付(手动部署)(持续部署自动部署)CD

持续集成服务器 jenkins,监控 git 代码仓库的改变,然后打包、测试,反馈给前端开发人员

git bash 比 cmd 好用,自带 ssh

Vue 是一套用于构建用户界面的渐进式框架,Vue 的核心库只关注视图层
使用声明式编程,开发者只需关注业务逻辑和视图,繁琐的处理由 Vue 处理。

1.组件通信常用的方式有以下 8 种:

10.$nextTick
定义:等待下一次 DOM 更新的方法
Vue 有个异步更新策略,意思是如果数据变化,Vue 不会立即更新 DOM,而是开启一个队列,把组件更新函数保存在队列中,在同一事件循环中发生的所有数据变更会异步的批量更新。
场景:
在 created 中也能获取 DOM
响应式数据变化后获取 DOM 更新后的状态,比如希望获取列表更新后的高度

created() {
    this.$nextTick(() => {
        console.log( this.$el )
    })
}

11.key 的作用
结论:主要是优化 diff 算法的性能

key 是 diff 算法中判断两个节点是否是同一个节点的必要条件(key 和元素类型等),相同节点可复用,减少 DOM 操作

虚拟 dom——virtual dom,提供一种简单 js 对象去代替复杂的 dom 对象,从而优化 dom 操作。virtual dom 是“解决过多的操作 dom 影响性能”的一种解决方案。virtual dom 很多时候都不是最优的操作,但它具有普适性,在效率、可维护性之间达到平衡。虚拟 DOM 方便实现跨平台,直接操作 dom 是有限制的比如 diff、clone
等操作,在 js 中会方便很多。
diff 算法是一种优化手段,将前后两个模块进行差异化比较,修补(更新)差异的过程叫做 patch,也叫打补丁。只有当新旧子节点的类型都是多个子节点时,核心 Diff 算法才派得上用场。diff 的目的是时间换空间:尽可能通过移动旧节点,复用旧节点 DOM 元素,减少新增 DOM 操作。通过首首、尾尾、首尾、尾首以及在旧节点列表遍历等方式逐个试探去找可复用的旧节点。
vue3 引入了最长递增子序列优化 diff:去掉相同的前缀和后缀,也就是首首、尾尾都比较完后剩余的旧节点列表和新节点列表进行 diff。在新节点列表中用一个数组,统计新节点出现在旧节点相同元素的 index;对这个数组求最长递增子序列。递增子序列的节点不需要移动(即使不连续),因为它们在新旧节点序列中的相对位置是一样的。

12.computed 和 watch
computed 具有响应式返回值(只读,传递对象时可写)
计算属性具备缓存,依赖的值不发生变化,对其取值时计算属性方法不会重新执行
计算属性可以简化模版中复杂的表达式,计算属性中不支持异步逻辑
watch 监听变化,执行回调,适合当数据改变时执行异步或者开销较大的操作
vue2 中监听对象的属性可以直接使用obj.xxx,在 vue3 中需要通过方法返回值的形式()=>obj.xxx

13.一些 Vue 的最佳实践
1️⃣ 编码风格方面:
v-for 务必加 key,切不和 v-if 写在同一个元素上
2️⃣ 性能方面
路由懒加载减少打包后 app.js 的体积
利用 SSR 做 SEO 优化,减少首屏加载时间
利用 v-once 渲染那些不需要更新的内容
长列表用虚拟滚动(开源库 vue-virtual-scroller,只渲染视口范围内的)
对深层嵌套对象的大数组使用 shallowRef 或者 shallowReactive 降低开销
v-show/v-if
keep-live
v-memo
图片懒加载 使用 vue-lazyload
按需加载第三方组件库
3️⃣ 安全
小心使用 v-html

14.ref 和 reactive
ref 通常用于处理单值的响应式(基本数据类型),底层 使用 Object.defineProperty
reactive 用于处理对象类型的数据响应式,底层使用 Proxy 代理对象,解构会失去响应式

15.Vue 组件化的理解
组件化的好处:可复用、高内聚、低耦合、可组合,降低更新范围,只重新渲染变化的组件
组件的核心组成:模板、属性、事件、插槽、生命周期
Vue 中每个组件都有一个渲染函数,响应式数据变化后调用函数完成页面更新【Vue2 中:watcher,Vue3 中:effect】
组件要合理划分,如果不拆分组件,那更新的时候整个页面都要更新,如果拆分的过多也会导致性能浪费。
如何组件化开发:
在 Vue 中进行组件化开发,首先要明确组件的划分原则,一般会按照功能或者业务逻辑划分,像把导航栏、侧边栏这些常用的部分做成独立组件,然后在 Vue 里通过 import 引入,在 components 选项里注册它,使用的时候像标签一样调用就可以了,而且组件之间可以通过 props 传递数据,用 emit 触发事件通信。

16.Watch 和 WatchEffect
watch 监听一个或多个响应式数据源,并在数据源变化时调用回调函数
watchEffect 立即运行一个函数,然后被动的追踪它的依赖,当这些依赖改变时重新执行该函数

17.Vue 组件的 data 为什么是函数?
目的是为了防止多个组件实例对象之间共用一个 data,产生数据污染。
所以需要通过工厂函数返回全新的 data 作为组件的数据源。

18.过滤器
常用于文本格式化、单位转换,使用|符号在差值表达式中使用,通过Vue.filter('name',()=>{})定义,Vue3 中已移除,使用方法代替。

19.v-once 指令
只渲染元素和组件一次,这可用于性能优化。vue3.2 之后增加了v-memo通过依赖列表的方式控制页面渲染

20.mixin 混入
可以用来扩展组件,将公共逻辑进行抽离。在需要该逻辑时进行混入,如果混入的数据和组件本身的数据有冲突,会采用组件的数据为准。
mixin 的缺陷:数据命名冲突问题、数据来源问题、无法按需引入,在 Vue3 中使用组合式 API 提取公共逻辑就比较方便
props、methods、inject、computed 同名时被替换
data 被合并
生命周期和 watch 方法会被合并成队列
components、directives、filters 会在原型链上叠加

21.组件的 name 选项
增加 name 选项会在 components 属性中增加组件本身,实现组件的递归调用
可以标识组件的具体名称方便调试和查找对应组件

22.自定义指令
指令的生命周期:
bind 只调用一次,指令第一次绑定到元素时调用,在这里可进行一次性初始化配置
inserted 被绑定元素插入父节点时调用
update 所在组件的 VNode 更新时调用,但可能发生在其子 VNode 更新前
componentUpdated 指令所在组件的 VNode 及其子 VNode 全部更新后调用 * unbind 只调用一次,指令与元素解绑时调用
v-lazy图片懒加载、v-debounce防抖、v-has 按钮权限、 拖拽指令等

23.Vue 中常见的设计模式

集中整理一下日常用到的组件传值方案。

一、通过 props 和$emit 实现父子组件传值:

// 父组件
<template>
  <div>
    // 通过属性方式给子组件传值;监听子组件的change事件
    <HelloWorld :msg="text" @change="handleChangeText" />
  </div>
</template>

<script>
  import HelloWorld from "@/components/HelloWorld.vue";

  export default {
    name: "HomeView",
    components: {
      HelloWorld,
    },
    data() {
      return {
        text: "Hello World",
      };
    },
    methods: {
      handleChangeText(str) {
        this.text = str; // 将父组件的text属性内容改为子组件传来的值
      },
    },
  };
</script>

// 子组件; 通过$emit触发自定义的change事件
<template>
  <div @click="$emit('change','你好,世界')">{{ msg }}</div>
</template>

<script>
  export default {
    name: "HelloWorld",
    props: {
      msg: String, // 需要在此声明有哪些属性需要接收,可设置default默认值
    },
    emits: ["change"], // 需要在此声明有哪些事件将会被触发
  };
</script>

二、使用 EventBus(事件总线)

创建一个 EventBus.js 文件并向外共享一个 Vue 的实例对象;

发送方调用 bus.$emit('事件名称',数据)触发自定义事件;

接收方使用 bus.$on('事件名称',事件处理函数)注册一个自定义事件;

// eventBus.js
import Vue from 'vue'
export default new Vue()

// 数据发送方 header.vue
import bus from '@/utils/eventBus.js'
...
methods:{
    send(){
      bus.$emit('change','hello')
    }
}

// 数据接收方 footer.vue
import bus from '@/utils/eventBus.js'
...
created(){
  bus.$on('change', data=>{
    console.log('事件被触发,传递值为:', data)
  })
}

三、Vuex

Vuex 实现了一个单向数据流,在全局拥有一个 State 存放数据,当组件需要修改数据时,必须通过 Mutation 进行,Mutation 同时提供订阅者模式供外部插件调用获取 State 的更新。异步操作或批量同步操作需要走 Actions,它无法直接修改 State,还是需要 Mutation 修改。

Vuex 内保存的数据一刷新就会被清空,可以搭配 localStorage 进行持久化存储。

// 不需要异步操作,简单的同步更改
// store/index.js
import Vue from "vue";
import Vuex from "vuex";

Vue.use(Vuex);

export default new Vuex.Store({
  state: {
    count: 0,
  },
  mutations: {
    addCount(state, num) {
      state.count += num;
    },
  },
});
// HelloWorld.vue
<template>
  <div @click="handleAddCount">{{ count }}</div>
</template>

<script>
  export default {
    name: "HelloWorld",
    computed: {
      count() {
        return this.$store.state.count;
      },
    },
    methods: {
      handleAddCount() {
        //如需同步更改
        this.$store.commit("addCount", 3);
      },
    },
  };
</script>
// --------------------------------------
...
this.$store.dispatch('delayAddCount', 2) // or ...mapActions(['delayAddCount'])
... 延迟一秒触发
actions: {
    delayAddCount({ commit }, payload) {
      setTimeout(() => {
        commit("addCount", payload);
      }, 1000);
    },
  },

四、$attrs/$listeners

$attrs中包含的是父组件传递过来但子组件没有接收的属性(style和class除外),可通过v-bind="$attrs"传递给孙子组件。通常和 inheritAttrs 选项一起使用。

$listeners中包含的是父组件绑定在子组件的事件监听(不包含.native修饰的),可通过v-on="$listeners"传递给孙子组件。

// 父组件 中 给子组件传参
<HelloWorld msg="1123" abc="aaa" />

// 子组件并没有接收,如果不加inheritAttrs:false属性并且不给孙子组件绑定$attrs时,父组件传来的属性会自动成为子组件最外层标签的属性
<template>
  <div ref="hhh">
    <SonS v-bind="$attrs"></SonS>
  </div>
</template>

<script>
import SonS from './SonS.vue';

export default {
  name: 'HelloWorld',
  // inheritAttrs:false,
  components:{
    SonS
  },
  mounted(){
    console.log(this.$refs['hhh']); // <div msg="1123" abc="aaa"><div>{}</div></div>
  }
// 如果绑定给孙子组件,子组件最外层和孙子组件都会拿到属性,这样是多余的,所以要在子组件加上inheritAttrs:false
// 这样就能把属性和值传递给孙子组件
// 孙子组件
<template>
    <div>{{ $attrs }}</div> //{ "msg": "1123", "abc": "aaa" }
  </template>

  <script>
  export default {
    name: 'SonS'
  }
  </script>

五、provide / inject

这对选项需要一起使用,以允许一个祖先组件向其所有子孙后代注入一个依赖,不论组件层次有多深,并在其上下游关系成立的时间里始终生效。如果你熟悉 React,这与 React 的上下文特性很相似。

// 父级组件提供 'foo'
var Provider = {
  provide: {
    foo: 'bar'
  },
  // ...
}

// 子组件注入 'foo'
var Child = {
  inject: ['foo'],
  created () {
    console.log(this.foo) // => "bar"
  }
  // ...
}
// 数据不是响应式的,如果修改了父组件的foo,那么子组件的值还是bar不会变。
Vue.observable优化响应式provide 【让一个对象变成响应式数据】

provide(){
  this.color = Vue.observable({ data:"#ccc" })
  return {
    color:this.color
  }
}
// 这样父组件中改变color中的data 孙子组件也能同步更改

六、ref 与$parent/$children

不推荐 这种方式就是通过获取 dom 上的属性或方法。

Vue3 在 setup 函数中,props 和$emits 的写法有所变化

// 父组件
  <script setup>
import HelloWorld from './components/HelloWorld.vue'
const handleChange = (data)=>{
  console.log(data);
}
</script>
<template>
  <HelloWorld msg="Vite + Vue" @change="handleChange" />
</template>
// 子组件
<script setup>
defineProps({
  msg: String,
})
const emits = defineEmits(['change'])
</script>
<template>
  <h1 @click="emits('change','hello')">{{ msg }}</h1>
</template>
可通过v-model的方式传值
// 父组件
<script setup>
import { ref } from "vue";
import HelloWorld from './components/HelloWorld.vue'

const data = ref('Hello')
</script>

<template>
  <HelloWorld v-model:title="data" />
</template>
// 子组件
<script setup>
defineProps({
  title: String,
})
const emits = defineEmits(['update:title'])
</script>

<template>
  <h1 @click="emits('update:title','你好')">{{ title }}</h1> // update是固定写法
</template>

// 点击 h1 标签后,显示内容由 Hello 变为你好
在 Vue3 中使用 ref 获取 dom 的方式来让父组件去操作子组件的属性或方法时,子组件需要使用 defineExpose 暴露属性 or 方法。

在 setup 方法中使用 provide 和 inject

// 父组件
import { provide } from 'vue'
provide('data',data.value)
// 子组件
import { inject } from 'vue'
const data = inject('data')

Vue3 中移除了$on、$off,所以如果想用事件总线的方式传值需要借助插件 比如:mitt

新建一个 bus.js 文件 在里面返回 new mitt()
记得在 unmounted 里移除

mitt.emit('方法名',参数) 触发
mitt.on('方法名',callback) 监听
mitt.off('移除方法名') 移除
在 Vue3 中 Pinia 会比 Vuex 更好

因为官方已经将 Pinia 作为官方库替换了 Vuex 的位置

然后 Pinia 体积更小、对 Ts 支持更好、与 CompositionAPI 紧密结合、采用分离模式每个组件有自己的 store 实例。

// stores/counter.js
import { defineStore } from "pinia";

export const useCounterStore = defineStore("counter", {
  state: () => {
    return { count: 0 };
  },
  // 也可以这样定义
  // state: () => ({ count: 0 })
  actions: {
    increment() {
      this.count++;
    },
  },
});
// ------
<script setup>
import { useCounterStore } from '@/stores/counter'

const counter = useCounterStore()

// 下面三行语句都可以修改count
counter.count++

counter.$patch({ count: counter.count + 1 })

counter.increment()
</script>

<template>
  <!-- 直接从 store 中访问 state -->
  <div>Current Count: {{ counter.count }}</div>
</template>

Github 仓库地址

前端编程范式主要就是两种:命令式编程、声明式编程
命令式性能优于声明式的性能,声明式的代码可维护性优于命令式的代码
企业应用的开发和设计原则,通常由项目成本和开发体验决定,项目的开发成本通常由开发周期决定,声明式的编程范式可维护性更高,可以使开发周期变短,升级变得更容易,从而节约开发成本。同时开发体验也会更好,所以这也是 Vue 变得受欢迎的原因。

框架的设计过程其实是一个不断取舍的过程

Vue 封装了命令式的逻辑,尽可能减少性能损耗,对外暴露声明式的接口

Vue 核心大致分为三大模块:响应性(reactivity)、运行时(runtime)、编译器(compiler)

运行时,编译时
在 Vue3 的源代码中存在一个 runtime-core 的文件夹,该文件夹内存放的就是运行时的核心代码逻辑。
runtime-core 对外暴露了一个函数render 渲染函数(用于编程式地创建组件虚拟 DOM 树的函数),我们可以通过 render 代替 template 完成 DOM 的渲染

// 创建一个render函数
const vnode = {
  type: "div",
  props: {
    class: "test",
  },
  children: "Hello World",
};
function render(vnode, parent) {
  const ele = document.createElement(vnode.type);
  ele.className = vnode.props.class;
  ele.innerText = vnode.children;
  parent.appendChild(ele);
}
render(vnode, document.querySelector("#app"));

运行时无法通过 HTML 标签结构的方式进行渲染解析,所以需要编译器(编译时),它在compiler-core文件夹中。
它的主要作用就是把 template 中的 html 编译成 render 函数

const { compile, createApp } = Vue;
const myHtml = `<h1 class="test">Hello World</h1>`;
const myRender = compile(myHtml);
const app = createApp({
  render: myRender,
});
app.mount("#app");

既然 compiler 可以直接解析 html 模板,为什么还要生成 render 函数再去渲染?
即:为什么 Vue 要设置成一个运行时 + 编译时的框架?为了平衡灵活性和性能优化
对于 dom 渲染而言可被分为两部分:初次渲染(挂载)、更新渲染(打补丁)
DOM 操作要比 JS 操作更耗时
目录结构

vue-next-3.2.37-master
├── tsconfig.json // TypeScript 配置文件
├── rollup.config.js // rollup 的配置文件
├── packages // 核心代码区
│   ├── vue-compat // 用于兼容 vue2 的代码
│   ├── vue // 重要:测试实例、打包之后的 dist 都会放在这里
│   ├── template-explorer // 提供了一个线上的测试(https://template-explorer.vuejs.org),用于把 tempalte 转化为 render
│   ├── size-check // 测试运行时包大小
│   ├── shared // 重要:共享的工具类
│   ├── sfc-playground // sfc 工具,比如:https://sfc.vuejs.org/
│   ├── server-renderer // 服务器渲染
│   ├── runtime-test // runtime 测试相关
│   ├── runtime-dom // 重要:基于浏览器平台的运行时
│   ├── runtime-core // 重要:运行时的核心代码,内部针对不同平台进行了实现
│   ├── reactivity-transform // 已过期,无需关注
│   ├── reactivity // 重要:响应性的核心模块
│   ├── global.d.ts // 全局的 ts 声明
│   ├── compiler-ssr // 服务端渲染的编译模块
│   ├── compiler-sfc // 单文件组件(.vue)的编译模块
│   ├── compiler-dom // 重要:浏览器相关的编译模块
│   └── compiler-core // 重要:编译器核心代码
├── package.json // npm 包管理工具
├── netlify.toml // 自动化部署相关
├── jest.config.js // 测试相关
├── api-extractor.json // TypeScript 的 API 分析工具
├── SECURITY.md // 报告漏洞,维护安全的声明文件
├── README.md // 项目声明文件
├── LICENSE // 开源协议
├── CHANGELOG.md // 更新日志
├── BACKERS.md // 赞助声明
├── test-dts // 测试相关,不需要关注
├── scripts // 配置文件相关,不需要关注
├── pnpm-workspace.yaml // pnpm 相关配置
└── pnpm-lock.yaml // 使用 pnpm 下载的依赖包版本

调试源码,先开启 source map,build 脚本后加 -s

制作一个 mini-vue
首先创建目录结构,然后导入 ts
tsconfig 的配置:

// https://www.typescriptlang.org/tsconfig,也可以使用 tsc -init 生成默认的 tsconfig.json 文件进行属性查找
{
  // 编辑器配置
  "compilerOptions": {
    // 根目录
    "rootDir": ".",
    // 严格模式标志
    "strict": true,
    // 指定类型脚本如何从给定的模块说明符查找文件。
    "moduleResolution": "node",
    // https://www.typescriptlang.org/tsconfig#esModuleInterop
    "esModuleInterop": true,
    // JS 语言版本
    "target": "es5",
    // 允许未读取局部变量
    "noUnusedLocals": false,
    // 允许未读取的参数
    "noUnusedParameters": false,
    // 允许解析 json
    "resolveJsonModule": true,
    // 支持语法迭代:https://www.typescriptlang.org/tsconfig#downlevelIteration
    "downlevelIteration": true,
    // 允许使用隐式的 any 类型(这样有助于我们简化 ts 的复杂度,从而更加专注于逻辑本身)
    "noImplicitAny": false,
    // 模块化
    "module": "esnext",
    // 转换为 JavaScript 时从 TypeScript 文件中删除所有注释。
    "removeComments": false,
    // 禁用 sourceMap
    "sourceMap": false,
    // https://www.typescriptlang.org/tsconfig#lib
    "lib": ["esnext", "dom"]
  },
  // 入口
  "include": ["packages/*/src"]
}

eslint+prettier
小而美的模块打包:rollup

响应系统的核心设计原则
会影响视图变化的数据称为响应数据
Vue2 响应式核心:Object.defineProperty

Vue3 中实现响应式数据

tsconfig.json
extends 配置继承自 xxx。 所以这个文件需要配置的东西不多。因为在父级已经配好了。
include 属性是表示哪些文件需要被编译 ts
paths 配置别名是为了 vscode 更好的路径提示,vite.config 中的别名是为了打包的时候进行转化
tsconfig.config.json
用来编译 vite.config.ts 等
composite:true可被合成到上面的配置文件
env.d.ts
reference 引入了声明文件
声明个模块 declare module "*.vue" {
import { DefineComponent } from 'vue'
const component: DefineComponent
export default component
}
默认 vue 组件是没有类型的 但按理说应该是组件类型。
所以我们需要自己声明一下
在组件里 导出时需要用 defineComponent 包裹【过渡阶段,现在已经不需要了】

EditorConfig 有助于为不同 IDE 编辑器上处理同一个项目的多个开发人员维护一致的编码风格
需要安装它的插件才可生效
Prettier 进行代码格式化
ESLint 检查代码规范。和 Prettier 一样,如果只安装依赖,是需要通过命令行去使用,为了方便我们也要安装编辑器插件。
【此处使用的版本:ESLint ^8.22.0 Prettier:^2.7.1 @vue/eslint-config-prettier: ^7.0.0 @vue/eslint-config-typescript ^11.0.0 eslint-plugin-vue ^9.3.0】
eslint-plugin-prettier ^4.2.1 处理两者间冲突,使用 prettier 格式化代码
路由配置 404 path: '/:pathMatch(.*)'

Vue3支持多实例