之前我就在想,为什么很多人遇到问题都是推荐说:看文档
为什么?
因为文档真的很有用(真香)
这篇文章是个人对于文档中出现内容的理解,在断断续续的一周时间内看一次文档后,compisition API真的太香了,ts真的太香了!
在通过脚手架创建项目的时候可以在main.js
中看到这样一行代码:
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App).mount('#app')
使用这个函数可以提供一个上下文应用实例,应用实例挂载的整个组件树共享同一个上下文。
这也就意味着,我们可以在创建实例的时候设置一个根prop
,他的所有子组件都可以通过。他的第一个参数接收一个根组件选项对象options作为第一个参数,使用第二个参数,可以将根prop传递给应用程序,例如:props
的方法获取到这个值
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import './index.css'
createApp(App,{username:"黑猫几绛"}).mount('#app')
// App.vue
{{username}}
最终在页面上的显示结果为 黑猫几绛
这个第二个参数似乎只能将props传递给根组件使用,对于他的深层次子组件来说,是看不到props的。
返回一个”虚拟节点“,通常缩写为 VNode:一个普通对象,其中包含向 Vue 描述它应在页面上渲染哪种节点的信息,包括所有子节点的描述。它的目的是用于手动编写的渲染函数:
render() {
return h('h1', {}, 'Some title')
}
h是用于创建
VNode
的实用程序,仅作为createVNode
函数的缩写,而render只是暴露给是开发者去使用createVNode
的钩子。
render
函数的优先级高于根据 template
选项或挂载元素的 DOM 内 HTML 模板编译的渲染函数。
注意!如果 Vue 选项中包含渲染函数,模板template将被忽略!
接收三个参数:type
,props
和 children
类型:String | Object | Function
详细:
HTML 标签名、组件、异步组件或函数式组件。使用返回 null 的函数将渲染一个注释。此参数是必需的。
类型:Object
详细:
一个对象,与我们将在模板中使用的 attribute、prop 和事件相对应。可选。
类型: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 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
比如说我现在需要实现一个可以通过传入数字,来控制标题大小的组件:
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()获取到所有传入到默认插槽中的数据
// 如果是具名插槽的话,比如在父组件中使用
// 那么具体获取到header部分插槽内容采用this.$slots.header()
this.$slots.default()
)
},
props:{
level:{
type: Number,
default: 5
}
}
})
app.mount('#app')
111
222
333
最后可以成功的在页面中获取到渲染结果:
在前面的渲染函数中,我们在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,传递给 h
的第一个参数应该是组件本身。
render() {
return h(ButtonCounter)
}
如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent
:
const { h, resolveComponent } = Vue
// ...
render() {
const ButtonCounter = resolveComponent('ButtonCounter')
return h(ButtonCounter)
}
这一部分的内容等到要用的时候看文档就行,知道有这个功能。
简单来说就是通过原生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导出的是一个函数,函数有两个参数:
this
引用,Vue 会把 props
当作第一个参数传入)import { h } from 'vue'
const DynamicHeading = (props, context) => {
return h(`h${props.level}`, context.attrs, context.slots)
}
DynamicHeading.props = ['level']
export default DynamicHeading
其实这个函数主要是为ts
或tsx
的类型推导服务的,如果说是使用js
来写项目,直接用默认的export default {}
就好,例如:
如果是使用ts的项目,defineComponent
最为重要的作用是给予了组件正确的参数类型推断。
首先看看文档中是怎么说的:
这个API提供了一个参数,参数的值可以为具有组件选项的对象,或者是一个
setup
函数,函数名称将作为组件名称来使用。
单独看这一句话可能不方便理解,所以借助一下源码来看,它只是对setup函数的一种封装,并返回options对象。如果我们在调用函数时提供了详细的组件选项,那么返回一个options对象;如果仅仅为了简写提供一个setup函数,那么返回一个只封装了setup函数的对象。
export function defineComponent(options: unknown){
return isFunction(options) ? { setup: options } : options
}
通过一个具体的变量,去接收defineComponent封装后传递回来的options组件选项,在这个封装中可以为数据/函数设置具体的类型。
而所谓的设置一个setup
函数表示,当组件中只有setup方法的话,可以进行的一种简写,例如:
可以创建一个只有在需要时才会加载的异步组件。
在大型应用中,我们可能需要将应用分割成为小一些的代码块,并且只在需要的时候才从服务器加载一个模块。
在Vue3.x中,对异步组件的使用跟Vue2.x不同的主要变化有三点:
component
选项更名为loader
loader
绑定的组件加载函数不再接受resolve
和reject
函数,而且必须返回一个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,
// 定义组件是否可挂起 | 默认值:true
suspensible: 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()
}
}
})
首先复习一下基础部分的知识:组件基础->动态组件 + 深入组件->异步组件 + 深入组件->组件注册
有的时候用一个事件来抛出一个特定的值是非常有用的。例如我们可能想让
组件决定它的文本要放大多少。这时可以使用 $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指令,为了让它正常工作,这个组件内的 必须:
value
attribute 绑定到一个名叫 modelValue
的 prop 上input
事件被触发时,将新的值通过自定义的 update:modelValue
事件抛出写成代码之后是这样的:
app.component('custom-input', {
// 父组件给子组件传递一个名为modelValue的属性,负责接收输入框中的内容
props: ['modelValue'],
emits: ['update:modelValue'],
template: `
`
})
现在 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: `
`,
computed: {
value: {
get() {
return this.modelValue
},
set(value) {
this.$emit('update:modelValue', value)
}
}
}
})
有的时候,在不同的组件之间进行动态切换是非常有用的,以文档举例,现在提供了三个组件以及三个对应的按钮。需要怎样操作,才能让三个组件在同一个位置上进行切换呢?点击按钮可以获取到按钮上的信息,通过这个信息可以找到对应组件的名字。
然后就需要查看一个新的知识点,component
标签:
<component :is="currentTabComponent">component>
在上述示例中,currentTabComponent
可以包括
因此,为了实现上述的功能,可以使用一个计算属性负责计算currentTabComponent
的值,当他表示的值与目标组件的名字相同时,就会替换为目标组件。不过,每次切换新标签的时候,Vue都创建了一个新的currentTabComponent
实例。
不仅如此,is
属性还有一个奇妙的方法,他可以用来创建常规的html元素。
有些 HTML 元素,诸如 这会导致我们使用这些有约束条件的元素时遇到一些问题。例如: 这个自定义组件 当它用于原生 HTML 元素时, 动态组件和组件基础中的内容基本上相同,唯一的差别是这里使用了keep-alive进行了数据的缓存。 而异步组件使用的就是这里介绍的API- 此方法接受一个返回 把 webpack 2 及以上版本和 ES2015 语法相结合后,我们就可以这样使用动态地导入: 当在局部注册组件时,你也可以使用 它是一个实验性的新特性,其API随时会发生改动,所以不详细描述,大致了解思路即可。 异步组件在默认情况下是可挂起的。这意味着如果它在父链中有一个 通过在其选项中指定 重要的是,异步组件不需要作为 自定义元素的一大好处就是它们可以与任何框架一起使用,甚至可以在没有框架的情况下使用。当你需要向可能使用不同前端技术栈的终端用户分发组件时,或者希望向最终应用程序隐藏其所用组件的实现细节时,使用自定义元素非常适合。 该方法接受和 用法示例: 自定义元素和 Vue 组件之间确实存在一定程度的功能重叠:它们都允许我们定义具有数据传递、事件发出和生命周期管理功能的可重用组件。然而,Web Components API 是相对低级和简单的。 默认情况下,Vue 会优先尝试将一个非原生的 HTML 标签解析为一个注册的 Vue 组件,如果失败则会将其渲染为自定义元素。这种行为会导致在开发模式下的 Vue 发出“failed to resolve component”的警告。 解决警告的全局配置方法 所谓的自定义元素可以理解为使用浏览器自带的API去创建一个可复用的组件(WebComponent),现代浏览器的API已经更新到你不需要使用一个框架就可以去创建一个可复用的组件。Custom Element和Shadow DOM都可以让你去创造可复用的组件。甚至,这些组件几乎可以无缝的接入到框架中去使用。 相关文章 自定义元素是简单的用户自定义HTML元素。它们通过使用CustomElementRegistry来定义。要注册一个新的元素,通过window.customElements中一个叫做define的方法来获取注册的实例。 第一个参数表明自定义元素标签的名字,采用短横线命名法; 第二个参数负责执行元素的构造函数: 通常来说,我们需要在connectedCallback之后进行元素的设置。因为这是唯一可以确定,所有的属性和子元素都已经可用的办法。构造函数一般是用来初始化状态和设置 你同样可以用过调用**customElements.get(‘my-element’)**来获取这个元素构造函数的引用,从而构造元素。前提是你已经通过customElement.define()去注册。然后你可以使用new element()来代替document.createElement()去实例一个元素。 之前在写微信小程序的时候遇到过 使用Shadow DOM,自定义元素的HTML和CSS完全封装在组件内。这意味着元素将以单个的HTML标签出现在文档的DOM树中。其内部的结构将会放在 实际上一些原生的HTML元素也使用了Shadow DOM。例如你再一个网页中有一个 Shadow DOM还提供了局部作用域的CSS。所有的CSS都只应用于组件本身。元素将只继承最小数量从组件外部定义的CSS,甚至可以不从外部继承任何CSS。不过你可以暴露这些CSS属性,以便用户对组件进行样式设置,例如: Hello world 这定义了一个带mode: open的Shadow root,这意味着可以再开发者工具找到它并与之交互,配置暴露出的CSS属性,监听抛出的事件。同样也可以定义mode:closed,会得到与之相反的表现。 可以使用 例如,自定义元素默认使用 这还允许你进行上下文的样式化。例如你想要通过disabled的attribute来改变组件的背景是否为灰色: 默认情况下,自定义元素从周围的CSS中继承一些属性,例如颜色和字体等,如果你想清空组件的初始状态并且将组件内的所有CSS都设置为默认的初始值,你可以使用: 非常重要,需要注意的一点是,从外部定义在组件本身的样式优先于使用 它将会被覆盖 不应该从外部去改变自定义元素的样式。如果你希望用户可以设置组件的部分样式,你可以暴露CSS变量去达到这个效果。例如你想让用户可以选择组件的背景颜色,可以暴露一个叫 现在用户可以在组件的外部设置它的背景颜色 你还可以在组件内设置一个默认值,以防用户没有设置 获取包裹shadow DOM区域的父节点标签 创建一个 将 以阮一峰关于在web component的文章中的一个案例来证明: 可以看到,关于user-card所有的元素结点以及样式都放在了 按照之前讲解的三个步骤我们来试试: 可以看到,我们手动添加的样式成功的放入了shadow-dom中并且生效。 不过,如果设置了mode为 与原生API中 如果说要在页面中获取多个自定义元素,并为他新增功能或者是某些样式,但是其中某个元素出现了许多次,例如: 用遍历数组的方法,对每个item进行修改会造成时间上的浪费,所以在这里介绍一种数组去重的方法来简化数组。 由于 首先, 因为 这样,我们就实现了数组去重。 如果在当前应用实例中可用,则允许按名称解析 返回一个 获取到组件后可以通过 将回调延迟到下次 DOM 更新循环之后执行。在修改数据之后立即使用它,然后等待 DOM 更新。 或者是通过实例方法: 好吧,虽然文档上面说nextTick就是为DOM更新服务的,但是这所谓的DOM更新又是什么时候进行呢?或者说,为什么在修改数据后,我们无法立即的获取到修改后的数据,必须得等DOM更新后才能拿到最新的值呢? 也许直接看概念可能还是有点难以理解,所以推荐你先看看这个案例 看完后可以理清一下思路:更新数据后,vue并不是实时更新的,dom 的更新是需要一定时间的。 数据更新到显示到页面有时间差,我们在时间差内立即去操作或者获取 dom 的话,其实还是操作和获取的未更新的 dom ,所以当然获取不到更新后的值。 也就是说:Vue在更新 DOM 时是异步执行的。 如果想稍微理解nextTick这个api,大概还需要了解一下浏览器的渲染机制,以及 浏览器(多进程)主要包含了以下进程: GUI渲染线程和JS引擎线程是互斥的,为了防止DOM渲染的不一致性,其中一个线程执行时另一个线程会被挂起。 这些线程中,和Vue的 浏览器页面初次渲染完毕后, 同步任务在 2.主线程之外, 3. 主线程在运行时会产生 需要明确记住,是主线程的执行栈调用了某些异步API后再在任务队列中添加事件,这些API比如对DOM的操作、ajax请求、定时器等。 栈中的代码执行完毕后,就会读取 JS中有两种任务类型:微任务(microtask)和宏任务(macrotask): 宏任务: script (主代码块)、 微任务: 虽然前面介绍到,主线程通过异步API的调用后在任务队列中添加事件,不过 而微任务是在当前宏任务执行结束之后立即执行的任务(在当前 宏任务执行之后,UI渲染之前执行的任务)。微任务的响应速度相比 执行顺序大致可以分为 宏任务->遇到微任务则添加到执行栈->检查是否有微任务队列->有则执行全部微任务->渲染UI->执行宏任务… 我们可以来看一个例子: 现在分析一下这段代码: 所以最后控制台内的输出顺序为click->promise->mutate->timeout 当你设置 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值。 我们平时可以通过组件实例直接获取到实例对象,比如 以 这一部分的内容关于响应性->深入响应式原理,并未涉及响应式基础、计算和侦听。 作为对响应式的理解,我们需要做到以下几点: 这段代码并非响应式的: 为了能够在数值变化时,随时运行我们的总和,我们首先要做的是将其包裹在一个函数中: 但我们如何告知 Vue 这个函数呢? Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。 这部分其实可以借助Promise.all来理解,我们创建了一个Promise数组,数组中存放多个Promise状态。这里我们可以创建一个用来执行副作用的栈,向栈中添加等待监听的函数。 因此我们需要的是能够包裹总和的东西,像这样: 在这里我们使用 请看仔细,这里是在 当我们的副作用被调用时,在调用 虽然 Vue 的公开 API 不包括任何直接创建副作用的方法,但它确实暴露了一个叫做 好吧,其实我之前对于函数的闭包并没有深入的理解,只是大致知道它的作用,所以在这里扩展一下闭包的知识。作为闭包的前置条件,首先需要了解js的执行上下文与作用域。 这一部分可以看这个视频 全局执行上下文就好比是点名册,我们可以通过它来找到代码数据具体保存在哪,并正确引用变量。 首先代码段会产生当前执行上下文,并指向全局执行上下文。此时会产生两块区域,首先指向全局的scope作用域(类似于块级作用域),如果在scope中没有找到需要的数据,就会延伸至第二块区域,也就是全局对象中进行数据的查找。 再看看这道题目,首先在全局对象中添加var声明的a,以及function函数对象foo。需要注意的是,在全局对象保存的仅仅只是 补充一个知识点,函数对象体内会保存函数创建时的执行上下文的文本环境,就是指创建了一个名为 接下来创建一个函数foo的执行上下文,然后形成一条 这条关系链表最后会指向函数对象保存的上下文文本环境。 最后调用foo函数,发现需要打印输出的a是没有初始化的数据,所以最后会报错。 再以一道经典的题目举例,首先在全局上下文的全局对象中填入 和前面处理函数体时一样, 查看函数与数据定义的部分已经完成,接下来运行liList[0]()。 使用let的时候会正常输出1,2,3,4,5,可如果循环体中使用的是var呢? 至于为什么找到的上下文环境中i=5,是因为i仅仅保存在全局执行上下文的全局对象中,每个函数对象所指向的上下文环境全部指向者全局对象,所以最后每个函数对象全部引用的是全局对象中值为5的i。 在理解了执行上下文后,就可以来解释闭包了。 虽然闭包这个词听起来很飘渺,其实你只需要记住这句话: 函数内部能访问到外部上级作用域的变量是因为作用域链的存在。从函数外部能访问函数内部的变量就是闭包。 所以闭包其实就是一种通过外部能够访问某个函数内自由变量的存在,闭包是「函数+自由变量」 你可以借助面向对象中的private数据来理解自由变量,闭包就是一个把这种私有数据暴露给外界使用,且无法从外界修改的过程。 在Vue中不能像前面的例子那样跟踪局部变量的重新分配,在 JavaScript 中没有这样的机制。我们可以跟踪的是对象 property 的变化。 当我们从一个组件的 还记得前面的表格吗?现在,我们对 Vue 如何实现这些关键步骤有了答案: 当一个值被读取时进行追踪:proxy 的 当某个值改变时进行检测:在 proxy 上调用 重新运行代码来读取原始值: 如果我们要用一个组件重写我们原来的例子,我们可以这样做: Vue 将把 一个组件的模板被编译成一个 一个 示例: 这一部分的内容不多,文档中主要放在了深入组件->Props中介绍 这里介绍几个容易传递错误的Prop。 当我们传递一个数字时,无论它是否是静态的,都需要用 当我们为某个传递属性设置 如果想要具体的传递值,都需要通过 如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的 例如,对于一个给定的对象 下面的模板: 等价于: 虽然说平时都知道,父子组件之间存在的是单向数据流。在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态。但是会有两种常见的试图改变prop的情形: 这个 prop 用来传递一个初始值;这个子组件接下来希望将其作为一个本地的 prop 数据来使用。 在这种情况下,最好定义一个本地的 data property 并将这个 prop 作为其初始值: 2. 这个 prop 以一种原始的值传入且需要进行转换。在这种情况下,最好使用这个 prop 的值来定义一个计算属性: HTML 中的 attribute 名是大小写不敏感的,所以浏览器会把所有大写字符解释为小写字符。 因此在传递prop的时候,于html中使用短横线分隔符命名,于javascript中使用驼峰命名。 当组件返回单个根节点时,非 prop 的 attribute (以及class、id)将自动添加到根节点的 attribute 中。 不过 如果我们希望让attribute应用于根节点之外的其他元素,可以通过将 如果不想绑定到根节点身上,必须要加上 计算属性将基于它们的响应依赖关系缓存。计算属性只会在相关响应式依赖发生改变时重新求值。这就意味着只要 需要注意,这里说是对相应依赖进行关系缓存,所以他对于一个非响应式的数据来说是不会更新的,例如: 上面介绍的是在options语法中通过属性配置来实现computed方法,现在介绍在v3版本中如何使用。 具体的理解和v2中使用时一致: 对于 参数: ----> 缓冲回调不仅可以提高性能,还有助于保证数据的一致性。在执行数据更新的代码完成之前,侦听器不会被触发。 返回: 最后传入回调数组的方法,大概想表达的意思是在监听某个响应式property时可以同时执行多个函数。 在看watch相关的文档时,经常会出现 ES6 数据类型除了 Number 、 String 、 Boolean 、 Object、 null 和 undefined 以外,还新增了 symbol其实就是一种用来区分变量名的工具。比如在购物车中添加苹果手机以及苹果水果的时候,他们都可以命名为apple,此时就会造成变量的命名冲突。不过一般情况下我们都会通过定义一个语义化的变量名来区分,比如phone-apple和fruit-apple。 使用symbol其实就可以把他当作是一个永远不会重复的字符串就行。 在上面这个例子中,对象中如果 name名重复,并且grade中是按照name的值作为属性名来保存的话,后面的数据会把前面的数据覆盖掉,使用 symbol 类型定义唯一值,可以避免覆盖问题。 单单看上面的例子可能会觉得,使用 现在创建了一个Cache类来模拟数据缓存器,在前后端分离的项目中,数据缓存器是必不可少的。现在假设出现了名为liziz的用户与购物车,后台需要将数据存入相应的数据库中,如何存?如果是按照发生了重名现象的 请不要觉得如果手动添加了前缀名就可以解决这样的冲突问题,毕竟项目是由多人协作完成,你无法确保其他人的命名习惯,也许当你为苹果手机命名为 如果说想要比较计算属性和侦听器之间的差别,计算属性可以对多个属性之间的联系进行计算,而侦听器只能逐一检测某个属性,比如计算一个fullname值,计算属性可以通过lastname和firstname进行拼接推出,而侦听器只能分别对这两个子属性进行侦听,在侦听的过程中计算fullname的值。 通过事件来触发某个声明的函数。 在调用函数的时候,我们不仅可以传递数据参数,还可以传递一个 也有不需要传递参数的情况,这个时候在template中无需手动声明 看文档的时候还发现,在某一个点击事件中可以同时触发多个函数! 这里仅介绍一些常用的修饰符。 如果是防止冒泡,那么给最里面的元素设置 使用修饰符时,顺序很重要;相应的代码会以同样的顺序产生。因此,用 当我们在监听元素滚动事件的时候,会一直触发onscroll事件,在pc端是没啥问题的,但是在移动端,会让我们的网页变卡,因此我们使用这个修饰符的时候,相当于给onscroll事件整了一个.lazy修饰符。 当 使用sync的时候,子组件传递的事件名必须为update:value,其中value必须与子组件中props中声明的名称完全一致。 这个修饰符在ui组件中较为常用,比如通过sync控制弹出框显示内容的同步修改。 之前在用 看文档的时候才知道原来他也可以用对象的方式来自定义配置。也对,毕竟这一系列都属于Vue的选项配置,应该都可以自定义配置的。 在Vue3.x中,出于setup函数的限制,在 在组件中所有的emit事件最好都能在emits选项中进行配置,使用对象方式的时候可以配置带校验emit事件,为null的时候代表不校验。进行校验的时候会把emit事件的参数传到校验函数的参数里面。当校验函数不通过,返回false的时候控制台会发出一个警告,但是 总的来说,emits无论是数组或者对象用法最终都会将事件给传递出去,数组或对象的使用只是为了记录实例中的emit事件,或者是验证事件中的参数,并不会因为验证不通过就取消事件的传递。 这个API将为3.2+版本的 返回一个渲染函数将阻止我们返回任何其它的东西。从内部来说这不应该成为一个问题,但当我们想要将这个组件的方法通过模板 ref 暴露给父组件时就不一样了。 我们可以通过调用 这个 为了在 当父组件通过模板 ref 的方式获取到当前组件的实例,获取到的实例会像这样 使用声明周期钩子有两种方式,一是使用配置项的方法,二是使用import的方式引入钩子函数。 对于Vue2.x版本和Vue3.x的介绍,可以看这篇文章,挺详细的。 在Vue3.x中新增了两个用于Debug的钩子函数: 这些生命周期钩子注册函数只能在 钩子函数基本使用格式如下: 这里记录一些看文档时感觉需要着重记住的内容: 声明一组可用于组件实例中的指令。 还是用上面的输入框自动聚焦来举例,不过这次是定义一个全局指令: 自定义指令中也存在生命周期 需要注意: 当我们使用生命周期的时候,是作为指令配置项的函数在使用。根据前面的知识来看,既然可以通过对象配置项的方法来实现,也可以通过函数的方法简写实现: 自定义指令钩子函数的参数有:el、binding、vnode、preVnode。 如果想要为使用了指令的组件,我们可以在该组件上通过赋值实现数据的传递,例如: 通过这样的设置,我们可以让指令绑定的元素固定在距离顶部200px的位置上。如果此时我们希望元素可以通过输入,手动的改变最终渲染的位置与位置的偏移量,此时我们可以手动的传递参数,通过这个参数来动态设置样式: mixin作为灵活的混入功能,一个mixin对象可以包含任意组件选项,它可以像组件一样使用组件的生命周期、methods等功能。 与mixin极度相似的一个功能叫做extends,用法与mixin一致,extends的优先级大于mixin。 当组件使用了该混入时: 若两者不具有同名选项,优先执行混入的生命周期钩子,再执行组件的代码部分: 若两者具有同名选项,这些选项将以恰当的方式进行合并,在数据的 property 发生冲突时,会以组件自身的数据为优先: 在 Vue 2 中,mixin 是将部分组件逻辑抽象成可重用块的主要工具。但是,他们有几个问题: 为了解决混入的缺陷,在 Vue 3 继续支持 mixin 的同时,组合式 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的位置称为 简单来说就是将前面的computed、watch等方法作为一个函数API引入,然后在 可是,这不就是把代码移到了 上述方法将与仓库信息相关的方法封装在了 看!这样使用组合式API后,组件中的内容是否大幅度减少了!我们再来看看如果使用了多个data属性经过封装后的结果: 我们之后在需要用什么变量的时候,直接通过与该变量相关的组合式API去查找细节即可。 对于 如果 传递给 请注意,与 如果你打算根据 在options语法中我们可以通过对象的形式,将需要使用的属性provide出去,然后其所有的子孙组件都可以inject该属性,例如: 在组合式API中我们需要手动的引入 还是之前那个例子,现在我们重构一下看看: 同样,在使用暴露出来的属性时也需要手动引入一个名为 然而,如果你传入了一个响应式的对象,那么其对象的 property 仍是响应式的。 根据 和options语法中 这个功能还可以在 侦听模板引用的变更可以替代前面例子中演示使用的生命周期钩子。 但与生命周期钩子的一个关键区别是, 因此,使用模板引用的侦听器应该用 这部分的内容没有动手接触过,等亲手写过了回头来介绍。参考文档 添加一个可以在应用的任何组件实例中访问的全局 property。组件的 property 在命名冲突时具有优先权。 这可以代替 Vue 2.x 的 Vue.prototype 扩展: 补充一个水群时讨论的access_token与refresh_token长token问题:
、
、 和
,对于哪些元素可以出现在其内部是有严格限制的。而有些元素,诸如
、
和 ,只能出现在其它某些特定的元素内部。
<table>
<blog-post-row>blog-post-row>
table>
会被作为无效的内容提升到外部,并导致最终渲染结果出错。我们可以使用特殊的 is
attribute 作为一个变通的办法:<table>
<tr is="vue:blog-post-row">tr>
table>
is
的值必须以 vue:
开头,才可以被解释为 Vue 组件。这是避免和原生自定义元素混淆。深入组件-组件注册
Vue.createApp({...}).component('组件名',{options选项})
创建import
引入组件之后在components
中声明深入组件-动态&异步组件
defineAsyncComponent
,例如:const { createApp, defineAsyncComponent } = Vue
const app = createApp({})
const AsyncComp = defineAsyncComponent(
() =>
new Promise((resolve, reject) => {
resolve({
template: '
Promise
的工厂函数。从服务器检索组件定义后,应调用 Promise 的 resolve
回调。你也可以调用 reject(reason)
,来表示加载失败。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新特性
,它将被视为该
的异步依赖。在这种情况下,加载状态将由
控制,组件自身的加载、错误、延迟和超时选项都将被忽略。suspensible: false
,异步组件可以退出 Suspense
控制,并始终控制自己的加载状态。
组件有两个插槽。它们都只接收一个直接子节点。default
插槽里的节点会尽可能展示出来。如果不能,则展示 fallback
插槽里的节点。
的直接子节点。它可以出现在组件树任意深度的位置,且不需要出现在和
自身相同的模板中。只有所有的后代组件都准备就绪,该内容才会被认为解析完毕。template>
defineCustomElement(3.2+)
defineComponent
相同的参数,但是返回一个原生的自定义元素,该元素可以用于任意框架或不基于框架使用。<my-vue-element>my-vue-element>
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// 这里是普通的 Vue 组件选项
props: {},
emits: {},
template: `...`,
// 只用于 defineCustomElement:注入到 shadow root 中的 CSS
styles: [`/* inlined css */`]
})
// 注册该自定义元素。
// 注册过后,页面上所有的 `
相关知识
高阶指南-Vue与Web Components
自定义元素
window.customElements.define('my-element', MyElement);
class MyElement extends HTMLElement {
constructor() {
super();
}
// 当元素被插入DOM树的时候将会触发connectedCallback方法
// 可以联想理解为React中的 componentDidMount
connectedCallback() {
// here the element has been inserted into the DOM
}
}
Shadow DOM
。当一个元素被创建时将会调用构造函数,而当一个元素已经被插入到DOM中时会调用connectedCallback。自定义元素的获取
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-root
,当Shadow root被创建之后,你可以使用document对象的所有DOM方法,例如this.shadowRoot.querySelector去查找元素。元素,它将会作为一个单独的标签展示,但它也将显示播放和暂停视频的控件。这些控件实际上就是video元素的Shadow DOM的一部分,因此默认情况下是隐藏的。要在Chrome中显示Shadow DOM,进入开发者工具中的Preferences中,选中Show user agent Shadow DOM。当你在开发者工具中再次查看video元素时,你就可以看到该元素的Shadow DOM了。
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
:host
选择器对组件本身进行样式设置。:host
CSS伪类选择包含其内部使用的CSS的shadow DOM的根元素 。换句话说,这允许你从其shadow DOM中选择一个自定义元素。display: inline
,所以如果你想要将组件展示为块元素,你可以这样做::host {
display: block;
}
:host([disabled]) {
opacity: 0.5;
}
:host {
all: initial;
}
:host
在Shadow DOM中定义的样式。如果你这样做my-element {
display: inline-block;
}
:host {
display: block;
}
--background-color
的CSS变量。 假设现在有一个Shadow DOM的根节点是 #container {
background-color: var(--background-color);
}
my-element {
--background-color: #ff0000;
}
:host {
--background-color: #ffffff;
}
#container {
background-color: var(--background-color);
}
修改shadow DOM样式(也许会很常用)
style
标签,在里面通过innerHtml
手动写入样式文件style
通过appendChild
的方式塞入shadow DOM中DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
<title>JS Bintitle>
head>
<body>
<user-card
image="https://semantic-ui.com/images/avatar2/large/kristy.png"
name="User Name"
email="[email protected]"
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 Johnbutton>
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>
#shadow-root
中。
#shadow-root
的直接父元素结点containerstyle
标签,并在标签中通过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)
closed
后,就无法获取暴露在外的元素,也就无法修改样式了。var shadow = this.attachShadow( { mode: 'closed' } );
使用Vue构建自定义元素
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 */`]
})
// 注册自定义元素
// 注册完成后,此页面上的所有的 `
Array.from去重
[a,a,b,c,a,a,a,a,d] // 用不同的符号表示不同的元素结点
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
只能在 render
或 setup
函数中使用。component
。Component
。如果没有找到,则返回接收的参数 name
。import { resolveComponent } from 'vue'
render() {
const MyComponent = resolveComponent('MyComponent')
}
h
辅助函数进行组件的渲染,这部分在h中已介绍。nextTick
import { createApp, nextTick } from 'vue'
const app = createApp({
setup() {
const message = ref('Hello!')
const changeMessage = async newMessage => {
message.value = newMessage
await nextTick()
console.log('Now DOM is updated')
}
}
})
createApp({
// ...
methods: {
// ...
example() {
// 修改数据
this.message = 'changed'
// DOM 尚未更新
this.$nextTick(function() {
// DOM 现在更新了
// `this` 被绑定到当前实例
this.doSomethingElse()
})
}
}
})
相关知识
tasks
、microtasks
、queues
等机制的概念,这部分的内容扩展设计的内容会有很多,这里仅仅记录一下大致的理解思路,具体的理解可以看看这篇文章和这篇描述。浏览器的进程
nextTick
息息相关的是JS引擎线程
和事件触发线程
。JS引擎线程和事件触发线程
JS引擎线程
结合事件触发线程
的工作流程如下:
JS引擎线程
(主线程)上执行,形成执行栈
(Execution Context Stack)。事件触发线程
管理着一个任务队列
(Task Queue)。只要异步任务有了运行结果,就在任务队列
之中放置一个事件。执行栈
中的同步任务执行完毕,系统就会读取任务队列
,如果有异步任务需要执行,将其加到主线程的执行栈
并执行相应异步任务。事件循环机制(Event Loop)
执行栈
,栈中的代码调用某些异步API时会在任务队列
中添加事件。任务队列
中的事件,去执行事件对应的回调函数,如此循环往复,形成事件循环机制。
setTimeout
、setInterval
、setImmediate
、I/O 、UI renderingprocess.nextTick
(Nodejs) 、promise
、Object.observe
、MutationObserver
宏任务
并非全是异步任务,主代码块就是宏任务的一种。宏任务是每次执行栈内执行的代码,包括每次从事件队列中获取一个事件回调并放到执行栈中执行。浏览器为了能够使得JS引擎线程与GUI渲染线程有序切换,会在当前宏任务结束之后,下一个宏任务执行开始之前,对页面进行重新渲染(宏任务 > 渲染 > 宏任务 > …)setTimeout
(下一个宏任务)会更快,因为无需等待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>Documenttitle>
<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>
Vue. 2.x nextTick
选项
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
会同时读取 val1
和 val2
。val1 = 3
。sum = val1 + val2
来更新 sum
的值。let val1 = 2
let val2 = 3
let sum = val1 + val2
console.log(sum) // 5
val1 = 3
console.log(sum) // 仍然是 5
const updateSum = () => {
sum = val1 + val2
}
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
数组中。这个数组可以用来检查当前正在运行的副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。watchEffect
的函数,它的行为很像我们例子中的 createEffect
函数。执行上下文
function foo
这一部分的函数名,而他后面具体的{...}
函数体中的内容并不会放在全局对象中。[[environment]]
的环境。foo的执行上下文->foo相关的全局scope->foo相关的全局对象
的链表。接下来看foo函数内部的定义,console.log先不考虑,这是运行时考虑的内容;遇到let a
这句话,于是在全局scope中创建一个未初始化的变量a。liList:[]
,然后向下进行到循环部分。for(;;)
的部分和具体的循环语句要分开讨论。
console.log(i)
liList:[]
与i:0
console.log(i)
闭包
Vue 如何跟踪变化
data
函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get
和 set
处理程序的 Proxy 中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。
get
处理函数中 track
函数记录了该 property 和当前副作用。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) // 5
vm.val1 = 3
console.log(vm.sum) // 6
data
返回的对象将被包裹在响应式代理中,并存储为 this.$data
。Property this.val1
和 this.val2
分别是 this.$data.val1
和 this.$data.val2
的别名,因此它们通过相同的代理。sum
的函数包裹在一个副作用中。当我们试图访问 this.sum
时,它将运行该副作用来计算数值。包裹 $data
的响应式代理将会追踪到,当副作用运行时,property val1
和 val2
被读取了。如何让渲染响应式变化
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,
// 对象或数组的默认值必须从一个工厂函数返回 // new
default() {
return { message: 'hello' }
}
},
// 具有默认值的函数
propG: {
type: Function,
// 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数 // new
default() {
return 'Default function'
}
}
}
})
相关知识
深入组件-Props
传入一个数字
v-bind
的方法来传递,例如:
<blog-post :likes="42">blog-post>
<blog-post :likes="post.likes">blog-post>
传入一个布尔值
type: Boolean
时,具体传值如果为true,则可以省略后面的值部分v-bind
的方法来传递,例如:
<blog-post is-published>blog-post>
<blog-post :is-published="false">blog-post>
<blog-post :is-published="post.isPublished">blog-post>
传入一个对象的所有property
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
props: ['initialCounter'],
data() {
return {
counter: this.initialCounter
}
}
props: ['size'],
computed: {
normalizedSize() {
return this.size.trim().toLowerCase()
}
}
Prop的大小写命名
const app = Vue.createApp({})
app.component('blog-post', {
// 在 JavaScript 中使用 camelCase
props: ['postTitle'],
template: '
{{ postTitle }}
'
})
<blog-post post-title="hello!">blog-post>
禁用Attribute继承
emits
选项中列出的事件不会从组件的根元素继承,也将从 $attrs
property 中移除。inheritAttrs
选项设置为 false
,然后将目标元素通过v-bind
的方法绑定$attrs
来实现。inheriAttrs
才能手动更改,例如:app.component('date-picker', {
inheritAttrs: false,
template: `
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()
}
}
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} flush
:flush
选项可以更好地控制回调的时间。它可以设置为 'pre'
、'post'
或 'sync'
。默认值是 'pre'
,指定的回调应该在渲染前被调用。它允许回调在模板运行前更新了其他值。'post'
值是可以用来将回调推迟到渲染之后的。如果回调需要通过 $refs
访问更新的 DOM 或子组件,那么则使用该值。对于 'pre'
和 'post'
,回调使用队列进行缓冲。回调只被添加到队列中一次,即使观察值变化了多次。值的中间变化将被跳过,不会传递给回调。更多关于 flush
的信息,请参阅副作用刷新时机。{Function} unwatch
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')
}
/* ... */
}
]
}
相关知识
es2015
中的symbol
特性,所以在这一节扩展学习下。es2015 symbol
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
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));
name
来存,势必会造成后台的数据冲突,所以我们可以手动的为数据对象添加一个值为Symbol
的key
值,存储时按照独一无二的key值来存便不会造成冲突。其实也可以把他理解为uuid。apple-phone
的时候,你的同事将苹果电脑也命名为apple-phone
。二者之间的差别
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)
}
}
$event
,直接在methods设置的函数中使用event参数即可,例如:<div id="event-with-method">
<button @click="greet">Greetbutton>
div>
methods: {
greet(event) {
// `event` 是原生 DOM event
if (event) {
alert(event.target.tagName)
}
}
}
<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 点击按钮输出 1
console.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 1
console.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才会输出 2
console.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标签 不会跳转只会输出 1
console.log(num)
}
}
v-on:click.prevent.self
会阻止所有默认事件的点击,而 v-on:click.self.prevent
只会阻止对元素自身默认事件的点击。.passive
<div @scroll.passive="onScroll">...div>
.exact
.exact
修饰符允许你控制由精确的系统修饰符组合触发的事件。
<button @click.ctrl="onClick">Abutton>
<button @click.ctrl.exact="onCtrlClick">Abutton>
<button @click.exact="onClick">Abutton>
.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)
emits
emits
的时候我一直用的时文档中介绍的第一种方法,即数组语法:// 数组语法
app.component('todo-item', {
emits: ['check'],
created() {
this.$emit('check')
}
})
// 对象语法
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
}
}
}
})
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
事件会继续执行。expose(3.2+)
setup
语法糖服务,使用 的组件是默认关闭的,也即通过模板 ref 或者
$parent
链获取到的组件的公开实例,不会暴露任何在 中声明的绑定。
expose
选项将限制公共实例可以访问的 property。export default {
// increment 将被暴露,
// 但 count 只能被内部访问
expose: ['increment'],
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}
expose
来解决这个问题,给它传递一个对象,其中定义的 property 将可以被外部组件实例访问:import { h, ref } from 'vue'
export default {
setup(props, { expose }) {
const count = ref(0)
const increment = () => ++count.value
expose({
increment
})
return () => h('div', count.value)
}
}
increment
方法现在将可以通过父组件的模板 ref 访问。 组件中明确要暴露出去的属性,使用
defineExpose
编译器宏:
{ a: number, b: number }
(ref 会和在普通实例中一样被自动解包)。生命周期钩子
onRenderTracked
和onRenderTriggered
,上面那篇文章中也有介绍。setup()
期间同步使用,因为它们依赖于内部的全局状态来定位当前活动的实例 。onxxxxx(()=>{
// ,,,
})
mounted
和updated
不会保证所有的子组件也都被挂载完成。如果你希望等待整个视图都渲染完毕,可以在 mounted
或updated
内部使用 vm.$nextTick
。并且,该钩子在服务器端渲染期间不被调用。mounted() {
this.$nextTick(function () {
// 仅在整个视图都被渲染之后才会运行的代码
})
}
beforeUpdate
在数据发生改变后,DOM 被更新之前被调用。这里适合在现有 DOM 将要被更新之前访问它,比如移除手动添加的事件监听器。directives
const app = createApp({})
app.component('focused-input', {
// 声明directives配置项
directives: {
focus: {
// 其实可以在配置中调用声明周期函数
// 并且参数可以接收到实例对象el
mounted(el) {
el.focus()
}
}
},
template: ``
})
相关知识
可复用&组合-自定义指令
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'
})
instance
:使用指令的组件实例。value
:传递给指令的值。例如,在 v-my-directive="1 + 1"
中,该值为 2
。oldValue
:先前的值,仅在 beforeUpdate
和 updated
中可用。值是否已更改都可用。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 是我们传递给指令的值——在这里是 200
el.style.top = binding.value + 'px'
}
})
<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
const myMixin = {
created() {
console.log('mixin 对象的钩子被调用')
}
}
const app = Vue.createApp({
mixins: [myMixin],
created() {
console.log('组件钩子被调用')
}
})
// => "mixin 对象的钩子被调用"
// => "组件钩子被调用"
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" }
}
})
相关知识
可复用&组合-组合式API
setup
。setup
中声明的某个变量附近使用这些方法,这样可以确保某个逻辑关注点中一个变量对应一块逻辑。setup
中吗,对于这个函数来说它不会变的非常臃肿吗?所以我们在继续其他任务之前需要将代码提取到多个独立的组合式函数中,将对不同属性的所有相关操作封装在一个.js
文件中。组件如果需要使用 这些方法,直接在某个js文件中查看即可。例如:// src/composables/useUserRepositories.js
import { 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 {
repositories
getUserRepositories
}
}
}
// 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
}
}
}
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
函数的第二个参数是 context
。context
是一个普通 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
不同,attrs
和 slots
的 property 是非响应式的。attrs
或 slots
的更改应用副作用,那么应该在 onBeforeUpdate生命周期钩子中执行此操作。生命周期钩子函数
Provide / Inject
provide
函数,该函数有两个参数:name:String
和value
。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')
// 直接获取祖先组件提供的property
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
provide
和 inject
绑定并不是响应式的。这是刻意为之的。数据以及对数据的修改操作放在父组件
这条原则来看,如果子孙组件想要修改数据时不免会影响到父组件(因为传递的可以是响应式数据),所以建议尽可能将对响应式 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)
}
}
模板引用
this.$refs.xxx
可以获取到具体的结点一致我们可以像往常一样声明 ref 并从 setup()返回:
v-for
中使用,动态的使用函数引用执行自定义处理:
watch()
和 watchEffect()
在 DOM 挂载或更新之前运行副作用,所以当侦听器运行时,模板引用还未被更新。flush: 'post'
选项来定义,这将在 DOM 更新后运行副作用,确保模板引用与 DOM 保持同步,并引用正确的元素。
可复用&组合-插件(未看)
globalProperties
app.config.globalProperties.foo = 'bar'
app.component('child-component', {
mounted() {
console.log(this.foo) // 'bar'
}
})
// 之前 (Vue 2.x)
Vue.prototype.$http = () => {}
// 之后 (Vue 3.x)
const app = createApp({})
app.config.globalProperties.$http = () => {}
你可能感兴趣的:(Vue3,vue.js,前端,typescript)