Vue3官网-高级指南(十七)响应式计算computed
和侦听watchEffect
(onTrack、onTrigger、onInvalidate、副作用的刷新时机、watch
、pre)、渲染机制和优化、Vue 2 中的更改检测警告
文章目录
- Vue3官网-高级指南(十七)响应式计算`computed`和侦听`watchEffect`(onTrack、onTrigger、onInvalidate、副作用的刷新时机、`watch` 、pre)、渲染机制和优化、Vue 2 中的更改检测警告
- 1. 响应式计算和侦听
- 计算值
- 调试 Computed 3.2+
- `watchEffect`
- 停止侦听
- 清除副作用
- 副作用刷新时机
- 侦听器调试
- `watch`(pre前缀)
- 侦听单个数据源
- 侦听多个数据源
- 侦听响应式对象
- 与 `watchEffect` 共享的行为
- 2. 渲染机制和优化
- 虚拟 DOM
- 3. Vue 2 中的更改检测警告
- 对于对象
- 对于数组
- 声明响应式 property
- 异步更新队列
总结:
补充
Vue (读音 /vjuː/,类似于 view) 是一套用于构建用户界面的渐进式框架。与其它大型框架不同的是,Vue 被设计为可以自底向上逐层应用。Vue 的核心库只关注视图层,不仅易于上手,还便于与第三方库或既有项目整合。
truthy(真值):在 JavaScript 中,truthy(真值)指的是在布尔值上下文中,转换后的值为真的值。所有值都是真值,除非它们被定义为 假值(即除
false
、0
、""
、null
、undefined
和NaN
以外皆为真值)。括号内都是假值falsy。.prevent 修饰符告诉 v-on 指令对于触发的事件调用 event.preventDefault()
==Event.preventDefault方法取消浏览器对当前事件的默认行为。==比如点击链接后,浏览器默认会跳转到另一个页面,使用这个方法以后,就不会跳转了;再比如,按一下空格键,页面向下滚动一段距离,使用这个方法以后也不会滚动了。该方法生效的前提是,事件对象的cancelable属性为true,如果为false,调用该方法没有任何效果。
vue:用组件(app.component)构建一个模板(template),并反复使用模板
父组件、子组件
const app = Vue.createApp({ components: { 'component-a': ComponentA, 'component-b': ComponentB } })
上面代码中app为父组件,ComponentA和ComponentB为子组件
context.emit
- 父组件通过
:data="data"
传递数据- 父组件通过
@fun="fun"
传递方法- 子组件通过
props
接收父组件传递的值- 子组件通过
$emit
调用父组件的方法并传递数据Vue中美元$符号的意思
- 除了数据属性,Vue 实例还暴露了一些有用的实例属性与方法。它们都有前缀
$
,以便与用户定义的属性区分开来。- vue中$refs的作用?
- 当我们需要获取一个dom元素进行操作的时候 可以利用原生js的getElementByXxx 而在vue中可以设置refs来获取这个dom元素
第三方网站
- greensock API(GSAP):是一套用于所有主流浏览器中制作高性能html5动画的工具。他们有一个很棒的 ease visualizer,帮助你建立精心制作的画架。
- GSAP的全名是==GreenSock Animation Platform,这个名字是有些怪异(官网还在各种安利你加入它的Club),但它的确是一个从flash时代一直发展到今天的专业动画库。==参照CSDN:https://blog.csdn.net/weixin_39939012/article/details/80833073
- animate.css :集成第三方 CSS 动画库
- CSS Triggers :来查看哪些属性会在动画时触发重绘。这个网站是用来告诉开发者不同内核浏览器对css属性修改的
重绘
/回流
情况,开发者知道了这些细节可以提高页面性能。JavaScript钩子
- 添加
:css="false"
,也会让 Vue 会跳过 CSS 的检测,除了性能略高之外,这可以避免过渡过程中 CSS 规则的影响。解构(Destructuring)
- ES6允许按照一定模式从数组和对象中提取值,然后对变量进行复制,这被称为解构(Destructuring)
API参考
- https://v3.cn.vuejs.org/api/refs-api.html#ref
自定义指令
- 注意,在 Vue 中,代码复用和抽象的主要形式是组件。然而,有的情况下,你仍然需要对普通 DOM 元素进行底层操作,这时候就会用到自定义指令。
渲染函数
- Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
响应式计算和侦听
响应式计算computed
computed
可接受一个带有onTrack
和onTrigger
选项的对象作为第二个参数:
onTrack
会在某个响应式 property 或 ref 作为依赖被追踪时调用。
onTrigger
会在侦听回调被某个依赖的修改触发时调用。所有回调都会收到一个 debugger 事件,其中包含了一些依赖相关的信息。推荐在这些回调内放置一个
debugger
语句以调试依赖。const plusOne = computed(() => count.value + 1, { onTrack(e) { // 当 count.value 作为依赖被追踪时触发 debugger }, onTrigger(e) { // 当 count.value 被修改时触发 debugger } }) // 访问 plusOne,应该触发 onTrack console.log(plusOne.value) // 修改 count.value,应该触发 onTrigger count.value++
watchEffect
- 简介
- [笔记十六包含的内容]虽然 Vue 的公开 API 不包括任何直接创建副作用的方法,但它确实暴露了一个叫做
watchEffect
的函数,它的行为很像我们例子中的createEffect
函数。我们会在该指南后面的部分详细讨论这个问题。- 为了根据响应式状态自动应用和重新应用副作用,我们可以使用
watchEffect
函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。- 清除副作用
onInvalidate
- 有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个
onInvalidate
函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)- 副作用刷新时机
- 组件的
update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数(watchEffect函数)进入队列时,默认情况下,会在所有的组件update
前执行- (下面这种是与正常情况相悖的方法,才会使用flush)如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有
flush
选项的附加options
对象 (默认为'pre'
)- 从 Vue 3.2.0 开始,
watchPostEffect
和watchSyncEffect
别名也可以用来让代码意图更加明显。
watch
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
- 与 watchEffect 比较,
watch
允许我们:
- 懒执行副作用;
- 更具体地说明什么状态应该触发侦听器重新运行;
- 访问侦听状态变化前后的值。
pre
pre代表先前数据
const firstName = ref('') const lastName = ref('') watch([firstName, lastName], (newValues, prevValues) => { console.log(newValues, prevValues) }) //pre前缀表示之前的数据 firstName.value = 'John' // logs: ["John", ""] ["", ""] lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
注意多个同步更改只会触发一次侦听器。
1. 响应式计算和侦听
本节使用单文件组件语法作为代码示例
计算值
有时我们需要依赖于其他状态的状态——在 Vue 中,这是用组件计算属性处理的,以直接创建计算值,我们可以使用 computed
函数:它接受 getter 函数并为 getter 返回的值返回一个不可变的响应式 ref 对象。
const count = ref(1)
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2
plusOne.value++ // error
或者,它可以使用一个带有 get
和 set
函数的对象来创建一个可写的 ref 对象。
const count = ref(1)
const plusOne = computed({
get: () => count.value + 1,
set: val => {
count.value = val - 1
}
})
plusOne.value = 1
console.log(count.value) // 0
调试 Computed 3.2+
computed
可接受一个带有 onTrack
和 onTrigger
选项的对象作为第二个参数:
onTrack
会在某个响应式 property 或 ref 作为依赖被追踪时调用。onTrigger
会在侦听回调被某个依赖的修改触发时调用。
所有回调都会收到一个 debugger 事件,其中包含了一些依赖相关的信息。推荐在这些回调内放置一个 debugger
语句以调试依赖。
const plusOne = computed(() => count.value + 1, {
onTrack(e) {
// 当 count.value 作为依赖被追踪时触发
debugger
},
onTrigger(e) {
// 当 count.value 被修改时触发
debugger
}
})
// 访问 plusOne,应该触发 onTrack
console.log(plusOne.value)
// 修改 count.value,应该触发 onTrigger
count.value++
onTrack
和 onTrigger
仅在开发模式下生效。
watchEffect
为了根据响应式状态自动应用和重新应用副作用,我们可以使用 watchEffect
函数。它立即执行传入的一个函数,同时响应式追踪其依赖,并在其依赖变更时重新运行该函数。
const count = ref(0)
watchEffect(() => console.log(count.value))
// -> logs 0
setTimeout(() => {
count.value++
// -> logs 1
}, 100)
停止侦听
当 watchEffect
在组件的 setup() 函数或生命周期钩子被调用时,侦听器会被链接到该组件的生命周期,并在组件卸载时自动停止。
在一些情况下,也可以显式调用返回值以停止侦听:
const stop = watchEffect(() => {
/* ... */
})
// later
stop()
清除副作用
有时副作用函数会执行一些异步的副作用,这些响应需要在其失效时清除 (即完成之前状态已改变了) 。所以侦听副作用传入的函数可以接收一个 onInvalidate
函数作入参,用来注册清理失效时的回调。当以下情况发生时,这个失效回调会被触发:
- 副作用即将重新执行时
- 侦听器被停止 (如果在
setup()
或生命周期钩子函数中使用了watchEffect
,则在组件卸载时)
watchEffect(onInvalidate => {
const token = performAsyncOperation(id.value)
onInvalidate(() => {
// id has changed or watcher is stopped.
// invalidate previously pending async operation
token.cancel()
})
})
我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它,是因为返回值对于异步错误处理很重要。
在执行数据请求时,副作用函数往往是一个异步函数:
const data = ref(null)
watchEffect(async onInvalidate => {
onInvalidate(() => {
/* ... */
}) // 我们在Promise解析之前注册清除函数
data.value = await fetchData(props.id)
})
我们知道异步函数都会隐式地返回一个 Promise[async],但是清理函数onInvalidate
必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误。
副作用刷新时机
Vue 的响应性系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个“tick” 中多个状态改变导致的不必要的重复调用。在核心的具体实现中,组件的 update
函数也是一个被侦听的副作用。当一个用户定义的副作用函数(watchEffect函数)进入队列时,默认情况下,会在所有的组件 update
前执行:
<template>
<div>{{ count }}</div>
</template>
<script>
export default {
setup() {
const count = ref(0)
watchEffect(() => {
console.log(count.value)
})
return {
count
}
}
}
</script>
在这个例子中:
count
会在初始运行时同步打印出来- 更改
count
时,将在组件更新前执行副作用(watchEffect函数)。
(下面这种是与正常情况相悖的方法,才会使用flush)如果需要在组件更新(例如:当与模板引用一起)后重新运行侦听器副作用,我们可以传递带有 flush
选项的附加 options
对象 (默认为 'pre'
):
// 在组件更新后触发,这样你就可以访问更新的 DOM。
// 注意:这也将推迟副作用的初始运行,直到组件的首次渲染完成。
watchEffect(
() => {
/* ... */
},
{
flush: 'post'
}
)
flush
选项还接受 sync
,这将强制效果始终同步触发。然而,这是低效的,应该很少需要。
从 Vue 3.2.0 开始,watchPostEffect
和 watchSyncEffect
别名也可以用来让代码意图更加明显。
侦听器调试
onTrack
和 onTrigger
选项可用于调试侦听器的行为。
onTrack
将在响应式 property 或 ref 作为依赖项被追踪时被调用。onTrigger
将在依赖项变更导致副作用被触发时被调用。
这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger
语句来检查依赖关系:
watchEffect(
() => {
/* 副作用 */
},
{
onTrigger(e) {
debugger
}
}
)
onTrack
和 onTrigger
只能在开发模式下工作。
watch
(pre前缀)
watch
API 完全等同于组件侦听器 property。watch
需要侦听特定的数据源,并在回调函数中执行副作用。默认情况下,它也是惰性的,即只有当被侦听的源发生变化时才执行回调。
- 与 watchEffect 比较,
watch
允许我们:- 懒执行副作用;
- 更具体地说明什么状态应该触发侦听器重新运行;
- 访问侦听状态变化前后的值。
侦听单个数据源
侦听器数据源可以是返回值的 getter 函数,也可以直接是 ref
:
// 侦听一个 getter
const state = reactive({ count: 0 })
watch(
() => state.count,
(count, prevCount) => {
/* ... */
}
)
// 直接侦听ref
const count = ref(0)
//pre前缀表示之前的数据
watch(count, (count, prevCount) => {
/* ... */
})
侦听多个数据源
侦听器还可以使用数组同时侦听多个源:
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
//pre前缀表示之前的数据
firstName.value = 'John' // logs: ["John", ""] ["", ""]
lastName.value = 'Smith' // logs: ["John", "Smith"] ["John", ""]
尽管如此,如果你在同一个函数里同时改变这些被侦听的来源,侦听器仍只会执行一次:
setup() {
const firstName = ref('')
const lastName = ref('')
watch([firstName, lastName], (newValues, prevValues) => {
console.log(newValues, prevValues)
})
const changeValues = () => {
firstName.value = 'John'
lastName.value = 'Smith'
// 打印 ["John", "Smith"] ["", ""]
}
return { changeValues }
}
注意多个同步更改只会触发一次侦听器。
通过更改设置 flush: 'sync'
,我们可以为每个更改都强制触发侦听器,尽管这通常是不推荐的。或者,可以用 nextTick 等待侦听器在下一步改变之前运行。例如:
const changeValues = async () => {
firstName.value = 'John' // 打印 ["John", ""] ["", ""]
await nextTick()
lastName.value = 'Smith' // 打印 ["John", "Smith"] ["John", ""]
}
侦听响应式对象
使用侦听器来比较一个数组或对象的值,这些值是响应式的,要求它有一个由值构成的副本。
const numbers = reactive([1, 2, 3, 4])
watch(
() => [...numbers],
(numbers, prevNumbers) => {
console.log(numbers, prevNumbers)
}
)
numbers.push(5) // logs: [1,2,3,4,5] [1,2,3,4]
尝试检查深度嵌套对象或数组中的 property 变化时,仍然需要 deep
选项设置为 true。
const state = reactive({
id: 1,
attributes: {
name: '',
}
})
watch(
() => state,
(state, prevState) => {
console.log('not deep', state.attributes.name, prevState.attributes.name)
}
)
watch(
() => state,
(state, prevState) => {
console.log('deep', state.attributes.name, prevState.attributes.name)
},
{ deep: true }
)
state.attributes.name = 'Alex' // 日志: "deep" "Alex" "Alex"
然而,侦听一个响应式对象或数组将始终返回该对象的当前值和上一个状态值的引用。为了完全侦听深度嵌套的对象和数组,可能需要对值进行深拷贝。这可以通过诸如 lodash.cloneDeep 这样的实用工具来实现。
import _ from 'lodash'
const state = reactive({
id: 1,
attributes: {
name: '',
}
})
watch(
() => _.cloneDeep(state),
(state, prevState) => {
console.log(state.attributes.name, prevState.attributes.name)
}
)
state.attributes.name = 'Alex' // 日志: "Alex" ""
与 watchEffect
共享的行为
watch
与 watchEffect
共享停止侦听,清除副作用 (相应地 onInvalidate
会作为回调的第三个参数传入)、副作用刷新时机和侦听器调试行为。
2. 渲染机制和优化
如果你仅仅想了解如何很好的使用 Vue,那么你不必阅读此页面。但如果你好奇框架底层的渲染工作是如何进行的,那么你可以阅读本页面从而获取更多的信息。
虚拟 DOM
现在我们知道了侦听器是如何更新组件的,你可能会问这些更改最终是如何应用到 DOM 中的!也许你以前听说过虚拟 DOM,包括 Vue 在内的许多框架都使用这种范式来确保界面能够有效地反映我们在 JavaScript 中更新的更改
我们用 JavaScript 生成名为 Virtual Dom 的 DOM 副本,这样做的原因是用 JavaScript 直接操作 DOM 的计算成本很高。虽然用 JavaScript 执行更新很快,但是找到所需的 DOM 节点并用 JavaScript 更新它们的成本却很高。所以我们批量处理调用,并一次性更改 DOM。
虚拟 DOM 是轻量级的 JavaScript 对象,由渲染函数创建。它包含三个参数:元素,具有数据、prop、attr 等的对象,以及一个数组。数组是我们传递子级的地方,子级也具有所有这些参数,然后它们也可以具有子级,依此类推,直到我们构建完整的元素树为止。
如果需要更新列表项,我们可以借助前面提到的响应性在 JavaScript 中进行。我们将更改应用至 JavaScript 副本、虚拟 DOM 中,然后在它们和实际 DOM 之间执行 diff。只有这样,我们才能对已更改的内容进行更新。虚拟 DOM 允许我们对 UI 进行高效的更新!
3. Vue 2 中的更改检测警告
该页面仅适用于 Vue 2.x 及更低版本,并假定你已经阅读了响应性部分。请先阅读该部分。
由于 JavaScript 的限制,有些 Vue 无法检测的更改类型。但是,有一些方法可以规避它们以维持响应性。
对于对象
Vue 无法检测到 property 的添加或删除。由于 Vue 在实例初始化期间执行 getter/setter 转换过程,因此必须在 data
对象中存在一个 property,以便 Vue 对其进行转换并使其具有响应式。例如:
var vm = new Vue({
data: {
a: 1
}
})
// `vm.a` 现在是响应式的
vm.b = 2
// `vm.b` 不是响应式的
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。但是,可以使用 Vue.set(object, propertyName, value)
方法向嵌套对象添加响应式 property:
Vue.set(vm.someObject, 'b', 2)
你还可以使用 vm.$set
实例方法,这也是全局 Vue.set
方法的别名:
this.$set(this.someObject, 'b', 2)
有时你可能需要为已有对象赋值多个新 property,比如使用 Object.assign()
或 _.extend()
。但是,这样添加到对象上的新 property 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 property 一起创建一个新的对象。
// 而不是 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })
对于数组
Vue 不能检测以下数组的变动:
- 当你利用索引直接设置一个数组项时,例如:
vm.items[indexOfItem] = newValue
- 当你修改数组的长度时,例如:
vm.items.length = newLength
例如:
var vm = new Vue({
data: {
items: ['a', 'b', 'c']
}
})
vm.items[1] = 'x' // 不是响应式的
vm.items.length = 2 // 不是响应式的
为了解决第一种问题,以下两种方式都可以实现和 vm.items[indexOfItem] = newValue
相同的效果,同时也会触发响应性系统的状态更新:
// Vue.set
Vue.set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
你也可以使用 vm.$set
实例方法,该方法是全局方法 Vue.set
的一个别名:
vm.$set(vm.items, indexOfItem, newValue)
为了解决第二种问题,你可以使用 splice
:
vm.items.splice(newLength)
声明响应式 property
由于 Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值:
var vm = new Vue({
data: {
// 声明 message 为一个空值字符串
message: ''
},
template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'
如果你未在 data 选项中声明 message
,Vue 将警告你渲染函数正在试图访问不存在的 property。
这样的限制在背后是有其技术原因的,它消除了在依赖追踪系统中的一类边界情况,也使组件实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:data
对象就像组件的状态结构 (schema)。提前声明所有的响应式 property,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。
异步更新队列
可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个侦听器被多次触发,它只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 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.component('example', {
template: '<span>{{ message }}</span>',
data: function() {
return {
message: 'not updated'
}
},
methods: {
updateMessage: function() {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
this.$nextTick(function() {
console.log(this.$el.textContent) // => 'updated'
})
}
}
})
因为 $nextTick()
返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = 'updated'
console.log(this.$el.textContent) // => 'not updated'
await this.$nextTick()
console.log(this.$el.textContent) // => 'updated'
}
}