Vue 核心 API 学习
Demo 地址:Vue 进阶学习
1. Vue 实例
实例:对某个类,通过 new ClassA() 初始化而来的对象,叫做实例。对象初始化参考
const app = new Vue({
el: '#root',
template: '<div>This is content</div>'
});
通过 new Vue(options
) 初始化的 Vue 实例,最终会通过 render function
将 template 的内容挂载到 #root
根节点上,在页面中展示,而且挂载的方式是使用 template render 生成的新节点,替换掉 指定的 #root
节点,正式这个原因,如果在 template 中存在多个根节点,Vue 会报一个警告:
注:如果 options 传入了 template,则将 template 编译到 render 函数中去,否则会将 el 外部的 html 作为 template 进行编译
[Vue warn]: Error compiling template:
Component template should contain exactly one root element. If you are using v-if on multiple elements, use v-else-if to chain them instead.
因为 Vue 不知道用哪个节点替换指定的根节点。
不仅仅可以通过 options.el
指定要挂载的根节点,还可以手动进行挂载:
app.$mount('#root');
在 webpack 配置中,通过插件,可以指定以某个文件为模板,生成最终的 html 页面:
new HTMLPlugin({
template: path.join(__dirname, 'index.html')
});
初始化 Vue 传入的 options
会与默认的初始化参数结合起来,最终产生 Vue 实例,而且 options
中的属性可以通过 app.$[attr]
访问,如:
app.$el
、app.$root
、app.$options
、app.$data
、app.$props
、app.$children
、app.$refs
、app.$slots
等等
-
app.$el
是对最终挂载生成 html 节点的引用 -
app.$root
是整个 Vue 树状结构的根节点,也是 Vue 实例,是对 app 的引用,即:app.$root === app
-
app.$options
是初始化 Vue 实例所有的参数,$options 中也可以看到诸如data
、props
、watch
等属性,但不是对app.$[attr]
的引用,所以如果更改app.$options.data
的值,页面是不会响应式变化的,直接更改app.$data
才有效 -
app.$data
定义在 data 对象中的属性,Vue 会复制一份引用到 app 层,即 app.[attr]
和 app.$data[attr]
访问到的是同一份地址,
定义在打 data 中的全局属性,如果不对属性进行字面量声明,则在 Vue 初始化完成之后,给 data 对象增加属性,或修改属性值,Vue 不会对这个对象做响应式处理,所以即使该对象的属性值变化了,也不会响应式地重新渲染
但可以通过 $set 方法给在初始化完成之后再给 data 增加或修改属性,而且也是响应式的
app.$set(app.obj, 'a', 'foo');
另外,也删除某个属性,同时会解除对该属性的响应式处理
app.$delete(app.obj, 'a');
强制组件重新渲染一次,也可以在没有字面量定义 data 属性时,页面重新渲染(尽量不使用这个方法)
app.$forceUpdate();
-
app.$refs
用于快速定位到模板的某个 html 节点,或某个组件实例 -
app.$isServer
用于服务端渲染判断 -
app.$watch
一下写法和在 options 中定义 watch 都可以监听到 foo 的变化:
const unWatch = app.$watch('foo', (newVal, oldVal) => {
console.log('newVal: ', newVal);
console.log('oldVal: ', oldVal);
})
区别在于,通过 app.$watch 定义,需要手动进行销毁(比如路由跳转后),该方法回调一个 unWatch 方法,用于手动回收:
unWatch();
而定义在 options 中,Vue 会自动回收销毁
app.$on
app.$emit
app.$once
app.$on
和 app.$emit
都只能同时作用于一个 Vue 对象,才会生效,Vue 对象 app 监听了 test 事件,那么必须由对象 app 自己触发事件 test,才会被监听到,而且不会像 dom 事件一样冒泡
app.$on('test', (a, b) => {
console.log('$on test emitted: ', a, b);
});
app.$emit('test', 1, 2);
app.$once 只会触发一次
app.$once('test', (a, b) => {
console.log('$once test emitted', a, b);
})
2. Vue 的生命周期方法
beforeCreate
和created
beforeCreate、created 方法在 new Vue(options
) 的过程中一定会被执行,而且 beforeCreate、created 两个生命周期中都不能进行 dom 操作,因为这时 Vue 还没有生成最终的根节点,一般操作 dom 相关的动作,要放在 mounted 中。操作数据相关的动作一般放在 created 中,但不要放在 beforeCreate 中,因为 beforeCreate 还没有进行数据的 reactive 响应式相关的初始化
beforeMount
和mounted
mounted 之后会把当前 Vue 生成的 html 挂载到 dom 上,即:把生成的 html 把 <div id="root"></div>
替换掉。如果在 options 中不指定 el,则不会执行 beforeMount、mounted。在 mounted 之后,所有生命周期中拿到的根节点,都是 mounted 之后产生的节点
beforeCreate、created、beforeMount、mounted 在整个组件生命周期中只会被调用一次,而且 beforeMount、mounted 在服务端渲染中不会被调用,因为这两个生命周期都和操作 dom 有关
beforeUpdate
和updated
只要响应式处理过的数据有变化,页面就会响应重新渲染,这两个方法也都会被重复调用
activated
和deactivated
与组件的 keep-alive 相关
beforeDestroy
和destroyed
beforeDestroy 中会解除所有事件监听以及所有的 watch 监听
render
render 方法第一次将在 beforeMount 和 mounted 之间执行,之后都将在 beforeUpdate 和 updated 之间执行
renderError
只有在开发环境才会触发,并且只能用于当前组件,它的子组件的错误无法捕获到
errorCaptured
不管是开发环境还是正式环境,只要捕获到当前组件,或者它的子组件渲染发生错误时,都会触发,除非子组件阻止了事件冒泡
3. Vue 的数据绑定
import Vue from 'vue';
var globalVar = '111'; // eslint-disable-line
new Vue({
el: '#root',
template: `
<div :id="id" :class="activeClass" @click="handleClick">
<div :class="{active: isActive}">
模板中只能做用一行语句就能有结果的表达式
{{isActive ? "active" : "not active"}}
</div>
<div :class="[{active: isActive}, inactiveClass]">
模板中可以访问 Vue 默认的全局变量白名单,但自己定义的全局变量不能访问(如 globalVar)
{{Date.now()}}
</div>
<div :style="[activeStyle, inactiveStyle]">
对 data 中定义的 HTML,Vue 会自动进行转移成纯的字符串,为了防止注入攻击
要以 v-html 方式使用
<div v-html="html"></div>
<div>{{getJoinedArr(arr)}}</div>
</div>
</div>
`,
data: {
id: 'aaa',
activeClass: 'active',
inactiveClass: 'inactive',
activeStyle: {
color: 'red',
// Vue 会给需要加前缀的样式属性名自动加上前缀
// 该属性用于消除浏览默认样式
appearance: 'none'
},
inactiveStyle: {
// html 中所写样式的 '-' 分隔符格式转化成驼峰格式
fontSize: '15px'
},
isActive: true,
arr: [1, 2, 3],
html: '<span>123</span>'
},
methods: {
handleClick () {
console.log('##### handleClick #####');
},
// 建议这种情况使用 computed,
// 因为 computed 会先判断数据源是否有变化,再来渲染页面,效率会更高
getJoinedArr (arr = []) {
return arr.join('、');
}
}
});
4. computed 和 watch
- computed:
定义在 computed
中的方法,可以像访问变量的属性一样去调用,这是因为 Vue 会对 fullName 声明 get
和 set
方法,并且会对计算的结果做缓存,只有当 computed
中所依赖的响应式变量(即:firstName 或 lastName)发生变化,才会重新计算。而如果是在 methods
中做同样的逻辑计算,那么只要 data
或者 props
中任何一个属性有变化,都会重新渲染页面,从而重新调用 methods
中的方法。所有定义在 computed
中性能消耗会更小,特别是计算的逻辑很复杂的情况
const computed = {
fullName () {
return `${this.firstName} ${this.lastName}`;
}
};
也可以通过显示地定义 get
和 set
方法来定义 computed
get:
获取 fullName 的值时会做哪些计算
set:
给 fullName 赋值时会做哪些计算。不建议使用 set,因为将多个值通过计算组装成一个值很简单,但把一个值拆解成多个值是很麻烦的,而且很容易出问题,造成死循环
const computed = {
fullName: {
get () {
return `${this.firstName} ${this.lastName}`;
},
set (val) {
const names = val.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
};
- watch:
watch
中可以对 data
props
computed
中的响应式变量进行监听,只要其发生变化,就执行一些逻辑处理
只有当 data.age 发生变化才会执行 age 方法中的逻辑
const watch = {
age (newVal, oldVal) {
this.notYoung = newVal >= 30;
}
}
也可以通过以下方式定义 watch
:
immediate
:表示是否立即执行
作用:如果 age 的初始值本身已经大于 30,那么按照上面👆的形式定义 watch
,第一次 handler 方法是不会执行的,只有当 age 再次发生变化才会执行,而 immediate
就是用来解决这个问题的
const watch = {
age: {
handler (newVal, oldVal) {
this.notYoung = newVal >= 30;
},
immediate: true
}
};
deep
:深度观察
作用:如果 other 是一个对象,那么只是修改 other 中属性的值,而不是直接给 other 重新赋值,那么按照上面👆的形式定义 watch
,handle 方法也是不会执行的。但如果设置 deep: true
,那么 other 不管哪个属性变化,handle 方法都会执行
原理:Vue 会逐层遍历 other 所有的属性,并为每个属性增加一个响应式监听,所有只要 other 任何层级的任何属性发生变化,都会执行 handle 方法
const watch = {
other: {
handler (newVal, oldVal) {
console.log('other changed');
},
deep: true
}
};
但这种写法对性能开销较大,建议使用以下👇写法
const watch = {
'other.hobby' () {
console.log('other.hobby changed');
}
};
5. Vue 的原生指令
import Vue from 'vue';
new Vue({
el: '#root',
template: `
<div>
<div>{{text}}</div>
<div v-text="text">aaa</div>
<div v-html="html">aaa</div>
<div>类似于 dom 的 innerText 和 innerHtml</div>
<br/>
<div v-pre>将内容当做纯文本显示: {{text}}</div>
<br/>
<div v-once="text">
<div>数据绑定的内容只执行一次,之后数据变化后也不会随之变化,</div>
<div>用处:展示静态内容时,通过声明 v-once 减少性能开销,</div>
<div>v-once 中的所有节点,Vue 都不会将其和虚拟 dom 进行检测对比,从而减少重新渲染的开销。</div>
</div>
<br/>
<div v-show="active">根据 show 的值,给 div 增加 display 的样式</div>
<div v-if="active">根据 show 的值,决定是否把 div 添加到 dom 流中</div>
<div v-else>else content</div>
<div>如果只是单纯想控制元素的显示和隐藏,那么最好使用 v-show,</div>
<div>因为 v-if 会对 dom 节点进行增删操作,导致重绘和重新排版,有性能的影响。</div>
<br/>
<div>v-for 中的 key 是用来做数据缓存的,需要保证唯一,</div>
<div>当数据源发生变化时,Vue 会根据每个 item 的 key 在缓存中寻找,是否已经存在 key,</div>
<div>如果已经存在,则直接在缓存中复用 item 的 dom 节点,而不重新创建新的 dom 节点,提高渲染性能。</div>
<div>注:不要用 idx 作为 key,因为数组元素的顺序和具体值没有什么直接关系,</div>
<div>用 idx 作为 key,在数据源发生增、删之后,可能会导致产生错误的缓存。</div>
<ul>
<li v-for="(item, idx) in arr" :key="item">index: {{idx}}, value: {{item}}</li>
</ul>
<ul>
<li v-for="(val, key, idx) in obj">key: {{key}}, value: {{val}}, index: {{idx}}</li>
</ul>
<br/>
<div v-on:click="divClicked">
<div>v-on 做的事情是:</div>
<div>如果 v-on 加在普通的 dom 节点元素上,则会通过 document.addEventListener 给该节点增加事件监听</div>
<div>如果 v-on 加在 Vue 组件上,实际上是在 Vue 对象实例上绑定一个事件</div>
</div>
<br/>
<div>
<input type="text" v-model="text">
<input type="text" v-model.number="number">
<input type="text" v-model.trim="text">
<input type="text" v-model.lazy="text">
<input type="checkbox" v-model="active">
</div>
<br/>
<div>
<div>每个 checkbox 所绑定的值是固定的,checkbox 的 active/inactive 变化后:</div>
<div>active -> inactive: 数据移除值为当前 checkbox 所绑定值的元素</div>
<div>inactive -> active: 数据 push 一个值为当前 checkbox 所绑定值的元素</div>
<input type="checkbox" value="a" v-model="arr">
<input type="checkbox" value="b" v-model="arr">
<input type="checkbox" value="c" v-model="arr">
</div>
<br/>
<div>
<input type="radio" value="one" v-model="picked">
<input type="radio" value="two" v-model="picked">
</div>
</div>
`,
data: {
text: 'text',
number: 0,
html: '<span>html</span>',
active: true,
arr: ['a', 'b', 'c'],
obj: {
a: 'a',
b: 'b',
c: 'c'
},
picked: 'one'
},
watch: {
text () {
console.log('##### text changed #####', this.text);
},
arr () {
console.log('##### arr changed #####', this.arr);
},
picked () {
console.log('##### arr picked #####', this.picked);
}
},
methods: {
divClicked () {
console.log('##### divClicked #####');
}
}
});
6. Vue 组件
6.1 组件的定义
- 组件的全局注册和局部注册
const component = {
template: `<div>This is a component</div>`
};
// 全局注册:
Vue.component('CompOne', component); // 组件名称命名规则:大驼峰命名法(因为 component 本身也是一个 Vue 的 class)
// 局部注册
new Vue({
component: {CompOne: component},
template: `<comp-one></comp-one>` // 组件使用命名规则:全小写 '-' 分割
});
- 组件
data
定义:
不是通过 new Vue({options
}) 创建的组件,data
必须以 function 形式返回,否则当一个父组件有多个相同类型的子组件时,所有的子组件都会引用同一份数据源,并且 Vue 会报警告:
[Vue warn]: The "data" option should be a function that returns a per-instance value in component definitions.
const component = {
template: `<div>This is a component</div>`,
data () {
return {
a: 'foo'
}
}
};
- 组件
props
定义:
const component = {
template: `<div @click="handleChange">This is a component</div>`,
props: {
active: {
type: Boolean, // 类型
required: true, // 是否必填
default: true, // 默认值
validator (val) { // 自定义校验
return typeof val === 'boolean'
}
},
propOne: Number, // 属性命名规则:小驼峰命名法
// 如果是对象,则必须以 function 形式返回一个对象
// 原因和不是通过 new Vue() 创建的组件,data 必须以 function 形式返回一样
obj () {
return {
default: {a: 'foo'}
}
}
},
// 也可以通过数组定义 props,但相对不严谨
// props: ['active', 'propOne'],
// mounted () {
// this.propOne = 1;
// },
methods: {
handleChange () {
this.$emit('change')
}
}
};
Vue 不推荐在子组件中更改 props
的值,如果直接更改,会报警告:
[Vue warn]: Avoid mutating a prop directly since the value will be overwritten whenever the parent component re-renders. Instead, use a data or computed property based on the prop's value. Prop being mutated: "propOne"
如果要更改,可以通过子组件 $emit
事件,回调到父组件进行更改
new Vue({
el: '#root',
components: {
Comp: component
},
template: `
<div>
// 传递 props 时,使用全小写 '-' 分割
<comp :active="true" :prop-one="propA" @change="handleChange"></comp>
</div>
`,
data: {
propA: 1
},
methods: {
handleChange () {
this.propA += 1;
}
}
});
6.2 组件的继承
const component = {
template: `
<div>
<input type="text" v-model="text">
<span v-show="active">show if active</span>
<span @click="handleChange">{{propOne}}</span>
</div>
`,
props: {
active: Boolean,
propOne: String
},
data () {
return {
text: 0
};
},
mounted () {
console.log('##### component mounted #####');
},
methods: {
handleChange () {
this.$emit('change');
}
}
};
以上 component 配置只是一个普通的 object,如何让配置生成 Vue 对象,除了通过 new Vue({component: {Comp: component}}
) 以外,还可以对 Vue 进行继承,生成 Vue 对象:
import Vue from 'vue';
const CompVue = Vue.extend(component);
new CompVue({
el: '#root',
// 通过 props 无法将外部属性传入
// props: {
// propOne: 'props 1'
// },
// 需要通过 propsData 传入
propsData: {
propOne: 'propsData 1'
},
// 可以传入 data 与 CompVue 默认的 data 合并覆盖
data () {
return {
text: 3
};
},
// 生命周期方法执行顺序:先执行 CompVue 默认配置中的生命周期方法,再执行 CompVue 实例中的生命周期方法
mounted () {
console.log('##### instance mounted #####');
}
});
CompVue 是 Vue 的一个子类,通过 new Vue(options) 初始化而来 Vue 对象,是默认内置配置下生成的,没有 data、props 等相关配置,通过 new CompVue() 生成的 Vue 对象会默认带有 data、props、methods 配置
也可以通过以下这种方式实现对 component 的继承:
import Vue from 'vue';
const component2 = {
extends: component,
data () {
return {
text: 1
};
},
mounted () {
console.log('##### instance mounted #####');
}
};
new Vue({
el: '#root',
components: {
Comp: component2
},
template: `<comp prop-one="123"></comp>`
});
使用场景:当我们开发好一个组件,可能比较公用,很多项目都会用到,而且功能描述比较泛,使用起来需要传入很多的配置项,而在某个具体项目中,很多参数都使用它的默认值,不需要传入,或者需要在原有基础上扩展一些特定的属性,这时候就可以通过 extend 的方式,在原有组件基础上,继承扩展出我们需要的组件,而不需要从头开始写一个新的组件
6.3 组件自定义绑定
import Vue from 'vue';
// 通过 v-model 实现父子组件的双向绑定
const component = {
template: `
<div>
<input type="text" :value="value1" @input="handleInput">
</div>
`,
model: {
prop: 'value1', // 指定双向绑定 prop 的 key
event: 'change' // 指定回调时间的方法名
},
props: ['value', 'value1'],
methods: {
handleInput (event) {
// this.$emit('input', event.target.value);
this.$emit('change', event.target.value);
}
}
};
new Vue({
el: '#root',
components: {
Comp: component
},
// template: `<comp :value="value" @input="value = arguments[0]"></comp>`,
template: `<comp v-model="value"></comp>`,
data () {
return {
value: '123'
};
}
});
6.4 组件高级属性
- slot 和 slot-scope
在子组件中可以通过指定不同插槽名,定义多个插槽:
import Vue from 'vue';
const component = {
template: `
<div :style="style">
<div class="header">
<slot name="header"></slot>
</div>
<div class="body">
<slot name="body"></slot>
</div>
<div class="footer">
<slot name="footer"></slot>
</div>
</div>
`,
data () {
return {
style: {
width: '200px',
height: '200px',
border: '1px solid #aaa'
}
};
}
};
在父组件中根据插槽名传入不同的插槽:
import Vue from 'vue';
new Vue({
el: '#root',
components: {
CompOne: component
},
template: `
<div>
<comp-one>
<div slot="header">slot header</div>
<div slot="body">slot body</div>
<div slot="footer">slot footer</div>
</comp-one>
</div>
`
});
带作用域的插槽:在子组件插槽中定义内部属性,父组件通过 slot-scop
可以访问到子组件的属性:
import Vue from 'vue';
const component = {
template: `
<div :style="style">
<slot :slotValue1="slotProp" :slotValue2="slotData"></slot>
</div>
`,
props: {
slotProp: String
},
data () {
return {
style: {
width: '200px',
height: '200px',
border: '1px solid #aaa'
},
slotData: 'abc'
};
}
};
通过指定 "props" 作为 key,将插槽中的属性包装成 object,这样父组件就可以通过 props[key] 来访问插槽中的属性
import Vue from 'vue';
new Vue({
el: '#root',
components: {
CompOne: component
},
template: `
<div>
<comp-one :slot-prop="value1">
<div slot-scope="props">
<div>slotValue1: {{props.slotValue1}}</div>
<div>slotValue2: {{props.slotValue2}}</div>
<div>selfValue: {{value2}}</div>
</div>
</comp-one>
</div>
`,
data () {
return {
value1: 'def',
value2: 'ijk'
};
}
});
- provide 和 inject
对于跨多级的组件中,父组件和子组件的参数传递,可以通过在父组件中定义 provide
,子组件中定义 inject
来实现
孙组件:
const GrandsonComponent = {
template: '<div>child component</div>',
inject: ['const', 'grandparent', 'value'],
mounted () {
console.log('##### const #####', this.const); // xyz
console.log('##### grandparent #####', this.grandparent); // undefined
console.log('##### value #####', this.value); // 在父组件中报错
}
};
子组件:
const ChildComponent = {
components: {
GrandsonComponent
},
template: `
<grandson-component></grandson-component>
`
};
父组件:
通过 provide: {}
的方式定义 provide
,在子孙组件中是拿不到父组件的 this
的,只能接收到传入的常量,因为这样初始化 provide
对象,其实 Vue 本身还没有初始化完成,所有访问不了 this
import Vue from 'vue';
new Vue({
el: '#root',
components: {
ChildComponent
},
provide: {
const: 'xyz',
grandparent: this,
value: this.value // 报错
},
template: `
<child-component></child-component>
`,
data () {
return {
value: 'abc'
};
}
});
孙组件:
const GrandsonComponent = {
template: '<div>child component: {{value}}</div>',
inject: ['const', 'grandparent', 'value'],
mounted () {
console.log('##### const #####', this.const); // xyz
console.log('##### grandparent #####', this.grandparent); // 父组件 Vue 实例
console.log('##### value #####', this.value); // abc
}
};
父组件:
通过方法返回定义 provide
,才子孙组件中接收到父组件的 this
,但是父组件中的响应式变量,不会在子孙组件中响应式地变化
import Vue from 'vue';
new Vue({
el: '#root',
components: {
ChildComponent
},
provide () {
return {
const: 'xyz',
grandparent: this,
value: this.value
};
},
template: `
<child-component></child-component>
`,
data () {
return {
value: 'abc'
};
}
});
孙组件:
const GrandsonComponent = {
template: '<div>child component: {{value}}</div>',
inject: ['const', 'grandparent', 'data'],
mounted () {
console.log('##### const #####', this.const); // xyz
console.log('##### grandparent #####', this.grandparent); // 父组件 Vue 实例
console.log('##### value #####', this.data); // {value: 父组件的 value}
}
};
父组件:
只有在 provide
中重定义 get()
方法,让子孙组件每次获取到的 value 都是其最新值,从而实现响应式。这也是 Vue 实现响应式的最基本原理
import Vue from 'vue';
new Vue({
el: '#root',
components: {
ChildComponent
},
provide () {
const data = {};
Object.defineProperty(data, 'value', {
get: () => this.value,
enumerable: true
});
return {
const: 'xyz',
grandparent: this,
data
};
},
template: `
<child-component></child-component>
<input type="text" v-model="value"/>
`,
data () {
return {
value: 'abc'
};
}
});