| |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
-> JavaScript知识库 -> Vue面试知识点总结------框架篇 -> 正文阅读 |
|
[JavaScript知识库]Vue面试知识点总结------框架篇 |
一、MVC 与 MVVM1、MVCMVC 全名是 Model View Controller,是模型(model)-视图(view)-控制器(controller)的缩写,一种软件设计典范
MVC 的思想:一句话描述就是 Controller 负责将 Model 的数据用 View 显示出来,换句话说就是在 Controller 里面把 Model 的数据赋值给 View。 2、MVVMMVVM 新增了 VM 类
MVVM 与 MVC 最大的区别就是:它实现了 View 和 Model 的自动同步,也就是当 Model 的属性改变时,我们不用再自己手动操作 Dom 元素,来改变 View 的显示,而是改变属性后该属性对应 View 层显示会自动改变(对应Vue数据驱动的思想) 整体看来,MVVM 比 MVC 精简很多,不仅简化了业务与界面的依赖,还解决了数据频繁更新的问题,不用再用选择器操作 DOM 元素。因为在 MVVM 中,View 不知道 Model 的存在,Model 和 ViewModel 也观察不到 View,这种低耦合模式提高代码的可重用性 😉 Tips 1: 为什么官方要说 Vue 没有完全遵循 MVVM 思想呢? 严格的 MVVM 要求 View 不能和 Model 直接通信,而 Vue 提供了$refs 这个属性,让 Model 可以直接操作 View,违反了这一规定,所以说 Vue 没有完全遵循 MVVM。 🎗 Tips 2: MVVM模型框架 View 层
ViewModel 层
Model 层
二、SPA 与 SSR1、SPASPA( single-page application )仅在 Web 页面初始化时加载相应的 HTML、JavaScript 和 CSS。一旦页面加载完成,SPA 不会因为用户的操作而进行页面的重新加载或跳转;取而代之的是利用路由机制实现 HTML 内容的变换,UI 与用户的交互,避免页面的重新加载。 优点:
缺点:
2、SSR优点:
缺点:
在应用中使用 SSR 之前,你需要问自己的第一个问题是:你是否真的需要它?它通常是由内容呈现时间对应用的重要程度决定的。例如,如果你正在搭建一个内部管理系统,几百毫秒的初始化加载时间对它来说无关紧要,这种情况下就没有必要使用 SSR。然而,如果内容呈现时间非常关键,SSR 可以助你实现最佳的初始加载性能。 🎱 Tips 1: SSR vs 预渲染 如果你仅希望通过 SSR 来改善一些推广页面 (例如 三、Data Property 和方法1、Data Property组件的
这些实例 property 仅在实例首次创建时被添加,所以你需要确保它们都在 直接将不包含在 Vue 使用 👀Tips 1:为什么 data 是一个函数 组件中的 data 写成一个函数,数据以函数返回值形式定义,这样每复用一次组件,就会返回一份新的 data,类似于给每个组件实例创建一个私有的数据空间,让各个组件实例维护各自的数据。而单纯的写成对象形式,就使得所有组件实例共用了一份 data,就会造成一个变了全都会变的结果 2、方法我们用
Vue 自动为 这些
在上面的例子中,点击 也可以直接从模板中调用方法。就像下一章节即将看到的,通常换做计算属性会更好。但是,在计算属性不可行的情况下,使用方法可能会很有用。你可以在模板支持 JavaScript 表达式的任何地方调用方法:
如果 从模板调用的方法不应该有任何副作用,比如更改数据或触发异步进程。如果你想这么做,应该使用生命周期钩子来替换。 3、防抖和节流Vue 没有内置支持防抖和节流,但可以使用 Lodash 等库来实现。 如果某个组件仅使用一次,可以在
但是,这种方法对于可复用组件有潜在的问题,因为它们都共享相同的防抖函数。为了使组件实例彼此独立,可以在生命周期钩子的
完整的使用示例
四、组件通讯1、子传父组件传递数据给父组件是通过$emit 触发事件来做到的
2、父传子父组件向子组件传递数据是通过 prop 传递的 1?? Prop 类型字符串数组形式列出的 prop:
对象形式的prop可以指定值类型
2?? 动态和静态传参
3?? 传入一个对象的全部 property如果想要将一个对象的所有 property 都作为 prop 传入,可以使用不带参数的
下面的模板:
等价于:
3、 获取组件1?? $parent获取父组件 2?? $refs尽管存在 prop 和事件,但有时你可能仍然需要在 JavaScript 中直接访问子组件。为此,可以使用
例如,你希望在组件挂载时,以编程的方式 focus 到这个 input 上,这可能有用:
此外,还可以向组件本身添加另一个
🐱?🏍Tips 1: 注意
3?? Provide / Inject
但是,如果我们尝试在此处 provide 一些组件的实例 property,这将是不起作用的:
要访问组件实例 property,我们需要将
这使我们能够更安全地继续开发该组件,而不必担心可能会更改/删除子组件所依赖的某些内容。这些组件之间的接口仍然是明确定义的,就像 prop 一样。 实际上,你可以将依赖注入看作是“长距离的 prop”,除了:
?Tips 1: 处理响应性 默认情况下,
在这种情况下,任何对 4、 非 Prop 的 Attribute1?? Attribute 继承当组件返回单个根节点时,非 prop 的 attribute 将自动添加到根节点的 attribute 中。例如,在 date-picker 组件的实例中:
如果我们需要通过
同样的规则也适用于事件监听器:
当一个具有
在这种情况下,
2?? 禁用 Attribute 继承如果你不希望组件的根元素继承 attribute,可以在组件的选项中设置 禁用 attribute 继承的常见场景是需要将 attribute 应用于根节点之外的其他元素。 通过将 使用上一节中的 date-picker 组件示例,如果需要将所有非 prop 的 attribute 应用于
有了这个新配置,
3?? 多个根节点上的 Attribute 继承与单个根节点组件不同,具有多个根节点的组件不具有自动 attribute allthrough (隐式贯穿行为)。如果未显式绑定
5、单向数据流所有的 prop 都使得其父子 prop 之间形成了一个单向下行绑定:父级 prop 的更新会向下流动到子组件中,但是反过来则不行。这样会防止从子组件意外变更父级组件的状态,从而导致你的应用的数据流向难以理解。 另外,每次父级组件发生变更时,子组件中所有的 prop 都将会刷新为最新的值。这意味着你不应该在一个子组件内部改变 prop。如果你这样做了,Vue 会在浏览器的控制台中发出警告。 这里有两种常见的试图变更一个 prop 的情形:
注意在 JavaScript 中对象和数组是通过引用传入的,所以对于一个数组或对象类型的 prop 来说,在子组件中改变这个对象或数组本身将会影响到父组件的状态,且 Vue 无法为此向你发出警告。作为一个通用规则,应该避免修改任何 prop,包括对象和数组,因为这种做法无视了单向数据绑定,且可能会导致意料之外的结果。 五、Vue 内置指令1、v-show 与 v-if
相比之下, 一般来说, 2、避免
|
选项式 API | Hook inside setup |
---|---|
beforeCreate | Not needed* |
created | Not needed* |
beforeMount | onBeforeMount |
mounted | onMounted |
beforeUpdate | onBeforeUpdate |
updated | onUpdated |
beforeUnmount | onBeforeUnmount |
unmounted | onUnmounted |
errorCaptured | onErrorCaptured |
renderTracked | onRenderTracked |
renderTriggered | onRenderTriggered |
activated | onActivated |
deactivated | onDeactivated |
? Tips 1: Vue 的父子组件生命周期钩子函数执行顺序
父 beforeCreate->父 created->父 beforeMount->子 beforeCreate->子 created->子 beforeMount->子 mounted->父 mounted
父 beforeUpdate->子 beforeUpdate->子 updated->父 updated
父 beforeUpdate->父 updated
父 beforeDestroy->子 beforeDestroy->子 destroyed->父 destroyed
🎭 Tips 2: 各个生命周期的作用
生命周期 | 描述 |
---|---|
beforeCreate | 组件实例被创建之初,组件的属性生效之前 |
created | 组件实例已经完全创建,属性也绑定,但真实 dom 还没有生成,$el 还不可用 |
beforeMount | 在挂载开始之前被调用:相关的 render 函数首次被调用 |
mounted | el 被新创建的 vm.$el 替换,并挂载到实例上去之后调用该钩子 |
beforeUpdate | 组件数据更新之前调用,发生在虚拟 DOM 打补丁之前 |
update | 组件数据更新之后 |
activited | keep-alive 专属,组件被激活时调用 |
deactivated | keep-alive 专属,组件被销毁时调用 |
beforeDestory | 组件销毁前调用 |
destoryed | 组件销毁后调用 |
生命周期示意图
在 setup()
中使用 provide
时,我们首先从 vue
显式导入 provide
方法。这使我们能够调用 provide
来定义每个 property。
provide
函数允许你通过两个参数定义 property:
<String>
类型)使用 MyMap
组件后,provide 的值可以按如下方式重构:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide } from 'vue'
import MyMarker from './MyMarker.vue'
export default {
components: {
MyMarker
},
setup() {
provide('location', 'North Pole')
provide('geolocation', {
longitude: 90,
latitude: 135
})
}
}
</script>
在 setup()
中使用 inject
时,也需要从 vue
显式导入。导入以后,我们就可以调用它来定义暴露给我们的组件方式。
inject
函数有两个参数:
使用 MyMarker
组件,可以使用以下代码对其进行重构:
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
return {
userLocation,
userGeolocation
}
}
}
</script>
为了增加 provide 值和 inject 值之间的响应性,我们可以在 provide 值时使用 ref 或 reactive。
使用 MyMap
组件,我们的代码可以更新如下:
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
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
})
provide('location', location)
provide('geolocation', geolocation)
}
}
</script>
现在,如果这两个 property 中有任何更改,MyMarker
组件也将自动更新!
当使用响应式 provide / inject 值时,建议尽可能将对响应式 property 的所有修改限制在定义 provide 的组件内部。
例如,在需要更改用户位置的情况下,我们最好在 MyMap
组件中执行此操作。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
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
})
provide('location', location)
provide('geolocation', geolocation)
return {
location
}
},
methods: {
updateLocation() {
this.location = 'South Pole'
}
}
}
</script>
然而,有时我们需要在注入数据的组件内部更新 inject 的数据。在这种情况下,我们建议 provide 一个方法来负责改变响应式 property。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
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)
}
}
</script>
<!-- src/components/MyMarker.vue -->
<script>
import { inject } from 'vue'
export default {
setup() {
const userLocation = inject('location', 'The Universe')
const userGeolocation = inject('geolocation')
const updateUserLocation = inject('updateLocation')
return {
userLocation,
userGeolocation,
updateUserLocation
}
}
}
</script>
最后,如果要确保通过 provide
传递的数据不会被 inject 的组件更改,我们建议对提供者的 property 使用 readonly
。
<!-- src/components/MyMap.vue -->
<template>
<MyMarker />
</template>
<script>
import { provide, reactive, readonly, 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', readonly(location))
provide('geolocation', readonly(geolocation))
provide('updateLocation', updateLocation)
}
}
</script>
is
attribute有的时候,在不同组件之间进行动态切换是非常有用的
<div id="dynamic-component-demo" class="demo">
<button
v-for="tab in tabs"
v-bind:key="tab"
v-bind:class="['tab-button', { active: currentTab === tab }]"
v-on:click="currentTab = tab"
>
{{ tab }}
</button>
<component v-bind:is="currentTabComponent" class="tab"></component>
</div>
const app = Vue.createApp({
data() {
return {
currentTab: 'Home',
tabs: ['Home', 'Posts', 'Archive']
}
},
computed: {
currentTabComponent() {
return 'tab-' + this.currentTab.toLowerCase()
}
}
})
app.component('tab-home', {
template: `<div class="demo-tab">Home component</div>`
})
app.component('tab-posts', {
template: `<div class="demo-tab">Posts component</div>`
})
app.component('tab-archive', {
template: `<div class="demo-tab">Archive component</div>`
})
app.mount('#dynamic-component-demo')
在上述示例中,currentTabComponent
可以包括:
keep-alive
我们之前在一个多标签的界面中使用 is
attribute 来切换不同的组件:
<component :is="currentTabComponent"></component>
当在这些组件之间切换的时候,你有时会想保持这些组件的状态,以避免反复渲染导致的性能问题。
你会注意到,如果你选择了一篇文章,切换到 Archive 标签,然后再切换回 Posts,是不会继续展示你之前选择的文章的。这是因为你每次切换新标签的时候,Vue 都创建了一个新的 currentTabComponent
实例。
重新创建动态组件的行为通常是非常有用的,但是在这个案例中,我们更希望那些标签的组件实例能够被在它们第一次被创建的时候缓存下来。为了解决这个问题,我们可以用一个 <keep-alive>
元素将其动态组件包裹起来。
<!-- 失活的组件将会被缓存!-->
<keep-alive>
<component :is="currentTabComponent"></component>
</keep-alive>
在大型应用中,我们可能需要将应用分割成小一些的代码块,并且只在需要的时候才从服务器加载一个模块。为了实现这个效果,Vue 有一个 defineAsyncComponent
方法:
const { createApp, defineAsyncComponent } = Vue
const 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)
,来表示加载失败。
你也可以在工厂函数中返回一个 Promise
,把 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')
)
}
})
基本例子
const app = Vue.createApp({})
// 注册一个全局自定义指令 `v-focus`
app.directive('focus', {
// 当被绑定的元素挂载到 DOM 中时……
mounted(el) {
// 聚焦元素
el.focus()
}
})
如果想注册局部指令,组件中也接受一个 directives
的选项:
directives: {
focus: {
// 指令的定义
mounted(el) {
el.focus()
}
}
}
然后你可以在模板中任何元素上使用新的 v-focus
attribute,如下:
<input v-focus />
一个指令定义对象可以提供如下几个钩子函数 (均为可选):
created
:在绑定元素的 attribute 或事件监听器被应用之前调用。在指令需要附加在普通的 v-on
事件监听器调用前的事件监听器中时,这很有用。
beforeMount
:当指令第一次绑定到元素并且在挂载父组件之前调用。
mounted
:在绑定元素的父组件被挂载前调用。
beforeUpdate
:在更新包含组件的 VNode 之前调用。
updated
:在包含组件的 VNode 及其子组件的 VNode 更新后调用。
beforeUnmount
:在卸载绑定元素的父组件之前调用
unmounted
:当指令与元素解除绑定且父组件已卸载时,只调用一次。
🏳?🌈 Tips 1: directive
参数:
{string} name
{Function | Object} [definition]
返回值:
definition
参数,则返回应用实例。definition
参数,则返回指令定义。用法:
注册或检索全局指令。
import { createApp } from 'vue'
const app = createApp({})
// 注册
app.directive('my-directive', {
// 指令具有一组生命周期钩子:
// 在绑定元素的 attribute 或事件监听器被应用之前调用
created() {},
// 在绑定元素的父组件挂载之前调用
beforeMount() {},
// 在绑定元素的父组件挂载之后调用
mounted() {},
// 在包含组件的 VNode 更新之前调用
beforeUpdate() {},
// 在包含组件的 VNode 及其子组件的 VNode 更新之后调用
updated() {},
// 在绑定元素的父组件卸载之前调用
beforeUnmount() {},
// 在绑定元素的父组件卸载之后调用
unmounted() {}
})
// 注册 (函数指令)
app.directive('my-directive', () => {
// 这将被作为 `mounted` 和 `updated` 调用
})
// getter, 如果已注册,则返回指令定义
const myDirective = app.directive('my-directive')
指令绑定到的元素。这可用于直接操作 DOM。
包含以下 property 的对象。
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
:一个对象,在注册指令时作为参数传递。例如,在以下指令中app.directive('focus', {
mounted(el) {
el.focus()
}
})
dir
将会是以下对象:
{
mounted(el) {
el.focus()
}
}
一个真实 DOM 元素的蓝图,对应上面收到的 el 参数。
上一个虚拟节点,仅在 beforeUpdate
和 updated
钩子中可用。
除了 el
之外,你应该将这些参数视为只读,并且永远不要修改它们。
<div id="dynamicexample">
<h3>Scroll down inside this section ↓</h3>
<p v-pin:[direction]="200">I am pinned onto the page at 200px to the left.</p>
</div>
const app = Vue.createApp({
data() {
return {
direction: 'right'
}
}
})
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
// binding.arg 是我们传递给指令的参数
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
app.mount('#dynamic-arguments-example')
我们的自定义指令现在已经足够灵活,可以支持一些不同的用例。为了使其更具动态性,我们还可以允许修改绑定值。让我们创建一个附加属性 pinPadding
,并将其绑定到 <input type="range">
。
<div id="dynamicexample">
<h2>Scroll down the page</h2>
<input type="range" min="0" max="500" v-model="pinPadding">
<p v-pin:[direction]="pinPadding">Stick me {{ pinPadding + 'px' }} from the {{ direction || 'top' }} of the page</p>
</div>
const app = Vue.createApp({
data() {
return {
direction: 'right',
pinPadding: 200
}
}
})
让我们扩展指令逻辑以在组件更新后重新计算固定的距离。
app.directive('pin', {
mounted(el, binding) {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
},
updated(el, binding) {
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
}
})
你可能想在 mounted
和 updated
时触发相同行为,而不关心其他的钩子函数。那么你可以通过将这个回调函数传递给指令来实现:
app.directive('pin', (el, binding) => {
el.style.position = 'fixed'
const s = binding.arg || 'top'
el.style[s] = binding.value + 'px'
})
如果指令需要多个值,可以传入一个 JavaScript 对象字面量。记住,指令函数能够接受所有合法的 JavaScript 表达式。
<div v-demo="{ color: 'white', text: 'hello!' }"></div>
app.directive('demo', (el, binding) => {
console.log(binding.value.color) // => "white"
console.log(binding.value.text) // => "hello!"
})
和非 prop 的 attribute 类似,当在组件中使用时,自定义指令总是会被应用在组件的根节点上。
<my-component v-demo="test"></my-component>
app.component('my-component', {
template: `
<div> // v-demo 指令将会被应用在这里
<span>My component content</span>
</div>
`
})
和 attribute 不同,指令不会通过 v-bind="$attrs"
被传入另一个元素。
有了片段支持以后,组件可能会有多个根节点。当被应用在一个多根节点的组件上时,指令会被忽略,并且会抛出一个警告。
Vue 推荐在绝大多数情况下使用模板来创建你的 HTML。然而在一些场景中,你真的需要 JavaScript 的完全编程的能力。这时你可以用渲染函数,它比模板更接近编译器。
让我们深入一个简单的例子,这个例子里 render
函数很实用。假设我们要生成一些带锚点的标题:
<h1>
<a name="hello-world" href="#hello-world">
Hello world!
</a>
</h1>
锚点标题的使用非常频繁,我们应该创建一个组件:
<anchored-heading :level="1">Hello world!</anchored-heading>
当开始写一个只能通过 level
prop 动态生成标题 (heading) 的组件时,我们很快就可以得出这样的结论:
const { createApp } = Vue
const app = createApp({})
app.component('anchored-heading', {
template: `
<h1 v-if="level === 1">
<slot></slot>
</h1>
<h2 v-else-if="level === 2">
<slot></slot>
</h2>
<h3 v-else-if="level === 3">
<slot></slot>
</h3>
<h4 v-else-if="level === 4">
<slot></slot>
</h4>
<h5 v-else-if="level === 5">
<slot></slot>
</h5>
<h6 v-else-if="level === 6">
<slot></slot>
</h6>
`,
props: {
level: {
type: Number,
required: true
}
}
})
这个模板感觉不太好。它不仅冗长,而且我们为每个级别标题重复书写了 <slot></slot>
。当我们添加锚元素时,我们必须在每个 v-if/v-else-if
分支中再次重复它。
虽然模板在大多数组件中都非常好用,但是显然在这里它就不合适了。那么,我们来尝试使用 render
函数重写上面的例子:
const { createApp, h } = Vue
const app = createApp({})
app.component('anchored-heading', {
render() {
return h(
'h' + this.level, // 标签名
{}, // prop 或 attribute
this.$slots.default() // 包含其子节点的数组
)
},
props: {
level: {
type: Number,
required: true
}
}
})
render()
函数的实现要精简得多,但是需要非常熟悉组件的实例 property。在这个例子中,你需要知道,向组件中传递不带 v-slot
指令的子节点时,比如 anchored-heading
中的 Hello world!
,这些子节点被存储在组件实例中的 $slots.default
中。
Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:
return h('h1', {}, this.blogTitle)
h()
到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为 VNode。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。
🌷 Tips 1: h()
参数
h()
函数是一个用于创建 VNode 的实用程序。也许可以更准确地将其命名为 createVNode()
,但由于频繁使用和简洁,它被称为 h()
。它接受三个参数:
// @returns {VNode}
h(
// {String | Object | Function} tag
// 一个 HTML 标签名、一个组件、一个异步组件、或
// 一个函数式组件。
//
// 必需的。
'div',
// {Object} props
// 与 attribute、prop 和事件相对应的对象。
// 这会在模板中用到。
//
// 可选的。
{},
// {String | Array | Object} children
// 子 VNodes, 使用 `h()` 构建,
// 或使用字符串获取 "文本 VNode" 或者
// 有插槽的对象。
//
// 可选的。
[
'Some text comes first.',
h('h1', 'A headline'),
h(MyComponent, {
someProp: 'foobar'
})
]
)
如果没有 prop,那么通常可以将 children 作为第二个参数传入。如果会产生歧义,可以将 null
作为第二个参数传入,将 children 作为第三个参数传入。
完整实例
const { createApp, h } = Vue
const app = createApp({})
/** 递归地从子节点获取文本 */
function getChildrenTextContent(children) {
return children
.map(node => {
return typeof node.children === 'string'
? node.children
: Array.isArray(node.children)
? getChildrenTextContent(node.children)
: ''
})
.join('')
}
app.component('anchored-heading', {
render() {
// 从 children 的文本内容中创建短横线分隔 (kebab-case) id。
const headingId = getChildrenTextContent(this.$slots.default())
.toLowerCase()
.replace(/\W+/g, '-') // 用短横线替换非单词字符
.replace(/(^-|-$)/g, '') // 删除前后短横线
return h('h' + this.level, [
h(
'a',
{
name: headingId,
href: '#' + headingId
},
this.$slots.default()
)
])
},
props: {
level: {
type: Number,
required: true
}
}
})
🚂 Tips 2: 约束 ------ VNodes 必须唯一
组件树中的所有 VNode 必须是唯一的。这意味着,下面的渲染函数是不合法的:
render() {
const myParagraphVNode = h('p', 'hi')
return h('div', [
// 错误 - 重复的 Vnode!
myParagraphVNode, myParagraphVNode
])
}
如果你真的需要重复很多次的元素/组件,你可以使用工厂函数来实现。例如,下面这渲染函数用完全合法的方式渲染了 20 个相同的段落:
render() {
return h('div',
Array.from({ length: 20 }).map(() => {
return h('p', 'hi')
})
)
}
🛩 Tips 3: 虚拟 DOM 有什么优缺点
由于在浏览器中操作 DOM 是很昂贵的。频繁的操作 DOM,会产生一定的性能问题。这就是虚拟 Dom 的产生原因。Vue2 的 Virtual DOM 借鉴了开源库 snabbdom 的实现。Virtual DOM 本质就是用一个原生的 JS 对象去描述一个 DOM 节点,是对真实 DOM 的一层抽象。
优点:
缺点:
💎 Tips 4: 虚拟 DOM 实现原理
虚拟 DOM 的实现原理主要包括以下 3 部分:
要为某个组件创建一个 VNode,传递给 h
的第一个参数应该是组件本身。
render() {
return h(ButtonCounter)
}
如果我们需要通过名称来解析一个组件,那么我们可以调用 resolveComponent
:
const { h, resolveComponent } = Vue
// ...
render() {
const ButtonCounter = resolveComponent('ButtonCounter')
return h(ButtonCounter)
}
resolveComponent
是模板内部用来解析组件名称的同一个函数。
render
函数通常只需要对全局注册的组件使用 resolveComponent
。而对于局部注册的却可以跳过,请看下面的例子:
// 此写法可以简化
components: {
ButtonCounter
},
render() {
return h(resolveComponent('ButtonCounter'))
}
我们可以直接使用它,而不是通过名称注册一个组件,然后再查找:
render() {
return h(ButtonCounter)
}
v-if
和 v-for
只要在原生的 JavaScript 中可以轻松完成的操作,Vue 的渲染函数就不会提供专有的替代方法。比如,在模板中使用的 v-if
和 v-for
:
<ul v-if="items.length">
<li v-for="item in items">{{ item.name }}</li>
</ul>
<p v-else>No items found.</p>
这些都可以在渲染函数中用 JavaScript 的 if
/else
和 map()
来重写:
props: ['items'],
render() {
if (this.items.length) {
return h('ul', this.items.map((item) => {
return h('li', item.name)
}))
} else {
return h('p', 'No items found.')
}
}
v-model
v-model
指令扩展为 modelValue
和 onUpdate:modelValue
在模板编译过程中,我们必须自己提供这些 prop:
props: ['modelValue'],
emits: ['update:modelValue'],
render() {
return h(SomeComponent, {
modelValue: this.modelValue,
'onUpdate:modelValue': value => this.$emit('update:modelValue', value)
})
}
v-on
我们必须为事件处理程序提供一个正确的 prop 名称,例如,要处理 click
事件,prop 名称应该是 onClick
。
render() {
return h('div', {
onClick: $event => console.log('clicked', $event.target)
})
}
对于 .passive
、.capture
和 .once
事件修饰符,可以使用驼峰写法将他们拼接在事件名后面:
实例:
render() {
return h('input', {
onClickCapture: this.doThisInCapturingMode,
onKeyupOnce: this.doThisOnce,
onMouseoverOnceCapture: this.doThisOnceInCapturingMode
})
}
对于所有其它的修饰符,私有前缀都不是必须的,因为你可以在事件处理函数中使用事件方法:
修饰符 | 处理函数中的等价操作 |
---|---|
.stop | event.stopPropagation() |
.prevent | event.preventDefault() |
.self | if (event.target !== event.currentTarget) return |
按键: .enter , .13 | if (event.keyCode !== 13) return (对于别的按键修饰符来说,可将 13 改为另一个按键码 |
修饰键: .ctrl , .alt , .shift , .meta | if (!event.ctrlKey) return (将 ctrlKey 分别修改为 altKey , shiftKey , 或 metaKey ) |
这里是一个使用所有修饰符的例子:
render() {
return h('input', {
onKeyUp: event => {
// 如果触发事件的元素不是事件绑定的元素
// 则返回
if (event.target !== event.currentTarget) return
// 如果向上键不是回车键,则终止
// 没有同时按下按键 (13) 和 shift 键
if (!event.shiftKey || event.keyCode !== 13) return
// 停止事件传播
event.stopPropagation()
// 阻止该元素默认的 keyup 事件
event.preventDefault()
// ...
}
})
}
你可以通过 [this.$slots
] 访问静态插槽的内容,每个插槽都是一个 VNode 数组:
render() {
// `<div><slot></slot></div>`
return h('div', {}, this.$slots.default())
}
props: ['message'],
render() {
// `<div><slot :text="message"></slot></div>`
return h('div', {}, this.$slots.default({
text: this.message
}))
}
要使用渲染函数将插槽传递给子组件,请执行以下操作:
const { h, resolveComponent } = Vue
render() {
// `<div><child v-slot="props"><span>{{ props.text }}</span></child></div>`
return h('div', [
h(
resolveComponent('child'),
{},
// 将 `slots` 以 { name: props => VNode | Array<VNode> } 的形式传递给子对象。
{
default: (props) => Vue.h('span', props.text)
}
)
])
}
插槽以函数的形式传递,允许子组件控制每个插槽内容的创建。任何响应式数据都应该在插槽函数内访问,以确保它被注册为子组件的依赖关系,而不是父组件。相反,对 resolveComponent
的调用应该在插槽函数之外进行,否则它们会相对于错误的组件进行解析。
// `<MyButton><MyIcon :name="icon" />{{ text }}</MyButton>`
render() {
// 应该是在插槽函数外面调用 resolveComponent。
const Button = resolveComponent('MyButton')
const Icon = resolveComponent('MyIcon')
return h(
Button,
null,
{
// 使用箭头函数保存 `this` 的值
default: (props) => {
// 响应式 property 应该在插槽函数内部读取,
// 这样它们就会成为 children 渲染的依赖。
return [
h(Icon, { name: this.icon }),
this.text
]
}
}
)
}
如果一个组件从它的父组件中接收到插槽,它们可以直接传递给子组件。
render() {
return h(Panel, null, this.$slots)
}
也可以根据情况单独传递或包裹住。
render() {
return h(
Panel,
null,
{
// 如果我们想传递一个槽函数,我们可以通过
header: this.$slots.header,
// 如果我们需要以某种方式对插槽进行操作,
// 那么我们需要用一个新的函数来包裹它
default: (props) => {
const children = this.$slots.default ? this.$slots.default(props) : []
return children.concat(h('div', 'Extra child'))
}
}
)
}
<component>
和 is
在底层实现里,模板使用 resolveDynamicComponent
来实现 is
attribute。如果我们在 render
函数中需要 is
提供的所有灵活性,我们可以使用同样的函数:
const { h, resolveDynamicComponent } = Vue
// ...
// `<component :is="name"></component>`
render() {
const Component = resolveDynamicComponent(this.name)
return h(Component)
}
就像 is
, resolveDynamicComponent
支持传递一个组件名称、一个 HTML 元素名称或一个组件选项对象。
通常这种程度的灵活性是不需要的。通常 resolveDynamicComponent
可以被换做一个更直接的替代方案。
例如,如果我们只需要支持组件名称,那么可以使用 resolveComponent
来代替。
如果 VNode 始终是一个 HTML 元素,那么我们可以直接把它的名字传递给 h
:
// `<component :is="bold ? 'strong' : 'em'"></component>`
render() {
return h(this.bold ? 'strong' : 'em')
}
同样,如果传递给 is
的值是一个组件选项对象,那么不需要解析什么,可以直接作为 h
的第一个参数传递。
与 <template>
标签一样,<component>
标签仅在模板中作为语法占位符需要,当迁移到 render
函数时,应被丢弃。
可以使用 withDirectives
将自定义指令应用于 VNode:
const { h, resolveDirective, withDirectives } = Vue
// ...
// <div v-pin:top.animate="200"></div>
render () {
const pin = resolveDirective('pin')
return withDirectives(h('div'), [
[pin, 200, 'top', { animate: true }]
])
}
resolveDirective
是模板内部用来解析指令名称的同一个函数。只有当你还没有直接访问指令的定义对象时,才需要这样做。
诸如 <keep-alive>
、<transition>
、<transition-group>
和 <teleport>
等内置组件默认并没有被全局注册。这使得打包工具可以 tree-shake,因此这些组件只会在被用到的时候被引入构建。不过这也意味着我们无法通过 resolveComponent
或 resolveDynamicComponent
访问它们。
在模板中这些组件会被特殊处理,即在它们被用到的时候自动导入。当我们编写自己的 render
函数时,需要自行导入它们:
const { h, KeepAlive, Teleport, Transition, TransitionGroup } = Vue
// ...
render () {
return h(Transition, { mode: 'out-in' }, /* ... */)
}
在我们目前看过的所有示例中,render
函数返回的是单个根 VNode。但其实也有别的选项。
返回一个字符串时会创建一个文本 VNode,而不被包裹任何元素:
render() {
return 'Hello world!'
}
我们也可以返回一个子元素数组,而不把它们包裹在一个根结点里。这会创建一个片段 (fragment):
// 相当于模板 `Hello<br>world!`
render() {
return [
'Hello',
h('br'),
'world!'
]
}
可能是因为数据依然在加载中的关系,组件不需要渲染,这时它可以返回 null
。这样我们在 DOM 中会渲染一个注释节点。
Babel 插件,用于在 Vue 中使用 JSX 语法,它可以让我们回到更接近于模板的语法上。
import AnchoredHeading from './AnchoredHeading.vue'
const app = createApp({
render() {
return (
<AnchoredHeading level={1}>
<span>Hello</span> world!
</AnchoredHeading>
)
}
})
app.mount('#demo')
函数式组件是自身没有任何状态的组件的另一种形式。它们在渲染过程中不会创建组件实例,并跳过常规的组件生命周期。
我们使用的是一个简单函数,而不是一个选项对象,来创建函数式组件。该函数实际上就是该组件的 render
函数。而因为函数式组件里没有 this
引用,Vue 会把 props
当作第一个参数传入:
const FunctionalComponent = (props, context) => {
// ...
}
第二个参数 context
包含三个 property:attrs
、emit
和 slots
。它们分别相当于实例的 $attrs
、$emit
和 $slots
这几个 property。
大多数常规组件的配置选项在函数式组件中都不可用。然而我们还是可以把 props
和 emits
作为 property 加入,以达到定义它们的目的:
FunctionalComponent.props = ['value']
FunctionalComponent.emits = ['click']
如果这个 props
选项没有被定义,那么被传入函数的 props
对象就会像 attrs
一样会包含所有 attribute。除非指定了 props
选项,否则每个 prop 的名字将不会基于驼峰命名法被一般化处理。
函数式组件可以像普通组件一样被注册和消费。如果你将一个函数作为第一个参数传入 h
,它将会被当作一个函数式组件来对待。
🏰 Tips 1: 模板编译原理
Vue 的模板实际上被编译成了渲染函数。
分为三步:
为了能够在数值变化时,随时运行我们的总和,我们首先要做的是将其包裹在一个函数中。
const updateSum = () => {
sum = val1 + val2
}
但我们如何告知 Vue 这个函数呢?
Vue 通过一个副作用 (effect) 来跟踪当前正在运行的函数。副作用是一个函数的包裹器,在函数被调用之前就启动跟踪。Vue 知道哪个副作用在何时运行,并能在需要时再次执行它。
为了更好地理解这一点,让我们尝试脱离 Vue 实现类似的东西,以看看它如何工作。
我们需要的是能够包裹总和的东西,像这样:
createEffect(() => {
sum = val1 + val2
})
我们需要 createEffect
来跟踪和执行。我们的实现如下:
// 维持一个执行副作用的栈
const runningEffects = []
const createEffect = fn => {
// 将传来的 fn 包裹在一个副作用函数中
const effect = () => {
runningEffects.push(effect)
fn()
runningEffects.pop()
}
// 立即自动执行副作用
effect()
}
当我们的副作用被调用时,在调用 fn
之前,它会把自己推到 runningEffects
数组中。这个数组可以用来检查当前正在运行的副作用。
副作用是许多关键功能的起点。例如,组件的渲染和计算属性都在内部使用副作用。任何时候,只要有东西对数据变化做出奇妙的回应,你就可以肯定它已经被包裹在一个副作用中了。
当我们从一个组件的 data
函数中返回一个普通的 JavaScript 对象时,Vue 会将该对象包裹在一个带有 get
和 set
处理程序的Proxy中。Proxy 是在 ES6 中引入的,它使 Vue 3 避免了 Vue 早期版本中存在的一些响应性问题。
🥤 Tips 1: Proxy
A Proxy
is created with two parameters:
target
: 要代理的原始对象handler
: 一个对象,它定义了哪些操作将被拦截以及如何重新定义被拦截的操作。const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property) {
console.log('intercepted!')
return target[property]
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// intercepted!
// tacos
除了控制台日志,我们可以在这里做任何我们想做的事情。如果我们愿意,我们甚至可以不返回实际值。这就是为什么 Proxy 对于创建 API 如此强大。
使用 Proxy 的一个难点是 this
绑定。我们希望任何方法都绑定到这个 Proxy,而不是目标对象,这样我们也可以拦截它们。值得庆幸的是,ES6 引入了另一个名为 Reflect
的新特性,它允许我们以最小的代价消除了这个问题:
const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
return Reflect.get(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
Vue 实现响应性的关键步骤:
get
处理函数中 track
函数记录了该 property 和当前副作用。set
处理函数。trigger
函数查找哪些副作用依赖于该 property 并执行它们。const dinner = {
meal: 'tacos'
}
const handler = {
get(target, property, receiver) {
track(target, property)
return Reflect.get(...arguments)
},
set(target, property, value, receiver) {
trigger(target, property)
return Reflect.set(...arguments)
}
}
const proxy = new Proxy(dinner, handler)
console.log(proxy.meal)
// tacos
从 Vue 3 开始,我们的响应性现在可以在一个独立包中使用。将 $data
包裹在一个代理中的函数被称为 reactive
。我们可以自己直接调用这个函数,允许我们在不需要使用组件的情况下将一个对象包裹在一个响应式代理中。
🍚 Tips 1: reactive和ref的区别
reactive 和 ref 都是用来定义响应式数据的 reactive更推荐去定义复杂的数据类型 ref 更推荐定义基本类型
ref 和 reactive 本质我们可以简单的理解为ref是对reactive的二次包装, ref定义的数据访问的时候要多一个.value
使用ref定义基本数据类型,ref也可以定义数组和对象。
🍳 Tips 2: reactive
将解包所有深层的 refs,同时维持 ref 的响应性。
const count = ref(1)
const obj = reactive({ count })
// ref 会被解包
console.log(obj.count === count.value) // true
// 它会更新 `obj.count`
count.value++
console.log(count.value) // 2
console.log(obj.count) // 2
// 它也会更新 `count` ref
obj.count++
console.log(obj.count) // 3
console.log(count.value) // 3
🥞 Tips 3: 当将 ref 分配给 reactive
property 时,ref 将被自动解包。
const count = ref(1)
const obj = reactive({})
obj.count = count
console.log(obj.count) // 1
console.log(obj.count === count.value) // true
Vue 在内部跟踪所有已经被转成响应式的对象,所以它总是为同一个对象返回相同的代理。
当从一个响应式代理中访问一个嵌套对象时,该对象在被返回之前也被转换为一个代理:
const handler = {
get(target, property, receiver) {
track(target, property)
const value = Reflect.get(...arguments)
if (isObject(value)) {
// 将嵌套对象包裹在自己的响应式代理中
return reactive(value)
} else {
return value
}
}
// ...
}
Proxy 的使用确实引入了一个需要注意的新警告:在身份比较方面,被代理对象与原始对象不相等 (===
)。例如:
const obj = {}
const wrapped = new Proxy(obj, handlers)
console.log(obj === wrapped) // false
其他依赖严格等于比较的操作也会受到影响,例如 .includes()
或 .indexOf()
。
这里的最佳实践是永远不要持有对原始对象的引用,而只使用响应式版本。
const obj = reactive({
count: 0
}) // 未引用原始
这确保了等值的比较和响应性的行为都符合预期。
请注意,Vue 不会在 Proxy 中包裹数字或字符串等原始值,所以你仍然可以对这些值直接使用 ===
来比较:
const obj = reactive({
count: 0
})
console.log(obj.count === 0) // true
一个组件的模板被编译成一个 render
函数。渲染函数创建 VNodes,描述该组件应该如何被渲染。它被包裹在一个副作用中,允许 Vue 在运行时跟踪被“触达”的 property。
一个 render
函数在概念上与一个 computed
property 非常相似。Vue 并不确切地追踪依赖关系是如何被使用的,它只知道在函数运行的某个时间点上使用了这些依赖关系。如果这些 property 中的任何一个随后发生了变化,它将触发副作用再次运行,重新运行 render
函数以生成新的 VNodes。然后这些举动被用来对 DOM 进行必要的修改。
要为 JavaScript 对象创建响应式状态,可以使用 reactive
方法:
import { reactive } from 'vue'
// 响应式状态
const state = reactive({
count: 0
})
reactive
相当于 Vue 2.x 中的 Vue.observable()
API,为避免与 RxJS 中的 observables 混淆因此对其重命名。该 API 返回一个响应式的对象状态。该响应式转换是“深度转换”——它会影响传递对象的所有嵌套 property。
Vue 中响应式状态的基本用例是我们可以在渲染期间使用它。因为依赖跟踪的关系,当响应式状态改变时视图会自动更新。
这就是 Vue 响应性系统的本质。当从组件中的 data()
返回一个对象时,它在内部交由 reactive()
使其成为响应式对象。模板会被编译成能够使用这些响应式 property 的渲染函数。
refs
ref
会返回一个可变的响应式对象,该对象作为一个响应式的引用维护着它内部的值,这就是 ref
名称的来源。该对象只包含一个名为 value
的 property:
import { ref } from 'vue'
const count = ref(0)
console.log(count.value) // 0
count.value++
console.log(count.value) // 1
当 ref 作为渲染上下文 (从 setup 中返回的对象) 上的 property 返回并可以在模板中被访问时,它将自动浅层次解包内部值。只有访问嵌套的 ref 时需要在模板中添加 .value
:
<template>
<div>
<span>{{ count }}</span>
<button @click="count ++">Increment count</button>
<button @click="nested.count.value ++">Nested Increment count</button>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
return {
count,
nested: {
count
}
}
}
}
</script>
当 ref
作为响应式对象的 property 被访问或更改时,为使其行为类似于普通 property,它会自动解包内部值:
const count = ref(0)
const state = reactive({
count
})
console.log(state.count) // 0
state.count = 1
console.log(count.value) // 1
如果将新的 ref 赋值给现有 ref 的 property,将会替换旧的 ref:
const otherCount = ref(2)
state.count = otherCount
console.log(state.count) // 2
console.log(count.value) // 1
Ref 解包仅发生在被响应式 Object
嵌套的时候。当从 Array
或原生集合类型如 Map
访问 ref 时,不会进行解包:
const books = reactive([ref('Vue 3 Guide')])
// 这里需要 .value
console.log(books[0].value)
const map = reactive(new Map([['count', ref(0)]]))
// 这里需要 .value
console.log(map.get('count').value)
import { reactive, toRefs } from 'vue'
const book = reactive({
author: 'Vue Team',
year: '2020',
title: 'Vue 3 Guide',
description: 'You are reading this book right now ;)',
price: 'free'
})
let { author, title } = toRefs(book)///如果直接用ES6结构会失去响应性
title.value = 'Vue 3 Detailed Guide' // 我们需要使用 .value 作为标题,现在是 ref
console.log(book.title) // 'Vue 3 Detailed Guide'
🎪 Tips 1: vue2 和 vue3 对数组的响应性对比
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Vue 测试数组响应式实例</title>
<!-- vue2 -->
<script src="https://cdn.staticfile.org/vue/2.2.2/vue.min.js"></script>
<!-- vue3 -->
<!-- <script src="https://unpkg.com/vue@next"></script> -->
</head>
<body>
<div id="app">
<h1>一维数组(length={{message.length}})</h1>
<p v-for="item in message">{{item}}</p>
<h1>JSON数组(length={{message1.length}})</h1>
<p v-for="item in message1">{{item.name}}</p>
</div>
<script>
console.log(typeof(Vue))
if(typeof(Vue) === 'function'){//vue2走这,可更换引入的js切换到vue3
new Vue({
el: '#app',
data() {
return {
message: [
1,
],
message1: [
{name:'name'},
],
}
},
mounted() {
setTimeout(()=>{
//操作一维数组, 一个个试,不要一次性打开多个
this.message[0] = 111 //不触发更新
// this.message.length = 99 //不触发更新
// this.message.push(222) //更新
// this.$set(this.message,2,333) //更新
//操作JSON数组
// this.message1[0] = {name:'name111'} //不触发更新
// this.message1.length = 99 //不触发更新
// this.message1.push({name:'name222'}) //更新
// this.$set(this.message1,2,{name:'name333'}) //更新
console.log('一维数组:',this.message)
console.log('JSON数组:',this.message1)
},2000)
},
})
}else if( typeof(Vue) ==='object'){ //vue3走这,可更换引入的js切换到vue2
const app = {
data() {
return {
message: [
1,
],
message1: [
{name:'name'},
],
}
},
mounted() {
setTimeout(()=>{
//操作一维数组, 一个个试,不要一次性打开多个
// this.message[0] = 111 //会更新
// this.message.length = 99 //会更新
// this.message.push(222) //会更新
// this.$set(this.message,2,333) //报错,vue3没有set方法
//操作JSON数组
// this.message1[0] = {name:'name111'} //会更新
// this.message1.length = 99 //会更新, 会报错可以先把<p v-for="item in message1">{{item.name}}</p>这块注释掉
// this.message1.push({name:'name222'}) //会更新
// this.$set(this.message1,2,{name:'name333'}) //报错,vue3没有set方法
console.log('一维数组:',this.message)
console.log('JSON数组:',this.message1)
},2000)
},
}
Vue.createApp(app).mount('#app')
}
</script>
</body>
</html>
|
JavaScript知识库 最新文章 |
ES6的相关知识点 |
react 函数式组件 & react其他一些总结 |
Vue基础超详细 |
前端JS也可以连点成线(Vue中运用 AntVG6) |
Vue事件处理的基本使用 |
Vue后台项目的记录 (一) |
前后端分离vue跨域,devServer配置proxy代理 |
TypeScript |
初识vuex |
vue项目安装包指令收集 |
|
上一篇文章 下一篇文章 查看所有文章 |
|
开发:
C++知识库
Java知识库
JavaScript
Python
PHP知识库
人工智能
区块链
大数据
移动开发
嵌入式
开发工具
数据结构与算法
开发测试
游戏开发
网络协议
系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程 数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁 |
360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 | -2024/11/24 12:43:02- |
|
网站联系: qq:121756557 email:121756557@qq.com IT数码 |