markRaw注意点 markRaw和 shallowXXX 一族的 API允许选择性的覆盖reactive或者readonly 默认创建的 "深层的" 特性【响应式】/或者使用无代理的普通对象 设计这种「浅层读取」有很多原因 一些值的实际上的用法非常简单,并没有必要转为响应式【例如三方库的实例/省市区json/Vue组件对象】 当渲染一个元素数量庞大,但是数据是不可变的,跳过 Proxy 的转换可以带来性能提升 这些 API 被认为是高级的,是因为这种特性仅停留在根级别,所以如果你将一个嵌套的,没有 markRaw 的对象设置为 reactive 对象的属性,在重新访问时,你又会得到一个 Proxy 的版本,在使用中最终会导致标识混淆的严重问题:执行某个操作同时依赖于某个对象的原始版本和代理版本(标识混淆在一般使用当中应该是非常罕见的,但是要想完全避免这样的问题,必须要对整个响应式系统的工作原理有一个相当清晰的认知)。 const foo = markRaw({ nested: {}, }) const bar = reactive({ // 尽管 `foo` 己经被标记为 raw 了, 但 foo.nested 并没有 nested: foo.nested, }) console.log(foo.nested === bar.nested) // false
foo.nested没有被标记为(永远不会转为响应式代理),导致最后的值一个reactive shallowReactive 只为某个对象的私有(第一层)属性创建浅层的响应式代理,不会对“属性的属性”做深层次、递归地响应式代理,而只是保留原样【第一层是响应式代理,深层次只保留原样(不具备响应式代理)】 const state = shallowReactive({ foo: 1, nested: { bar: 2, }, }) // 变更 state 的自有属性是响应式的【第一层次响应式】 state.foo++ // ...但不会深层代理【深层次不是响应式】(渲染性能) isReactive(state.nested) // false state.nested.bar++ // 非响应式
shallowReadonly 类似于shallowReactive,区别是: 第一层将会是响应式代理【第一层修改属性会失败】,属性为响应式 深层次的对象属性可以修改,属性不是响应式 const state = shallowReadonly({ foo: 1, nested: { bar: 2, }, }) // 变更 state 的自有属性会失败 state.foo++ // ...但是嵌套的对象是可以变更的 isReadonly(state.nested) // false state.nested.bar++ // 嵌套属性依然可修改
ref 响应式系统工具集 unref unref是val = isRef(val) ? val.value : val 的语法糖 unref(ref(0))===unref(0)===0 返回number function useFoo(x: number | Ref<number>) { const unwrapped = unref(x) // unwrapped 一定是 number 类型 }
toRef toRef 可以用来为一个 reactive 对象的属性【某个属性区别toRefs每一个属性】创建一个 ref。这个 ref 可以被传递并且能够保持响应性 const state = reactive({ foo: 1, bar: 2, }) //reactive获取单个属性转为ref【fooRef只是一个代理】 const fooRef = toRef(state, 'foo') fooRef.value++ console.log(state.foo) // 2 state.foo++ console.log(fooRef.value) // 3
toRefs
把一个响应式对象转换成普通对象,该普通对象的每个 property 都是一个 ref ,和响应式对象 property 一一对应 const state = reactive({ foo: 1, bar: 2, }) const stateAsRefs = toRefs(state) /* stateAsRefs 的类型如下: { foo: Ref<number>, bar: Ref<number> } */ // ref 对象 与 原属性的引用是 "链接" 上的 state.foo++ console.log(stateAsRefs.foo) // 2 stateAsRefs.foo.value++ console.log(state.foo) // 3
可以通过toRefs返回可解构的reactive,因为toRefs包裹之后返回一一对应的ref属性 function useFeatureX() { const state = reactive({ foo: 1, bar: 2, }) // 对 state 的逻辑操作 // 返回时将属性都转为 ref return toRefs(state) } export default { setup() { // 可以解构,不会丢失响应性 const { foo, bar } = useFeatureX() return { foo, bar, } }, }
isRef
检查一个值是否为一个 ref 对象 ref 高级响应式系统API customRef 用于自定义一个 ref,可以显式地控制依赖追踪和触发响应,接受一个工厂函数,两个参数分别是用于追踪的 track 与用于触发响应的 trigger,并返回一个一个带有 get 和 set 属性的对象【实际上就是手动 track追踪 和 trigger触发响应】 以下代码可以使得v-model防抖 function useDebouncedRef(value, delay = 200) { let timeout return customRef((track, trigger) => { return { get() { /*初始化手动追踪依赖讲究什么时候去触发依赖收集*/ track() return value }, set(newValue) { /*修改数据的时候会把上一次的定时器清除【防抖】*/ clearTimeout(timeout) timeout = setTimeout(() => { /*把新设置的数据给到ref数据源*/ value = newValue /*再有依赖追踪的前提下触发响应式*/ trigger() }, delay) }, } }) } setup() { return { /*暴露返回的数据加防抖*/ text: useDebouncedRef('hello'), } }
shallowRef
创建一个 ref ,将会追踪它的 .value 更改操作,但是并不会对变更后的 .value 做响应式代理转换(即变更不会调用 reactive) 前面我们说过如果传入 ref 的是一个对象,将调用 reactive 方法进行深层响应转换,通过shallowRef创建的ref,将不会调用reactive【对象不会是响应式的】 const refOne = shallowRef({}); refOne.value = { id: 1 }; refOne.id == 20; console.log(isReactive(refOne.value),refOne.value);//false { id: 1 }
triggerRef 【与shallowRef配合】 手动执行与shallowRef相关的任何效果 const shallow = shallowRef({ greet: 'Hello, world' }) // 第一次运行打印 "Hello, world" watchEffect(() => { console.log(shallow.value.greet) }) // 这不会触发效果,因为ref是shallow shallow.value.greet = 'Hello, universe' // 打印 "Hello, universe" triggerRef(shallow)
Computed and watch【监控变化】
computed 传入一个 getter 函数,返回一个默认不可手动修改的 ref 对象【默认传入的是get函数的对象】 传入一个拥有 get 和 set 函数的对象,创建一个可手动修改的计算状态 const count = ref(1) /*不支持修改【只读的】 */ const plusOne = computed(() => count.value + 1) plusOne.value++ // 错误! /*【可更改的】 */ const plusOne = computed({ get: () => count.value + 1, set: (val) => { count.value = val - 1 }, }) plusOne.value = 1 console.log(count.value) // 0
watchEffect
立即执行传入的一个函数,并响应式追踪其依赖,并在其依赖变更时重新运行该函数 computed与watchEffect区别: computed计算属性可通过setup return,再模板中使用,watchEffect不能; computed可以使用多个,并且对多个属性进行不同的响应计算,watchEffect会存在副作用 const count = ref(0) watchEffect(() => console.log(count.value)) // -> 打印出 0 setTimeout(() => { count.value++ // -> 打印出 1 }, 100)
停止观察
当在组件的setup()函数或生命周期钩子期间调用watchEffect时,监视程序会链接到组件的生命周期,并在卸载组件时自动停止 一般情况下watchEffect返回可以stop 操作,停止监听程序 const stop = watchEffect(() => { /* ... */ }) // 停止监听程序 stop()
副作用(函数式编程)
一个带有副作用的函数不仅只是简单的返回一个值,还干了一些其他的事情,比如: 修改一个变量 直接修改数据结构 设置一个对象的成员 抛出一个异常或以一个错误终止 打印到终端或读取用户的输入 读取或写入一个文件 在屏幕上绘画 buyCoffee的例子(p3):函数只不过是需要返回一杯咖啡,可是却对费用进行了持久化操作(产生副作用),我们可以在buyCoffee方法返回咖啡时也把费用作为值一并返回,将费用这条记录交给其他程序来做持久化,以此来去除副作用 ====》通过把这些副作用推到程序的外层,来转换任何带有副作用的函数(纯的内核和一层很薄的外围来处理副作用) 如果一个函数内外有依赖于外部变量或者环境时,常常我们称之为其有副作用,如果我们仅通过函数签名不打开内部代码检查并不能知道该函数在干什么,作为一个独立函数我们期望有明确的输入和输出,副作用是bug的发源地,作为程序员开发者应尽量少的开发有副作用的函数或方法,副作用也使得方法通用性下降不适合扩展和可重用性 清除副作用 [^]: watchEffect函数都是副作用 在一些时候监听函数将执行异步副作用【一个响应式依赖被修改了,会做其他事情】,这些响应需要在其失效时清除(例如在效果完成前状态改变)。effect函数接收一个onInvalidate 函数作入参, 用来注册清理失效时的回调。这个 invalidation函数 在什么时候会被调用: 监听函数重新被执行的时候【响应式依赖的数据被修改】 监听停止的时候(如果watchEffect在setup()或者生命周期函数中被使用的时候组件会被卸载)【停止观察】 watchEffect(onInvalidate => { /*这是个异步操作*/ const token = performAsyncOperation(id.value)//id依赖 onInvalidate(() => { // id被修改了或者监听停止了会触发token.cancel()事件【这块区域的代码】. // 这里是异步事件的话,前面的peding的异步操作无效【这里的异步事件只执行一次】 token.cancel()/*异步操作*/ console.log('onInvalidate') }) })
从上面看:我们之所以是通过传入一个函数去注册失效回调,而不是从回调返回它(如 React useEffect
中的方式),是因为返回值对于异步错误处理很重要
const data = ref(null) watchEffect(async onInvalidate => { onInvalidate(() => {...}) // 我们在Promise的resolves之前注册清理函数(cleanup function) data.value = await fetchData(props.id) })
我们知道异步函数都会隐式地返回一个 Promise,但是清理副作用的函数必须要在 Promise 被 resolve 之前被注册。另外,Vue 依赖这个返回的 Promise 来自动处理 Promise 链上的潜在错误 副作用刷新时机 Vue 的响应式系统会缓存副作用函数,并异步地刷新它们,这样可以避免同一个 tick 中多个状态改变导致的不必要的重复调用。在核心的具体实现中, 组件的更新函数也是一个被侦听的副作用。当一个用户定义的副作用函数进入队列时, 会在所有的组件更新后执行 <template> <div>{{ count }}</div> </template> <script> export default { setup() { const count = ref(0) watchEffect(() => { console.log(count.value) }) return { count, } }, } </script>
count 会在初始运行时同步打印出来 更改 count 时,将在组件更新后执行副作用 初始化运行是在组件 mounted 之前执行的【你希望在编写副作用函数时访问 DOM(或模板 ref),请在 onMounted 钩子中进行】 onMounted(() => { watchEffect(() => { // 在这里可以访问到 DOM 或者 template refs }) })
如果副作用需要同步或在组件更新之前重新运行,我们可以传递一个拥有 flush 属性的对象作为选项(默认为 'post') // 同步运行 watchEffect( () => { /* ... */ }, { flush: 'sync', } ) // 组件更新前执行 watchEffect( () => { /* ... */ }, { flush: 'pre', } )
侦听器调试【响应式调试用的】
onTrack 和 onTrigger 选项可用于调试一个侦听器的行为。 当一个 reactive 对象属性或一个 ref 作为依赖被追踪时,将调用 onTrack【调用次数为被追踪的数量】 依赖项变更会导致重新追踪依赖,从而onTrack被调用【调用次数为被追踪的数量】 依赖项变更导致副作用被触发时,将调用 onTrigger 这两个回调都将接收到一个包含有关所依赖项信息的调试器事件。建议在以下回调中编写 debugger 语句来检查依赖关系:【onTrack 和 onTrigger 仅在开发模式下生效】 watchEffect( () => { /* 副作用的内容 */ }, { onTrigger(e) { /*副作用依赖修改*/ debugger }, onTrack(e) { /*副作用依赖修改*/ debugger }, } )
watch
watch API 完全等效于 2.x watch 中相应的选项。watch 需要侦听特定的数据源,并在回调函数中执行副作用【默认情况是懒执行的,也就是说仅在侦听的源变更时才执行回调】 watch允许我们: 懒执行副作用 更明确哪些状态的改变会触发侦听器重新运行副作用 访问侦听状态变化前后的值 侦听单个数据源 侦听器的数据源可以是一个拥有返回值的 getter 函数,也可以是 ref: // 侦听一个 getter const state = reactive({ count: 0 }) watch( () => state.count, (count, prevCount) => { /* ... */ } ) // 直接侦听一个 ref const count = ref(0) watch(count, (count, prevCount) => { /* ... */ })
侦听多个数据源
watcher 也可以使用数组来同时侦听多个源 watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar]) => { /* ... */ })
与 watchEffect 共享的行为
watch 和 watchEffect 在停止侦听, 清除副作用 (相应地 onInvalidate 会作为回调的第三个参数传入),副作用刷新时机 和 侦听器调试 等方面行为一致 watch([fooRef, barRef], ([foo, bar], [prevFoo, prevBar],onInvalidate) => { /* ... */ onInvalidate(() => {...}) }, { onTrigger(e) { /*副作用依赖修改*/ debugger }, onTrack(e) { /*副作用依赖修改*/ debugger }, })
生命周期钩子函数
与 2.x 版本生命周期相对应的组合式 API beforeCreate -> 使用 setup() created -> 使用 setup() beforeMount -> onBeforeMount mounted -> onMounted beforeUpdate -> onBeforeUpdate updated -> onUpdated beforeDestroy -> onBeforeUnmount destroyed -> onUnmounted errorCaptured -> onErrorCaptured import { onMounted, onUpdated, onUnmounted } from 'vue' setup() { onMounted(() => { console.log('mounted!') }) onUpdated(() => { console.log('updated!') }) onUnmounted(() => { console.log('unmounted!') }) }
这些生命周期钩子注册函数只能在 setup() 期间同步使用, 因为它们依赖于内部的全局状态来定位当前组件实例(正在调用 setup() 的组件实例), 不在当前组件下调用这些函数会抛出一个错误。 组件实例上下文也是在生命周期钩子同步执行期间设置的,因此,在卸载组件时,在生命周期钩子内部同步创建的侦听器和计算状态也将自动删除。 新增的钩子函数 除了和 2.x 生命周期等效项之外,组合式 API 还提供了以下调试钩子函数: onRenderTracked onRenderTriggered 两个钩子函数都接收一个 DebuggerEvent,与 watchEffect 参数选项中的 onTrack 和 onTrigger 类似: export default { onRenderTracked(e){ debugger // 检查有响应和追踪的依赖性 }, onRenderTriggered(e) { debugger // 检查哪个依赖性导致组件重新渲染 }, }
Vue提供的内置组件
component 与Vue2.x一致 渲染一个“元组件”为动态组件。依 is 的值,来决定哪个组件被渲染。 <!-- 动态组件由 vm 实例的 `componentId` property 控制 --> <component :is="componentId"></component> <!-- 也能够渲染注册过的组件或 prop 传入的组件 --> <component :is="$options.components.child"></component>
transition 与 Vue2.x 【基本】 一致有差异
Props新增: persisted - boolean 如果为true,则表示这是一个转换,实际上不会插入/删除元素,而是切换显示/隐藏状态。 transition 过渡挂钩被注入,但会被渲染器跳过。 相反,自定义指令可以通过调用注入的钩子(例如v-show)来控制过渡 enter-class----->enter-from-class leave-class----->leave-from-class 事件 before-appear transition-group 与 Vue2.x 一致 slot 与 Vue2.x 一致 teleport 【新增组件】 Props to - string 必填属性,必须是一个有效的query选择器,或者是元素(如果在浏览器环境中使用)。中的内容将会被放置到指定的目标元素中 <!-- 正确的 --> <teleport to="#some-id" /> <teleport to=".some-class" /> /*元素*/ <teleport to="[data-teleport]" /> <!-- 错误的 -->
<teleport to="h1" /> <teleport to="some-string" /> disabled - boolean 这是一个可选项 ,做一个是可以用来禁用的功能,这意味着它的插槽内容不会移动到任何地方,而是按没有teleport组件一般来呈现【默认为false】 <teleport to="#popup" :disabled="displayVideoInline"> <h1>999999</h1> </teleport>
注意,这将移动实际的DOM节点,而不是销毁和重新创建,并且还将保持任何组件实例是活动的。所有有状态HTML元素(比如一个正在播放的视频)将保持它们的状态。【控制displayVideoInline并不是销毁重建,它保持实例是存在的,不会被注销】
关于Teleport 其他内容 Vue鼓励我们通过将UI和相关行为封装到组件中来构建UI。我们可以将它们彼此嵌套在一起,以构建构成应用程序UI的树 但是,有时组件模板的一部分逻辑上属于这个组件,而从技术角度来看,最好将这一部分模板移到DOM中的其他地方,放到Vue应用程序之外 一个常见的场景是创建一个包含全屏模态的组件。在大多数情况下,您希望模态的逻辑驻留在组件中,但是模态框的定位问题很快就很难通过CSS解决,或者需要更改组件的组成 考虑下面的HTML结构: <body> <div style="position: relative;"> <h3>Tooltips with Vue 3 Teleport</h3> <div> <modal-button></modal-button> </div> </div> </body>
让我们看看 mode -button 该组件将有一个button元素来触发模态的打开,还有一个div元素,其类为.modal,它将包含模态的内容和一个自关闭按钮 const app = Vue.createApp({}); app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! </button> <div v-if="modalOpen" class="modal"> <div> I'm a modal! <button @click="modalOpen = false"> Close </button> </div> </div> `, data() { return { modalOpen: false } } })
让我们修改我们的modal-button来使用并告诉Vue "teleport this HTML to the "body"标签"。 app.component('modal-button', { template: ` <button @click="modalOpen = true"> Open full screen modal! (With teleport!) </button> <teleport to="body"> <div v-if="modalOpen" class="modal"> <div> I'm a teleported modal! (My parent is "body") <button @click="modalOpen = false"> Close </button> </div> </div> </teleport> `, data() { return { modalOpen: false } } })