搞懂 Vue 需要先回答好以下五个问题:
- 初始化时可以写哪些对象?选项
options
里面有什么 - vm自身有哪些属性?
- Vue函数自身有哪些属性?
- Vue.prototype里面有哪些属性?
- Vue.prototype的原型链里面还有哪些属性?
1. Vue实例
const vm = new Vue(options)
// 可简写为 new Vue
vm.__proto__ = Vue.prototype
// 任何一个对象的原型 = 其构造函数的共有属性
vue.__proto__ = Function.prototype
vue的实例vm
就是对象,options
是new Vue
的参数,vm会根据给的选项options
得出一个对象,这个对象封装了对视图层的所有操作,包括数据读写、事件绑定、DOM更新(不包括网络层的AJAX)
2. new Vue的参数options
里面有什么:
options的五类属性 | 具体内容 |
---|---|
数据 | |
DOM | |
生命周期钩子 | |
资源 | |
组合 |
2.1 数据
数据 | 备注 |
---|---|
data | 定义内部数据 |
methods | 定义方法 事件处理函数 或 普通函数 每次渲染都会执行 |
props | 定义外部属性 |
computed | 定义计算属性 |
watch | 监听data变化时做响应改变 |
propsData | 本质是 propsValue 单元测试时用到 全局扩展时的数据传递 |
方法与函数的区别:方法是面向对象的,函数是数学概念;下面代码在面向对象里称为方法,在数学里称为函数,而方法依附于对象,也就是对象.方法,而在经典面向对象里,sayhi()是不能单独出现的,JS融合后,就可以单独写。
obj.sayhi() //sayhi() 就是方法 function (p1,p2){ return xx }
2.1.1 data 内部数据(支持对象和函数,优先用函数)
- 用函数的原因?
通俗的理解:为了组件的复用,使得每个组件都可以有一份data的拷贝,防止不同组件修改数据时被相互覆盖;防止指向同一个内存地址。
import Demo from './Demo.vue'
render: h => h(Demo) // 把 Demo 传给new Vue后,会自动把Demo前面加 new Vue(Demo)
render:h => h(X, [h(Demo),h(Demo)])
// data若为对象,前后两次传demo,就会把一个内部数据传给两个组件,共用一个data,其中一个data修改另外一个也会变化
// data若为函数,先会调用Demo.data()得到Demo真正的data,第二次调用的时候,还是会调用
// 使得每次调用都是一个全新的对象,避免了两个组件共用data的问题
Demo._data = Demo.data()
- data 的使用
const Vue = window.Vue
new Vue({
data:{ // data为一个对象
n:0
},
data:function(){ //data为函数,可缩写为 data(){}
return{
n:0
}
},
template:`
<div id="app">
{{n}}
<button @click="add">+1</button>
</div>
`,
methods:{
add(){
this.n += 1
}
}
}).$mount('#app')
- data扩展之:
setter
和getter
let obj0 = {
姓: "高",
名: "圆圆",
age: 18
};
//如何得到姓名
let obj1 = {
姓: "高",
名: "圆圆",
姓名(){
return this.姓 + this.名
},
age: 18
};
console.log("需求一:" + obj.姓名())
//删掉姓名函数后面的括号,不用加括号也能得出值
let obj2 = {
姓: "高",
名: "圆圆",
get 姓名(){
return this.姓 + this.名
},
age: 18
};
console.log("需求一:" + obj2.姓名);
// getter就是不加括号的函数,上例中`obj2.姓名`即为`计算属性`
let obj3 = {
姓: "高",
名: "圆圆",
get 姓名(){
return this.姓 + this.名
},
set 姓名(xxx){
this.姓 = xxx[0]
this.名 = xxx.substring(1)
},
age: 18
};
obj3.姓名 = '高圆圆'
console.log(`需求三:姓 ${obj3.姓},名 ${obj3.名}`)
// setter就是用'xxx'触发'set函数'
-
data扩展之:
Object.defineProperty
-
Object.defineProperty
- 可以给对象添加属性
value
- 可以给对象添加getter/setter
- getter/setter用于对属性的读写进行监控
适用于:定义完一个对象之后,想在它身上额外的添加
getter
和setter
,通过Object.defineProperty
定义属性来实现,所传的参数,第一个是define的对象,第二个是定义的具体内容,
var _xxx = 0
Object.defineProperty(obj3, 'xxx',{
get(){
return _xxx //_xxx用来存放其值
},
set(value){
_xxx = value
}
})
vm = new Vue({data: myData})
- 会让vm成为myData的代理(proxy)
- 会让myData的所有属性进行监控
- 为什么要监控,为了防止myData的属性变了,vm不知道;
- vm知道属性变了就可以调用render(data),UI=render(data)
- 啥是代理(设计模式)
- 对myData对象的属性读写,全权由另一个对象vm负责
- vm就是myData的代理
- data扩展之:数据响应式
Vue的data是响应式:
const vm = new Vue({data:{n:0}})
我如果修改vm.n,那么UI中的n就会响应我
Vue通过Object.defineProperty
来实现数据响应式
- data扩展之:Vue.set或者this.$set
对象中新增的 key
Vue 没有办法事先监听和代理 要使用 set 来新增 key,创建监听和代理,更新 UI 最好提前把属性都写出来,不要新增 key 但数组做不到「不新增 key」
- data扩展之:data中数组
数组中新增的 key
用 set 新增 key,会更新 UI,但不会创建监听和代理 不过尤玉溪篡改了 7 个 API 方便你对数组进行增删 这 7 个 API 会更新 UI,但不会自动处理监听和代理 this.array[n] = xxx,即不会更新 UI,也不会自动处理监听和代理 结论:数组新增 key 最好通过 7 个 API
2.1.2 methods:事件处理函数或普通函数
const Vue = window.Vue
new Vue({
data(){
return{
n:0,
array:[1,2,3,4,5,6,7,8]
}
},
template:`
<div id="app">
{{n}}
<button @click="add">+1</button>
<hr>
{{filter()}}
</div>
`,
mothods:{
add(){
this.n += 1
},
filter(array){
this.array.filter(i => i % 2 === 0)
}
}
- props外部属性:与data内部数据有所不同,由外部来传值。
<template>
<div class="red">
这里是 Demo 的内部
{{message}}
</div>
</template>
<script>
export default{
props:['message'] //声明方式是写上属性名,从外部接受一个'message',并自动绑在this上面
}
</script>
<style scoped>
.red{
color:red;
border: 1px solid red;
}
</style>
const Vue = window.Vue
window.Vue.config.productionTip = false
import Demo from './Demo.vue'
new Vue({
components:{Demo},
data:{
visible: true
},
template:`
<Demo message="你好 props"/> //从外部传'message',传的方式是在组件后面,加上key=value
`,
methods:{
toggle(){
this.visible = !this.visible
}
}
// render: h=>h(Demo)
}).$mount('#frank')
- propers能够把内部数据data内容传入呢?
默认的html
写法,传的是字符串,比如把data
里的变量n
写入,却不是显示0
,而是显示字符串;如何把变量n
传入呢?需要在key前面加上冒号:<Demo :message="">
,加上冒号就是说其后面的内容不是字符串,而是JS代码,也就是说写成<Demo :message=" n ">
,意思是变量n,优先从内部数据data里面找,因为data有n的定义n:0
,所以会显示为0
,总结为以下两种写法都表示,传字符串0
,而且带上冒号:
template:`
<div>
<Demo message="0"/>
<Demo :message=" '0' "/>
</div>
`
propers能否传methods方法呢?实现逻辑为:main.js
里面的实例的add方法
,传给了Demo.vue
,Demo.vue
里面的click
事件调用了add
,让子元素去add
这个n
,也可以直接再子元素里面添加:message="n"
,也实现在子元素里add
这个n
<template>
<div class="red">
这里是 Demo 的内部
{{message}}
<button @click="fn">call fn</button>
</div>
</template>
<script>
export default{
props:['message','fn'] //声明
}
</script>
import Demo from './Demo.vue'
new Vue({
components:{Demo},
data:{
visible: true,
n:0
},
template:`
<div>
{{n}}
<Demo :message="n" :fn="add"/>
</div>
`,
methods:{
add(){
this.n += 1
},
2.2 DOM
DOM | 备注 |
---|---|
el | 指定替换数据的容器 等同于 append 与$mount有替换关系 |
template(完整版) | html + 占位符 |
render(非完整版) | 很少用 不会手写 用 vue-loader 生成 render |
renderError |
2.2.1 el:挂载点
new Vue({
el:'#app' //对应index.html里面的元素id属性
render:h =>h(Demo)
}).$mount('#app') //与el效果相同,可以二选一
const vm = new Vue({
render:h =>h(Demo)
})
vm.$mount('#app')
2.3 生命周期钩子(本质就是回调函数):
Vue在关键时刻调用(勾住)的特殊名称的函数
生命周期函数this指向vm或组件对象;
主要流程 | 回调函数 |
---|---|
挂载 | beforeCreate |
挂载 | created |
挂载 | beforeMount |
挂载 | mounted |
更新 | beforeUpdate |
更新 | updated |
销毁 | beforeDestroy |
销毁 | destroyed |
一切的起源是 new Vue()
,先来解释周期:
(一)挂载流程
- Init Events & Lifecycle
初始化:生命周期、事件、但数据代理还未开始
- beforeCreate
此时无法通过vm访问到data中的数据、methods中的方法;
- Init injections & reactivity
初始化:数据监测、数据代理
- created
此时可以通过vm访问到data中的数据、methods中配置的方法;
- Compile el's outerHTML as template
此阶段开始解析模板,生成虚拟DOM,页面还不能显示解析好的内容
- beforeMount
页面呈现未经Vue编译的DOM结构,所有对DOM操作最终都不奏效
- Create vm $el and replace 'el' with it
将内存中的虚拟DOM转为真实DOM插入页面
- mounted
重要的钩子:页面呈现的是经Vue编译的DOM,所有对DOM操作有效,初始化过程结束。
(二)更新流程
- beforeUpdate
此时数据时新的,页面是旧的
- Virtual DOM re-render and patch
完成了Model到View的更新
- updated
此时数据时新的,页面是新的
(三)销毁流程
- beforeDestroy
马上要执行销毁过程,vm中的data等处于可用状态
-
Teardown watchers,child components and event listeners
-
destroyed
createElement('div')创建完created
--> append('#app')挂载mounted
--> n:0 =>1 更新updated
--> 消亡destroyed
created(){
debugger //可以判断钩子是否生效
console.log('这个死鬼出现在内存中,没有出现在页面中')
},
mounted(){
console.log('我已出现在页面中')
},
updated(){
console.log('更新了')
console.log(this.n)
},
destroyed(){
console.log('已经消亡了')
},
destroyed钩子需要写一个组件:
<template>
<div class="red">
{{n}}
<button @click="add">+1</button>
</div>
</template>
<script>
export default{
data(){
return{
n:0,
array:[1,2,3,4,5,6,7,8,9]
}},
created(){
console.log('实例出现在内存中,没有出现在页面中')
},
mounted(){
console.log('实例出现在页面中',Vue完成模板的解析并把初始的真实DOM元素放入页面后调用mounted,完成挂载)
},
updated(){
console.log('实例更新了')
console.log(this.n)
},
destroyed(){
console.log('实例已经消亡了')
},
methods:{
add(){
this.n += 1
},
},
}
</script>
<style>
</style>
const Vue = window.Vue
window.Vue.config.productionTip = false
import Demo from './Demo.vue'
new Vue({
components:{Demo},
data:{
visible: true
},
template:`
<div>
<button @click="toggle">toggle</button>
<hr>
<Demo v-if="visible === true "/>
</div>
`,
methods:{
toggle(){
this.visible = !this.visible
}
}
// render: h=>h(Demo)
}).$mount('#frank')
2.4 资源
资源 | 备注 | 使用 |
---|---|---|
directives 指令 | 减少DOM操作的重复 | 全局用 Vue.directive('x',{...}) 局部用 options.directives |
filters 过滤器 | 数组里筛选 | 尽量少用 |
components 组件 | 引用其他vue文件 |
2.5 组合
组合 | 备注 | 使用 |
---|---|---|
mixins 混入 | 减少data、methods、钩子的重复 | -- |
extends 扩展 | 作用跟mixin一样,也是复制 | 全局用 Vue.extend({...}) 局部用 options.extends:{...} |
provide 依赖 | 大范围、隔n代共享信息 祖先提供东西 | -- |
inject 注入 | 大范围、隔n代共享信息 后代注入东西 | -- |
parent | -- | 全局用 Vue.mixin({...}) 局部用 options.mixins:[mixin1,mixin2] |
3. Vue组件(components)
3.1 何为组件:
实现应用中局部功能代码和资源的集合;
区别于new Vue({})
的实例写法,而是用Vue.component
或者是Demo.vue
的文件就称之为组件;
- 非单文件组件:一个文件中包含有n个组件 (样式不写在文件里)
- 单文件组件:一个文件中只包含有1个组件
3.2 单文件组件
Xx.vue
文件是要通过vue/cli脚手架
的方式编译(也可以通过webpack
编译)
- 结构:
<template>
//组件的结构
<template>
<script>
//组件交互相关的代码(数据、方法)
export default {
name:'Money'
}
</script>
<style>
//组件的样式
</style>
3.3 如何引入Vue组件
- 方法一:
在new Vue 里用 components:{}
,优先使用此方法。
<template>
<div class='red'>
demo
<div>
</template>
const Vue = window.Vue
window.Vue.config.productionTip = false
import Demo from './Demo.vue' //1)先引入组件详细内容的vue文件
const vm = new Vue({
components:{ //2)引入组件的方法一
Frank:Demo //我要用的组件名为Frank,值为Demo
},
template:`
<div id="app">
{{n}}
<button @click="add">+1</button>
<Frank/> //3)对应的是Demo组件
<hr>
{{filter()}}
</div>
`,
})
- 方法二:声明一个全局的,整个demo都可以用,直接把组件的内容写完整,用
Vue.component()
//1)使用组件方法二,直接写内容
Vue.component("Demo2",{
template:`
<div>demo2222222</div>
`
})
new Vue({
data(){
return{
n:0,
array:[1,2,3,4,5,6,7,8,9]
}},
template:`
<div class="red">
{{n}}
<button @click="add">+1</button>
<Demo2/> //2)对应的是Demo2组件
<hr>
{{filter()}}
</div>
`,
...
- 方法三:结合两种方法
这种写法可以视为组件就是实例中的实例,对象Frank的写法跟外面实例中的写法完全一样的,
import Demo from './Demo.vue'
new Vue({
components:{
Frank:{
data(){
return {n:0}
},
template:`
<div>Frank's n:{{n}}</div>
`
}
},
data(){
return{
n:0,
array:[1,2,3,4,5,6,7,8,9]
}},
template:`
<div class="red">
{{n}}
<button @click="add">+1</button>
<Frank/>
<hr>
{{filter()}}
</div>
`,
...
- 综合三种方法,有一种简洁写法:
import Demo from './Demo.vue'
new Vue({
components:{
Frank: Demo //可以把Frank直接命名为Demo
},
- 可以把Frank直接命名为Demo,可以简写为:
new Vue({
components:{
Demo: Demo
//合并为{Demo}
},
import Demo from './Demo.vue' //大小写不敏感,组件最好首字母大写,避开原生的
new Vue({
components:{Demo},
template:`
...
<Demo/>
...
`
本例完整代码;
const Vue = window.Vue
window.Vue.config.productionTip = false
import Demo from './Demo.vue'
console.log(window.Vue)
new Vue({
components:{
Frank:Demo
},
data(){
return{
n:0,
array:[1,2,3,4,5,6,7,8,9]
}},
template:`
<div class="red">
{{n}}
<button @click="add">+1</button>
<Frank/>
<hr>
{{filter()}}
</div>
`,
methods:{
add(){
this.n += 1
},
filter(array){
return this.array.filter(i=>i % 2 ===0)
}
},
}).$mount('#frank')