概述
- Vue 中 nextTick 的实现原理
- v-if 和 v-show 的区别
- Vue 中的 key 有什么作用
- 如何理解ref toRef和toRefs
- Composition API如何实现代码逻辑复用?
Vue 中 nextTick 的实现原理
在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。
// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
})
// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick()
.then(function () {
// DOM 更新了
})
2.1.0 起新增:如果没有提供回调且在支持 Promise 的环境中,则返回一个 Promise。
请注意 Vue 不自带 Promise 的 polyfill,所以如果你的目标浏览器不原生支持 Promise (IE),你得自己提供 polyfill。
更多精彩内容,请微信搜索“前端爱好者
“, 戳我 查看 。
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。
如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。
然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 Promise.then 、 MutationObserver 和 setImmediate ,如果执行环境不支持,则会采用setTimeout(fn, 0) 代替。
例如,当你设置 vm.someData = ‘new value’ ,该组件不会立即重新渲染。
当刷新队列时,组件会在下一个事件循环“tick”中更新。
多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数将在 DOM 更新完成后被调用
。
案例
例如:
<div id="example">{{message}}</div>
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
在组件内使用 vm.$nextTick() 实例方法特别方便,因为它不需要全局 Vue ,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上:
Vue.component( 'example', {
template: '<span>{{ message }}</span>',
data: function () {
return {
message: '未更新 '
}
},
methods: {
updateMessage: function () {
this.message = '已更新 '
console.log(this.$el.textContent) // => '未更新 '
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新 '
})
}
}
})
因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同 的事情:
methods: {
updateMessage: async function () {
this.message = '已更新 '
console.log(this.$el.textContent) // => '未更新 '
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新 '
}
}
总结:
nextTick 方法主要是使用了宏任务和微任务,定义了一个异步方法,多次调用 nextTick 方法会将方法存 入队列中,通过这个异步方法清空当前队列。所以这个 nextTick 方法就是异步方法。
v-if 和 v-show 的区别
v-if 是“真正” 的条件渲染,因为它会确保在切换过程中条件块内的事件监听器和子组件适当地被销毁和 重建。
- true:渲染
- false:销毁不渲染,元素就不存在了
v-if 也是惰性的:如果在初始渲染时条件为假,则什么也不做 ——直到条件第一次变为真时,才会开 始渲染条件块。
相比之下,v-show 就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 进行切换。
- true:display: block
- false: display: none
一般来说,v-if 有更高的切换开销,而 v-show 有更高的初始渲染开销。
因此,如果需要非常频繁地 切换,则使用 v-show 较好;如果在运行时条件很少改变,则使用 v-if 较好。
特别注意
在 vue2 中 v-for 的优先级更高,但是在 vue3 中优先级改变了。v-if 的优先级更高。
在 vue2 中 v-for 和 v-if 同时出现时,可放在一个标签内,如下写法:
<div v-for="item in ss" v-if="item.show" :key="item.id">
{{ item.show }}
</div>
data(){
return {
ss:[
{ id: 1, show: true, name: '1' },
{ id: 2, show: false, name: '2' },
{ id: 3, show: true, name: '3' },
]
}
}
在 vue3 中这样写会报错,就是因为 v-if 的优先级更高,所以 item.show 是未定义报错了。
Vue 中的 key 有什么作用
key 是为 Vue 中 vnode 的唯一标记
,通过这个 key,我们的 diff 操作可以更准确、更快速。
Vue 的 diff 过程可以概括为:
oldCh 和 newCh 各有两个头尾的变量 oldStartIndex 、oldEndIndex 和 newStartIndex 、newEndIndex,它们会新节点和旧节点会进行两两对比,即一共有4种比较方式:
- newStartIndex 和oldStartIndex
- newEndIndex 和 oldEndIndex
- newStartIndex 和 oldEndIndex
- newEndIndex 和 oldStartIndex,
如果以上 4 种比较都没匹配
如果设置了key,就会用 key 再进行比较,在比较的过程中,遍历会往中间靠,一旦 StartIdx > EndIdx 表明 oldCh 和 newCh 至少有一个已经遍历完了,就会结束比较。
具体有无 key 的 diff 过程,可以查看作者写的另一篇详解虚拟 DOM 的文 章《深入剖析:Vue核心之虚拟DOM》
所以 Vue 中 key 的作用是:
key 是为 Vue 中 vnode 的唯一标记,通过这个 key,我们的 diff 操作可以 更准确、更快速
更准确:因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地 复用的情况。所以会更加准确。
更快速:利用 key 的唯一性生成 map 对象来获取对应节点,比遍历方式更快,源码如下:
function createKeyToOldIdx (children, beginIdx, endIdx) {
let i, key
const map = {}
for (i = beginIdx; i <= endIdx; ++i) {
key = children[i].key
if (isDef(key)) map[key] = i
}
return map
}
如何理解ref toRef和toRefs
ref
- 生成值类型的响应式数据
- 可用于模板和reactive
- 通过.value修改值
<template>
<div>{{ ageRef }}</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const ageRef = ref(20)
setInterval(() => {
ageRef.value += 1
}, 1000)
return {
ageRef
}
},
}
</script>
toRef
- 针对一个响应式对象(reactive封装)的prop
- 创建一个ref,具有响应式
- 两者保持引用关系
<template>
<div>{{ state.age }} --- {{ ageRef }}</div>
</template>
<script>
import { toRef, reactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'JL',
age: 18
})
const ageRef = toRef(state, 'age')
setTimeout(() => {
state.age = 20
}, 1000)
setTimeout(() => {
ageRef.value = 21
}, 2000)
return {
state,
ageRef
}
},
}
</script>
使用toRef将state的age属性变成一个响应式变量,然后在1秒后将state的age值变为20,此时ageRef也会变成20;在2秒后将ageRef的值变为21,此时state的age值也会变成21,说明了两者保持相互引用关系
toRef针对的是响应式,针对的不是普通对象,如果用于非响应式,产出的结果不具有响应式
toRefs,避免模板中导出都是state
- 将响应式对象(reactive封装)转换成普通对象
- 对象的每个prop都是对应的ref
- 两者保持引用关系
<template>
<div>{{ name }}---{{ age }}</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
const state = reactive({
name: 'JL',
age: 18
})
const stateRefs = toRefs(state)
setTimeout(() => {
state.age = 20
}, 1000)
setTimeout(() => {
stateRefs.age.value = 21
}, 2000)
return stateRefs
},
}
</script>
使用toRefs将state转变成一个普通对象,这时候就可以直接返回stateRefs,这时候在template就可以直接调用name和age。
最佳使用方式
- 用reactive做对象的响应式,用ref做值类型的响应式
- setup中返回toRefs(state),或者toRef(state, ‘prop’)
- ref的变量命名都用xxxRef
- 合成函数返回响应式对象时,用toRefs
<template>
<div>x:{{x}} y:{{y}}</div>
</template>
<script>
import { reactive, toRefs } from 'vue'
export default {
setup() {
function test() {
const state = reactive({
x: 1,
y: 2
})
return toRefs(state)
}
const {x, y} = test()
setTimeout(() => {
x.value = 2
}, 1000)
return {
x,
y
}
}
}
</script>
上面的代码,test函数中定义了响应式对象state,并通过toRefs将其转为普通对象并返回,这时候可以结构赋值,并且值是响应式的
Composition API如何实现代码逻辑复用?
- 抽离逻辑代码到一个函数
- 函数命名约定为useXXX格式(React Hooks也是)
- 在setup中引用useXXX函数
案例:获取当前鼠标位置的方法
第一种,直接使用ref定义的useMousePosition–这种方式,导出和导入都可以随意解构
// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue'
// 1. 定义一个函数,抽离逻辑,命名使用 useXXX
function useMousePosition() {
// 使用ref定义
const x = ref(0)
const y = ref(0)
function update(e) {
console.log(x.value, y.value);
x.value = e.pageX
y.value = e.pageY
}
onMounted(() => {
console.log('开始监听鼠标划动事件');
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('解除监听鼠标划动事件');
window.removeEventListener('mousemove', update)
})
return {
x,
y
}
}
// 导出这个函数
export default useMousePosition
调用
<!-- 在任意一个组件,都可以调用这个方法 -->
<template>
<p>mouse position: {{x}}, {{y}}</p>
</template>
<script>
import useMousePosition from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
// useMousePosition是使用ref定义变量的,这种可以解构
const { x, y } = useMousePosition()
console.log(x, y)
return {
x, y
}
}
}
</script>
第二种,使用reactive定义鼠标坐标对象 这种导出的方式,在组件中导入时是不能解构的
// useMousePosition.js'
import { onMounted, onUnmounted, reactive } from 'vue'
export function useMousePosition2() {
// 使用reactive定义
const mouse = reactive({
x: 0,
y: 0
})
function update(e) {
mouse.x = e.pageX
mouse.y = e.pageY
}
onMounted(() => {
console.log('开始监听鼠标划动事件');
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('解除监听鼠标划动事件');
window.removeEventListener('mousemove', update)
})
return {
mouse
}
}
<template>
<!-- 使用对象方式显示信息 -->
<p>mouse position2: {{mouse.x}}, {{mouse.y}}</p>
</template>
<script>
import { useMousePosition2 } from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
// useMousePosition2是使用reactive定义的,这种不可以解构
const { mouse } = useMousePosition2()
return {
mouse
}
}
}
</script>
第三种,使用toRefs 使用这种方式,可以将reactive对象,解构为ref对象
export function useMousePosition3() {
// 使用reactive定义
const mouse = reactive({
x: 0,
y: 0
})
function update(e) {
mouse.x = e.pageX
mouse.y = e.pageY
}
onMounted(() => {
console.log('开始监听鼠标划动事件');
window.addEventListener('mousemove', update)
})
onUnmounted(() => {
console.log('解除监听鼠标划动事件');
window.removeEventListener('mousemove', update)
})
// 这里,使用toRefs解构成ref对象
return toRefs(mouse)
}
调用
<template>
<p>mouse position: {{x}}, {{y}}</p>
</template>
<script>
import { useMousePosition3 } from './useMousePosition'
export default {
name: 'MousePosition',
setup() {
// 使用reactive定义鼠标坐标对象,然后通过toRefs将其解构成ref对象
const { x, y } = useMousePosition()
console.log(x, y)
return {
x, y
}
}
}
</script>
三种方式都可以实现,但是我们一般使用时,都会返回ref对象,所以比较建议使用第一种和第三种,尽量不使用第二种
参考地址:
- https://tangjiusheng.com/web/4935.html
- https://blog.csdn.net/weixin_41759744/article/details/125305021
- https://www.qetool.com/scripts/view/27412.html