复习一遍Vue,看看你忘了些什么?

Vue复习

基本特性

基本使用

模板(插值、指令)

  • 插值、表达式
  • 指令、动态属性
  • v-html:会有XSS风险、会覆盖子组件
options API写法
js 复制代码
<template>
    <div>
        <p>文本插值 {{message}}</p>
        <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
        <p :id="dynamicId">动态属性 id: {{ dynamicId }}</p>
        <hr/>
        <p v-html="rawHtml">
            
        </p>
    </div>
</template>

<script lang="ts">
export default {
    data() {
        return {
            message: 'hello vue',
            flag: true,
            rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
            dynamicId: `id-${Date.now()}`
        }
    }
}
</script>
composition API写法
js 复制代码
<template>
    <div>
        <p>文本插值 {{message}}</p>
        <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>
        <p :id="dynamicId">动态属性 id: {{ dynamicId }}</p>
        <hr/>
        <p v-html="rawHtml">
        </p>
    </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const message = ref('hello vue')
const flag = ref(true)
const rawHtml = ref('指令 - 原始 html <b>加粗</b> <i>斜体</i>')
const dynamicId = ref(`id-${Date.now()}`)
</script>

v-html会覆盖子组件

computed

  • computed 有缓存
  • data 不变不会重新计算
options API写法
js 复制代码
<template>
    <div>
        <p>num {{num}}</p>
        <p>double1 {{double1}}</p>
        <input v-model="double2"/>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 20
        }
    },
    computed: {
        double1() {
            return this.num * 2
        },
        double2: {
            get() {
                return this.num * 2
            },
            set(val) {
                this.num = val/2
            }
        }
    }
}
</script>
composition API写法
js 复制代码
<template>
    <div>
        <p>num {{num}}</p>
        <p>double1 {{double1}}</p>
        <input v-model="double2"/>
    </div>
</template>

<script lang="ts" setup>
import { computed, ref } from 'vue'
const num = ref(20)

const double1 = computed(() => num.value * 2)
const double2 = computed<number>({
  get: () => num.value * 2,
  set: (value) => {
    num.value = value / 2
  }
})
</script>

watch

  • watch如何深度监听?
  • watch监听引用类型,拿不到oldValue
options API写法
js 复制代码
<template>
  <div>
    <input v-model="name"/>
    <input v-model="info.city"/>
  </div>
</template>

<script lang="ts">
export default {
  data() {
    return {
      name: 'ljx',
      info: {
        city: '上海'
      }
    }
  },
  watch: {
    name(oldVal, val) {
      // 值类型,可正常拿到 oldVal 和 val
      console.log('watch name', oldVal, val) 
    },
    info: {
      handler(oldVal, val) {
        // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
        console.log('watch info', oldVal, val) 
      },
      deep: true // 深度监听
    }
  }
}
</script>
composition API写法
js 复制代码
<template>
  <div>
    <input v-model="name"/>
    <input v-model="info.city"/>
  </div>
</template>

<script lang="ts" setup>
import { watch, ref } from 'vue'

const name = ref('ljx')
const info = ref({
  city: '上海'
})

watch(name, (oldVal, val) => {
  // 值类型,可正常拿到 oldVal 和 val
  console.log('watch name', oldVal, val) 
})

watch(info, (oldVal, val) => {
  // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
  console.log('watch info', oldVal, val) 
}, {
  // deep: true
})
</script>

如图:引用类型,获取不到oldVal

如果使用shallowRef代替ref深度监听有用吗?

不能

js 复制代码
<script lang="ts" setup>
import { watch, shallowRef } from 'vue'

const name = shallowRef('ljx')
const info = shallowRef({
  city: '上海'
})

watch(name, (oldVal, val) => {
  // 值类型,可正常拿到 oldVal 和 val
  console.log('watch name', oldVal, val) 
})

watch(info, (oldVal, val) => {
  // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
  console.log('watch info', oldVal, val) 
}, {
  deep: true
})
</script>

class和style

  • 使用动态属性
  • 使用驼峰式写法
scoped

scoped 是 Vue 中的一个样式作用域修饰符,用于将样式限制在当前组件的作用域内。使用 scoped 可以确保组件中的样式不会影响到其他组件或全局样式,提供了更好的样式隔离和组件复用性。

  1. 样式隔离:当在组件的 <style> 标签上使用 scoped 修饰符时,该组件的样式只会应用于当前组件的 DOM 元素,而不会影响其他组件中的相同类名样式。这样,即使组件名称相同,样式也能保持彼此独立,不会发生冲突。
  2. 组件复用:通过将样式限制在组件的作用域内,scoped 可以增加组件的可复用性。当组件被用于不同的场景时,其样式不会被外部样式所污染,可以在不同的上下文中保持一致的样式表现。
  3. 组件样式优先级:使用 scoped 后,组件内部的样式会优先级比全局样式高,这意味着在组件中可以方便地覆盖和修改全局样式。这对于需要进行样式调整或自定义的组件尤为有用。

如何击穿修改样式?v-deep()

optionsAPI写法
js 复制代码
<template>
  <div :class="myClass" :style="myStyle">
    <p>Example Component</p>
    <button @click="toggleStyle">toggle</button>
  </div>
</template>

<script lang="ts">
export default {
  data() {
    return {
      myClass: 'my-component',
      myStyle: {
        color: 'red',
        fontSize: '20px'
      }
    };
  },
  methods: {
    toggleStyle() {
      // 切换样式
      this.myClass = this.myClass === 'my-component' ? 'my-component-alt' : 'my-component';
      this.myStyle.color = this.myStyle.color === 'red' ? 'blue' : 'red';
    }
  }
};
</script>

<style scoped>
.my-component {
  background-color: #ccc;
  padding: 10px;
}

.my-component-alt {
  background-color: #eee;
  padding: 20px;
}
</style>
compositionAPI写法
js 复制代码
<template>
  <div :class="myClass" :style="myStyle">
    <p>Example Component</p>
    <button @click="toggleStyle">toggle</button>
  </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'
const myClass = ref('my-component');
const myStyle = ref({
  color: 'red',
  fontSize: '20px'
});

function toggleStyle() {
  myClass.value = myClass.value === 'my-component' ? 'my-component-alt' : 'my-component';
  myStyle.value.color = myStyle.value.color === 'red' ? 'blue' : 'red';
}
</script>

<style scoped>
.my-component {
  background-color: #ccc;
  padding: 10px;
}

.my-component-alt {
  background-color: #eee;
  padding: 20px;
}
</style>

条件渲染

  • v-ifv-else 可使用变量,也可以使用 === 表达式

  • v-ifv-show的区别

    1. 初始化渲染:v-if在初始渲染时会根据条件决定是否渲染元素,如果条件为假,则元素不会被渲染到DOM中;而v-show会在初始渲染时始终将元素渲染到DOM中,但是通过CSS的display属性来控制元素的显示与隐藏。

    2. 条件变化时的渲染:当条件变化时,v-if会根据新的条件来重新渲染或销毁元素,从而改变DOM结构,因此,当条件频繁变化时,v-if的性能相对较低;而v-show只是通过修改CSS的display属性来控制元素的显示与隐藏,不会引起DOM的重新渲染和销毁,因此,当条件频繁变化时,v-show的性能相对较高。

    3. 切换开销:由于v-if在条件为假时会销毁元素,再次变为条件为真时需要重新创建元素,因此在切换时可能存在较大的开销;而v-show只是通过修改CSS的display属性来控制元素的显示与隐藏,不会销毁和重新创建元素,因此在切换时开销较小。

    4. 编译条件:v-if的条件表达式在编译时会被完全忽略,只有在条件为真时才进行编译;而v-show的条件表达式在编译时会被解析和编译,不管条件为真还是为假,都会进行编译。

  • v-ifv-show的使用场景有哪些?

    v-if适用于在渲染时对条件进行判断并且可能较少更改的场景,而v-show适用于在渲染时频繁切换条件的场景。在使用时,可以根据具体需求和性能考量选择适合的指令来控制组件或元素的显示与隐藏。

js 复制代码
<template>
    <div>
        <p v-if="type === 'a'">A</p>
        <p v-else-if="type === 'b'">B</p>
        <p v-else>other</p>

        <p v-show="type === 'a'">A by v-show</p>
        <p v-show="type === 'b'">B by v-show</p>
    </div>
</template>

<script lang="ts">
export default {
    data() {
        return {
            type: 'a'
        }
    }
}
</script>

循环渲染

  • v-for 既可以遍历数组,也可以遍历对象
  • v-forv-if不能一起使用
  • key 的重要性(diff算法)
js 复制代码
<template>
    <div>
        <p>遍历数组</p>
        <ul>
            <li v-for="(item, index) in listArr" :key="item.id">
                {{index}} - {{item.id}} - {{item.title}}
            </li>
        </ul>

        <p>遍历对象</p>
        <ul >
            <li v-for="(val, key, index) in listObj" :key="key">
                {{index}} - {{key}} -  {{val.title}}
            </li>
        </ul>
    </div>
</template>

<script lang="ts" setup>
import { ref } from 'vue'

const listArr = ref([
    { id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
    { id: 'b', title: '标题2' },
    { id: 'c', title: '标题3' }
])

const listObj = ref({
    a: { title: '标题1' },
    b: { title: '标题2' },
    c: { title: '标题3' },
})
</script>

事件

  • event参数
  • 自定义参数
  • 事件被绑定到哪里? vue的事件是原生的,绑定到当前元素上的
  • 事件修饰符有哪些?
js 复制代码
<!-- 单击事件将停止冒泡 -->
<a @click.stop="doThis"></a>

<!-- 提交事件将不再重新加载页面 -->
<form @submit.prevent="onSubmit"></form>

<!-- 修饰语可以使用链式书写 -->
<a @click.stop.prevent="doThat"></a>

<!-- 也可以只有修饰符 -->
<form @submit.prevent></form>

<!-- 仅当 event.target 是元素本身时才会触发事件处理器 -->
<!-- 例如:事件处理器不来自子元素 -->
<div @click.self="doThat">...</div>

<!-- 添加事件监听器时,使用 `capture` 捕获模式 -->
<!-- 例如:指向内部元素的事件,在被内部元素处理前,先被外部处理 -->
<div @click.capture="doThis">...</div>

<!-- 点击事件最多被触发一次 -->
<a @click.once="doThis"></a>

<!-- 滚动事件的默认行为 (scrolling) 将立即发生而非等待 `onScroll` 完成 -->
<!-- 以防其中包含 `event.preventDefault()` -->
<div @scroll.passive="onScroll">...</div>
optionsAPI写法
js 复制代码
<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script lang="ts">
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
      increment1(event: MouseEvent) {
            // @ts-ignore
            console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
            console.log(event.target === event.currentTarget)
            console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
            this.num++

            // 1. event 是原生的
            // 2. 事件被挂载到当前元素
            // 和 DOM 事件一样
        },
        increment2(val: number, event: MouseEvent) {
            console.log(event.target)
            this.num = this.num + val
        },
        loadHandler() {
            // do some thing
            console.log('load')
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>
compositionAPI写法
js 复制代码
<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue'

const num = ref(0)

const increment1 = (event: MouseEvent) => {
    // @ts-ignore
    console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
    console.log(event.target === event.currentTarget)
    console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
    num.value++

    // 1. event 是原生的
    // 2. 事件被挂载到当前元素
    // 和 DOM 事件一样
}

const increment2 = (val: number, event: MouseEvent) => {
    console.log(event.target)
    num.value = num.value + val
}

const loadHandler = () => {
  // do some thing
    console.log('load')
}

onMounted(() => {
    window.addEventListener('load', loadHandler)
})

onUnmounted(() => {
    //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
    // 自己绑定的事件,需要自己销毁!!!
    window.removeEventListener('load', loadHandler)
})
</script>

表单

  • v-model
  • 常见表单项 textareacheckboxradioselect
  • 修饰符 .lazy.number.trim

组件

props和emit

optionsAPI写法
js 复制代码
emits: ['inFocus', 'submit'],
// props: ['list']
props: {
    // prop 类型和默认值
    list: {
        type: Array,
        default() {
         return []
        }
    }
}

optionsApi写法可通过this.$emit()触发父组件传递的自定义事件

compositionAPI写法
js 复制代码
<script setup>
const props = defineProps({
  foo: String
})

const emit = defineEmits(['change', 'delete'])
// setup 代码
</script>

注意:如果解构了 props 对象,解构出的变量将会丢失响应性。因此需要通过 props.xxx 的形式来使用其中的 props

如果确实需要解构 props 对象,或者需要将某个 prop 传到一个外部函数中并保持响应性,应该使用 toRefs()toRef() 这两个工具函数:

js 复制代码
import { toRefs, toRef } from 'vue'

// 将 `props` 转为一个其中全是 ref 的对象,然后解构
const { title } = toRefs(props)
// `title` 是一个追踪着 `props.title` 的 ref
console.log(title.value)

// 或者,将 `props` 的单个属性转为一个 ref
const title = toRef(props, 'title')

事件总线

  • Vue2可以通过new Vue实现事件总线
js 复制代码
import Vue from 'vue'

export default new Vue()
  • vue3请使用mitt

组件生命周期

  1. 创建阶段(Creation)

    • beforeCreate:在实例初始化之后,数据观测和事件配置之前被调用。
    • created:在实例创建完成后被立即调用。此阶段完成了数据观测和事件配置,并且还没有开始编译模板。
  2. 挂载阶段(Mounting)

    • beforeMount:在挂载开始之前被调用,相关的 render 函数首次被调用。
    • mounted:el 被新创建的 vm.$el 替换,并且挂载到实例上后调用该钩子。
  3. 更新阶段(Updating)

    • beforeUpdate:在数据更新之前调用,发生在虚拟 DOM 重新渲染和打补丁之前。
    • updated:在由于数据更改导致的虚拟 DOM 重新渲染和打补丁之后调用。
  4. 销毁阶段(Destruction)

    • beforeDestroy:在实例销毁之前调用。在这一步,实例仍然完全可用。
    • destroyed:在实例销毁之后调用。此时,所有的事件监听器都已被移除,子实例也被销毁。

此外,还有一个错误捕获阶段 (Error Capturing):

  • errorCaptured:当捕获一个来自子孙组件的错误时被调用。此钩子可以返回 false 以阻止该错误继续向上传播。

这些生命周期钩子允许你在组件的不同生命周期阶段执行代码,从而实现想要的功能。注意,不建议在 beforeCreatecreated 阶段依赖于响应式的数据或计算属性,因为这些在此阶段还未初始化。

vue3的生命周期有哪些变化?

  • setup(): 在组件实例化之前被调用,是一个新的组件组合 API 的入口点,代替beforeCreatecreated
  • beforeUnmount: 在卸载开始之前被调用,相关的卸载逻辑被触发。替代了Vue 2中的beforeDestroy
  • unmounted: 在卸载之后调用。替代了Vue 2中的destroyed
  • Vue 3不再提供beforeUpdateupdated, 而是通过watchwatchEffect等响应式API来实现类似的功能。

高级特性

自定义v-model

nextTick

为什么需要nextTick

  1. vue是异步渲染的,data改变之后,DOM不会立刻渲染,因此紧接着执行DOM操作可能会产生问题

  2. nextTick会在DOM完成渲染之后触发,以获取最新DOM节点

个人应用点:

  • 自定义一个组件渲染echarts图表,并管理其生命周期,通过props接收配置项
  • onMounted调用echarts.init + watch监听props配置变化及时setOption
  • 如果在onMounted之前props发生了很多次改变就会多次触发setOption,报错
  • 因此需要确保每一次setOption都发生在对应的DOM已经挂载了的情况下,所以使用nextTick,确保在DOM发生渲染之后再setOption

slot

  • 具名插槽
  • 动态插槽名
  • 作用域插槽

动态插槽名:

js 复制代码
<base-layout>
  <template v-slot:[dynamicSlotName]>
    ...
  </template>

  <!-- 缩写为 -->
  <template #[dynamicSlotName]>
    ...
  </template>
</base-layout>

作用域插槽:

js 复制代码
<!-- <MyComponent> 的模板 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>

个人实际应用场景:

  1. 基于element-plus二次封装表格组件
  2. 每次都把后端数据传入该组件,使表格渲染该数据
  3. 原始数据里,有的是状态码,ui需要展示对应的中文tag
  4. 遍历生成每个column时,绑定动态插槽名与column对应字段一致,通过作用域插槽,将原始数据吐到父组件中
  5. 父组件可通过插槽名,获取数据,并可以自定义该column的单元格使用何种组件

动态组件

  • <component :is="component-name">用法
  • 需要根据数据,动态渲染的场景。即组件类型不确定

应用场景:vue-routerkeep-alive根据路由动态加载组件

异步组件

  • import()
  • 按需加载,异步加载大组件

注意: 对比同步方式,异步组件的import位置明显不同

一个同时结合了动态组件和异步组件的例子

js 复制代码
<template>
  <div>
    <button @click="handleToggle">toggle</button>
    <Suspense>
      <component :is="AsyncComponent"></component>
      <!-- 加载中状态 -->
      <template #fallback>
        正在加载...
      </template>
    </Suspense>
  </div>
</template>

<script lang="ts" setup>
import { defineAsyncComponent, ref, computed } from 'vue'

const state = ref< 'A' | 'B'>('A')

const AsyncComponent = computed(() => {
  if(state.value === 'B') return defineAsyncComponent({
    loader: () => import('./watchO.vue')
  })
  return defineAsyncComponent({
    loader: () => import('./classAndStyleO.vue')
  })
})

const handleToggle = () => {
  if (state.value === 'A') {
    state.value = 'B'
  } else if (state.value === 'B') {
    state.value = 'A'
  }
}
</script>

应用场景:可以根据权限,动态加载页面对应的模块,让不同角色看到的同一模块是不同的组件

keep-alive

  • 缓存组件
  • 频繁切换,不需要重复渲染
  • vue常见性能优化

mixin

  • 多个组件有相同的逻辑,抽离出来
  • mixin并不是完美的解决方案,会有一些问题,如:变量来源不明确,多mixin造成命名冲突

组件之间有哪些通信方式?

  1. 父子组件通信:父组件可以通过 props 向子组件传递数据,而子组件可以通过事件(emit)向父组件发送消息。这是最常见和简单的组件通信方式。

  2. 子组件访问父组件的方法:子组件可以通过 this.$parent 访问父组件的属性和方法。这种方式适用于简单的层次结构。

  3. 父组件可以通过this.$refs访问子组件的属性和方法,需注意Vue2Vue3的区别,再compositionAPI中,子组件可以通过defineExport主动暴露供父组件访问的属性和方法。

  4. 兄弟组件通信:对于没有直接父子关系的组件,可以通过一个共同的父组件作为中间人来进行通信。兄弟组件可以通过在父组件中定义的事件来进行通信。

  5. 事件总线:可以使用一个空的Vue 实例作为事件总线,在其中定义事件和监听器。组件可以通过事件总线来进行通信,利用 emit 发送消息和 on 监听消息。vue3使用mitt

  6. Vuex/Pinia状态管理:Vuex/PiniaVue的官方状态管理库,用于处理应用程序中的共享状态。组件可以通过Vuex/Pinia存储数据,而其他组件可以从Vuex/Pinia中获取数据。这种方式适用于较大的应用程序,有复杂的数据共享和状态管理需求。

  7. 使用provide/inject实现祖先与后代之间的直接通信等

  8. 使用插槽(slot)进行内容分发

周边

vuex

  • state
  • getters
  • action 异步操作、整合多个mutation
  • mutation 原子操作,必须是同步的
  • vue组件调用时
    • dispatch
    • commit
    • mapState
    • mapGetters
    • mapActions
    • mapMutations

有了vuex为什么还要发展出来pinia

  1. API 设计:Pinia 是在 Vue 3 基础上重新设计的状态管理库,它的设计更加符合 Vue 3 的响应性原理和 CompositionAPI。Pinia 的 API 更加简洁和直观,使用类似于组件的方式来定义和使用状态。相比之下,Vuex 是为Vue 2设计的,其 API 更加基于对象和选项,需要通过定义 mutations、actions 和 getters 等来管理状态。

  2. TypeScript 支持:Pinia 是完全使用 TypeScript 编写的,并且提供了完整的类型推导和类型安全支持。Pinia 可以更好地与 TypeScript 集成,提供更好的开发体验和更可靠的类型检查。虽然 Vuex 也支持 TypeScript,但它的类型定义相对较简单并且基于鸭子类型。

  3. 性能优化:PiniaVuex 都采用了类似的响应式原理,但 Pinia 在内部进行了一些性能优化,例如对状态的惰性获取、局部更新等,以提供更快的状态访问和更新速度。Pinia 还支持批量读取和写入状态,以减少响应式更新的次数。

pinia

  1. Pinia没有mutation,只有stategetteraction(异步、同步)

  2. Pinia没有Module配置,但是我们可以通过defineStore创建Store甚至隐式嵌套Store实现不同Store间的stategetteraction共享,也就是灵活性更强

原理

Vue2响应式原理

js 复制代码
 function defineReactive(target, key, value) {
   //深度监听
   observer(value);
 
   Object.defineProperty(target, key, {
     get() {
       return value;
     },
     set(newValue) {
       //深度监听
       observer(value);
       if (newValue !== value) {
         value = newValue;
 
         updateView();
       }
     }
   });
 }
 
 function observer(target) {
   if (typeof target !== "object" || target === null) {
     return target;
   }
 
   if (Array.isArray(target)) {
     target.__proto__ = arrProto;
   }
 
   for (let key in target) {
     defineReactive(target, key, target[key]);
   }
 }
 
 // 重新定义数组原型
 const oldAddrayProperty = Array.prototype;
 const arrProto = Object.create(oldAddrayProperty);
 ["push", "pop", "shift", "unshift", "spluce"].forEach(
   methodName =>
     (arrProto[methodName] = function() {
       updateView();
       oldAddrayProperty[methodName].call(this, ...arguments);
     })
 );
 
 // 视图更新
  function updateView() {
   console.log("视图更新");
 }
 
 // 声明要响应式的对象
 const data = {
   name: "zhangsan",
   age: 20,
   info: {
     address: "北京" // 需要深度监听
   },
   nums: [10, 20, 30]
 };
 
 // 执行响应式
 observer(data);
  • Vue2如何深度监听data变化?

当初始化,以及set时,都可以进行深度监听

  • Vue2如何监听数组变化

通过重新定义数组原型的方式监听数组变化

Vue3为何使用proxy替换了Object.defineProperty?

Vue2相比Vue3有哪些不足?

  • 对象新增属性时,Object.defineProperty监听不到,无法触发视图更新,所以有Vue.set
  • 对象删除属性时,Object.defineProperty也监听不到,无法触发视图更新,所以有Vue.delete
  • 深度监听时,Object.defineProperty需要一次性递归到底,无法分段处理,一次性计算量很大
  • Mixin容易出现命名冲突,它的本质是代码片段,容易被不停覆盖,无法解决逻辑复用
  • 对于ts支持不足
  • 过度依赖this,导致无法进行treeshaking

Vue3响应式原理

可以先看我的这篇源码调试文章,再看下面,会感觉更清晰 我的ref调试之旅

reactive的响应式是如何实现的?

js 复制代码
function reactive(obj) {
  return new Proxy(obj, {
    get(target, key, receiver) {
      // 执行一些特定的操作,如依赖追踪等
      track(target, key);

      const value = Reflect.get(target, key, receiver); // 使用 Reflect.get() 获取原对象的属性值

      // 如果属性的值仍然是对象,则递归地将该对象转换为响应式对象
      if (typeof value === 'object' && value !== null) {
        return reactive(value);
      }

      return value;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];

      // 执行一些特定的操作,如派发更新等
      trigger(target, key, value, oldValue);

      const result = Reflect.set(target, key, value, receiver); // 使用 Reflect.set() 设置原对象的属性值

      return result;
    }
  });
}

const data = reactive({
  name: 'Alice',
  age: 25
});

// 进行数据读取和设置的操作
console.log(data.name); // 控制台输出:读取数据 name

data.age = 30; // 控制台输出:设置数据 age 30
  1. 我们通过reactive接收到一个原始对象,并且创建一个proxy对象,对其进行代理
  2. 我们可以通过proxygetset拦截到对原始对象的赋值操作以及获取操作。比如:set时通知视图渲染
  3. 如果属性的值仍然是对象,递归将其也转为响应式数据,shallowRef则会跳过这一步
  4. proxy还可以正确处理ArrayMap等数据结构

ref的响应式是如何实现的?

js 复制代码
function ref(value) {
  const r = {
    value: value
  };

  return new Proxy(r, {
    get(target, key, receiver) {
      // 执行一些特定的操作,如依赖追踪等
      track(target, 'value');

      const result = Reflect.get(target, key, receiver); // 使用 Reflect.get() 获取原对象的属性值

      return result;
    },
    set(target, key, value, receiver) {
      const oldValue = target[key];

      // 执行一些特定的操作,如派发更新等
      trigger(target, 'value', value, oldValue);

      const result = Reflect.set(target, key, value, receiver); // 使用 Reflect.set() 设置原对象的属性值

      return result;
    }
  });
}

const count = ref(0);

// 进行数据读取和设置的操作
console.log(count.value); // 控制台输出:读取数据 0

count.value = 1; // 控制台输出:设置数据 1

ref和reactive有什么区别呢?

从上可以看出,其实二者差距很小

  • ref包容性更强,可以将普通值转为响应式对象
  • reactive则只针对引用类型

Virtual DOM 和 diff

  • DOM操作非常耗费性能,js操作快得多,性能好得多,因此有了VDOM
  • VDOM 是实现vuereact的重要基石,vdom即用JS模拟DOM结构
  • diff算法是VDOM中最核心,最关键的部分,VueReact通过diff算法比较新老VDOM树,找出最小变更,精确操作DOM

JS模拟DOM结构(用VNode模拟HTML片段):

通过snabbdom学习vdom

vue3重写了VDOM的代码,优化了性能,react的VDOM和Vue也不同,但VDOM的基本理念不变

js 复制代码
import {
  init,
  classModule,
  propsModule,
  styleModule,
  eventListenersModule,
  h,
} from "snabbdom";

const patch = init([
  // 通过传入模块初始化 patch 函数
  classModule, // 开启 classes 功能
  propsModule, // 支持传入 props
  styleModule, // 支持内联样式同时支持动画
  eventListenersModule, // 添加事件监听
]);

const container = document.getElementById("container");

const vnode = h("div#container.two.classes", { on: { click: someFn } }, [
  h("span", { style: { fontWeight: "bold" } }, "This is bold"),
  " and this is just normal text",
  h("a", { props: { href: "/foo" } }, "I'll take you places!"),
]);

// 传入一个空的元素节点 - 将产生副作用(修改该节点)
patch(container, vnode);

const newVnode = h(
  "div#container.two.classes",
  { on: { click: anotherEventHandler } },
  [
    h(
      "span",
      { style: { fontWeight: "normal", fontStyle: "italic" } },
      "This is now italic type"
    ),
    " and this is still just normal text",
    h("a", { props: { href: "/bar" } }, "I'll take you places!"),
  ]
);

// 再次调用 `patch`
patch(vnode, newVnode); // 将旧节点更新为新节点

diff算法

  • diff算法是VDOM中最核心,最关键的部分
  • diff算法能在日常中使用vuereact时体现出来(如:key)
  • diff算法即对比,是一个广泛的概念,如linux diff命令,git diff

树的普通diff算法:

  1. 遍历tree1
  2. 遍历tree2
  3. 更新,排序

1000个节点计算了1亿次,时间复杂度On3,算法不可用

优化时间复杂度到On:

  • 只比较同一层级,不跨级比较

  • tag不相同,则直接删除重建,不再深度比较

  • tagkey,两者都相同,则认为是相同节点,不再深度比较

h函数:生成vnode

我们通过h函数传入tag,data,children/text等数据,内部进行一系列处理后,构造出vnode进行返回,因此我们也可以说通过h函数构造了vnode

patch函数:将vnode渲染为真实DOM

ts 复制代码
function patch(
    oldVnode: VNode | Element | DocumentFragment,
    vnode: VNode
  ): VNode {
    let i: number, elm: Node, parent: Node;
    const insertedVnodeQueue: VNodeQueue = [];
    for (i = 0; i < cbs.pre.length; ++i) cbs.pre[i]();
    
    // 如果传入的不是Vnode而是真实DOM
    if (isElement(api, oldVnode)) {
      oldVnode = emptyNodeAt(oldVnode);
    } else if (isDocumentFragment(api, oldVnode)) {
      oldVnode = emptyDocumentFragmentAt(oldVnode);
    }
    
    // 相同的Vnode key和sel(tag)都相等
    if (sameVnode(oldVnode, vnode)) {
      patchVnode(oldVnode, vnode, insertedVnodeQueue);
      
    // 不同的vnode,直接删掉重建
    } else {
      elm = oldVnode.elm!;
      parent = api.parentNode(elm) as Node;
      
      // 重建vnode
      createElm(vnode, insertedVnodeQueue);
      
      // 删除老的vnode
      if (parent !== null) {
        api.insertBefore(parent, vnode.elm!, api.nextSibling(elm));
        removeVnodes(parent, [oldVnode], 0, 0);
      }
    }

    for (i = 0; i < insertedVnodeQueue.length; ++i) {
      insertedVnodeQueue[i].data!.hook!.insert!(insertedVnodeQueue[i]);
    }
    for (i = 0; i < cbs.post.length; ++i) cbs.post[i]();
    return vnode;
  };
}

patchVnode 对比并且更新vnode的细节部分

ts 复制代码
function patchVnode(
    oldVnode: VNode,
    vnode: VNode,
    insertedVnodeQueue: VNodeQueue
  ) {
    // 执行 prepatch hook
    const hook = vnode.data?.hook;
    hook?.prepatch?.(oldVnode, vnode);
    
    // 设置vnode的element,复用老的真实dom
    const elm = (vnode.elm = oldVnode.elm)!;
    if (oldVnode === vnode) return;
    
    // 执行 update hook
    if (
      vnode.data !== undefined ||
      (isDef(vnode.text) && vnode.text !== oldVnode.text)
    ) {
      vnode.data ??= {};
      oldVnode.data ??= {};
      for (let i = 0; i < cbs.update.length; ++i)
        cbs.update[i](oldVnode, vnode);
      vnode.data?.hook?.update?.(oldVnode, vnode);
    }
    
    // 老的children
    const oldCh = oldVnode.children as VNode[];
    // 新的children
    const ch = vnode.children as VNode[];
    
    // vnode.test === undefined,意味着有children
    if (isUndef(vnode.text)) {
      
      // 如果vnode与oldVnode都有children
      if (isDef(oldCh) && isDef(ch)) {
      
        // 如果vnode.children不等于oldVnode.children,更新children
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue);
        
        // 如果vnode有children而oldVnode没有children
      } else if (isDef(ch)) {
        
        // oldVnode没有chilrden,清空text
        if (isDef(oldVnode.text)) api.setTextContent(elm, "");
        
        // 新建children的vnode
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue);
        
        // oldVnode有children而vnode没有children,干掉所有children
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
        
        // vnode与oldVnode都有text,更新text
      } else if (isDef(oldVnode.text)) {
        api.setTextContent(elm, "");
      }
      
    // 新的vnode有text且与oldVnode.text不相同
    } else if (oldVnode.text !== vnode.text) {
    
      // oldVnode有children,直接干掉所有children
      if (isDef(oldCh)) {
        
        // 移除children的vnode
        removeVnodes(elm, oldCh, 0, oldCh.length - 1);
      }
      
      // 设置真实DOM的text
      api.setTextContent(elm, vnode.text!);
    }
    
    // 执行 postpatch hook
    hook?.postpatch?.(oldVnode, vnode);
  }

updateChildren(双端算法位置)

ts 复制代码
function updateChildren(
    parentElm: Node,
    oldCh: VNode[],
    newCh: VNode[],
    insertedVnodeQueue: VNodeQueue
  ) {
  
    // 定义4指针,分别指向oldChildren开始与结束,newChildren开始与结束
    let oldStartIdx = 0;
    let newStartIdx = 0;
    let oldEndIdx = oldCh.length - 1;
    let oldStartVnode = oldCh[0];
    let oldEndVnode = oldCh[oldEndIdx];
    let newEndIdx = newCh.length - 1;
    let newStartVnode = newCh[0];
    let newEndVnode = newCh[newEndIdx];
    let oldKeyToIdx: KeyToIndexMap | undefined;
    let idxInOld: number;
    let elmToMove: VNode;
    let before: any;
    
    // 当新老children的指针都相碰了,终止循环
    while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
      if (oldStartVnode == null) {
        oldStartVnode = oldCh[++oldStartIdx]; // Vnode might have been moved left
      } else if (oldEndVnode == null) {
        oldEndVnode = oldCh[--oldEndIdx];
      } else if (newStartVnode == null) {
        newStartVnode = newCh[++newStartIdx];
      } else if (newEndVnode == null) {
        newEndVnode = newCh[--newEndIdx];
        
        // oldChildren开始节点 与 newChildren开始节点 相同
      } else if (sameVnode(oldStartVnode, newStartVnode)) {
        patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue);
        // 向中间移动指针
        oldStartVnode = oldCh[++oldStartIdx];
        newStartVnode = newCh[++newStartIdx];
        
        // oldChildren结束节点 与 newChildren结束节点 相同
      } else if (sameVnode(oldEndVnode, newEndVnode)) {
        patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue);
        // 向中间移动指针
        oldEndVnode = oldCh[--oldEndIdx];
        newEndVnode = newCh[--newEndIdx];
        
        // oldChilren开始节点 与 newChildren结束节点 相同
      } else if (sameVnode(oldStartVnode, newEndVnode)) {
        // Vnode moved right
        patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue);
        api.insertBefore(
          parentElm,
          oldStartVnode.elm!,
          api.nextSibling(oldEndVnode.elm!)
        );
        // 向中间移动指针
        oldStartVnode = oldCh[++oldStartIdx];
        newEndVnode = newCh[--newEndIdx];
        
        // oldChildren结束节点 与 newChildren开始节点 相同
      } else if (sameVnode(oldEndVnode, newStartVnode)) {
        // Vnode moved left
        patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue);
        api.insertBefore(parentElm, oldEndVnode.elm!, oldStartVnode.elm!);
        // 向中间移动指针
        oldEndVnode = oldCh[--oldEndIdx];
        newStartVnode = newCh[++newStartIdx];
        
        // 以上4个都未命中
      } else {
        if (oldKeyToIdx === undefined) {
          oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx);
        }
        
        // vnode的key是否能够在oldChildren下的vnode中找到
        idxInOld = oldKeyToIdx[newStartVnode.key as string];
        
        // 如果找不到
        if (isUndef(idxInOld)) {
          // New element
          api.insertBefore(
            parentElm,
            createElm(newStartVnode, insertedVnodeQueue),
            oldStartVnode.elm!
          );
        
        // 如果找的到key相同的oldVnode,说明是vnode内容发生了更新
        } else {
          elmToMove = oldCh[idxInOld];
          
          // tag不同,直接删除重新创建
          if (elmToMove.sel !== newStartVnode.sel) {
            api.insertBefore(
              parentElm,
              createElm(newStartVnode, insertedVnodeQueue),
              oldStartVnode.elm!
            );
            
          // tag相同,其他不同,更新vnode
          } else {
            patchVnode(elmToMove, newStartVnode, insertedVnodeQueue);
            oldCh[idxInOld] = undefined as any;
            api.insertBefore(parentElm, elmToMove.elm!, oldStartVnode.elm!);
          }
        }
        
        // 移动指针
        newStartVnode = newCh[++newStartIdx];
      }
    }

    if (newStartIdx <= newEndIdx) {
      before = newCh[newEndIdx + 1] == null ? null : newCh[newEndIdx + 1].elm;
      addVnodes(
        parentElm,
        before,
        newCh,
        newStartIdx,
        newEndIdx,
        insertedVnodeQueue
      );
    }
    if (oldStartIdx <= oldEndIdx) {
      removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx);
    }
  }
  1. 如果找得到完全相同的vnode,直接移动vnode,并且移动指针即可
  2. 找不到完全相同的vnode,开始找key是否能对应上,看是不是oldVnode内容发生了改变
    • 如果找不到对应的key,直接创建
    • 如果找到了对应的key
      • tag不同,直接删除,重新创建
      • tag相同,其他不同,更新vnode

模板编译

  • 模板是vue开发中最常用的部分,即与使用相关联的原理
  • 它不是html,有指令、插值、js表达式,到底是什么?
  • 面试官不会直接问,但会通过"组件渲染和更新过程"考察
  • vue template compiler 将模板编译为render函数
  • 执行render函数生成vnode

js的with语法

  • 改变代码快内自由变量的查找规则,当作接受参数的属性来查找
  • 如果找不到匹配的参数属性,就会直接报错
  • with打破了作用域规则,易读性变差,别瞎鸡脖用

with(obj)能将代码快内的自由变量当作obj的属性来查找

vue模板被编译成什么

  • 模板不是html,有指令、插值、js表达式,能实现判断、循环
  • html是标签语言,只有js才能实现判断、循环(图灵完备)
  • 图灵完备的语言:能顺序执行,判断,循环的语言
  • 因此,模板一定是转换为了某种JS代码(render函数),才能执行v-if,v-for等对应的判断,循环操作
  • render函数本身是with代码块,执行render就是执行这个with,返回vnode
  • 基于vnode再执行patchdiff
  • 使用webpackvue-loader会在开发环境下编译模板

vue组件中可以使用render代替template

  • 因为不容易理解,不易读,所以都是在用template
  • 有些复杂情况,不能用template,可以考虑用render
  • react一直用render,没有模板

小结

  • 一个组件渲染到页面,修改data触发更新(数据驱动视图)
  • 响应式:监听datagettersetter,数组的监听
  • 模板编译:模板 => render => vnode
  • vdom: patch(elem, vnode)patch(oldVnode,vnode)

vue组件时如何渲染和更新的

初次渲染 => 更新过程 => 异步渲染

初次渲染过程:

  • 解析模板为render函数(或在开发环境已完成,vue-loader
  • 触发响应式,监听data属性gettersetter
  • 执行render函数,生成vnodepatch(elem, vnode),我们在执行render的时候就已经触发了datagetter

更新过程:

  • 修改data,触发setter(此前在getter中已被监听)
  • 重新执行render函数,生成vnode
  • patch(oldVnode, vnode)

常问题

为什么vue组件要做异步渲染?

  • 汇总data的修改,一次性更新视图
  • 减少DOM操作次数,提高性能

Vue的每个生命周期都做了什么?

beforeCreate

  • 创建一个空白的Vue实例
  • datamethod尚未被初始化,不可使用

created

  • Vue实例初始化完成,完成响应式绑定
  • datamethod初始化完成,可调用
  • 尚未开始渲染模板

beforeMount

  • 编译模板,调用render生成VDom
  • 还没有开始渲染DOM

mounted

  • 完成DOM渲染
  • 组件创建完成
  • 开始由 创建阶段 进度 运行阶段

beforeUpdate

  • data发生变化之后
  • 准备更新DOM

updated

  • data发生变化,且DOM更新完成
  • 不要再updated中修改data,可能会导致死循环

beforeUnmount

  • 组件进如销毁阶段(尚未销毁,可正常使用)
  • 可移除、解绑一些全局事件、自定义事件

unMounted

  • 组件被销毁了
  • 所有子组件也都被销毁了

onActivated

  • 缓存组件被激活

onDeactivated

  • 缓存组件被隐藏

Vue什么时候操作DOM比较合适?

  • Mountedupdated都不能保证子组件全部挂在完成
  • 使用nextTick渲染DOM

Ajax应该放在哪个生命周期?

  • created或者Mounted
  • 推荐Mounted

CompositionAPI生命周期由什么区别?

  • 使用setup代替了beforeCreatecreated
  • 使用Hooks函数的形式

React、Vue2、Vue3的diff算法的区别是什么?

React:

总结: 如果是左移需要操作2个节点,右移仅需要操作1个节点,更清晰,更简便

Vue2 双端相互比较:

总结:更好的应对中间插入节点

Vue3 最长递增子序列:

Vue React为什么必须使用key?

  • diff算法根据key判断元素是否需要删除?
  • 匹配了key则只移动元素
  • 未匹配key则删除重建

你在实际工作中,做过哪些Vue的优化?

  • v-ifv-show
  • 使用computed缓存
  • keep-alive缓存组件
  • 异步组件
  • 路由懒加载
  • SSR

你在使用Vue过程中遇到过哪些坑?

  • 内存泄漏:全局变量,全局事件,全局定时器,自定义事件
  • Vue2响应式的缺陷:无法直接修改数组数据arr[index]=valueVue.setVue.delete
  • 路由切换时scroll到了顶部,在列表页缓存数据和scrollTop,再次返回该页面时,手动设置到之前的滚动位置

如何监听Vue组件报错

  • window.onerror监听所有JS错误
  • errorCaptured捕捉下级组件错误
  • errorHandler监听Vue全局报错,配置在main.ts中,无法监听异步错误,需要靠window.onerror
  • window.onunhandledrejection监听promisecatch到的错误

watch和watchEffect有什么区别?

  • watch需要明确监听哪个属性?
  • watchEffect根据其中的属性,自动监听其变化,初始化时一定会执行一次,收集要监听的数据

setup中如何获取组件实例?

需要借助getCurrentInstance这个API

相关推荐
星星会笑滴21 分钟前
vue+node+Express+xlsx+emements-plus实现导入excel,并且将数据保存到数据库
vue.js·excel·express
XINGTECODE26 分钟前
海盗王集成网关和商城服务端功能golang版
开发语言·后端·golang
程序猿进阶32 分钟前
堆外内存泄露排查经历
java·jvm·后端·面试·性能优化·oom·内存泄露
FIN技术铺36 分钟前
Spring Boot框架Starter组件整理
java·spring boot·后端
Backstroke fish42 分钟前
Token刷新机制
前端·javascript·vue.js·typescript·vue
临枫54144 分钟前
Nuxt3封装网络请求 useFetch & $fetch
前端·javascript·vue.js·typescript
RAY_CHEN.1 小时前
vue3 pinia 中actions修改状态不生效
vue.js·typescript·npm
酷酷的威朗普1 小时前
医院绩效考核系统
javascript·css·vue.js·typescript·node.js·echarts·html5
_Legend_King1 小时前
vue3 + elementPlus 日期时间选择器禁用未来及过去时间
javascript·vue.js·elementui
凡人的AI工具箱1 小时前
15分钟学 Go 第 60 天 :综合项目展示 - 构建微服务电商平台(完整示例25000字)
开发语言·后端·微服务·架构·golang