vue2和vue3的一些区别

为什么vue3中去掉了vue构造函数?

在过去,如果遇到一个页面中有多个vue应用的时候,往往会遇到一些问题,像这样

vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<!--vue2-->
<div id="app1"></div>
<div id="app2"></div>
<script>
Vue.use(...) // 会影响所有的vue实例
Vue.mixin(...) // 会影响所有的vue实例
Vue.component(...) // 会影响所有的vue实例
new Vue({
// 配置项
}).$mount("#app1")
new Vue({
// 配置项
}).$mount("#app2")
</script>

在vue3中,我们可以这样做

vue
1
2
3
4
5
6
7
8
9
10
11
12
13
<!--vue3-->
<div id="app1"></div>
<div id="app2"></div>
<script>
const app1 = createApp({
// 配置项
})
const app2 = createApp({
// 配置项
})
app1.use(...).component(...).mount("#app1") // 只会影响app1
app2.mount("#app2")
</script>

在vue3中,createApp返回的是一个应用实例,而不是一个构造函数,这样就可以避免全局污染,也可以更好的支持多个vue应用的情况。

完整回答:
vue2的全局构造函数带来的一些问题:

  1. 调用构造函数的静态方法会对所有的vue应用生效,不利于隔离不同的应用
  2. vue2构造函数继承了太多的功能,不利于tree-shaking,vue3把这些功能使用普通函数的方式导出,能够充分利用tree-shaking优化打包体积
  3. vue2没有把组件实例和vue应用两个概念区分开,在vue2里面,通过new Vue创建的对象,既是vue应用,也是一个特殊的vue组件实例。
  4. vue3中,通过createApp创建的是vue应用,它内部提供的一些方法是针对整个vue应用的,而不再是一个特殊的组件。通过createComponent创建的是vue组件实例,这样就能够很好的区分开来。

谈谈对vue3数据响应式的理解

vue3不再使用Object.defineProperty来实现数据响应式,而是使用Proxy来实现数据响应式。Proxy相比Object.defineProperty有以下优点:

  1. Proxy可以直接监听对象而非属性,所以不需要深度遍历整个对象
  2. Proxy不仅可以代理对象,还可以代理数组
  3. Proxy返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改
  4. Proxy有多达13种拦截方法,不仅可以实现数据劫持,还可以实现数据验证和过滤等功能
  5. 同时由于Proxy可以监控到成员的新增和删除,因此,在Vue3中新增成员、删除成员、索引访问等均可以触发重新渲染,而Vue2中新增成员和删除成员是无法触发重新渲染的。

vue3的效率提升主要体现在哪些方面

静态提升

将静态节点提升到render函数外部,只在初始化时执行一次,后续更新时不再执行,减少了不必要的计算,从而提高了渲染性能。
下面的静态节点会被提升

  • 元素节点
  • 没有绑定动态内容
1
2
3
4
5
6
7
8
9
10
11
12
13
// vue2的静态节点
render()
{
createVNode('div', null, 'hello world')
// ....
}
// vue3的静态节点
const _hoisted_1 = createVNode('div', null, 'hello world')
render()
{
_hoisted_1
// ....
}

静态属性会被提升

template
1
2
3
4

<div class="user">
{{user.name}}
</div>
1
2
3
4
5
const _hoisted_1 = {class: 'user'}

function render() {
createVNode('div', _hoisted_1, user.name)
}

当一个对象的属性是静态的,那么这个对象也会被提升

预字符串化

1
2
3
4
5
6
7
8
9
10
11
12
13
14

<div>
<span>hello</span>
<span>world</span>
</div>
<ul class="nav">
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
<li><a href="">menu</a></li>
</ul>
<div class="user">
<span>{{user.name}}</span>
</div>

当编译器遇到这样的大段的静态内容的时候,vue3会将这些静态内容预先转换成一个普通的字符串节点,然后在渲染的时候直接插入到dom中,减少了dom操作的次数,提高了渲染性能。

缓存事件处理函数

1
2

<button @click="count++">click me</button>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// vue2
render(ctx)
{
return createVNode("button", {
onClick: function ($event) {
ctx.count++
}
})
}

// vue3
render(ctx, _cache)
{
return createVNode("button", {
onClick: _cache[0] || (_cache[0] = function ($event) {
ctx.count++
})
})
}

因为事件处理函数是不会变化的,所以可以将事件处理函数缓存起来,减少不必要的函数创建。

Block tree

在vue2中,每次更新都会重新创建一个新的vnode树,然后对比新旧vnode树,找出差异,然后更新dom。这样的做法会导致大量的dom操作,降低渲染性能。
在vue3中,引入了block tree,当更新时,只会更新block tree中的节点,而不是整个vnode树,这样可以减少不必要的dom操作,提高渲染性能。

Patch flag

vue2在对比每一个节点的时候,都会对比节点的所有属性,这样会导致大量的计算。
vue3引入了patch flag,当更新时,只会对比patch flag,比如这个节点的样式是否改变,内容是否改变,事件是否改变,而不是所有的属性,这样可以减少不必要的计算,提高渲染性能。

响应式api练习题

看程序写输出结果

题目1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 写出程序的运行结果
import {reactive, computed, readonly, ref} from "vue";

const state = reactive({
firstName: "Xu Ming",
lastName: "Deng",
})

const fullName = computed(() => {
console.log("changed")
return `${state.lastName},${state.firstName}`
})
console.log("state ready")
console.log("full name is", fullName.value)
console.log("full name is", fullName.value)
const imState = readonly(state)
console.log(imState === state)

const stateRef = ref(state)

console.log(stateRef.value === state)

state.firstName = "Cheng"
state.lastName = "Ji"

console.log(imState.firstName, imState.lastName)
console.log("full name is", fullName.value)
console.log("full name is", fullName.value)

const imState2 = readonly(stateRef)
console.log(imState2.value === stateRef.value)

state ready
changed
full name is Deng,Xu Ming
full name is Deng,Xu Ming
false
true
Cheng Ji
changed
full name is Ji,Cheng
full name is Ji,Cheng
false

分析

  1. 第一条log是同步的,会立即执行,所以先输出了state ready
  2. 第二条log是计算属性,这个时候会先运行一遍回调,输出changed,然后把计算之后的属性返回
  3. 第三条log会输出返回的计算属性,输出 full name is Deng, Xu Ming
  4. 第四条log由于计算属性存在缓存,并且数据源并没有改变,所以computed中的回调并不会再次执行,而是直接从缓存里面取,所以继续输出
    full name is Deng, Xu Ming
  5. 第五条imState是一个只读的响应式对象,state是一个响应式对象,当然不是一个东西,所以输出 false
  6. 第六条stateRef是一个ref对象,来自于state,他的stateRef.value指向的就是这个state,这两个是一个东西,所以输出 true
  7. 第七条imState的数据源是state,但是在这行代码执行之前,数据源已经改动,所以这个只读的响应式对象的属性也会跟着更改,所以会输出
    Cheng Ji
  8. 第八条由于fullName所依赖的数据源更改了,所以这个时候不再使用缓存,而是再次执行computed里面的回调,获取新的计算属性,所以会输出
    full name is Ji,Cheng
  9. 第九条使用计算属性缓存,继续输出 full name is Ji,Cheng
  10. 第十条imState2是一个只读的响应式对象,与可修改的ref对象不一样,输出 false

题目2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import {reactive, watch, watchEffect} from "vue";

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

watchEffect(() => {
console.log("watchEffect", state.count)
})
watch(() => state.count, (newVal, oldValue) => {
console.log("watch", newVal, oldValue)
})
console.log("start")
setTimeout(() => {
console.log("time out")
state.count++
state.count++
})
state.count++
state.count++
console.log("end")
1
2
3
4
5
6
7
8
watchEffect 0
start
end
watchEffect 2
watch 2 0
time out
watchEffect 4
watch 4 2

分析

  1. 由于watchEffect()的特性,在定义的时候就会先执行一遍回调,以便收集相关的依赖,所以会先输出 watchEffect 0;
  2. 输出 start;
  3. 遇到了定时器,定时器会被放到宏队列当中,先执行同步任务。遇到了state.count++,这个时候出发了watchEffect和watch这两个侦听器,但是由于是异步的,会放到异步队列的微队列当中,所以代码继续往下走,输出了 end
  4. 由于异步队列中,微队列的优先级是要比宏队列的优先级高的,所以这个时候会先执行微队列里面的任务,先是watchEffect,输出 watchEffect 2
  5. 然后是watch,输出 watch 2 0
  6. 微队列里面的任务执行完了,轮到宏队列里的任务执行了,这个时候输出了 time out
  7. state.count的值发生变化,微队列来活了,宏队列执行完毕转到微队列中执行
  8. watchEffect回调执行,输出 watchEffect 4
  9. watch 回调执行, 输出 watch 4 2

函数补全

函数1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {reactive, readonly} from "vue";

/**
* A custom hook that provides a reactive user object and functions to update its properties.
*
* @returns {Object} An object containing:
* - `user` {Object}: 一个响应式的只读对象
* - `setUserName` {Function}: 函数,用于修改用户名字
* - `setUserAge` {Function}: 函数用于修改用户的年龄
*/
function useUser() {
// 补全该函数
return {
user,
setUserName,
setUserAge,
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import {reactive, readonly} from "vue";

/**
* A custom hook that provides a reactive user object and functions to update its properties.
*
* @returns {Object} An object containing:
* - `user` {Object}: 一个响应式的只读对象
* - `setUserName` {Function}: 函数,用于修改用户名字
* - `setUserAge` {Function}: 函数用于修改用户的年龄
*/

function useUser() {
// 补全该函数
const originalUser = reactive({})
const setUserName = (name) => {
originalUser.name = name
}
const setUserAge = (age) => {
originalUser.age = age
}
const user = readonly(originalUser)
return {
user,
setUserName,
setUserAge,
}
}

const {user, setUserName, setUserAge} = useUser()
setUserName("小欧")
setUserAge(18)
console.log(user)

函数2

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import {reactive, readonly} from "vue";

/**
* @param {Object} obj 传入的对象
* @param {number} duration 防抖的时间,单位:ms
* @returns {Object}
* - `value` {readonly} 一个只读的响应式对象
* - `setValue` {Function} 一个函数,传入一个新的对象,混入源数据对象
**/
function useDebounce(obj, duration) {
//在这里补全函数
return {
value, //这里是一个只读的对象,响应式数据,默认值为参数值
setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要做防抖处理
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import {reactive, readonly} from "vue";

function useDebounce(obj, duration) {
//在这里补全函数
const originalObj = reactive(obj)
let timer = null
const setValue = (newObj) => {
clearTimeout(timer)
timer = setTimeout(() => {
Object.assign(originalObj, newObj)
}, duration)
}
const value = readonly(originalObj)

return {
value, //这里是一个只读的对象,响应式数据,默认值为参数值
setValue, // 这里是一个函数,传入一个新的对象,需要把新对象中的属性混合到原始对象中,混合操作需要做防抖处理
}
}