之前我就在想,为什么很多人遇到问题都是推荐说:看文档
为什么?
因为文档真的很有用(真香)
这篇文章是个人对于文档中出现内容的理解,在断断续续的一周时间内看一次文档后,compisition API真的太香了,ts真的太香了!

全局API

createApp

在通过脚手架创建项目的时候可以在main.js中看到这样一行代码:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'createApp(App).mount('#app')

使用这个函数可以提供一个上下文应用实例,应用实例挂载的整个组件树共享同一个上下文。

这也就意味着,我们可以在创建实例的时候设置一个根prop,他的所有子组件都可以通过props的方法获取到这个值。他的第一个参数接收一个根组件选项对象options作为第一个参数,使用第二个参数,可以将根prop传递给应用程序,例如:

// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'createApp(App,{username:"黑猫几绛"}).mount('#app')
// App.vue
<template><div id="app"><defineComp/>{{username}}</div>
</template><script>
import defineComp from './components/defineComp.vue'
export default {components: { defineComp },name: 'App',props:{username:{type: String,default: 'neko_zzz'}}
}
</script>

最终在页面上的显示结果为 黑猫几绛

更正

这个第二个参数似乎只能将props传递给根组件使用,对于他的深层次子组件来说,是看不到props的。

h

返回一个”虚拟节点“,通常缩写为 VNode:一个普通对象,其中包含向 Vue 描述它应在页面上渲染哪种节点的信息,包括所有子节点的描述。它的目的是用于手动编写的渲染函数:

render() {return h('h1', {}, 'Some title')
}

h是用于创建VNode的实用程序,仅作为createVNode函数的缩写,而render只是暴露给是开发者去使用createVNode的钩子。

render 函数的优先级高于根据 template 选项或挂载元素的 DOM 内 HTML 模板编译的渲染函数。

注意!如果 Vue 选项中包含渲染函数,模板template将被忽略!

属性

接收三个参数:typepropschildren

type
  • 类型:String | Object | Function

  • 详细:

    HTML 标签名、组件、异步组件或函数式组件。使用返回 null 的函数将渲染一个注释。此参数是必需的。

props
  • 类型:Object

  • 详细:

    一个对象,与我们将在模板中使用的 attribute、prop 和事件相对应。可选。

children
  • 类型:String | Array | Object

  • 详细:

    子代 VNode,使用 h() 生成,或者使用字符串来获取“文本 VNode”,或带有插槽的对象。可选。

    h('div', {}, ['Some text comes first.',h('h1', 'A headline'),h(MyComponent, {someProp: 'foobar'})
    ])
    

相关知识

这一部分的知识点在可复用&组合 -> 渲染函数,关于h函数的API详情介绍放在这里来讲。

可复用&组合-渲染函数

Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。

创建标签VNode

比如说我现在需要实现一个可以通过传入数字,来控制标题大小的组件:

import { createApp, h } from 'vue'
import App from './App.vue'
import './index.css'
const app = createApp(App)// 注册全局组件
app.component('word-level',{render(){return h(// 这里放的是标签的名称,还可以为组件名/异步组件名'h' + this.level,// 这里放的是为标签添加的样式等信息,比如class attribute{},// 通过default()获取到所有传入到默认插槽中的数据// 如果是具名插槽的话,比如在父组件中使用<template #header>// 那么具体获取到header部分插槽内容采用this.$slots.header()this.$slots.default()    )},props:{level:{type: Number,default: 5}}
})app.mount('#app')
<template><div id="app"><word-level :level="1">111</word-level><word-level :level="2">222</word-level><word-level :level="3">333</word-level></div>
</template><script>
export default {name: 'App'
}
</script>

最后可以成功的在页面中获取到渲染结果:

在前面的渲染函数中,我们在render中返回了h函数的结果,这个结果返回的其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode

需要注意,VNodes 必须唯一,组件树中的所有VNode必须是唯一的,也就意味着,下面的渲染函数是不合法的:

render() {const myParagraphVNode = h('p', 'hi')return h('div', [// 错误 - 重复的 Vnode!// 这里第二个参数为子代VNode,可以省略掉原本应出现在这里的attribute,不过最好还是设置一个null表示占位myParagraphVNode, myParagraphVNode])
}

如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:

render() {return h('div',Array.from({ length: 20 }).map(() => {return h('p', 'hi')}))
}
创建组件VNode

要为某个组件创建一个 VNode,传递给 h 的第一个参数应该是组件本身。

render() {return h(ButtonCounter)
}

如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent

const { h, resolveComponent } = Vue// ...render() {const ButtonCounter = resolveComponent('ButtonCounter')return h(ButtonCounter)
}
使用 JavaScript 代替模板功能

这一部分的内容等到要用的时候看文档就行,知道有这个功能。

简单来说就是通过原生js去把vue提供的模板功能都替换了,毕竟只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。

https://v3.cn.vuejs.org/guide/render-function.html#%E5%88%9B%E5%BB%BA%E7%BB%84%E4%BB%B6-vnode

顺路复习一下作用域插槽:

函数式组件

Vue提供了一种称为函数式组件的组件类型,用来定义那些没有响应数据,也不需要有任何生命周期的场景,不过Vue 3.x 对有状态组件的性能进行了提升,与函数式组件的性能相差无几。所以,建议只使用有状态组件。

  • 在 Vue 3.x 中,所有的函数式组件都是由普通函数创建的。

  • export default导出的是一个函数,函数有两个参数:

    • props(因为函数式组件里没有 this 引用,Vue 会把 props 当作第一个参数传入)
    • context(上下文):context是一个对象,包含attrs、slot、emit属性
import { h } from 'vue'const DynamicHeading = (props, context) => {return h(`h${props.level}`, context.attrs, context.slots)
}DynamicHeading.props = ['level']export default DynamicHeading

defineComponent

其实这个函数主要是为tstsx的类型推导服务的,如果说是使用js来写项目,直接用默认的export default {}就好,例如:

<script>
export default {name: 'App'
}
</script>

如果是使用ts的项目,defineComponent最为重要的作用是给予了组件正确的参数类型推断。

首先看看文档中是怎么说的:

这个API提供了一个参数,参数的值可以为具有组件选项的对象,或者是一个setup函数,函数名称将作为组件名称来使用。

单独看这一句话可能不方便理解,所以借助一下源码来看,它只是对setup函数的一种封装,并返回options对象。如果我们在调用函数时提供了详细的组件选项,那么返回一个options对象;如果仅仅为了简写提供一个setup函数,那么返回一个只封装了setup函数的对象。

export function defineComponent(options: unknown){return isFunction(options) ? { setup: options } : options
}

通过一个具体的变量,去接收defineComponent封装后传递回来的options组件选项,在这个封装中可以为数据/函数设置具体的类型。

<script lang="ts">
import { defineComponent, PropType, ref, computed, reactive } from 'vue'// 声明reactive的时候推荐使用接口
interface Student{name: string,age: number
}export default defineComponent({name: 'defineComp',props: {callback: {type: Function as PropType<()=>void>},success: {type: Boolean,default: true}},setup(){// 其实如果不给定ref具体的类型,vue3也可以根据初始值来进行类型推导// 不过指定复杂数据类型的时候可以手动传递一个泛型const month = ref<string| number>(9)const nextMonth = computed((): string => {return ('' + month.value + 1)})// 有以下三种方法可以定义一个reactive变量const st1: Student = reactive({name:'黑猫几绛',age:20})const st2 = reactive<Student>({name:'黑猫几绛',age:20})const st3 = reactive({name:'黑猫几绛',age:20}) as Studentreturn{month,nextMonth,st1,st2,st3}}
})
</script>

而所谓的设置一个setup函数表示,当组件中只有setup方法的话,可以进行的一种简写,例如:

<script lang="ts">
import { defineComponent, ref} from 'vue'
const defineComp = defineComponent(()=>{const month = ref<string| number>(9)return{month}
})
</script>

defineAsyncComponent

可以创建一个只有在需要时才会加载的异步组件。

在大型应用中,我们可能需要将应用分割成为小一些的代码块,并且只在需要的时候才从服务器加载一个模块。

在Vue3.x中,对异步组件的使用跟Vue2.x不同的主要变化有三点:

  1. Vue3.x新增了这个辅助函数来显示声明异步组件
  2. 异步组件高级声明方法中的component选项更名为loader
  3. loader绑定的组件加载函数不再接受resolvereject函数,而且必须返回一个promise

在 Vue 2.x 中,声明一个异步组件只需这样:

const asyncPage = () => import('./v2AsyncComp.vue')

但是,到了 Vue 3.x 上面的用法就不适用了,因为此时如果想要调用异步组件,需要使用defineAsyncComponent辅助函数进行包裹:

Now, in Vue 3, since functional components are defined as pure functions, async components definitions need to be explicitly defined by wrapping it in a new defineAsyncComponent helper.

对于高阶用法,defineAsyncComponent 可以接受一个对象:

defineAsyncComponent 方法还可以返回以下格式的对象:

import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent({// 工厂函数loader: () => import('./Foo.vue'),// 加载异步组件时要使用的组件loadingComponent: LoadingComponent,// 加载失败时要使用的组件errorComponent: ErrorComponent,// 在显示 loadingComponent 之前的延迟 | 默认值:200(单位 ms)delay: 200,// 如果提供了 timeout ,并且加载组件的时间超过了设定值,将显示错误组件// 默认值:Infinity(即永不超时,单位 ms)timeout: 3000,// 定义组件是否可挂起 | 默认值:truesuspensible: false,/**** @param {*} error 错误信息对象* @param {*} retry 一个函数,用于指示当 promise 加载器 reject 时,加载器是否应该重试* @param {*} fail  一个函数,指示加载程序结束退出* @param {*} attempts 允许的最大重试次数*/onError(error, retry, fail, attempts) {if (error.message.match(/fetch/) && attempts <= 3) {// 请求发生错误时重试,最多可尝试 3 次retry()} else {// 注意,retry/fail 就像 promise 的 resolve/reject 一样:// 必须调用其中一个才能继续错误处理。fail()}}
})

相关知识

首先复习一下基础部分的知识:组件基础->动态组件 + 深入组件->异步组件 + 深入组件->组件注册

组件基础-监听子组件事件

有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让 <blog-post> 组件决定它的文本要放大多少。这时可以使用 $emit 的第二个参数来提供这个值:

<button @click="$emit('enlargeText', 0.1)">Enlarge text
</button>

然后当在父级组件监听这个事件的时候,我们可以通过 $event 访问到被抛出的这个值:

<blog-post ... @enlarge-text="postFontSize += $event"></blog-post>

或者,如果这个事件处理函数是一个方法:

<blog-post ... @enlarge-text="onEnlargeText"></blog-post>

那么这个值将会作为第一个参数传入这个方法:

methods: {onEnlargeText(enlargeAmount) {this.postFontSize += enlargeAmount}
}

接下来尝试一下网课中经常出现的手写组件身上的v-model指令,为了让它正常工作,这个组件内的 <input> 必须:

  • 将其 value attribute 绑定到一个名叫 modelValue 的 prop 上
  • 在其 input 事件被触发时,将新的值通过自定义的 update:modelValue 事件抛出

写成代码之后是这样的:

app.component('custom-input', {// 父组件给子组件传递一个名为modelValue的属性,负责接收输入框中的内容props: ['modelValue'],emits: ['update:modelValue'],template: `<input:value="modelValue"@input="$emit('update:modelValue', $event.target.value)">`
})

现在 v-model 就可以在这个组件上完美地工作起来了:

<custom-input v-model="searchText"></custom-input>

在该组件中实现 v-model 的另一种方法是使用 computed property 的功能来定义 getter 和 setter。get 方法应返回 modelValue property,set 方法应该触发相应的事件。

app.component('custom-input', {props: ['modelValue'],emits: ['update:modelValue'],template: `<input v-model="value">`,computed: {value: {get() {return this.modelValue},set(value) { this.$emit('update:modelValue', value)}}}
})
组件基础-动态组件

有的时候,在不同的组件之间进行动态切换是非常有用的,以文档举例,现在提供了三个组件以及三个对应的按钮。需要怎样操作,才能让三个组件在同一个位置上进行切换呢?点击按钮可以获取到按钮上的信息,通过这个信息可以找到对应组件的名字。

然后就需要查看一个新的知识点,component标签:

<!-- 组件会在 `currentTabComponent` 改变时改变 -->
<component :is="currentTabComponent"></component>

在上述示例中,currentTabComponent 可以包括

  • 已注册组件的名字,或
  • 一个组件的选项对象

因此,为了实现上述的功能,可以使用一个计算属性负责计算currentTabComponent的值,当他表示的值与目标组件的名字相同时,就会替换为目标组件。不过,每次切换新标签的时候,Vue都创建了一个新的currentTabComponent实例。

不仅如此,is属性还有一个奇妙的方法,他可以用来创建常规的html元素。

有些 HTML 元素,诸如 <ul><ol><table><select>,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如 <li><tr><option>,只能出现在其它某些特定的元素内部。

这会导致我们使用这些有约束条件的元素时遇到一些问题。例如:

<table><blog-post-row></blog-post-row>
</table>

这个自定义组件 <blog-post-row> 会被作为无效的内容提升到外部,并导致最终渲染结果出错。我们可以使用特殊的 is attribute 作为一个变通的办法:

<table><tr is="vue:blog-post-row"></tr>
</table>

当它用于原生 HTML 元素时,is 的值必须以 vue: 开头,才可以被解释为 Vue 组件。这是避免和原生自定义元素混淆。

深入组件-组件注册
  • 全局注册:使用Vue.createApp({...}).component('组件名',{options选项})创建
  • 局部注册:通过import引入组件之后在components中声明
深入组件-动态&异步组件

动态组件和组件基础中的内容基本上相同,唯一的差别是这里使用了keep-alive进行了数据的缓存。

而异步组件使用的就是这里介绍的API-defineAsyncComponent,例如:

const { createApp, defineAsyncComponent } = Vueconst app = createApp({})const AsyncComp = defineAsyncComponent(() =>new Promise((resolve, reject) => {resolve({template: '<div>I am async!</div>'})})
)app.component('async-example', AsyncComp)

此方法接受一个返回 Promise 的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve 回调。你也可以调用 reject(reason),来表示加载失败。

把 webpack 2 及以上版本和 ES2015 语法相结合后,我们就可以这样使用动态地导入:

import { defineAsyncComponent } from 'vue'const AsyncComp = defineAsyncComponent(() =>import('./components/AsyncComponent.vue')
)app.component('async-component', AsyncComp)

当在局部注册组件时,你也可以使用 defineAsyncComponent

import { createApp, defineAsyncComponent } from 'vue'createApp({// ...components: {AsyncComponent: defineAsyncComponent(() =>import('./components/AsyncComponent.vue'))}
})
Suspense新特性

它是一个实验性的新特性,其API随时会发生改动,所以不详细描述,大致了解思路即可。

异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 <Suspense>,它将被视为该 <Suspense> 的异步依赖。在这种情况下,加载状态将由 <Suspense> 控制,组件自身的加载、错误、延迟和超时选项都将被忽略。

通过在其选项中指定 suspensible: false,异步组件可以退出 Suspense 控制,并始终控制自己的加载状态。

<suspense> 组件有两个插槽。它们都只接收一个直接子节点。default 插槽里的节点会尽可能展示出来。如果不能,则展示 fallback 插槽里的节点。

重要的是,异步组件不需要作为 <suspense> 的直接子节点。它可以出现在组件树任意深度的位置,且不需要出现在和 <suspense> 自身相同的模板中。只有所有的后代组件都准备就绪,该内容才会被认为解析完毕。

template><suspense><template #default><todo-list /></template><template #fallback><div>Loading...</div></template></suspense>
</template><script>
export default {components: {TodoList: defineAsyncComponent(() => import('./TodoList.vue'))}
}
</script>

defineCustomElement(3.2+)

自定义元素的一大好处就是它们可以与任何框架一起使用,甚至可以在没有框架的情况下使用。当你需要向可能使用不同前端技术栈的终端用户分发组件时,或者希望向最终应用程序隐藏其所用组件的实现细节时,使用自定义元素非常适合。

该方法接受和 defineComponent 相同的参数,但是返回一个原生的自定义元素,该元素可以用于任意框架或不基于框架使用。

用法示例:

<my-vue-element></my-vue-element>
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({// 这里是普通的 Vue 组件选项props: {},emits: {},template: `...`,// 只用于 defineCustomElement:注入到 shadow root 中的 CSSstyles: [`/* inlined css */`]
})
// 注册该自定义元素。
// 注册过后,页面上所有的 `<my-vue-element>` 标记会被升级。
customElements.define('my-vue-element', MyVueElement)
// 你也可以用编程的方式初始化这个元素:
// (在注册之后才可以这样做)
document.body.appendChild(new MyVueElement({// 初始化的 prop (可选)})
)

相关知识

高阶指南-Vue与Web Components

自定义元素和 Vue 组件之间确实存在一定程度的功能重叠:它们都允许我们定义具有数据传递、事件发出和生命周期管理功能的可重用组件。然而,Web Components API 是相对低级和简单的。

默认情况下,Vue 会优先尝试将一个非原生的 HTML 标签解析为一个注册的 Vue 组件,如果失败则会将其渲染为自定义元素。这种行为会导致在开发模式下的 Vue 发出“failed to resolve component”的警告。

解决警告的全局配置方法

所谓的自定义元素可以理解为使用浏览器自带的API去创建一个可复用的组件(WebComponent),现代浏览器的API已经更新到你不需要使用一个框架就可以去创建一个可复用的组件。Custom Element和Shadow DOM都可以让你去创造可复用的组件。甚至,这些组件几乎可以无缝的接入到框架中去使用。

自定义元素

相关文章

自定义元素是简单的用户自定义HTML元素。它们通过使用CustomElementRegistry来定义。要注册一个新的元素,通过window.customElements中一个叫做define的方法来获取注册的实例。

window.customElements.define('my-element', MyElement);

第一个参数表明自定义元素标签的名字,采用短横线命名法;

第二个参数负责执行元素的构造函数:

class MyElement extends HTMLElement {constructor() {super();}// 当元素被插入DOM树的时候将会触发connectedCallback方法// 可以联想理解为React中的 componentDidMountconnectedCallback() {// here the element has been inserted into the DOM}
}

通常来说,我们需要在connectedCallback之后进行元素的设置。因为这是唯一可以确定,所有的属性和子元素都已经可用的办法。构造函数一般是用来初始化状态和设置Shadow DOM。当一个元素被创建时将会调用构造函数,而当一个元素已经被插入到DOM中时会调用connectedCallback

自定义元素的获取

你同样可以用过调用**customElements.get(‘my-element’)**来获取这个元素构造函数的引用,从而构造元素。前提是你已经通过customElement.define()去注册。然后你可以使用new element()来代替document.createElement()去实例一个元素。

customElements.define('my-element', class extends HTMLElement {...});...const el = customElements.get('my-element');
const myElement = new el();  // same as document.createElement('my-element');
document.body.appendChild(myElement);
shadow DOM

之前在写微信小程序的时候遇到过shadow DOM,苦恼于怎么也无法从外部修改某个组件库中的样式。。

使用Shadow DOM,自定义元素的HTML和CSS完全封装在组件内。这意味着元素将以单个的HTML标签出现在文档的DOM树中。其内部的结构将会放在#shadow-root,当Shadow root被创建之后,你可以使用document对象的所有DOM方法,例如this.shadowRoot.querySelector去查找元素。

实际上一些原生的HTML元素也使用了Shadow DOM。例如你再一个网页中有一个<video>元素,它将会作为一个单独的标签展示,但它也将显示播放和暂停视频的控件。这些控件实际上就是video元素的Shadow DOM的一部分,因此默认情况下是隐藏的。要在Chrome中显示Shadow DOM,进入开发者工具中的Preferences中,选中Show user agent Shadow DOM。当你在开发者工具中再次查看video元素时,你就可以看到该元素的Shadow DOM了。

Shadow DOM还提供了局部作用域的CSS。所有的CSS都只应用于组件本身。元素将只继承最小数量从组件外部定义的CSS,甚至可以不从外部继承任何CSS。不过你可以暴露这些CSS属性,以便用户对组件进行样式设置,例如:

const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `<p>Hello world</p>`;

这定义了一个带mode: open的Shadow root,这意味着可以再开发者工具找到它并与之交互,配置暴露出的CSS属性,监听抛出的事件。同样也可以定义mode:closed,会得到与之相反的表现。

可以使用:host选择器对组件本身进行样式设置。:host CSS伪类选择包含其内部使用的CSS的shadow DOM的根元素 。换句话说,这允许你从其shadow DOM中选择一个自定义元素。

例如,自定义元素默认使用display: inline,所以如果你想要将组件展示为块元素,你可以这样做:

:host {display: block;
}

这还允许你进行上下文的样式化。例如你想要通过disabled的attribute来改变组件的背景是否为灰色:

:host([disabled]) {opacity: 0.5;
}

默认情况下,自定义元素从周围的CSS中继承一些属性,例如颜色和字体等,如果你想清空组件的初始状态并且将组件内的所有CSS都设置为默认的初始值,你可以使用:

:host {all: initial;
}

非常重要,需要注意的一点是,从外部定义在组件本身的样式优先于使用:host在Shadow DOM中定义的样式。如果你这样做

my-element {display: inline-block;
}

它将会被覆盖

:host {display: block;
}

不应该从外部去改变自定义元素的样式。如果你希望用户可以设置组件的部分样式,你可以暴露CSS变量去达到这个效果。例如你想让用户可以选择组件的背景颜色,可以暴露一个叫 --background-color的CSS变量。 假设现在有一个Shadow DOM的根节点是 <div id="container">

#container {background-color: var(--background-color);
}

现在用户可以在组件的外部设置它的背景颜色

my-element {--background-color: #ff0000;
}

你还可以在组件内设置一个默认值,以防用户没有设置

:host {--background-color: #ffffff;
}#container {background-color: var(--background-color);
}
修改shadow DOM样式(也许会很常用)
  • 获取包裹shadow DOM区域的父节点标签

  • 创建一个style标签,在里面通过innerHtml手动写入样式文件

  • style通过appendChild的方式塞入shadow DOM中

以阮一峰关于在web component的文章中的一个案例来证明:

<!DOCTYPE html>
<html>
<head><meta charset="utf-8"><meta name="viewport" content="width=device-width"><title>JS Bin</title>
</head>
<body>
<user-cardimage="https://semantic-ui.com/images/avatar2/large/kristy.png"name="User Name"email="yourmail@some-email.com"class="father"
></user-card><template id="userCardTemplate"><style>:host {display: flex;align-items: center;width: 450px;height: 180px;background-color: #d4d4d4;border: 1px solid #d5d5d5;box-shadow: 1px 1px 5px rgba(0, 0, 0, 0.1);border-radius: 3px;overflow: hidden;padding: 10px;box-sizing: border-box;font-family: 'Poppins', sans-serif;}.image {flex: 0 0 auto;width: 160px;height: 160px;vertical-align: middle;border-radius: 5px;}.container {box-sizing: border-box;padding: 20px;height: 160px;}.container > .name {font-size: 20px;font-weight: 600;line-height: 1;margin: 0;margin-bottom: 5px;}.container > .email {font-size: 12px;opacity: 0.75;line-height: 1;margin: 0;margin-bottom: 15px;}.container > .button {padding: 10px 25px;font-size: 12px;border-radius: 5px;text-transform: uppercase;}</style><img class="image"><div class="container"><p class="name"></p><p class="email"></p><button class="button">Follow John</button></div>
</template><script>class UserCard extends HTMLElement {constructor() {super();var shadow = this.attachShadow( { mode: 'open' } );var templateElem = document.getElementById('userCardTemplate');var content = templateElem.content.cloneNode(true);content.querySelector('img').setAttribute('src', this.getAttribute('image'));content.querySelector('.container>.name').innerText = this.getAttribute('name');content.querySelector('.container>.email').innerText = this.getAttribute('email');shadow.appendChild(content);}}window.customElements.define('user-card', UserCard);</script>
</body>
</html>

可以看到,关于user-card所有的元素结点以及样式都放在了#shadow-root中。

按照之前讲解的三个步骤我们来试试:

  • 获取#shadow-root的直接父元素结点container
  • 手动创建一个style标签,并在标签中通过innerHtml的方式手动输入具体的样式
  • style标签插入父节点中
 const container = document.querySelector('.father')let style = document.createElement("style")style.innerHTML = " .container{ background-color: #000; font-size: 30px; color: #fff; width: 100% } "container.shadowRoot.appendChild(style)

可以看到,我们手动添加的样式成功的放入了shadow-dom中并且生效。

不过,如果设置了mode为closed后,就无法获取暴露在外的元素,也就无法修改样式了。

var shadow = this.attachShadow( { mode: 'closed' } );
使用Vue构建自定义元素

与原生API中defineElements类似,Vue支持使用 defineCustomElement方法创建自定义元素,并且使用与 Vue 组件完全一致的 API。该方法接受与 defineComponent 相同的参数,但是会返回一个扩展自 HTMLElement 的自定义元素构造函数:

<my-vue-element></my-vue-element>
import { defineCustomElement } from 'vue'const MyVueElement = defineCustomElement({// 在此提供正常的 Vue 组件选项props: {},emits: {},template: `...`,// defineCustomElement 独有特性: CSS 会被注入到隐式根 (shadow root) 中styles: [`/* inlined css */`]
})// 注册自定义元素
// 注册完成后,此页面上的所有的 `<my-vue-element>` 标签会被更新
customElements.define('my-vue-element', MyVueElement)// 你也可以编程式地实例化这个元素:
// (只能在注册后完成此操作)
document.body.appendChild(new MyVueElement({// initial props (optional)})
)
Array.from去重

如果说要在页面中获取多个自定义元素,并为他新增功能或者是某些样式,但是其中某个元素出现了许多次,例如:

[a,a,b,c,a,a,a,a,d] // 用不同的符号表示不同的元素结点

用遍历数组的方法,对每个item进行修改会造成时间上的浪费,所以在这里介绍一种数组去重的方法来简化数组。

由于 Array.from() 的入参是可迭代对象,因而我们可以利用其与 Set 结合来实现快速从数组中删除重复项。

function unique(array) {return Array.from(new Set(array));
}unique([1, 1, 2, 3, 3]); // => [1, 2, 3]

首先,new Set(array) 创建了一个包含数组的集合,Set 集合会删除重复项。

因为 Set 集合是可迭代的,所以可以使用 Array.from() 将其转换为一个新的数组。

这样,我们就实现了数组去重。

resolveComponent

resolveComponent 只能在 rendersetup 函数中使用。

如果在当前应用实例中可用,则允许按名称解析 component

返回一个 Component。如果没有找到,则返回接收的参数 name

import { resolveComponent } from 'vue'
render() {const MyComponent = resolveComponent('MyComponent')
}

获取到组件后可以通过h辅助函数进行组件的渲染,这部分在h中已介绍。

nextTick

将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。

import { createApp, nextTick } from 'vue'const app = createApp({setup() {const message = ref('Hello!')const changeMessage = async newMessage => {message.value = newMessageawait nextTick()console.log('Now DOM is updated')}}
})

或者是通过实例方法:

createApp({// ...methods: {// ...example() {// 修改数据this.message = 'changed'// DOM 尚未更新this.$nextTick(function() {// DOM 现在更新了// `this` 被绑定到当前实例this.doSomethingElse()})}}
})

好吧,虽然文档上面说nextTick就是为DOM更新服务的,但是这所谓的DOM更新又是什么时候进行呢?或者说,为什么在修改数据后,我们无法立即的获取到修改后的数据,必须得等DOM更新后才能拿到最新的值呢?

也许直接看概念可能还是有点难以理解,所以推荐你先看看这个案例

看完后可以理清一下思路:更新数据后,vue并不是实时更新的,dom 的更新是需要一定时间的。 数据更新到显示到页面有时间差,我们在时间差内立即去操作或者获取 dom 的话,其实还是操作和获取的未更新的 dom ,所以当然获取不到更新后的值。 也就是说:Vue在更新 DOM 时是异步执行的。

相关知识

如果想稍微理解nextTick这个api,大概还需要了解一下浏览器的渲染机制,以及tasksmicrotasksqueues等机制的概念,这部分的内容扩展设计的内容会有很多,这里仅仅记录一下大致的理解思路,具体的理解可以看看这篇文章和这篇描述。

浏览器的进程

浏览器(多进程)主要包含了以下进程:

  • Browser进程(浏览器的主进程)
  • 第三方插件进程
  • GPU进程(浏览器渲染进程),其中GPU进程(多线程)和Web前端密切相关,主要包含以下线程:
    • GUI渲染线程
    • JS引擎线程
    • 事件触发线程(和EventLoop密切相关)
    • 定时触发器线程
    • 异步HTTP请求线程

GUI渲染线程JS引擎线程是互斥的,为了防止DOM渲染的不一致性,其中一个线程执行时另一个线程会被挂起。

这些线程中,和Vue的nextTick息息相关的是JS引擎线程事件触发线程

JS引擎线程和事件触发线程

浏览器页面初次渲染完毕后,JS引擎线程结合事件触发线程的工作流程如下:

  1. 同步任务在JS引擎线程(主线程)上执行,形成执行栈(Execution Context Stack)。

    2.主线程之外,事件触发线程管理着一个任务队列(Task Queue)。只要异步任务有了运行结果,就在任务队列之中放置一个事件。

    3.执行栈中的同步任务执行完毕,系统就会读取任务队列,如果有异步任务需要执行,将其加到主线程的执行栈并执行相应异步任务。

事件循环机制(Event Loop)

主线程在运行时会产生执行栈,栈中的代码调用某些异步API时会在任务队列中添加事件。

需要明确记住,是主线程的执行栈调用了某些异步API后再在任务队列中添加事件,这些API比如对DOM的操作、ajax请求、定时器等。

栈中的代码执行完毕后,就会读取任务队列中的事件,去执行事件对应的回调函数,如此循环往复,形成事件循环机制。

JS中有两种任务类型:微任务(microtask)和宏任务(macrotask):

  • 宏任务: script (主代码块)、setTimeoutsetIntervalsetImmediate 、I/O 、UI rendering

  • 微任务process.nextTick(Nodejs) 、promiseObject.observeMutationObserver

虽然前面介绍到,主线程通过异步API的调用后在任务队列中添加事件,不过宏任务并非全是异步任务,主代码块就是宏任务的一种。宏任务是每次执行栈内执行的代码,包括每次从事件队列中获取一个事件回调并放到执行栈中执行。浏览器为了能够使得JS引擎线程GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务执行开始之前,对页面进行重新渲染(宏任务 > 渲染 > 宏任务 > …)

微任务是在当前宏任务执行结束之后立即执行的任务(在当前 宏任务执行之后,UI渲染之前执行的任务)。微任务的响应速度相比setTimeout(下一个宏任务)会更快,因为无需等待UI渲染。当前宏任务执行后,会将在它执行期间产生的所有微任务都执行一遍。

执行顺序大致可以分为 宏任务->遇到微任务则添加到执行栈->检查是否有微任务队列->有则执行全部微任务->渲染UI->执行宏任务…

我们可以来看一个例子:

<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>Document</title><style>.outer {height: 200px;background-color: #FF8CB0;padding: 10px;}.inner {height: 100px;background-color: #FFD9E4;margin-top: 50px;}</style>
</head>
<body><div class="outer"><div class="inner"></div></div>
</body>
<script>
let inner = document.querySelector('.inner')
let outer = document.querySelector('.outer')
// 监听outer元素的attribute变化
new MutationObserver(function() {console.log('mutate')
}).observe(outer, {attributes: true
})
// click监听事件
function onClick() {console.log('click')setTimeout(function() {console.log('timeout')}, 0)Promise.resolve().then(function() {console.log('promise')})outer.setAttribute('data-random', Math.random())
}
inner.addEventListener('click', onClick)
</script>
</html>

现在分析一下这段代码:

  1. 当我们点击外部容器的时候触发onClick函数,从上往下执行函数体

    1. 首先输出click,遇到setTimeout定时器作为第二个宏任务
    2. 遇到Promise.resolve,将这段代码作为微任务
    3. 遇到改变ui的功能,放在下一轮的宏任务与本轮微任务之间执行
  2. onclick函数执行完毕,查询本轮中是否有微任务,如果有,则执行所有的微任务
    1. 执行微任务Promise.resolve后面的then函数,输出promise
    2. 在开启下一轮宏任务前执行修改ui的函数,输出mutate
  3. 第一轮宏任务全部执行完毕,开始第二轮的宏任务
    1. 执行setTimeout里的内容,输出timeout

所以最后控制台内的输出顺序为click->promise->mutate->timeout

Vue. 2.x nextTick

当你设置 vm.someData = ‘new value’ ,该组件不会立即重新渲染。当刷新队列时,组件会在事件循环队列清空时的下一个“tick”更新。多数情况我们不需要关心这个过程,但是如果你想在 DOM 状态更新后做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员沿着“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们确实要这么做。为了在数据变化之后等待 Vue 完成更新 DOM ,可以在数据变化之后立即使用 Vue.nextTick(callback) 。这样回调函数在 DOM 更新完成后就会调用。

task的执行优先级为: Promise -> MutationObserver -> setImmediate -> setTimeout

nextTick的渲染经历了多种迭代,最终在2.6+版本中确定为微任务,但是对事件执行做了一些改动。以阻止早期发现的一些情况下由于微任务优先级太高导致的函数执行。可是在测试中发现微任务的时候已经可以获取到渲染过后的DOM元素结点了。

这里额外介绍一个例题,这题讲解的是两个结构相仿的结点,同时对某个boolean类型的数据进行操作时,由于微任务的高优先级性导致的意外渲染,解决办法是为两个结构相仿的结点设置不同的key值。

选项

data

我们平时可以通过组件实例直接获取到实例对象,比如vm.a,其实是因为组件实例代理了data对象上所有的property,因此访问 vm.a 等价于访问 vm.$data.a

_$ 开头的 property 不会被组件实例代理,因为它们可能和 Vue 内置的 property、API 方法冲突。你可以使用例如 vm.$data._property 的方式访问这些 property。

// 直接创建一个实例
const data = { a: 1 }// 这个对象将添加到组件实例中
const vm = createApp({data() {return data}
}).mount('#app')console.log(vm.a) // => 1

相关知识

这一部分的内容关于响应性->深入响应式原理,并未涉及响应式基础、计算和侦听。

响应性-深入响应式原理

作为对响应式的理解,我们需要做到以下几点:

  • 当一个值被读取时进行追踪,例如 val1 + val2 会同时读取 val1val2
  • 当某个值改变时进行检测,例如,当我们赋值 val1 = 3
  • 重新运行代码来读取原始值,例如,再次运行 sum = val1 + val2 来更新 sum 的值。

这段代码并非响应式的:

let val1 = 2
let val2 = 3
let sum = val1 + val2console.log(sum) // 5val1 = 3console.log(sum) // 仍然是 5

为了能够在数值变化时,随时运行我们的总和,我们首先要做的是将其包裹在一个函数中:

const updateSum = () => {sum = val1 + val2
}

但我们如何告知 Vue 这个函数呢?

Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。

这部分其实可以借助Promise.all来理解,我们创建了一个Promise数组,数组中存放多个Promise状态。这里我们可以创建一个用来执行副作用的栈,向栈中添加等待监听的函数。

因此我们需要的是能够包裹总和的东西,像这样:

createEffect(() => {sum = val1 + val2
})

在这里我们使用 createEffect 来跟踪和执行:

// 维持一个执行副作用的栈
const runningEffects = []const createEffect = fn => {// 将传来的 fn 包裹在一个副作用函数中const effect = () => {runningEffects.push(effect)fn()runningEffects.pop()}// 立即自动执行副作用effect()
}

请看仔细,这里是在createEffect 中定义了一个名为effect的副作用函数,在函数中定义函数其实并不罕见,这就是所谓的闭包,为了私有化函数数据。无论何时当我们调用 createEffect或者是effect函数的时候,内部的effect函数都会同时被调用。不过effect函数无法在createEffect函数外部被访问。

当我们的副作用被调用时,在调用 fn 之前,它会把自己推到 runningEffects 数组中。这个数组可以用来检查当前正在运行的副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。

虽然 Vue 的公开 API 不包括任何直接创建副作用的方法,但它确实暴露了一个叫做 watchEffect 的函数,它的行为很像我们例子中的 createEffect 函数。

好吧,其实我之前对于函数的闭包并没有深入的理解,只是大致知道它的作用,所以在这里扩展一下闭包的知识。作为闭包的前置条件,首先需要了解js的执行上下文与作用域。

执行上下文

这一部分可以看这个视频

全局执行上下文就好比是点名册,我们可以通过它来找到代码数据具体保存在哪,并正确引用变量。

首先代码段会产生当前执行上下文,并指向全局执行上下文。此时会产生两块区域,首先指向全局的scope作用域(类似于块级作用域),如果在scope中没有找到需要的数据,就会延伸至第二块区域,也就是全局对象中进行数据的查找。

  • var、function的声明创建在全局对象中
  • let、const、class声明的变量创建在全局scope中

再看看这道题目,首先在全局对象中添加var声明的a,以及function函数对象foo。需要注意的是,在全局对象保存的仅仅只是function foo这一部分的函数名,而他后面具体的{...}函数体中的内容并不会放在全局对象中。

补充一个知识点,函数对象体内会保存函数创建时的执行上下文的文本环境,就是指创建了一个名为[[environment]]的环境。

接下来创建一个函数foo的执行上下文,然后形成一条foo的执行上下文->foo相关的全局scope->foo相关的全局对象的链表。接下来看foo函数内部的定义,console.log先不考虑,这是运行时考虑的内容;遇到let a这句话,于是在全局scope中创建一个未初始化的变量a。

这条关系链表最后会指向函数对象保存的上下文文本环境。

最后调用foo函数,发现需要打印输出的a是没有初始化的数据,所以最后会报错。

再以一道经典的题目举例,首先在全局上下文的全局对象中填入liList:[],然后向下进行到循环部分。

和前面处理函数体时一样,for(;;)的部分和具体的循环语句要分开讨论。

  • 首先在for循环()的文本环境中创建一个i=0的数据(由于使用的是let,所以无法放到全局执行上下文中)
  • 接下来针对liList[0]创建一个函数上下文对象,负责存放函数创建时的上下文
  • 之后定义liList[0]的函数对象内容,这个对象指向他创建时的上下文

查看函数与数据定义的部分已经完成,接下来运行liList[0]()。

  • 首先在执行前创建一个运行时的对象,这个对象中存放函数体console.log(i)
  • 发现运行时没有i这个数据,于是向上找他的函数对象
  • 在他的函数对象中找到了该函数运行上下文的环境中有i=0这条数据,并成功返回且打印数据
  • 以此类推,最终打印1,2,3,4,5

使用let的时候会正常输出1,2,3,4,5,可如果循环体中使用的是var呢?

  • 首先创建全局对象liList:[]i:0
  • 为liList[i]创建函数上下文对象,其中保存定义该函数时的环境,所以指向的是全局执行上下文
  • 定义liList[0]的函数对象内容,这个对象指向他创建时的上下文
  • 运行liList[i]()时创建一个运行时的对象,这个对象中存放函数体console.log(i)
  • 发现运行时没有i这个数据,于是向上找他的函数对象
  • 在他的函数对象中找到了该函数运行上下文的环境中有i=5这条数据,并成功返回且打印数据
  • 以此类推,最终打印1,2,3,4,5

至于为什么找到的上下文环境中i=5,是因为i仅仅保存在全局执行上下文的全局对象中,每个函数对象所指向的上下文环境全部指向者全局对象,所以最后每个函数对象全部引用的是全局对象中值为5的i。

闭包

在理解了执行上下文后,就可以来解释闭包了。

虽然闭包这个词听起来很飘渺,其实你只需要记住这句话:

函数内部能访问到外部上级作用域的变量是因为作用域链的存在。从函数外部能访问函数内部的变量就是闭包

所以闭包其实就是一种通过外部能够访问某个函数自由变量的存在,闭包是「函数+自由变量」

你可以借助面向对象中的private数据来理解自由变量,闭包就是一个把这种私有数据暴露给外界使用,且无法从外界修改的过程。

Vue 如何跟踪变化

在Vue中不能像前面的例子那样跟踪局部变量的重新分配,在 JavaScript 中没有这样的机制。我们可以跟踪的是对象 property 的变化。

当我们从一个组件的 data 函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 getset 处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。

还记得前面的表格吗?现在,我们对 Vue 如何实现这些关键步骤有了答案:

  • 当一个值被读取时进行追踪:proxy 的 get 处理函数中 track 函数记录了该 property 和当前副作用。

  • 当某个值改变时进行检测:在 proxy 上调用 set 处理函数。

  • 重新运行代码来读取原始值trigger 函数查找哪些副作用依赖于该 property 并执行它们

如果我们要用一个组件重写我们原来的例子,我们可以这样做:

const vm = createApp({data() {return {val1: 2,val2: 3}},computed: {sum() {return this.val1 + this.val2}}
}).mount('#app')console.log(vm.sum) // 5vm.val1 = 3console.log(vm.sum) // 6

data 返回的对象将被包裹在响应式代理中,并存储为 this.$data。Property this.val1this.val2 分别是 this.$data.val1this.$data.val2 的别名,因此它们通过相同的代理。

Vue 将把 sum 的函数包裹在一个副作用中。当我们试图访问 this.sum 时,它将运行该副作用来计算数值。包裹 $data 的响应式代理将会追踪到,当副作用运行时,property val1val2 被读取了。

如何让渲染响应式变化

一个组件的模板被编译成一个 render 函数。渲染函数创建 VNodes,描述该组件应该如何被渲染。它被包裹在一个副作用中,允许 Vue 在运行时跟踪被“触达”的 property。

一个 render 函数在概念上与一个 computed property 非常相似。Vue 并不确切地追踪依赖关系是如何被使用的,它只知道在函数运行的某个时间点上使用了这些依赖关系。如果这些 property 中的任何一个随后发生了变化,它将触发副作用再次运行,重新运行 render 函数以生成新的 VNodes。然后这些举动被用来对 DOM 进行必要的修改。

props

示例:

const app = createApp({})// 简单语法
app.component('props-demo-simple', {props: ['size', 'myMessage']
})// 对象语法,提供验证
app.component('props-demo-advanced', {props: {// 类型检查// 多个可能的类型height: [Number, String],// 类型检查 + 其他验证age: {type: Number,default: 0,required: true,validator: value => {return value >= 0},default: 100},// 带有默认值的对象propE: {type: Object,// 对象或数组的默认值必须从一个工厂函数返回     // newdefault() {return { message: 'hello' }}},// 具有默认值的函数propG: {type: Function,// 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数       // newdefault() {return 'Default function'}}}
})

这一部分的内容不多,文档中主要放在了深入组件->Props中介绍

相关知识

深入组件-Props

这里介绍几个容易传递错误的Prop。

传入一个数字

当我们传递一个数字时,无论它是否是静态的,都需要用v-bind的方法来传递,例如:

<!-- 即便 `42` 是静态的,我们仍需通过 `v-bind` 来告诉 Vue     -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :likes="42"></blog-post><!-- 用一个变量进行动态赋值。-->
<blog-post :likes="post.likes"></blog-post>
传入一个布尔值

当我们为某个传递属性设置type: Boolean时,具体传值如果为true,则可以省略后面的值部分

如果想要具体的传递值,都需要通过v-bind的方法来传递,例如:

<!-- 包含该 prop 没有值的情况在内,都意味着 `true`。          -->
<!-- 如果没有在 props 中把 is-published 的类型设置为 Boolean,
则这里的值为空字符串,而不是“true”。 -->
<blog-post is-published></blog-post><!-- 即便 `false` 是静态的,我们仍需通过 `v-bind` 来告诉 Vue  -->
<!-- 这是一个 JavaScript 表达式而不是一个字符串。             -->
<blog-post :is-published="false"></blog-post><!-- 用一个变量进行动态赋值。                                -->
<blog-post :is-published="post.isPublished"></blog-post>
传入一个对象的所有property

如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 v-bind (用 v-bind 代替 :prop-name)。这一点有点类似于作用域插槽v-slot:obj="objName",直接将某个对象作为作用域插槽中的内容发送给父组件。

例如,对于一个给定的对象 post

post: {id: 1,title: 'My Journey with Vue'
}

下面的模板:

<blog-post v-bind="post"></blog-post>

等价于:

<blog-post v-bind:id="post.id" v-bind:title="post.title"></blog-post>
避免子组件修改Prop

虽然说平时都知道,父子组件之间存在的是单向数据流。在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。但是会有两种常见的试图改变prop的情形:

  1. 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用

    在这种情况下,最好定义一个本地的 data property 并将这个 prop 作为其初始值:

props: ['initialCounter'],
data() {return {counter: this.initialCounter}
}

​ 2. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性:

props: ['size'],
computed: {normalizedSize() {return this.size.trim().toLowerCase()}
}
Prop的大小写命名

HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。

因此在传递prop的时候,于html中使用短横线分隔符命名,于javascript中使用驼峰命名。

const app = Vue.createApp({})app.component('blog-post', {// 在 JavaScript 中使用 camelCaseprops: ['postTitle'],template: '<h3>{{ postTitle }}</h3>'
})
<!-- 在 HTML 中使用 kebab-case -->
<blog-post post-title="hello!"></blog-post>
禁用Attribute继承

当组件返回单个根节点时,非 prop 的 attribute (以及class、id)将自动添加到根节点的 attribute 中。

不过emits 选项中列出的事件不会从组件的根元素继承,也将从 $attrs property 中移除。

如果我们希望让attribute应用于根节点之外的其他元素,可以通过将 inheritAttrs 选项设置为 false,然后将目标元素通过v-bind的方法绑定$attrs来实现。

如果不想绑定到根节点身上,必须要加上inheriAttrs才能手动更改,例如:

app.component('date-picker', {inheritAttrs: false,template: `<div class="date-picker"><input type="datetime-local" v-bind="$attrs" /></div>`
})

computed + watch

computed

计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要他所依赖的数据对象 还没有发生改变,多次访问设定的计算属性时计算属性会立即返回之前的计算结果,而不必再次执行函数。

const app = createApp({data(){return{a: 1}},computed:{// 仅读取aDouble() {return this.a * 2},// 读取和设置aPlus: {get() {return this.a + 1},set(v) {this.a = v - 1}}}
})

需要注意,这里说是对相应依赖进行关系缓存,所以他对于一个非响应式的数据来说是不会更新的,例如:

computed: {now() {return Date.now()}
}

上面介绍的是在options语法中通过属性配置来实现computed方法,现在介绍在v3版本中如何使用。

具体的理解和v2中使用时一致:

const count = ref(1)
// 默认情况下只接收一个getter函数
const plusOne = computed(() => count.value + 1)
console.log(plusOne.value) // 2// 或者,接受一个具有 get 和 set 函数的对象,用来创建可写的 ref 对象
// 因此在 computed 函数中参数为一个对象,对象成员为 get 函数与 set 函数
const plusOne2 = computed({get: () => count.value + 1,set: val => {count.value = val - 1}
})
plusOne2.value = 1
console.log(count.value) // 0

watch

对于watch部分来说,这里介绍几个配置项记住就行。

  • 参数:

    • {string | Function} source

    • {Function | Object} callback

    • {Object} [options]

      • {boolean} deep:为了发现对象内部值的变化,可以在选项参数中指定 deep: true。同样适用于监听数组变更。(注意:当变更(不是替换)对象或数组并使用 deep 选项时,旧值将与新值相同,因为它们的引用指向同一个对象/数组。Vue 不会保留变更之前值的副本。)

      • {boolean} immediate:在选项参数中指定 immediate: true 将立即以表达式的当前值触发回调。

      • {string} flushflush 选项可以更好地控制回调的时间。它可以设置为 'pre''post''sync'。默认值是 'pre',指定的回调应该在渲染前被调用。它允许回调在模板运行前更新了其他值。'post' 值是可以用来将回调推迟到渲染之后的。如果回调需要通过 $refs 访问更新的 DOM 或子组件,那么则使用该值。对于 'pre''post',回调使用队列进行缓冲。回调只被添加到队列中一次,即使观察值变化了多次。值的中间变化将被跳过,不会传递给回调。更多关于 flush 的信息,请参阅副作用刷新时机。

        ----> 缓冲回调不仅可以提高性能,还有助于保证数据的一致性。在执行数据更新的代码完成之前,侦听器不会被触发。

  • 返回:{Function} unwatch

最后传入回调数组的方法,大概想表达的意思是在监听某个响应式property时可以同时执行多个函数。

watch: {// 侦听顶级 property// 和computed一样,如果采用简写的方法,就写一个名为监听对象的函数// 如果需要在里面添加配置,则采用对象的方式来表示a(val, oldVal) {console.log(`new: ${val}, old: ${oldVal}`)},// 该回调会在任何被侦听的对象的 property 改变时被调用,不论其被嵌套多深c: {handler(val, oldVal) {console.log('c changed')},deep: true},// 侦听单个嵌套 property'c.d': function (val, oldVal) {// do something},// 其实对于嵌套属性来说还有一种写法(这部分应该放在此处watch配置外面的,仅作写法参考)this.$watch(() => this.c.d,(newVal, oldVal) => {// 做点什么})// 该回调将会在侦听开始之后被立即调用e: {handler(val, oldVal) {console.log('e changed')},immediate: true},// 你可以传入回调数组,它们会被逐一调用// 需要注意这里的格式// 在使用handler表示处理函数时,需要使用大括号将其包裹f: ['handle1',function handle2(val, oldVal) {console.log('handle2 triggered')},{handler: function handle3(val, oldVal) {console.log('handle3 triggered')}/* ... */}]
}
相关知识

在看watch相关的文档时,经常会出现es2015中的symbol特性,所以在这一节扩展学习下。

es2015 symbol

ES6 数据类型除了 Number 、 String 、 Boolean 、 Object、 null 和 undefined 以外,还新增了Symbol

symbol其实就是一种用来区分变量名的工具。比如在购物车中添加苹果手机以及苹果水果的时候,他们都可以命名为apple,此时就会造成变量的命名冲突。不过一般情况下我们都会通过定义一个语义化的变量名来区分,比如phone-apple和fruit-apple。

使用symbol其实就可以把他当作是一个永远不会重复的字符串就行。

let user1 = {name: "李四",key: Symbol(),
};
let user2 = {name: "李四",key: Symbol(),
};
let grade_conflict = {[user1.name]: { js: 99, css: 89 },[user2.name]: { js: 56, css: 100 },
};let grade = {[user1.key]: { js: 99, css: 89 },[user2.key]: { js: 56, css: 100 },
};
console.log(grade_conflict); // 李四: css: 100 js: 56
console.log(grade); // Symbol(): css: 89 js: 99;Symbol(): css: 100 js: 56

在上面这个例子中,对象中如果 name名重复,并且grade中是按照name的值作为属性名来保存的话,后面的数据会把前面的数据覆盖掉,使用 symbol 类型定义唯一值,可以避免覆盖问题。

单单看上面的例子可能会觉得,使用Symbol似乎没有自己手动加入前缀方便有效,所以看看下面这个例子:

class Cache {static data = {};static set(name, value) {this.data[name] = value;}static get(name) {return this.data[name];}
}let user = {name: "liziz",key: Symbol(),
};let cart = {name: "liziz",key: Symbol(),
};Cache.set(user.key, user);
Cache.set(cart.key, cart);
console.log(Cache.get(user.key));

现在创建了一个Cache类来模拟数据缓存器,在前后端分离的项目中,数据缓存器是必不可少的。现在假设出现了名为liziz的用户与购物车,后台需要将数据存入相应的数据库中,如何存?如果是按照发生了重名现象的name来存,势必会造成后台的数据冲突,所以我们可以手动的为数据对象添加一个值为Symbolkey值,存储时按照独一无二的key值来存便不会造成冲突。其实也可以把他理解为uuid。

请不要觉得如果手动添加了前缀名就可以解决这样的冲突问题,毕竟项目是由多人协作完成,你无法确保其他人的命名习惯,也许当你为苹果手机命名为apple-phone的时候,你的同事将苹果电脑也命名为apple-phone

二者之间的差别

如果说想要比较计算属性和侦听器之间的差别,计算属性可以对多个属性之间的联系进行计算,而侦听器只能逐一检测某个属性,比如计算一个fullname值,计算属性可以通过lastname和firstname进行拼接推出,而侦听器只能分别对这两个子属性进行侦听,在侦听的过程中计算fullname的值。

watch: {firstName(val) {this.fullName = val + ' ' + this.lastName},lastName(val) {this.fullName = this.firstName + ' ' + val}
},
computed: {fullName() {return this.firstName + ' ' + this.lastName}
}

methods

通过事件来触发某个声明的函数。

传参

在调用函数的时候,我们不仅可以传递数据参数,还可以传递一个$event属性作为最后一个参数,负责操控全局DOM元素,例如:

<button @click="warn('Form cannot be submitted yet.', $event)">Submit
</button>
// ...
methods: {warn(message, event) {// 现在可以访问到原生事件if (event) {event.preventDefault()}alert(message)}
}

也有不需要传递参数的情况,这个时候在template中无需手动声明$event,直接在methods设置的函数中使用event参数即可,例如:

<div id="event-with-method"><!-- `greet` 是在下面定义的方法名 --><button @click="greet">Greet</button>
</div>
methods: {greet(event) {// `event` 是原生 DOM eventif (event) {alert(event.target.tagName)}}
}

看文档的时候还发现,在某一个点击事件中可以同时触发多个函数!

<!-- 这两个 one() 和 two() 将执行按钮点击事件 -->
<button @click="one($event), two($event)">Submit
</button>

事件修饰符

这里仅介绍一些常用的修饰符。

.stop

stop修饰符可以阻止冒泡,默认情况下按照冒泡来执行。

<div @click="clickEvent(2)" style="width:300px;height:100px;background:red"><button @click.stop="clickEvent(1)">点击</button>
</div>methods: {clickEvent(num) {// 不加 stop 点击按钮输出 1 2// 加了 stop 点击按钮输出 1console.log(num)}
}
.capture

capture修饰符的作用和stop反过来,设置由外往内进行捕获,而不是禁止。

<div @click.capture="clickEvent(2)" style="width:300px;height:100px;background:red"><button @click="clickEvent(1)">点击</button>
</div>methods: {clickEvent(num) {不加 capture 点击按钮输出 1 2加了 capture 点击按钮输出 2 1console.log(num)}
}

如果是防止冒泡,那么给最里面的元素设置.stop修饰符;如果是手动设置捕获,那么给最外面的元素设置.capture

.self

self修饰符作用是,只有点击事件绑定的本身才会触发事件.

<div @click.self="clickEvent(2)" style="width:300px;height:100px;background:red"><button @click="clickEvent(1)">点击</button>
</div>methods: {clickEvent(num) {// 不加 self 点击按钮输出 1 2// 加了 self 点击按钮输出 1 点击div才会输出 2console.log(num)}
}

.self修饰符也许也是用来防止冒泡用的,如果点击的是内部元素就不向上冒泡,而如果点击的是外部元素本身则可以触发事件。

.prevent

prevent修饰符的作用是阻止默认事件(例如a标签的跳转)

<a href="#" @click.prevent="clickEvent(1)">点我</a>
<!-- 提交事件不再重载页面 -->
<form @submit.prevent="onSubmit"></form>methods: {clickEvent(num) {// 不加 prevent 点击a标签 先跳转然后输出 1// 加了 prevent 点击a标签 不会跳转只会输出 1console.log(num)}
}

使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 v-on:click.prevent.self 会阻止所有默认事件的点击,而 v-on:click.self.prevent 只会阻止对元素自身默认事件的点击。

.passive

当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符。

<!-- 滚动事件的默认行为 (即滚动行为) 将会立即触发,   -->
<!-- 而不会等待 `onScroll` 完成,                    -->
<!-- 以防止其中包含 `event.preventDefault()` 的情况  -->
<div @scroll.passive="onScroll">...</div>
.exact

.exact 修饰符允许你控制由精确的系统修饰符组合触发的事件。

<!-- 即使 Alt 或 Shift 被一同按下时也会触发 -->
<button @click.ctrl="onClick">A</button><!-- 有且只有 Ctrl 被按下的时候才触发 -->
<button @click.ctrl.exact="onCtrlClick">A</button><!-- 没有任何系统修饰符被按下的时候才触发 -->
<button @click.exact="onClick">A</button>
.sync

父组件传值进子组件,子组件想要改变这个值时,可以这么做:

父组件里
<children :foo="bar" @update:foo="val => bar = val"></children>子组件里
this.$emit('update:foo', newValue)
复制代码

sync修饰符的作用就是,可以简写:

父组件里
<children :foo.sync="bar"></children>子组件里
this.$emit('update:foo', newValue)

使用sync的时候,子组件传递的事件名必须为update:value,其中value必须与子组件中props中声明的名称完全一致。

这个修饰符在ui组件中较为常用,比如通过sync控制弹出框显示内容的同步修改。

emits

之前在用emits的时候我一直用的时文档中介绍的第一种方法,即数组语法:

// 数组语法
app.component('todo-item', {emits: ['check'],created() {this.$emit('check')}
})

看文档的时候才知道原来他也可以用对象的方式来自定义配置。也对,毕竟这一系列都属于Vue的选项配置,应该都可以自定义配置的。

// 对象语法
app.component('reply-form', {emits: {// 没有验证函数click: null,// 带有验证函数submit: payload => {if (payload.email && payload.password) {return true} else {console.warn(`Invalid submit event payload!`)return false}}}
})

在Vue3.x中,出于setup函数的限制,在setup()中是不指向当前实例的,所以需要使用context参数。

<script>
import { defineComponent } from 'vue'
export default defineComponent({emits: {'on-change': null}setup (props, ctx) {const clickBtn = () => {ctx.emit("on-change", "hi~");};return { clickBtn }}
})
</script>

在组件中所有的emit事件最好都能在emits选项中进行配置,使用对象方式的时候可以配置带校验emit事件,为null的时候代表不校验。进行校验的时候会把emit事件的参数传到校验函数的参数里面。当校验函数不通过,返回false的时候控制台会发出一个警告,但是emit事件会继续执行。

总的来说,emits无论是数组或者对象用法最终都会将事件给传递出去,数组或对象的使用只是为了记录实例中的emit事件,或者是验证事件中的参数,并不会因为验证不通过就取消事件的传递。

expose(3.2+)

这个API将为3.2+版本的setup语法糖服务,使用 <script setup> 的组件是默认关闭的,也即通过模板 ref 或者 $parent 链获取到的组件的公开实例,不会暴露任何在 <script setup> 中声明的绑定。

expose 选项将限制公共实例可以访问的 property。

export default {// increment 将被暴露,// 但 count 只能被内部访问expose: ['increment'],data() {return {count: 0}},methods: {increment() {this.count++}}
}

返回一个渲染函数将阻止我们返回任何其它的东西。从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。

我们可以通过调用 expose 来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问:

import { h, ref } from 'vue'
export default {setup(props, { expose }) {const count = ref(0)const increment = () => ++count.valueexpose({increment})return () => h('div', count.value)}
}

这个 increment 方法现在将可以通过父组件的模板 ref 访问。

为了在 <script setup> 组件中明确要暴露出去的属性,使用 defineExpose 编译器宏:

<script setup>
import { ref } from 'vue'const a = 1
const b = ref(2)defineExpose({a,b
})
</script>

当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 { a: number, b: number } (ref 会和在普通实例中一样被自动解包)。

生命周期钩子

使用声明周期钩子有两种方式,一是使用配置项的方法,二是使用import的方式引入钩子函数。

对于Vue2.x版本和Vue3.x的介绍,可以看这篇文章,挺详细的。

在Vue3.x中新增了两个用于Debug的钩子函数:onRenderTrackedonRenderTriggered,上面那篇文章中也有介绍。

这些生命周期钩子注册函数只能在 setup() 期间同步使用,因为它们依赖于内部的全局状态来定位当前活动的实例 。

钩子函数基本使用格式如下:

onxxxxx(()=>{// ,,,
})

这里记录一些看文档时感觉需要着重记住的内容:

  1. mountedupdated不会保证所有的子组件也都被挂载完成。如果你希望等待整个视图都渲染完毕,可以在 mountedupdated 内部使用 vm.$nextTick。并且,该钩子在服务器端渲染期间不被调用。

    mounted() {this.$nextTick(function () {// 仅在整个视图都被渲染之后才会运行的代码})
    }
    
  2. beforeUpdate在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。

directives

声明一组可用于组件实例中的指令。

const app = createApp({})
app.component('focused-input', {// 声明directives配置项directives: {focus: {// 其实可以在配置中调用声明周期函数// 并且参数可以接收到实例对象elmounted(el) {el.focus()}}},template: `<input v-focus>`
})

相关知识

可复用&组合-自定义指令

还是用上面的输入框自动聚焦来举例,不过这次是定义一个全局指令:

const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {// 当被绑定的元素挂载到 DOM 中时……mounted(el) {// 聚焦元素el.focus()}
})
指令的钩子函数

自定义指令中也存在生命周期

// 注册
app.directive('my-directive', {// 指令是具有一组生命周期的钩子:// 在绑定元素的 attribute 或事件监听器被应用之前调用created() {},// 在绑定元素的父组件挂载之前调用beforeMount() {},// 绑定元素的父组件被挂载时调用// 在这个时候可以获取到el实例元素mounted() {},// 在包含组件的 VNode 更新之前调用beforeUpdate() {},// 在包含组件的 VNode 及其**子组件的 VNode** 更新之后调用updated() {},// 在绑定元素的父组件卸载之前调用beforeUnmount() {},// 卸载绑定元素的父组件时调用unmounted() {}
})

需要注意:

当我们使用生命周期的时候,是作为指令配置项的函数在使用。根据前面的知识来看,既然可以通过对象配置项的方法来实现,也可以通过函数的方法简写实现:

// 你可能想在 mounted 和 updated 时触发相同行为,而不关心其他的钩子函数。
// 那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pin', (el, binding) => {el.style.position = 'fixed'const s = binding.arg || 'top'el.style[s] = binding.value + 'px'
})

自定义指令钩子函数的参数有:el、binding、vnode、preVnode。

  • el指令绑定到的元素。这可用于直接操作 DOM。
  • binding是一个对象,包含以下属性。
    • instance:使用指令的组件实例。
    • value:传递给指令的值。例如,在 v-my-directive="1 + 1" 中,该值为 2
    • oldValue:先前的值,仅在 beforeUpdateupdated 中可用。值是否已更改都可用。
    • arg:参数传递给指令 (如果有)。例如在 v-my-directive:foo 中,arg 为 "foo"
    • modifiers:包含修饰符 (如果有) 的对象。例如在 v-my-directive.foo.bar 中,修饰符对象为 {foo: true,bar: true}
    • dir:一个对象,在注册指令时作为参数传递。例如,在以下指令中
动态指令参数

如果想要为使用了指令的组件,我们可以在该组件上通过赋值实现数据的传递,例如:

<p v-pin="200">Stick me 200px from the top of the page</p>app.directive('pin', {mounted(el, binding) {el.style.position = 'fixed'// binding.value 是我们传递给指令的值——在这里是 200el.style.top = binding.value + 'px'}
})

通过这样的设置,我们可以让指令绑定的元素固定在距离顶部200px的位置上。如果此时我们希望元素可以通过输入,手动的改变最终渲染的位置与位置的偏移量,此时我们可以手动的传递参数,通过这个参数来动态设置样式:

<p v-pin:[direction]="pinPadding">I am pinned onto the page at 200px to the left.</p>
const app = Vue.createApp({data() {return {direction: 'right',pinPadding: 200}}
})app.directive('pin', {mounted(el, binding) {el.style.position = 'fixed'// binding.arg 是我们传递给指令的参数 这是binding参数的属性值之一const s = binding.arg || 'top'el.style[s] = binding.value + 'px'}
})

mixins

mixin作为灵活的混入功能,一个mixin对象可以包含任意组件选项,它可以像组件一样使用组件的生命周期、methods等功能。

与mixin极度相似的一个功能叫做extends,用法与mixin一致,extends的优先级大于mixin。

当组件使用了该混入时:

  • 若两者不具有同名选项,优先执行混入的生命周期钩子,再执行组件的代码部分:

  • const myMixin = {created() {console.log('mixin 对象的钩子被调用')}
    }const app = Vue.createApp({mixins: [myMixin],created() {console.log('组件钩子被调用')}
    })// => "mixin 对象的钩子被调用"
    // => "组件钩子被调用"
    
  • 若两者具有同名选项,这些选项将以恰当的方式进行合并,在数据的 property 发生冲突时,会以组件自身的数据为优先:

  • const myMixin = {data() {return {message: 'hello',foo: 'abc'}}
    }const app = Vue.createApp({mixins: [myMixin],data() {return {message: 'goodbye',bar: 'def'}},created() {console.log(this.$data) // => { message: "goodbye", foo: "abc", bar: "def" }}
    })
    

在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题:

  • Mixin 很容易发生冲突:因为每个 mixin 的 property 都被合并到同一个组件中,所以为了避免 property 名冲突,你仍然需要了解其他每个特性。
  • 可重用性是有限的:我们不能向 mixin 传递任何参数来改变它的逻辑,这降低了它们在抽象逻辑方面的灵活性。
  • 全局mixin的自定义选项推荐是作为插件发布,因为全局将会影响每一个后创建的子组件。

相关知识

为了解决混入的缺陷,在 Vue 3 继续支持 mixin 的同时,组合式 API是更推荐的在组件之间共享代码的方式。这里介绍可复用&组合->插件,可复用&组合->组合式API

可复用&组合-组合式API

在创建组件的时候,在data中我们假设会有a、b、c三个变量。在methods中可能存在对于a、b、a、c这个顺序不同操作的方法,在computed中可能存在a、a、c、b这个顺序相关的计算属性,在watch中又会是b、c、a这个顺序的侦听顺序。此时逻辑关注点的列表就会增长,对于一开始没有编写组件的人来说,观看代码时方法顺序并非按照a、b、c的顺序来,而是一种乱序的查看,经常会到处跳转不同的函数,这会导致组件难以阅读和理解。

这是一个大型组件的示例,其中逻辑关注点按颜色进行分组。

这种碎片化使得理解和维护复杂组件变得困难。选项的分离掩盖了潜在的逻辑问题。此外,在处理单个逻辑关注点时,我们必须不断地“跳转”相关代码的选项块。

如果能够将同一个逻辑关注点相关代码收集在一起会更好。而这正是组合式 API 使我们能够做到的。

在Vue组件中,一个可以实际使用组合式API的位置称为setup

简单来说就是将前面的computed、watch等方法作为一个函数API引入,然后在setup中声明的某个变量附近使用这些方法,这样可以确保某个逻辑关注点中一个变量对应一块逻辑。

可是,这不就是把代码移到了setup中吗,对于这个函数来说它不会变的非常臃肿吗?所以我们在继续其他任务之前需要将代码提取到多个独立的组合式函数中,将对不同属性的所有相关操作封装在一个.js文件中。组件如果需要使用 这些方法,直接在某个js文件中查看即可。例如:

// src/composables/useUserRepositories.jsimport { fetchUserRepositories } from '@/api/repositories'
import { ref, onMounted, watch } from 'vue'export default function useUserRepositories(user) {const repositories = ref([])const getUserRepositories = async () => {repositories.value = await fetchUserRepositories(user.value)}onMounted(getUserRepositories)watch(user, getUserRepositories)return {repositories,getUserRepositories}
}

上述方法将与仓库信息相关的方法封装在了useUserRepositories.js文件中,组件中直接引入该函数,就能像之前那样正常使用:

// src/components/UserRepositories.vue
import useUserRepositories from '@/composables/useUserRepositories'
import { toRefs } from 'vue'export default {components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },props: {user: {type: String,required: true}},setup (props) {const { user } = toRefs(props)const { repositories, getUserRepositories } = useUserRepositories(user)return {repositoriesgetUserRepositories}}
}

看!这样使用组合式API后,组件中的内容是否大幅度减少了!我们再来看看如果使用了多个data属性经过封装后的结果:

// src/components/UserRepositories.vue
import { toRefs } from 'vue'
import useUserRepositories from '@/composables/useUserRepositories'
import useRepositoryNameSearch from '@/composables/useRepositoryNameSearch'
import useRepositoryFilters from '@/composables/useRepositoryFilters'export default {components: { RepositoriesFilters, RepositoriesSortBy, RepositoriesList },props: {user: {type: String,required: true}},setup(props) {const { user } = toRefs(props)const { repositories, getUserRepositories } = useUserRepositories(user)const {searchQuery,repositoriesMatchingSearchQuery} = useRepositoryNameSearch(repositories)const {filters,updateFilters,filteredRepositories} = useRepositoryFilters(repositoriesMatchingSearchQuery)return {repositories: filteredRepositories,getUserRepositories,searchQuery,filters,updateFilters}}
}

我们之后在需要用什么变量的时候,直接通过与该变量相关的组合式API去查找细节即可。

setup

对于setup的第一个参数props来说,接收到的参数是响应式的,无法通过结构赋值获取某个具体的参数,如果你这么做,它将失去响应式。不过,也可以使用toRefs方法包裹props之后进行结构赋值,例如:

 const { title } = toRefs(props)

如果 title 是可选的 prop,则传入的 props 中可能没有 title 。在这种情况下,toRefs 将不会为 title 创建一个 ref 。你需要使用 toRef 替代它:

// MyBook.vue
import { toRef } from 'vue'
setup(props) {const title = toRef(props, 'title')console.log(title.value)
}

传递给 setup 函数的第二个参数是 contextcontext 是一个普通 JavaScript 对象,暴露了其它可能在 setup 中有用的值:

// MyBook.vue
export default {setup(props, context) {// Attribute (非响应式对象,等同于 $attrs)console.log(context.attrs)// 插槽 (非响应式对象,等同于 $slots)console.log(context.slots)// 触发事件 (方法,等同于 $emit)console.log(context.emit)// 暴露公共 property (函数)console.log(context.expose)}
}

请注意,与 props 不同,attrsslots 的 property 是响应式的。

如果你打算根据 attrsslots 的更改应用副作用,那么应该在 onBeforeUpdate生命周期钩子中执行此操作。

生命周期钩子函数

Provide / Inject

在options语法中我们可以通过对象的形式,将需要使用的属性provide出去,然后其所有的子孙组件都可以inject该属性,例如:

<script>
import MyMarker from './MyMarker.vue'export default {data() {return {todos: ['Feed a cat', 'Buy tickets']}},components: {MyMarker},// 传递普通propertyprovide: {location: 'North Pole',geolocation: {longitude: 90,latitude: 135}// 传递组件实例property时我们需要将provide转换为一个返回对象的函数provide() {return {todoLength: this.todos.length}}
}
</script>
<script>
export default {inject: ['location', 'geolocation'],// Inject 也可以通过设置默认值使其变成可选项inject: {foo: { default: 'foo' }},// 如果它需要从一个不同名字的 property 注入,则使用 from 来表示其源 propertyinject: {foo: {from: 'bar',default: 'foo'}}
}
</script>

在组合式API中我们需要手动的引入provide函数,该函数有两个参数:name:Stringvalue

还是之前那个例子,现在我们重构一下看看:

import { provide } from 'vue'
import MyMarker from './MyMarker.vue'export default {components: {MyMarker},setup() {provide('location', 'North Pole')provide('geolocation', {longitude: 90,latitude: 135})}
}

同样,在使用暴露出来的属性时也需要手动引入一个名为inject的函数,该函数有两个参数:name默认值(可选)

import { inject } from 'vue'export default {setup() {// 如果想要获取location,祖先组件却没有提供时,可以使用默认值const userLocation = inject('location', 'The Universe')// 直接获取祖先组件提供的propertyconst userGeolocation = inject('geolocation')return {userLocation,userGeolocation}}
}

provideinject 绑定并不是响应式的。这是刻意为之的。

然而,如果你传入了一个响应式的对象,那么其对象的 property 仍是响应式的。

根据数据以及对数据的修改操作放在父组件这条原则来看,如果子孙组件想要修改数据时不免会影响到父组件(因为传递的可以是响应式数据),所以建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部, provide 一个方法来负责改变响应式 property来解决在注入数据的组件内部更新 inject 的数据。例如:

import { provide, reactive, ref } from 'vue'
import MyMarker from './MyMarker.vue'export default {components: {MyMarker},setup() {const location = ref('North Pole')const geolocation = reactive({longitude: 90,latitude: 135})const updateLocation = () => {location.value = 'South Pole'}provide('location', location)provide('geolocation', geolocation)provide('updateLocation', updateLocation)}
}
模板引用

和options语法中this.$refs.xxx可以获取到具体的结点一致我们可以像往常一样声明 ref 并从 setup()返回:

<template> <div ref="root">This is a root element</div>
</template><script>import { ref, onMounted } from 'vue'export default {setup() {// 该变量与 ref 绑定的结点名称相同 const root = ref(null)onMounted(() => {// DOM 元素将在初始渲染后分配给 refconsole.log(root.value) // <div>This is a root element</div>})return {root}}}
</script>

这个功能还可以在v-for中使用,动态的使用函数引用执行自定义处理:

<template><!--动态的使用函数绑定ref,每遇到一个结点就往结点数组中塞入一个数据--><div v-for="(item, i) in list" :ref="el => { if (el) divs[i] = el }">{{ item }}</div>
</template><script>import { ref, reactive, onBeforeUpdate } from 'vue'export default {setup() {const list = reactive([1, 2, 3])const divs = ref([])// 确保在每次更新之前重置refonBeforeUpdate(() => {divs.value = []})return {list,divs}}}
</script>

侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。

但与生命周期钩子的一个关键区别是,watch()watchEffect() 在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。

因此,使用模板引用的侦听器应该用 flush: 'post' 选项来定义,这将在 DOM 更新运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。

<template><div ref="root">This is a root element</div>
</template><script>import { ref, watchEffect } from 'vue'export default {setup() {const root = ref(null)watchEffect(() => {console.log(root.value) // => <div>This is a root element</div>}, {flush: 'post'})return {root}}}
</script>
可复用&组合-插件(未看)

这部分的内容没有动手接触过,等亲手写过了回头来介绍。参考文档

globalProperties

添加一个可以在应用的任何组件实例中访问的全局 property。组件的 property 在命名冲突时具有优先权。

app.config.globalProperties.foo = 'bar'app.component('child-component', {mounted() {console.log(this.foo) // 'bar'}
})

这可以代替 Vue 2.x 的 Vue.prototype 扩展:

// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}

补充一个水群时讨论的access_token与refresh_token长token问题:

详细分析 Vue3 文档相关推荐

  1. ctk介绍、安装、使用详细说明pdf文档(中文).rar_Minio 安装和使用详解,还有对.net api进行了二次封装...

    背景 最近项目要上文件和图片存储,就开始在网上找相关的存储的文件分布式系统,了解了FastDFS.MinIO等相关的文件系统,最后选用了MinIO,今天着重讲MinIO,先讲相关的安装教程. MinI ...

  2. 文档布局分析 扭曲文档图像恢复

    向AI转型的程序员都关注了这个号???????????? 机器学习AI算法工程   公众号:datayx 对文本进行OCR前,必须分析和定义文档的逻辑结构.例如文本块.段落.行的位置:是否有应该重建的 ...

  3. springfox源码_springfox 源码分析(七) 文档初始化

    时间:2019-5-23 20:12:04 地点:家中 通过前面几篇文章对springfox的介绍,以及我们的学习准备工作,这篇我们将正式来探索springfox是如何初始化的 我们在学算法的时候,其 ...

  4. 史上最详细log4j使用文档

    日志是应用软件中不可缺少的部分,Apache的开源项目log4j是一个功能强大的日志组件,提供方便的日志记录.在apache网站:jakarta.apache.org/log4j 可以免费下载到Log ...

  5. Java练习题:字节缓冲流性能分析、 文档顺序恢复

    字节缓冲流性能分析 分别使用四种字节缓冲流进行文件复制测试其速度 这里准备了一个较大的视频文件130MB的视频文件进行测试 import com.wukong.io.BufferedInputStre ...

  6. php - 超详细将 pdf 文档格式转换为图片格式,将 offce pdf 演示文稿转成图像 png / jpg(小白一看就懂的详细教程,附带完整示例源代码)

    效果图 其他教程都有点乱而且有bug,本文站在新手小白的角度比较靠谱,超详细的完整流程及详细注释代码. 本文实现了 php 将 pdf 文档转为图片(png / jpg),完整流程及示例源代码, 你可 ...

  7. vuex 最简单、最详细的入门文档

    前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家.点击跳转到教程. 如果你在使用 vue.js , 那么我想你可能会对 vue 组件之间的通信感到崩溃 . 我在使用基于 ...

  8. unittestreport 详细功能使用文档(V1.1.1)——测试派柠檬班

    前言 关于 unittestreport 最初在开发的时候,只是计划开发一个 unittest 生成 HTML 测试报告的模块,所以起名叫做 unittestreport.在开发的过程中结合使用的小伙 ...

  9. MySQL与Python最详细的使用文档

    MySQL数据库 1.1 MySQL简介 Mysql是最流行的RDBMS(Relational Database Management System:关系数据库管理系统),特别是在WEB应用方面. 数 ...

最新文章

  1. 当进度对话框和后台线程处于活动状态时,如何处理屏幕方向变化?
  2. stm32的时钟系统
  3. 随机数生成器,完成后打开文件。
  4. nginx主模块指令
  5. PAT:组个最小数(C++)
  6. 如何进行手机web远程调试——chrome beta
  7. iOS中控制器的实践和学习(2)-认识XCode4模版(A1,A3,B2简易图)
  8. 获取学信网学历电子版流程
  9. C++程序设计:相反数
  10. python就业班-淘宝-目录.txt
  11. 微信PC端多开的秘密
  12. html5+资源甘特图,VARCHART XGantt VSW资讯:HTML5甘特图中的各个资源工作时间
  13. 20130723 上海OOW第二日
  14. python中复制上一行快捷键_Pycharm最常用的快捷键及使用技巧
  15. 打印机驱动下载,有哪些下载途径推荐?
  16. 完全平方数(简单的数论)
  17. Hbuild-X使用生成签名证书失败,怎么解决 +云打包
  18. 计算机 小学数学应用题教学设计,小学数学如何有效地进行应用题教学设计
  19. PDF的页面设置工具在哪里?如何使用并调整PDF页面?
  20. RichErp精密铸造管理系统

热门文章

  1. 我所能做的,就是真实地记录 | 小别手记
  2. LVGL官方文档-7.11.0-8-Porting-Logging
  3. 虚拟化技术 — GuestOS Agent
  4. Netty第二章 2020 7-4——Netty在Dubbo中的应用(3)关于单一长连接
  5. 行业解决方案】高速公路供配电监控系统-安科瑞 周莉娜
  6. React基础(肆)———状态和循环渲染
  7. 南宁二中三中高考2021成绩查询,2021广西重点高中名单及排名
  8. 《信号与系统学习笔记》—线性时不变系统(一)
  9. f文件服务器,f文件服务器
  10. [WPF 如何] 如何向 ComboBox 添加一个空白选项