vue2-vue3面试

v-text/v-html/v-once/v-show/v-if/v-for/v-bind/v-on


  • beforeCreate()
    • 已有DOM节点:可以
    • data选项:不可以
    • 虚拟DOM节点:不可以
  • created():掌握
    • 已有DOM节点:可以
    • data选项:可以
    • 虚拟DOM节点:不可以
  • beforeMount()
    • 已有DOM节点:可以
    • data选项:可以
    • 虚拟DOM节点:不可以
  • mounted():掌握
    • 已有DOM节点:可以
    • data选项:可以
    • 虚拟DOM节点:可以

父子组件传值

解决方案:自定义属性

语法结构:子组件中通过props选项接受自定义属性

① 编辑子组件,添加自定义属性

<template>
    <p>
        data中的数据:{{ msg }}
    </p>
    <p>
        自定义属性数据:{{ dataFromParent }}
    </p>
</template>

<script>
export default {
    // 声明自定义属性:一个组件可能会存在多个属性,第一种格式通过数组接受数据
    // 定义的属性名称,用于存储接受的数据,可以和data选项中的变量一样使用;不能和data选项变量重名
    props: ['dataFromParent'],
    data() {
        return {
            msg: "xxxx"
        }
    }
    ....
}
</script>

② 编辑父组件,通过属性传递数据

<template>
    <!-- 父组件给子组件传递数据 -->
    <My-child :dataFromParent="info"></My-child>
    <My-child :data-from-parent="info"></My-child>
</template>
<script>
import MyChild from "./MyChild"
export default {
    data() {
        info: "父组件数据"
    }
}
</script>

③ 自定义属性扩展

自定义属性通过props选项定义,除了基本语法定义,还可以给自定义属性添加类型约束和默认值

  • 类型检查
export default {
    props: {
        dataFromParent: String
    }
}
  • 默认数据
export default {
    props: {
        dataFromParent: String,
        // default: "默认值"
        default: function() {
            return "默认值"
        }
    }
}
  • 数据校验
export default {
    props: {
        dataFromParent: String,
        default: "默认值",
        validator: function(value) {
            //执行自定义属性接受的数据的校验,可以在控制台产生校验信息
            return true/false
        }
    }
}

子父组件传值

解决方案:自定义事件

基本语法:通过实例选项的this.$emit()触发自定义事件

① 子组件触发事件

<template>
    <button @click="sendData">
        发送数据给父组件
    </button>
</template>
<script>
export default {
    data() {
        return {
            msg: "子组件数据"
        }
    },
    methods: {
        sendData() {
            // 触发自定义事件,传递数据
            this.$emit("childEvent", this.msg)
        }
    }
}
</script>

② 父组件监听事件

<template>
    <!-- 标签节点上,通过监听自定义事件,接受子组件数据 -->
    <My-child @childEvent="getData"></My-child>
    <My-child @child-event="getData"></My-child>
</template>
<script>
import MyChild from "./MyChild"
export default {
    data() {
        return {
            dataFromChild: ""
        }
    },
    methods: {
        getData(value) {
            this.dataFromChild = value
        }
    },
    components: {
        MyChild
    }
}
</script>

(4) 组件之间传值

组件之间的传值,任意一个项目中的两个组件都有直接或者间接的关系,完全可以通过父子组件传值完成不同组件之间的传值,如图所示:

组件之间的传值,完全可以通过自定义事件中心,避免多个组件参与传递数据的复杂度!

① 定义事件中心

创建:src/utils/events.js:创建一个空白的vue实例,主要用于数据传递

/**
 * 自定义事件中心
 *     events.js
 */
import Vue from "vue"

const vm = new Vue()

export default vm

② 组件1发送数据

<template>
    <button @click="sendData2">
        发送数据给其他组件
    </button>
</template>
<script>
// 导入事件中心
import vm from "../utils/event"
    
export default {
    data() {
        return {
            msg: "子组件数据"
        }
    },
    methods : {
        sendData2() {
            // 事件中心 触发自定义事件
            vm.$emit("customEvent", this.msg)
        }
    }
}
</script>

③ 组件2接受数据

<template>
    <p>
        等待接受数据:{{ }}
    </p>
</template>
<script>
// 导入事件中心
import vm from "../utils/event"
    
export default {
    created() {
        // 组件加载的时候执行函数,监听自定义事件
        this.getData();
    },
    data() {
        return {
            dataFromOther: "等待接受"
        }
    },
    methods: {
        getData() {
            vm.$on("customEvent", value => {
                // 接受自定义事件传递的数据
                this.dataFromOther = value
            })
        }
    }
}
</script>

小总结:关于父子组件传值问题

父子组件之间的传值不涉及页面视图的切换!

一定是在单个页面组件中,已经存在父子组件嵌套关系,才是父子组件传值的操作场景!

问题:如果出现多个页面组件切换,如何完成数据传递? 解决方案:路由传参

vuex

state mutations同步函数 actions异步函数 getters

vuex解耦合:辅助函数

(1)state选项:

vue组件中,需要通过耦合语法访问数据

this.$store.state.brandList

vuex中提供了一个函数mapState,用于将数据进行解耦合访问

<template>
    <p>
        {{ $store.state.brandList }}
    </p>
    <p>
        {{ brandList }}
    </p>
</template>
<script>
import {mapState} from "vuex"
export default {
    computed: {
        ...mapState({
            brandList: state=> state.brandList
        })
    }
}
</script>

(2) actions选项

vue组件中,需要通过耦合语法访问函数

this.$store.dispath('getBrandListActions')

vuex中提供了辅助函数mapActions,通过解耦合的方式访问对应的函数操作

<template>
</template>
<script>
    import {mapActions} from "vuex"
    export default {
        created(){
            // 原始耦合
            this.$store.dispatch('getBrandListActions')
            // 解耦合
            this.getBrandListActions()
        },
        methods: {
            ...mapActions(['getBrandListActions'])
        }
    }
</script>

(3) getters选项

vue组件中,需要通过耦合语法访问函数

this.$store.getters.函数名称

vuex提供了解耦合语法mapGetters提供了解耦合访问方式

<template>
    <p>
        {{ $store.getters.brandCounter }}
    </p>
    <p>
        {{ brandCounter }}
    </p>
</template>
<script>
    import {mapGetters} from "vuex"
    export default {
        computed: {
            ...mapGetters(['brandCounter'])
        }
    }
</script>

(4)mutations选项[了解]

vue组件中,需要通过耦合语法访问函数

this.$store.commit('mutations函数')

vuex提供解耦合辅助函数mapMutations,提供解耦合访问方式

import {mapMutations} from "vuex"

export default {
    methods: {
        ...mapMutations(['getBrandListMutations'])
    },
    create() {
        // 1、原始
        this.$store.commit('getBrandListMutations')
        // 2、解耦合
        this.getBrandListMutations()
    }
}


vue3

vue2 vue3

beforeCreate setup()

created setup()

beforeMount onbeforeMount

mounted onMounted

beforeUpdate onBeforeUpdate

updated onUpdated

beforeDestroy onBeforeUnmount

destroyed onUnmounted

activated onActivated

deactivated onDeactivated

errorCaptured onErrorCaptured

import {watch computed} from 'vue'
export default{
name:'App',
setup(){
  watch(
  ()=>{},()=>{}
  )
  const a=computed(()=>{})
}
}
//vue3.2
<script setup>
import {watch compted} from 'vue'
watch(
()=>{},()=>{}
)
const a=computed(()=>{})
</script>

setup存在的意义,就是为了使用新增的组合API,并且这些组合API只能在setup内使用

setup调用的时机是创建组件实例然后初始化props,紧接着就是调用setup函数,从生命周期钩子的角度看,他会在beforeCreate钩子之前被调用,所以setup内拿不到this上下文

除去beforeCreate和created,在setup方法中,有9个旧的生命周期钩子,可以在setup中访问

1.setup():开始创建组件之前,在beforeCreate和created之前执行,创建的是date和method

2.onBeforeMount():组件挂载到节点之前执行的函数

3.onMounted():组件挂载完成之后执行的函数

4.onBeforeUpdate():组件更新之前执行的函数

5.onUpdated():组件更新完成之后执行额函数

6.onBeforeUnmount():组件卸载之前执行的函数

7.onUnmounted():组件卸载完成之后执行的函数

8.onActived():被keep-alive缓存的组件激活是调用

9.onDeactivated():被 keep-alive 缓存的组件停用时调用,比如从 A 组件,切换到 B 组件,A 组件消失时执行。

10.onErrorCaptured():当捕获一个来自子孙组件的异常时激活钩子函数。

组件如何接受外部传入的参数

Vue 3.2 为我们在 script 内默认添加了三个方法,这三个方法分别是**defineProps **、 **defineEmits **和 defineExpose

defineProps

我们现在需要一个父子组件的传值例子,修改上述 src/App.vue 文件如下:

<template>
  <Test :count="count"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'

const count = ref(0)
</script>

在 src/components 下新增 Test.vue 组件:

<template>
  <div>{{ props.count }}</div>
</template>
<script setup>
const props = defineProps({ count: Number })
console.log('props', props)
</script>

在页面中我们打印了 props,可以发现它被 Proxy 代理过,这是 Vue3 实现响应式的核心 API,也就是说从父亲组件传到子组件的 count 变量,已经是响应式数据。

并且在子组件内,可以通过 watchEffect 和 watch 观察到数据的变化情况,我们来试试让数据在父组件变化起来,分别做如下修改:

// App.vue
<template>
  <Test :count="count"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'

const count = ref(0)
setTimeout(() => {
  count.value = 100
}, 2000)
</script>
// Test.vue
<template>
  <div>{{ props.count }}</div>
</template>
<script setup>
import { watchEffect } from 'vue'
const props = defineProps({ count: Number })
watchEffect(() => {
    console.log('props.count = ', props.count)
  })
</script>

defineEmits

该属性的作用是在子组件获取父组件传递进来的方法,我们同样用一个例子来演示该属性的作用,在 App.vue 添加一个 add 方法如下:

<template>
  <Test :count="count" @add="add"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'

const count = ref(0)
const add = () => {
  count.value += 1
}
</script>

这里声明 add 方法,就不用再像 setup 函数那样,将其 return 出去。 在子组件内,通过 defineEmits 接受方法,如下所示:

<template>
  <div>{{ props.count }}</div>
  <button @click="add">+1</button>
</template>
<script setup>
const props = defineProps({ count: Number })
// 获取父组件传进来的add方法
const emit = defineEmits(['add'])

const add = () => {
  // 执行父组件传进来的add方法
  emit('add')
}
</script>

使用上,和 Vue 2 的区别不大,主要区别在于如何获取上

defineExpose

在 Vue 3.2.x 版本出来前,我们使用 Vue3 开发项目都是用 setup 函数的方式,在这种方式下,父组件通过 ref 去获取子组件 return 出来的方法是比较方便的,我们修改 App.vue 和 Test.vue 组件如下:

// App.vue
<template>
  <Test :count="count" ref="TestRef"></Test>
</template>
<script setup>
import { ref } from 'vue'
import Test from './components/Test.vue'

const count = ref(1)
const TestRef = ref()
console.log('TestRef', TestRef)
</script>
// Test.vue
<template>
  <div>{{ props.count }}</div>
</template>
<script>
export default {
  name: 'Test',
  props: {
    count: Number
  },
  setup(props) {
    const testFn = () => {
      console.log('我是测试方法')
    }
    return {
      props,
      testFn // 将这个测试方法 return 出去
    }
  }
}
</script>

上图显示 TestRef 的打印结果,也就意味着 App.vue 可以拿到 Test.vue 内部的方法。这个特性能帮我们实现很多有趣的组件。

但是到了 Vue 3.2.x 版本,使用 script setup 后,父组件就拿不到子组件的内部方法了,修改Test.vue 如下所示:

// Test.vue
<template>
  <div>{{ props.count }}</div>
</template>
<script setup>
const props = defineProps({ count: Number })
const testFn = () => {
  console.log('这是测试方法')
}
</script>

此时,defineExpose 的作用就得以体现了。在 Test.vue 中,将想要往外抛出的方法作为参数放到 defineExpose 中,如下所示:

defineExpose({ testFn })

reactive

reactive 是 Vue 3 中提供的实现响应式数据的方法。在 Vue 2 中实现响应式数据是通过 Object 的 defineProPerty 属性来实现的,而在 Vue 3 中的响应式是通过 ES2015 的 Proxy 来实现

reactive 参数必须是对象
reactive 方法接受一个对象(json 或 Array)。

<!--App.vue-->
<template>
  <p>{{ state.title }}</p>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
  title: 'json'
})
</script>

尝试着修改上面的 reactive 参数为:

<template>
  <p>{{ state }}</p>
</template>
const state = reactive(['arr1', 'arr2', 'arr3'])

同学们可能会有疑问,为什么数组也可以直接渲染呢。这里你可以把数组理解为特殊的对象。我们平时常用的普通对象如下所示:

const obj = { a: ‘1’, b: ‘2’ }
数组作为特殊的对象,如下:

const arr = [‘a’, ‘b’]
此时你可以把它看作:

const arr = { 0: ‘a’, 1: ‘b’ }
所以我们同样可以使用键值对的形式获取值,如 arr[0]。所以这就解释了为什么 reactive 还可以接受数组的原因。

reactive 包裹的对象,已经通过 Proxy 进行响应式赋能,所以我们可以通过如下形式修改值,会直接体现在 template 模板上。

<template>
  <p>{{ state.title }}</p>
</template>
<script setup>
import { reactive } from 'vue'
const state = reactive({
  title: '十三'
})

setTimeout(() => {
  state.title= '十六'
}, 2000)
</script>

2 秒后你将会在浏览器上看到“十三”变成了“十六”。

响应式转换是“深层的”,会影响对象内部所有嵌套的属性。基于 ES2015 的 Proxy 实现,返回的代理对象不等于原始对象。建议仅使用代理对象而避免依赖原始对象。

上述是官方文档描述 reactive 返回的代理后的对象,内部的二级三级属性,都会被赋予响应式的能力,所以官方建议,使用 reactive 返回的值,而不要去使用原始值。

ref

ref 和 reactive 一样,同样是实现响应式数据的方法。在业务开发中,我们可以使用它来定义一些简单数据,如下所示:

<template>
  <p>{{ count }}</p>
</template>
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

修改数据,可以通过 count.value = 1 类似这样的语法去修改。但是为什么它需要这样去修改变量,而 reactive 返回的对象可以直接修改如 state.count = 1 。

原因是 Vue 3 内部将 ref 悄悄的转化为 reactive,如上述代码会被这样转换:

ref(0) => reactive({ value: 0 })
所以 count 相当于 reactive 返回的一个值,根据 reactive 修改值的方式,就可以理解为什么 ref 返回的值是通过 .value 的形式修改值了。

还有一点需要注意,当 ref 作为渲染上下文的属性返回(即在 setup() 返回的对象中)并在模板中使用时,它会自动解套,无需在模板内额外书写 .value。之所以会自动解套,是因为 template 模板在被解析的时候,Vue3 内部通过判断模板内的变量是否是 ref 类型。如果是,那就加上 .value,如果不是则为 reactive 创建的响应集代理数据。

我们不妨打印一下 ref 创建的对象 console.log(count),浏览器控制台如下图所示:

没错,就是通过上图 __v_isRef 变量去判断,模板内的变量是否为 ref 类型。判断类型也可以通过 isRef 方法,如下:

<template>
  <p>{{ count }}</p>
</template>
<script setup>
import { ref, isRef } from 'vue'
const count = ref(0)
console.log(isRef(count)) // true
</script>

在 Vue 2 中,我们可以通过给元素添加 ref=“xxx” 属性,然后在逻辑代码中通过 this.$refs.xxx 获取到对应的元素。

到了 Vue 3 后,setup 函数内没有 this 上下文,因此我们可以通过 ref 方法去获取,并且还需要在页面挂载以后才能拿到元素。

<template>
  <div ref='shisanRef'>十三</div>
</template>
<script setup>
import { ref, onMounted } from 'vue'
const shisanRef = ref(null)
onMounted(() => {
  console.log(shisanRef)
})
</script> 

computed
Vue 2 时代,computed 作为选项出现在页面中,而到了 Vue 3 时代,它将会以钩子函数的形式出现。我们先来修改 App.vue 下的代码,如下所示:

<template>
  <p>{{ text }}</p>
</template>
<script setup>
import { reactive, computed } from 'vue'
const state = reactive({
  name: '十三',
  desc: '你好'
})

const text = computed(() => {
  console.log('11')
  return state.name + state.desc
})

setTimeout(() => {
  state.name = '十六'
}, 2000)
</script>

上述代码通过 computed 函数将 name 和 desc 变量拼接,返回 text 渲染在模板上。

这里要注意的是 2 秒后,name 变量将会被重新赋值,那么 computed 函数内带有 state.name,所以会被动态计算,重新返回 text 值,浏览器会有如下变化:

若是将 computed 方法内的函数做如下改动:


const text = computed(() => {
console.log(‘11’)
return state.desc
})

少了 state.name,2 秒后,你将不会看到控制台打印 11,因为函数内不会被检测执行。

上述形式 computed 返回的值是不可修改的,通过 get 和 set 的形式返回的值是可修改的,不过这种情况的使用场景不多,这里不作深究。

readonly
readonly 顾名思义,用于创建一个只读的数据,并且所有的内容都是只读,不可修改。我们看如下代码:

<template>
  <p>{{ state.name }}</p>
  <p>{{ state.desc }}</p>
  <button @click="fn">修改</button>
</template>
<script setup>
import { reactive, computed, readonly } from 'vue'
const state = readonly({
  name: '十三',
  desc: '你好'
})

const fn = () => {
  state.name = '十六'
  state.desc = '他好'
  console.log('state', state)
}
</script>

我们用 readonly 创建一个数据,将其渲染在 template 模板下,并且通过 fn 函数,修改这个数据,看看浏览器会有什么反馈。

点击按钮之后,如上图所示,控制台报警告了,并且 state 打印出来之后,内部数据也没有变化。

watchEffect

首先 watchEffect 会追踪响应式数据的变化,并且还会在第一次渲染的时候立即执行,我们来看看下面的例子:

<template>
  <div>
    <h1>{{ state.search }}</h1>
    <button @click="handleSearch">改变查询字段</button>
  </div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let state = reactive({
  search: Date.now()
})
watchEffect(() => {
  console.log(`监听查询字段${state.search}`)
})

const handleSearch = () => {
  state.search = Date.now()
}
</script>

watchEffect 接受一个回调函数作为参数,并且该回调函数内如果有响应式变量,那么当我执行 handleSearch 方法改变 search 变量时,回调函数也会被执行,如下所示:

watchEffect 函数返回一个新的函数,我们可以通过执行这个函数或者当组件被卸载的时候,来停止监听行为。来看下面代码:

let timer = null
let state = reactive({
  search: Date.now()
})
// 返回停止函数
const stop = watchEffect((onInvalidate) => {
  console.log(`监听查询字段${state.search}`)
})
const handleSearch = () => {
  state.search = Date.now()
}
setTimeout(() => {
  console.log('执行 stop 停止监听')
  stop() // 2 秒后停止监听行为
}, 2000)

我们一直点击按钮,控制台会一直打印改变的数据。当 2 秒是 stop 方法被执行,停止监听后,控制台不再打印数据。如下图所示:

watchEffect 的回调方法内有一个很重要的方法,用于清除副作用。它接受的回调函数也接受一个函数 onInvalidate。名字不重要,重要的是它将会在 watchEffect 监听的变量改变之前被调用一次,具体执行顺序我们通过代码来解释:

<template>
  <div>
    <h1>{{ state.search }}</h1>
    <button @click="handleSearch">改变查询字段</button>
  </div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let state = reactive({
  search: Date.now()
})
const stop = watchEffect((onInvalidate) => {
  console.log(`监听查询字段${state.search}`)
  onInvalidate(() => {
    console.log('执行 onInvalidate')
  })
})

const handleSearch = () => {
  state.search = Date.now()
}
</script>

每当我点击按钮,改变 search 值时,onInvalidate 会在监听打印之前被执行一次。

那么要它何用呢?用处非常大。举个例子,我们需要监听 search 的变化,去请求接口数据,此时接口是异步返回的,每当我改变 search 都会去请求一次接口,那么有可能 search 改变的很频繁,那就会频繁的去请求接口,导致服务端压力倍增。我们可以通过这个特性去降低服务端的压力,具体逻辑如下:

<template>
  <div>
    <h1>{{ state.search }}</h1>
    <button @click="handleSearch">改变查询字段</button>
  </div>
</template>
<script setup>
import { reactive, watchEffect } from 'vue'
let timer = null
let state = reactive({
  search: Date.now()
})
watchEffect((onInvalidate) => {
  console.log(`监听查询字段${state.search}`)
  timer = setTimeout(() => {
    console.log('模拟接口异步请求,3 秒之后返回详情信息')
  }, 3000)
  onInvalidate(() => {
    console.log('清除');
    clearInterval(timer);
  })
})

const handleSearch = () => {
  state.search = Date.now()
}
</script>

在 watchEffect 回调函数内,我用 setTimeout 的形式去模拟响应时间为 3 秒的异步请求,上面代码可以理解为 3 秒之内如果你不去改变 search 变量,那么页面就成功返回接口数据,如果在 3 秒之内你再次点击按钮改变了 search 变量,onInvalidate 将会被触发,从而清理掉上一次的接口请求,然后根据新的 search 变量去执行新的请求。我们来看浏览器的表现:

watch

watch 的功能和之前的 Vue 2 的 watch 是一样的。和 watchEffect 相比较,区别在 watch 必须指定一个特定的变量,并且不会默认执行回调函数,而是等到监听的变量改变了,才会执行。并且你可以拿到改变前和改变后的值,代码如下:

<template>
  <div>
    <h1>{{ state.search }}</h1>
    <button @click="handleSearch">改变查询字段</button>
  </div>
</template>
<script setup>
import { reactive, watch } from 'vue'
let timer = null
let state = reactive({
  search: Date.now()
})
watch(() => {
  return state.search
}, (nextData, preData) => {
  console.log('preData', preData)
  console.log('nextData', nextData)
})

const handleSearch = () => {
  state.search = Date.now()
}
</script>

提供/注入(provide/inject)

祖先 想要传递数据给 儿子 的的话,正常情况下,需要先传递给 父亲 组件,然后 父亲 组件再将数据传给 儿子 组件。

现在我们有了 provide/inject,便可以在 祖先组件 声明 provide,然后在 儿子组件 通过 inject 拿到数据。下面我们用代码来诠释上面的分析。

Vue 2写法
清空上述 App.vue 的代码,将其作为 祖先组件,代码如下:

<template>
  <div>
    <h1>提供/注入</h1>
    <Father />
  </div>
</template>
<script>
import Father from './components/Father.vue'

export default {
  components: {
    Father
  },
  provide: {
    name: '陈尼克'
  }
}
</script>

在 src/components 文件夹新建两个文件 Father.vue 和 Son.vue 如下:

<!--Father.vue-->
<template>
  <div>我是父亲</div>
  <Son />
</template>
<script>
import Son from './Son.vue'
export default {
  name: 'Father',
  components: {
    Son
  }
}
</script>
<!--Son.vue-->
<template>
  <div>我是儿子,{{ name }}</div>
</template>
<script>
export default {
  name: 'Son',
  inject: ['name']
}
</script>

Vue 3 写法
之前说过 Vue 3 作出最大的改动就是将 options 的书写形式改成了 hooks 的钩子函数形式。privide/inject 也不例外,我们使用它们需要通过 vue 去解构出来,下面我们修改上述代码如下:

<!--App.vue-->
<template>
  <div>
    <h1>提供/注入</h1>
    <Father />
  </div>
</template>
<script setup>
import { provide } from 'vue'
import Father from './components/Father.vue'

provide('name', '陈尼克') // 单个声明形式
provide('info', {
  work: '前端开发',
  age: '18'
}) // 多个声明形式
</script>
<!--Son.vue-->
<template>
  <div>我是儿子,{{ name }}</div>
  <div>职业:{{ info.work }}</div>
  <div>年龄:{{ info.age }}</div>
</template>
<script setup>
import { inject } from 'vue'
const name = inject('name', '嘻嘻') // 第二个参数为默认值,可选
const info = inject('info')
</script>

当我们需要修改传入的数据时,Vue 不建议我们直接在接收数据的页面修改数据源,用上述的例子就是不建议在 Son.vue 组件内去修改数据源,我们可以在 App.vue 组件内通过 provide 传递一个修改数据的方法给 Son.vue,通过在 Son.vue 内调用该方法去改变值。我们将代码做如下修改:

<!--App.vue-->
<template>
  <div>
    <h1>提供/注入</h1>
    <Father />
  </div>
</template>
<script setup>
import { provide, ref } from 'vue'
import Father from './components/Father.vue'

const name = ref('陈尼克')
provide('name', name) // 单个声明形式
provide('info', {
  work: '前端开发',
  age: '18'
}) // 多个声明形式

const changeName = () => {
  name.value = '李尼克'
}

provide('changeName', changeName)
</script>
<!--Son.vue-->
<template>
  <div>我是儿子,{{ name }}</div>
  <div>职业:{{ info.work }}</div>
  <div>年龄:{{ info.age }}</div>
  <button @click="changeName">修改名字</button>
</template>
<script setup>
import { inject } from 'vue'
const name = inject('name', '嘻嘻') // 第二个参数为默认值,可选
const info = inject('info')
const changeName = inject('changeName')
</script>

这里解释一下,在 Son.vue 组件中,你可以直接修改 inject 传进来的 name 值。但是你细想,数据源存在于 App.vue 中,你在 Son.vue 中私自修改了数据源传进来的值,那两边的值就会产生紊乱,上述业务逻辑属于简单的,当你在公司正式项目中这样做的时候,数据源就会变得杂乱无章,页面组件变得难以维护。综上所述,一定要控制好数据源,保持单一数据流。

vue3性能和业务层面上的提升

虚拟 DOM 性能优化

PatchFlag(静态标记)

Vue 2 中的虚拟 DOM 是全量对比的模式,而到了 Vue 3 开始,新增了静态标记(PatchFlag)**。在更新前的节点进行对比的时候,只会去对比带有静态标记的节点。**并且 PatchFlag 枚举定义了十几种类型,用以更精确的定位需要对比节点的类型。下面我们通过图文实例分析这个对比的过程。假设我们有下面一段代码:

老八食堂

{{ message }}

通过上图,我们发现,Vue 2 的 diff 算法将每个标签都比较了一次,最后发现带有 {{ message }} 变量的标签是需要被更新的标签,显然这还有优化的空间。

在 Vue 3 中,对 diff 算法进行了优化,在创建虚拟 DOM 时,根据 DOM 内容是否会发生变化,而给予相对应类型的静态标记(PatchFlag)

观察上图,不难发现视图的更新只对带有 flag 标记的标签进行了对比(diff),所以只进行了 1 次比较,而相同情况下,Vue 2 则进行了 3 次比较。这便是 Vue 3 比 Vue 2 性能好的第一个原因。

我们再通过把模板代码转译成虚拟 DOM,来验证我们上述的分析是否正确。我们可以打开模板转化网站,对上述代码进行转译:

上图蓝色框内为转译后的虚拟 DOM 节点,第一个 p 标签为写死的静态文字,而第二个 p 标签则为绑定的变量,所以打上了 1 标签,代表的是 TEXT(文字),标记枚举类型如下:


诸如上述代码,如果将 PAGE_SIZE = 10 写在 getData 方法内,每次调用 getData 都会重新定义一次变量。

Vue 3 在这方面也做了同样的优化,继续用我们上一个例子写的代码,观察编译之后的虚拟 DOM 结构,如下所示。

export const enum PatchFlags {
  TEXT = 1,// 动态的文本节点
  CLASS = 1 << 1,  // 2 动态的 class
  STYLE = 1 << 2,  // 4 动态的 style
  PROPS = 1 << 3,  // 8 动态属性,不包括类名和样式
  FULL_PROPS = 1 << 4,  // 16 动态 key,当 key 变化时需要完整的 diff 算法做比较
  HYDRATE_EVENTS = 1 << 5,  // 32 表示带有事件监听器的节点
  STABLE_FRAGMENT = 1 << 6,   // 64 一个不会改变子节点顺序的 Fragment
  KEYED_FRAGMENT = 1 << 7, // 128 带有 key 属性的 Fragment
  UNKEYED_FRAGMENT = 1 << 8, // 256 子节点没有 key 的 Fragment
  NEED_PATCH = 1 << 9,   // 512
  DYNAMIC_SLOTS = 1 << 10,  // 动态 solt
  HOISTED = -1,  // 特殊标志是负整数表示永远不会用作 diff
  BAIL = -2 // 一个特殊的标志,指代差异算法
}

hoistStatic(静态提升)

我们平时在开发过程中写函数的时候,定义一些写死的变量时,都会将变量提升出去定义,如下所示:

const PAGE_SIZE = 10
function getData () {
    $.get('/data', {
      data: {
        page: PAGE_SIZE
    },
    ...
  })
}

没有做静态提升前:

hoist.png

选择 Option 下的 hoistStatic:

optionh.png

静态提升后:

hoist2.png

细心的同学会发现, 老八食堂 被提到了 render 函数外,每次渲染的时候只要取 _hoisted_1 变量便可。认真看文章的同学又会发现一个细节, _hoisted_1 被打上了 PatchFlag ,静态标记值为 -1 ,特殊标志是负整数表示永远不会用作 diff。也就是说被打上 -1 标记的,将不在参与 diff 算法,这又提升了 Vue 的性能。

cacheHandler(事件监听缓存)

默认情况下 @click 事件被认为是动态变量,所以每次更新视图的时候都会追踪它的变化。但是正常情况下,我们的 @click 事件在视图渲染前和渲染后,都是同一个事件,基本上不需要去追踪它的变化,所以 Vue 3 对此作出了相应的优化叫 事件监听缓存,我们在上述代码中加一段:

giao.png

在未开启 事件监听缓存 的情况下,我们看到这串代码编译后被静态标记为 8,之前讲解过被静态标记的标签就会被拉去做比较,而静态标记 8 对应的是“动态属性,不包括类名和样式”。 @click 被认为是动态属性,所以我们需要开启 Options 下的 cacheHandler 属性,如下图所示:

giao1.png

细心的同学又会发现,开启 cacheHandler 之后,编译后的代码已经没有静态标记(PatchFlag),也就表明图中 p 标签不再被追踪比较变化,进而提升了 Vue 的性能。

SSR 服务端渲染

当你在开发中使用 SSR 开发时,Vue 3 会将静态标签直接转化为文本,相比 React 先将 jsx 转化为虚拟 DOM,再将虚拟 DOM 转化为 HTML,Vue 3 已经赢了。

ssr.jpg

StaticNode(静态节点)
上述 SSR 服务端渲染,会将静态标签直接转化为文本。在客户端渲染的时候,只要标签嵌套得足够多,编译时也会将其转化为 HTML 字符串,如下图所示:

staticnode.jpg

需要开启 Options 下的 hoistStatic

Tree-shaking
说到 Tree-shaking 这个属于,官方的解释用大白话说就是,没有被应用到的代码,编译后自动将其剔除。

我个人是这么记住 Tree-shaking 的,翻译成中文就是 摇树,树上有枯叶和绿叶,我摇动树干,枯叶掉了,新叶留着。这里的枯叶就是指没用到的代码,新叶指被应用到的代码,这么记就完全可以技术这个技术点。

在 Vue 2 中,无论有没有用到全部的功能,这些功能的代码都会被打包到生产环境中。究其原因,主要还是怪 Vue 2 生成实例是单例,这样打包的时候就无法检测到实例中的各个方法是否被引用到。如下:

import Vue from ‘vue’

Vue.nextTick(() => {})
而 Vue 3 经过改良之后,引入了 Tree-shaking 的特性,所有的方法通过模块化导入的形式。如下:

import { nextTick, onMounted } from ‘vue’

nextTick(() => {})
nextTick 方法会被打进生产包,而 onMounted 在代码里没有用到,最终不会出现在编译后的代码里。

Tree-shaking 做了两件事:

编译阶段利用 ES 的模块化判断有哪些模块已经加载。
判断哪些模块和变量,没有被使用或者引用到,从而删除对应的代码。
光看文字没有说服力,我们通过代码实例来演示一遍,通过 Vue CLI 启动一个 Vue 2 的项目,修改 App.vue 如下所示:

{{ test }}
运行打包指令 npm run build,打完包后体积如下:

我们再加一个 Option,如下所示:

{{ test }}
再次运行 npm run build,如下所示:

业务代码从 2.04 -> 2.10,而工具包还是 89.66。由此可见,增减代码,并不会影响工具包的打包大小。

我们再来看看 Vue 3 的表现,通过 Vue CLI 启动一个 Vue 3 的项目,App.vue 作如下修改:

{{ state.test }}
运行 npm run build,打包后,体积如下:

我们加一个添加一个 computed 方法,如下所示:

<template>
  <div>{{ state.test }}</div>
</template>
<script>
import { reactive, computed } from 'vue'
const state = reactive({
  test: '十三'
})
const testc = computed(() => {
  return state.test + '测试'
})
</script>

添加了 computed 之后,可以看到,工具包的大小从 87.35 -> 87.39,变大了。也就是说,之前没有用到 computed,它是不会被打包到生产环境的工具包里的。

综上所述,Vue 3 的 Tree-shaking 为我们带来了一下几个升级:

减少打包后的静态资源体积
程序执行更快

相关推荐

  1. vue2-vue3面试

    2024-04-12 02:42:03       29 阅读
  2. vue2vue3部分面试

    2024-04-12 02:42:03       41 阅读
  3. vue3面试

    2024-04-12 02:42:03       48 阅读
  4. Vue 3面试

    2024-04-12 02:42:03       56 阅读
  5. vue2vue3

    2024-04-12 02:42:03       37 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-04-12 02:42:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-04-12 02:42:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-04-12 02:42:03       82 阅读
  4. Python语言-面向对象

    2024-04-12 02:42:03       91 阅读

热门阅读

  1. Animation动画控制脚本

    2024-04-12 02:42:03       38 阅读
  2. 通过 CLI 和引入的方式使用 React:基础入门

    2024-04-12 02:42:03       38 阅读
  3. 001 spring ioc(xml)

    2024-04-12 02:42:03       28 阅读
  4. 程序员面试经典——01.01. 判定字符是否唯一

    2024-04-12 02:42:03       34 阅读
  5. 设计基于锁的并发数据结构

    2024-04-12 02:42:03       38 阅读