这是一道API题,我们可能写的自定义指令少,但是我们用的多呀,多举几个例子就行。
定义一个包含类似组件生命周期钩子的对象,钩子函数会接收指令挂钩的dom元素:
const focus = { mounted: (el) => el.focus() } export default { directives: { // enables v-focus in template focus } } <input v-focus />
<input v-focus />
v-mode
l或v-for
,同时Vue也允许用户注册自定义指令来扩展Vue能力编译后的自定义指令会被withDirective函数装饰,进一步处理生成的vnode,添加到特定属性中。
API考察,但$attrs和listeners已经移除,还是有细节可说的。
一个包含组件透传属性的对象。
An object that contains the component's fallthrough attributes.
<template> <child-component v-bind="$attrs"> 将非属性特性透传给内部的子组件 </child-component> </template>
查看透传属性foo和普通属性bar,发现vnode结构完全相同,这说明vue3中将分辨两者工作由框架完成而非用户指定:
<template> <h1>{{ msg }}</h1> <comp foo="foo" bar="bar" /> </template>
<template> <div> {{$attrs.foo}} {{bar}} </div> </template> <script setup> defineProps({ bar: String }) </script>
_createVNode(Comp, { foo: "foo", bar: "bar" })
v-once
是Vue中内置指令,很有用的API,在优化方面经常会用到,不过小伙伴们平时可能容易忽略它。
仅渲染元素和组件一次,并且跳过未来更新
Render the element and component once only, and skip future updates.
<!-- single element --> <span v-once>This will never change: {{msg}}</span> <!-- the element have children --> <div v-once> <h1>comment</h1> <p>{{msg}}</p> </div> <!-- component --> <my-component v-once :comment="msg"></my-component> <!-- `v-for` directive --> <ul> <li v-for="i in list" v-once>{{i}}</li> </ul>
v-once
是什么v-memo
v-once
是vue的内置指令,作用是仅渲染指定组件或元素一次,并跳过未来对其更新。v-once
,这样哪怕这些数据变化,vue也会跳过更新,是一种代码优化手段。v-memo
指令,可以有条件缓存部分模板并控制它们的更新,可以说控制力更强了。下面例子使用了v-once:
<script setup> import { ref } from 'vue' const msg = ref('Hello World!') </script> <template> <h1 v-once>{{ msg }}</h1> <input v-model="msg"> </template>
我们发现v-once出现后,编译器会缓存作用元素或组件,从而避免以后更新时重新计算这一部分:
// ... return (_ctx, _cache) => { return (_openBlock(), _createElementBlock(_Fragment, null, [ // 从缓存获取vnode _cache[0] || ( _setBlockTracking(-1), _cache[0] = _createElementVNode("h1", null, [ _createTextVNode(_toDisplayString(msg.value), 1 /* TEXT */) ]), _setBlockTracking(1), _cache[0] ), // ...
递归组件我们用的比较少,但是在Tree、Menu这类组件中会被用到。
组件通过组件名称引用它自己,这种情况就是递归组件。
An SFC can implicitly refer to itself via its filename.
<template> <li> <div> {{ model.name }}</div> <ul v-show="isOpen" v-if="isFolder"> <!-- 注意这里:组件递归渲染了它自己 --> <TreeItem class="item" v-for="model in model.children" :model="model"> </TreeItem> </ul> </li> <script> export default { name: 'TreeItem', // ... } </script>
name
属性,用来查找组件定义,如果使用SFC,则可以通过SFC文件名推断。组件内部通常也要有递归结束条件,比如model.children这样的判断。resolveComponent
,这样实际获取的组件就是当前组件本身。递归组件编译结果中,获取组件时会传递一个标识符 _resolveComponent("Comp", true)
const _component_Comp = _resolveComponent("Comp", true)
就是在传递maybeSelfReference
export function resolveComponent( name: string, maybeSelfReference?: boolean ): ConcreteComponent | string { return resolveAsset(COMPONENTS, name, true, maybeSelfReference) || name }
resolveAsset中最终返回的是组件自身:
if (!res && maybeSelfReference) { // fallback to implicit self-reference return Component }
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/helpers/resolveAssets.ts#L22-L23
因为异步路由的存在,我们使用异步组件的次数比较少,因此还是有必要两者的不同。
大型应用中,我们需要分割应用为更小的块,并且在需要组件时再加载它们。
In large applications, we may need to divide the app into smaller chunks and only load a component from the server when it's needed.
import { defineAsyncComponent } from 'vue' // defineAsyncComponent定义异步组件 const AsyncComp = defineAsyncComponent(() => { // 加载函数返回Promise return new Promise((resolve, reject) => { // ...可以从服务器加载组件 resolve(/* loaded component */) }) }) // 借助打包工具实现ES模块动态导入 const AsyncComp = defineAsyncComponent(() => import('./components/MyComponent.vue') )
defineAsyncComponent定义了一个高阶组件,返回一个包装组件。包装组件根据加载器的状态决定渲染什么内容。
https://github1s.com/vuejs/core/blob/HEAD/packages/runtime-core/src/apiAsyncComponent.ts#L43-L44