<div id="app">
<fieldset>
<legend>组件实例</legend>
{{ message }}
<button @click="change">Click</button>
</fieldset>
</div>
<script>
const App = {
data() {
return {
message: 'Hello Vue!!'
}
},
methods: {
change() {
console.log(this)
this.message += '!'
}
},
}
const app = Vue.createApp(App)
console.log(app)
const vm = app.mount('#app')
console.log(vm)
</script>
app:应用实例 vm:组件实例 this指的是什么?this指的是组件实例
组件生命周期
beforeCreate
实例初始化完成后,数据观测(data observer)和 event/watcher 事件配置之前调用
Created
在实例创建完成后被立即调用。在这一步,实例已经完成以下的配置,数据观测、property和方法的运算,watch/event 事件回调。然而,挂载阶段还没开始,$elproperty目前不能用
beforeMount
在挂载之前调用:相关的render函数被首次调用
Mounted
实例在被挂载后调用,这时Vue.createApp({}).mount()被新创建的vm.$el替换了。如果根实例挂载到了一个文档内的元素上,当mounted被调用时vm.$el也在文档内。
beforeUpdated
数据更新时调用,发生在虚拟 DOM 打补丁之前
Updated
由于数据更改导致的虚拟 DOM 重新渲染和打补丁,在这之后会调用该钩子
beforeUnmount
在卸载组件之前调用。在这个阶段,实例仍然是完全正常的
unmounted
卸载组件后被调用。调用此钩子前,组件实例的所有指令都被解除绑定,所有事件侦听器都被移除,所有子组件实例被卸载 例子:
<div id="app">
<fieldset>
<legend>⽣命周期钩⼦</legend>
<span id="msg">{{ message }}</span>
<button @click="change">Click</button>
</fieldset>
</div>
<script>
const App = {
data() {
return { message: 'Hello Vue'}
},
methods: {
change() {
this.message += '!'
}
},
beforeCreate() {
console.log('beforeCreate', this.message, this.$el)
},
created() {
console.log('created', this.message, this.$el)
},
beforeMount() {
console.log('beforeMount', this.message, this.$el)
},
mounted() {
console.log('mounted', this.message, this.$el)
},
beforeUpdate() {
console.log('beforeUpdate', this.message, document.querySelector('#msg').innerText)
},
updated() {
console.log('updated', this.message, document.querySelector('#msg').innerText)
}
}
Vue.createApp(App).mount('#app')
</script>
例子2:
const App = {
data() {
return { isShow: true }
},
beforeCreate() {
console.log('root beforeCreate', this.message, this.$el)
},
created() {
console.log('root created', this.message, this.$el)
},
beforeMount() {
console.log('root beforeMount', this.message, this.$el)
},
mounted() {
console.log('root mounted', this.message, this.$el)
}
}
const app = Vue.createApp(App)
app.component('child', {
template: '<div>{{text}}</div>',
data() {
return {text: 'I am child'}
},
created() {
console.log('child created')
},
mounted() {
console.log('child mounted')
},
beforeUnmount() {
console.log('child beforeUnmount')
},
unmounted() {
console.log('child unmounted')
},
})
app.mount('#app')
<div id="app">
<fieldset>
<legend>⽣命周期钩⼦</legend>
<child v-if="isShow"></child>
<button @click="isShow = !isShow">Click</button>
</fieldset>
</div>
由此可以解决四个问题:
• Vue有哪些生命周期钩⼦?
• 如果需要发送Ajax请求,最好放在哪个钩⼦内?
理论上除了beforecreate,unmounted外都可以放
• 父子组件嵌套时,父组件视图和子组件视图渲染完成谁先谁后?
由上图可知不确定。因为子组件先挂载,但是没有父组件视图上也不会显示。
• 父子组件嵌套时,如果希望在所有组件视图都渲染完成后再执行操作,该如何做?
mounted(){
this.$nextTick(function){
// 仅在渲染整个视图之后运行的代码
}
}
模板语法:
<span v-once>这个将不会改变: {{ msg }}</span>
<span v-html="rawHtml"></span>
<a :href="url"> ... </a>
<a v-bind:href="url"> ... </a>
<a v-on:click="doSomething"> ... </a>
<a @click="doSomething"> ... </a>
<a @[event]="doSomething"> ... </a>
注意事项:
- 不能在模板表达式里访问用户定义的全局变量
- v-html容易引发XSS,谨慎使用
Data Property 和 methods
- data选项是一个函数,返回一个对象
- 对尚未提供所需值的property使用null、undefined等占位
- 实例创建后再添加的property,响应式系统不会跟踪
注意事项:
- Vue2的data可以是对象,但Vue3的data只能是函数
- Vue2的自定义组件里的data要使用函数,否则创建的多个自定义组件实例会共用数据。一般来说组件需要维护的状态应该是独立的。
计算属性
要点:计算属性会依赖data中的属性,属性发生改变自动触发计算属性的变化 注意事项:
- 计算属性有缓存机制,如果依赖的数据未发生改变,则不会重新计算而是直接使用缓存值
- 注意methods和computed里面的方法不要使用箭头函数,否则this就不是vm对象了
Watch
要点:watch会监控data中某个property的变化,执行函数 什么时候需要用watch:
- 当只需要根据data中某个property的变化做出反应,但不一定需要结果
- 当有异步操作时
- 当需要用旧值时
四个问题:
• v-text 和 v-html 有什么区别
v-text和v-html都是模板插值,前者相当于innerText,会把字符串作为文本内容插入页面;后者相当于innerHTML,会把字符串作为HTML片段插入页面,这会引起XSS攻击。
• data 为什么要是函数
Vue3里强制规定data的值为函数,Vue2里未做强制规定。在Vue2里一般建议data的值为函数,返回一个新的对象,特别是针对自定义组件。如果自定义组件的data的值是一个对象,这个对象会被多个组件实例共享,带来数据管理的混乱。
• 计算属性缓存是什么
当依赖data中的某个属性发生变化时,依赖该属性的计算属性会自动变化。计算属性有缓存机制,如果依赖的数据未发生改变,使用该计算属性时不会重新计算而是直接使用缓存值。
• watch、计算属性有什么区别
watch会监控data中某个property的变化,执行函数。而计算属性是根据监控某个property变化得到的一个返回值。 在这些场景使用watch:1. 当只需要根据data 中某个property的变化做出反应,但不⼀定需要结果 时;2. 当有异步操作时;3. 当需要用旧值时
响应式原理
数据响应式
- 追踪数据的变化,在读取数据或者设置数据时能劫持做一些操作
- 使用 Object.defineProperty
- 使用Proxy
Object.defineProperty 实现响应式
function observe(data) {
if(!data || typeof data !== 'obeject') return
for(key in data) {
let val = data[key]
Object.defineProperty(data,key,{
enumrable: true,
configurable: true,
get: function() {
track(data,key)
return val
},
set: function() {
trigger(data,key,newval)
val = newVal
}
})
if(typeof val === 'object'){
observe(val)
}
}
}
function track(data, key) {
console.log('get data ', key)
}
function trigger(data, key, value) {
console.log('set data', key, ":",
value)
}
const data = {
name: 'yang',
friends: [1,2,3]
}
observe(data)
console.log(data.name)
data.name = 'valley'
data.friends[0] = 4
data.friends[3] = 5 // ⾮响应式
data.age = 6 //⾮响应式
Proxy 和 Reflect
Proxy 对象用于创建一个对象的代理,从而实现基本操作的拦截和自定义(如属性查找,赋值,枚举,函数调用等)
Reflect 是一个内置的对象,它提供拦截 JS 操作的方法,这些方法与Proxy handler相同
Reflect.set(target, propertyKey, value [, receiver]) 将值分配给属性的函数。返回⼀个Boolean,如果更 新成功,则返回true。
Reflect.get(target, propertyKey[, receiver]) 获取对象身上某个属性的值,类似 于 target[name]。
Proxy 实现响应式
function reactive(obj) {
const handler = {
get(target,prop,receiver){
track(target,prop)
const value = Reflect.get(...arguments)
if(typeof value === 'object'){
return reactive(value)
}else {
return value
}
},
set(target,key,value,receiver){
trigger(target,key,value)
return Reflect.set(...arguments)
}
}
return new Proxy(obj,handler)
}
function track(data, key) {
console.log('get data ', key)
}
function trigger(data, key, value) {
console.log('set data', key, ":", value)
}
const dinner = {
meal: 'tacos'
}
const proxy = reactive(dinner)
proxy.meal = 'apple'
proxy.list = []
proxy.list.push(1) //响应式
两个问题:
- Vue3和Vue2的响应式原理有什么不同?
前者用proxy,后者使用Object.defineProperty
- 用Proxy和Object.defineProperty相比有什么优点
Proxy可以劫持整个对象,而Object.defineProperty只能劫持对象的属性;前者递归返回属性对应的值的代理即可实现响应式,后者需要深度遍历每个属性;后者对于数组很不友好
Vue 条件渲染、列表、事件、组件
v-if 和 v-show 的区别在哪里?
v-if会创建和删除元素,对生命周期来说它会 created 然后 unmounted;v-show 会创建而不会删除元素,而是用 display:none 来隐藏,对应的是 created 一个生命周期。
v-for 列表渲染
- v-for 可以基于数组渲染列表
- 也可基于对象渲染列表
- 可以使用值的范围
- 可在组件上循环渲染
- v-for 默认使用 “就地更新”策略,数据项的顺序被改变,Vue 不会移动DOM元素来匹配数据项的顺序,而是就地更新每个元素。这在小部分情况下,会造成BUG
- 为能跟踪每个节点的身份,重用和重新排序现有元素,提升性能,需要使用key
事件处理
- @click的值既可以是methods里的函数名,执行函数时参数是点击事件
- 也可以是函数的调用,执行函数时参数是调用时传递的参数,可以传递固定值,可以传递data的属性,也可传递$event
@click="fn()"和@click="fn"都对
v-model
<input v-model="message" /> {{ message }}
<textarea v-model.lazy="message"></textarea> {{ message }}
<input type="checkbox" v-model="checked" /> {{checked}}
<!-- 复选框 -->
<input type="checkbox" value="a" v-model="list" />
<input type="checkbox" value="b" v-model="list" /> {{list}}
<!-- 单选框 -->
<input type="radio" value="a" v-model="theme" />
<input type="radio" value="b" v-model="theme" /> {{theme}}
<!-- select -->
<select v-model="selected">
<option value="AA">A</option>
<option value="BB">B</option>
<option value="CC">C</option>
</select>
{{selected}}
组件基础
每个组件维护独立数据 父组件通过prop向子组件传递数据 子组件通过触发事件来实现子传父
<div id="app">
<font-size step="1" :val="fontSize" @plus="fontSize += $event"
@minus="fontSize -= $event"></font-size>
<font-size step="3" :val="fontSize" @plus="fontSize += $event"
@minus="fontSize -= $event"></font-size>
<p :style="{fontSize:fontSize+'px'}">Hello {{fontSize}}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() { return { fontSize: 16 } }
})
app.component('font-size', {
props: ['val', 'step'],
template: `
<div>step: {{step}}
<button @click="onPlus">+</button>
<button @click="$emit('minus', step)">-</button>
</div>`,
methods: {
onPlus() { this.$emit('plus', parseInt(this.step)) }
}
})
app.mount('#app')
</script>
v-model实现双向绑定
- 父组件通过 v-model="属性" 把属性传递给子组件
- 子组件内有一个modelValue的prop,接受父组件传递的数据
- 子组件通过触发 update:modelValue 修改父组件绑定的属性
<input v-model="searchText" />
<!--等价于-->
<input :value="searchText" @input="searchText =
$event.target.value" />
//--------------------------------------------------------
<div id="app">
<font-size step="1" v-model="fontSize"></font-size>
<font-size step="4" v-model="fontSize"></font-size>
<p :style="{fontSize:fontSize+'px'}">Hello {{fontSize}}</p>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() { return { fontSize: 16 } }
})
app.component('font-size', {
props: ['modelValue', 'step'],
template: `
<div>fontSize: {{modelValue}}
<button @click="$emit('update:modelValue',
+step+modelValue)">+</button>
<button @click="$emit('update:modelValue', modelValuestep)">-</button>
</div>`
})
app.mount('#app')
</script>
单向数据流指的是什么?有什么好处?
什么是单向数据流?
很多框架都使用单向数据流,指的是父组件能直接传递数据给子组件,子组件不能随意修改父组件的状态
为什么要单向?
单向数据流的目的是让数据的传递变得简单、可控、可追溯。假设都是⽤双向数据流,父组件维护⼀个状态,并且传递给所 有的⼦组件。当其中⼀个⼦组件通过双向数据流直接修改父组件的这个状态时,其他⼦组件也会被修改。当这个结果产⽣时 我们⽆法得知是拿个⼦组件修改了数据,在项⽬庞⼤时数据的管理和追溯变得更复杂。
如果要双向如何实现?
⼀般来说,父组件可⽤通过设置⼦组件的props直接传递数据给⼦组件。子组件想传递数据给父组件时,可以在内部emit⼀个自定义事件,父组件可在子组件上绑定该事件的监听,来处理子组件emit的事件和数据。
组件注册、Props、自定义事件、插槽、Provide/Inject
全局组件和局部组件
全局组件可以在任何组件内使用,局部组件只能在声明它的组件内部使用
// 声明全局组件
app.component('component-a', {
template: `
<div>a</div>
<component-b></component-b>
`
})
app.component('ComponentB', {
template: `<div>b</div>`
})
// 声明局部组件
const ComponentB = {
template: `<div>ComponentB</div>`
}
const ComponentA = {
components: {
// 'component-b': ComponentB,
ComponentB
},
template: `
<div>ComponentA</div>
<component-b></component-b>
`
}
组件命名: PascalCase 多个首字母大写单词直接连接,模板里可以用kebab-case和PascalCase
Props
// 几种Props写法范例
props: ['name','age']
props: {
name: String,
age: Number
}
props: {
name: {
type: String,
required: true,
// default: 'hello'
}
}
// v-bind
<example v-bind="post"></example>
<example v-bind:id="post.id" v-bind:name="post.name"></example>
// 以下两种写法有什么区别
<User :name="3+4"></User>
<User name="3+4"></User>
//如果要传入的是表达式而不是字符串,就用v-bind:property绑定
非Props的Attribute
要点:
- 指的是父组件模板里在使用子组件时设置了属性,但子组件内没有通过props接收
- 当组件返回单个根节点时,非props attribute将自动添加到根节点的attribute中
- 在子组件里可以通过attrs获取attributes
- 如果想在非根节点**应用(比如触发事件)**传递的attribute,使用v-bind=“$attrs”
<div id="app">
<username class="username" :error="errorMsg" @input="onInput"></username>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const Username = {
props: ['error'],
template: `
<fieldset>
<legend>用户名</legend>
<input v-bind="$attrs">
<div>{{error}}</div>
</fieldset>
`
}
Vue.createApp({
components: { Username },
data() {
return { errorMsg: '' }
},
methods: {
onInput(e){
this.errorMsg = e.target.value.length<6?"长度不够":""
}
}
}).mount('#app')
</script>
自定义事件
- 子组件触发事件用this.$emit('my-event')
- 父组件使用子组件时绑定
<component-a @my-event="doSomeThing"></component-a> - v-model的语法糖
<com v-model:foo="bar"></com>
// 等价于
<com :foo="bar" @update:foo="bar=$event"></com>
插槽slot
- 子组件里预留一个空位(slot)
- 父组件使用子组件时可以在子组件标签内插入内容/组件
- 父组件往插槽插入的内容只能使用父组件实例的属性
问题一:如果父组件想要子组件里插槽的变量,该怎么拿?
具名插槽
<div id="app">
<layout>
<template v-slot:header>
<h1>页面header</h1>
</template>
<template #default>
<p>页面content</p>
</template>
<template #footer>
<div>页面footer</div>
</template>
</layout>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const Layout = {
template: `<div class="container">
<header> <slot name="header"></slot> </header>
<main> <slot></slot></main>
<footer><slot name="footer"></slot></footer>
</div>`
}
Vue.createApp({
components: { Layout },
}).mount('#app')
</script>
作用域插槽: 问题一可以用作用域插槽解决
<div id="app">
<news>👉hello world</news>
<news v-slot="props">👉 {{props.item}}</news>
<news v-slot="props">👉第{{props.index}}章 {{props.item}}</news>
<news v-slot="{item, index}">✔第{{index}}章 {{item}}</news>
<!-- <news v-slot="{ item }">✔ {{item}}</news> -->
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const News = {
data() { return { news: ['first news', 'second news'] } },
template: `<ul>
<li v-for="(item, index) in news">
<slot :item="item" :index="index"></slot>
</li>
</ul>`
}
Vue.createApp({
components: { News },
}).mount('#app')
</script>
keep-alive
- 页面第一次进入,钩子的触发顺序 created->mounted->activated
- 退出时触发 deactivated
- 当再次进入只触发activated
Provide/inject
- 适用与深度嵌套的组件,父组件可以为所有的子组件直接提供数据
<div id="app">
<toolbar></toolbar>
<button @click="isDark=!isDark">切换</button>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const ThemeButton = {
inject: ['theme'],
template: `
<div :class="['button', theme]" ><slot></slot><
/div>
`
}
const Toolbar = {
components: { ThemeButton },
inject: ['theme'],
template: `<div :class="['toolbar', theme]">
<theme-button>确定</theme-button>
</div>`
}
Vue.createApp({
data() { return { isDark: false } },
provide: { theme: 'dark'},
// provide() {
// return { theme: this.isDark?'dark':'white'
}
// },
components: { Toolbar },
}).mount('#app')
</script>
Vue实现过渡和动画
使用class实现动画
- 在CSS里配好样式,通过切换class实现效果切换
<div id="app">
<div :class="['box', {active: isActive}]">
{{isActive?'大': '小'}}
<button @click="isActive = !isActive">Toggle size</button>
</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() { return { isActive: false } }
}).mount('#app')
</script>
<style>
.box {
width: 200px;
height: 200px;
margin: 30px;
box-shadow: 0 0 4px 0px rgba(0, 0, 0, 0.2);
display: flex;
align-items: center;
justify-content: center;
transition: all .3s;
}
.box.active { transform: scale(1.2); }
</style>
修改style实现动画
- 修改style和data中数据绑定
<div id="app">
<div class="color" :style="{backgroundColor: `rgb(${R},${G},${B})`}"></div>
<div>
<input type="range" v-model="R" min="0" max="255" />
<input type="range" v-model="G" min="0" max="255" />
<input type="range" v-model="B" min="0" max="255" />
</div>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return { R: 0, G: 0, B: 0 }
}
}).mount('#app')
</script>
<style>
.color {
width: 40px;
height: 40px;
}
</style>
transition组件
- v-enter-from: 在元素被插入之前生效,在元素被插入后的下一帧移除
- v-enter-active: 定义进入过渡生效时的状态
- v-enter-to: 定义进入过渡的结束状态。在元素被插入之后下一帧生效,在过渡/动画完成之后移除
- v-leave-from:在元素过渡被触发时生效,下一帧被移除
- v-leave-active: 在离开过渡生效时的状态
- v-leave-to:离开过渡的结束状态。在离开过渡被触发后下一帧生效,在过渡/动画完成之后移除
transition对以下内容可以封装:
- v-if
- v-show
- 动态组件
- 组件根节点
transition范例:
// 注意name的用法
<div id="app">
<button @click="show = !show">Toggle</button>
<transition name="fade">
<p v-if="show">hello</p>
</transition>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
show: true
}
}
}).mount('#app')
</script>
<style>
.fade-enter-active,
.fade-leave-active {
transition: opacity 0.5s ease;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
transition 和 做动画的CSS库组合
- 自定义过渡class名,结合库写动画更简单
<div id="app">
<button @click="show = !show">Toggle</button>
<transition name="fade"
enter-active-class="animate__animated animate__fadeInDown"
leave-active-class="animate__animated animate__fadeInUp"
>
<p v-if="show">hello</p>
</transition>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return { show: true }
}
}).mount('#app')
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
rel="stylesheet"/>
多个元素过渡:
- 多元素过渡指的是多元素进行切换,同一事件只展示一个
- 使用不同的key来提高性能
- mode属性可以解决两个元素同时存在的现象,out-in:当前元素先离开,下一个元素再进来;
in-out:下一个元素先进来,当前元素后离开
<div id="app">
<button @click="show = !show">Toggle</button>
<transition
mode="out-in"
enter-active-class="animate__animated animate__fadeInUp"
leave-active-class="animate__animated animate__fadeOutDown"
>
<p v-if="show" key="hello">hello</p>
<p v-else key="world">world</p>
</transition>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return { show: true }
}
}).mount('#app')
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css"
rel="stylesheet"/>
多组件切换
- 使用动态组件实现Tab切换效果
- 如果动态组件使用了keep-active,需要放在transition内部
<div id="app">
<button v-for="tab in tabs" :key="tab" :class="{ active: currentTab === tab }" @click="currentTab = tab">
{{ tab }}
</button>
<transition mode="out-in" name="fade">
<keep-alive>
<component :is="currentTab" class="tab"></component>
</keep-alive>
</transition>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
const app = Vue.createApp({
data() {
return {
currentTab: 'Tab1',
tabs: ['Tab1', 'Tab2']
}
},
})
app.component('Tab1', {
template: `<div>Tab1 content</div>`,
})
app.component('Tab2', {
template: `<div>
<input v-model="value" /> {{value}}
</div>`,
data() { return { value: 'hello' } },
})
app.mount('#app')
</script>
<link href="https://cdnjs.cloudflare.com/ajax/libs/animate.css/4.1.0/animate.min.css" rel="stylesheet" />
<style>
.active {
background: #ccc;
}
.fade-enter-active,
.fade-leave-active {
transition: all .3s;
}
.fade-enter-from,
.fade-leave-to {
opacity: 0;
}
</style>
列表的过渡效果
- 使用transition-group实现列表过渡
<div id="app">
<button @click="add">添加</button>
<transition-group name="list" tag="div">
<div v-for="(value, index) in news" :key="value">
{{value}}
<button @click="news.splice(index, 1)">删除</button>
</div>
</transition-group>
</div>
<script src="https://unpkg.com/vue@next"></script>
<script>
Vue.createApp({
data() {
return {
news: []
}
},
methods: {
add() {
let value = window.prompt('输入新闻')
if(value) {
this.news.push(value)
this.news = this.news.sort()
}
}
}
}).mount('#app')
</script>
<style>
.list-enter-active,
.list-leave-active {
transition: all .4s ease;
}
.list-enter-from {
opacity: 0;
transform: translateX(-30px);
}
.list-leave-to {
opacity: 0;
transform: translateX(30px);
}
</style>
- Vue里有哪些方法实现过渡或者动画效果?
class,style,transition,transition-group
- transition组件实现过渡效果怎么使用
注意name的使用,把要做动画的元素用transition包起来
关于Vue CLI
- 用于快速创建Vue项目模板,包含一些预定义的配置
- 单文件组件
- 构建webpack和webpack-server之上
- 包含丰富的插件
目录结构
单文件组件:
- 文件名后缀是.vue
- 一个文件是一个组件
- 模板放在