Vue

响应式

只有当实例被创建时就已经存在于 data 中的 property 才是响应式的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
var data = { name: "yck" };
observe(data);
let name = data.name; // -> get value
data.name = "yyy"; // -> change value

function observe(obj) {
// 判断类型
if (!obj || typeof obj !== "object") {
return;
}
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}

function defineReactive(obj, key, val) {
// 递归子属性
observe(val);
Object.defineProperty(obj, key, {
// 可枚举
enumerable: true,
// 可配置
configurable: true,
// 自定义函数
get: function reactiveGetter() {
console.log("get value");
return val;
},
set: function reactiveSetter(newVal) {
console.log("change value");
val = newVal;
},
});
}

响应式原理

Vue2.0

响应式主要是三个部分,Observer 进行数据劫持,Dep 进行依赖收集发生变化触发观察者,Watcher 生成观察者更新视图,将观察者的实例挂载到 Dep 类中,(Dep.target = Watcher 的实例).数据变化,调用回调更新视图.

  • 首先,利用Object.defineProperty将传入的对象属性添加 getter,setter.
  • 当数据读取时, 在 getter 中进行依赖收集(Dep),也就是把当前对象的 watcher 实例添加到依赖中,
  • 在 setter 中设置依赖通知,在数据变化的时候,依赖通知会通知所有的 watcher 实例执行更新,比如 watch 和 computed 就执行自定义的方法.

Vue3.0

Vue3 最大的变化时是 Observer 响应式中使用了 Proxy.
解决的 Vue2 中:

  1. 对属性的添加,删除的监测(之前使用$set)
  2. 对数组基于下标的修改,.length 修改的监测(数组内部变化,之前重写了数组方法)
  3. 对 Map,Set 等的支持

依赖收集

Vue3 通过track收集依赖,通过trigger触发更新.本质上通过weakMap, Map,Set实现.

模板字符串

1
`(${monthLine.label})`;

数据传递

1
2
3
4
5
6
7
v-model='value'
//等同于
:value='value'
@input='fatherM=>this.fatherM = value'
//等同于
:fatherM='fatherM'
:value.sync='fatherM=>this.fatherM = value'

给父组件传值

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//第一种
this.$emit('useMoney',100)
//父组件绑定useMoney事件
<Father @useMoney="useMoney" />
//父级再出发,向上传递

//第二种
this.$parent.$emit('useMoney',100)
//直接触发父级上的useMoney事件,不用再写触发事件的methods

//第三种
//封装方法
//main.js
extendEvent(Vue)
//event.js
export default function(Vue){
Vue.prototype.$eventDispatch = function(name, value){
let parent = $parent

while(parent){
parent.$emit(name, value)
parent = parent.$parent
}
}

Vue.prototype.$eventNotice = function(name,value){
let bc = children => {
children.map(c =>{
c.$emit(name, value)
if(c.$children){
bc(C.$children)
}
})
}
bc(this.$children)
}
}

监听的顺序

关于$emit$on的顺序问题,注意是先开启$on进行监听,然后触发$emit.否则触发的时候,$on并没有执行.也就监听不到.
比如使用uni.$emit进行触发时,如果跳转到另一个页面,$on是监听不到的,因为已经先触发了emit.
正确使用方法: 比如需要回退,那么先打开监听$on,然后在另一页面触发后,回退到该页面,该页面就已经监听到了.

组件传值

子组件通过$emit给父组件传值,附带参数. this.$emit('change', data) .
传递多个参数可以使用函数,this.$emit('change', {data0, data1})父组件在页面接收时不用写接收参数.@change='change()'  括号里不用写参数.函数里再写.e

nextTick

  1. created中进行 DOM 操作一定要放在Vue.nextTick
  2. 在数据变化后要执行某个操作,而这个操作需要使用随数据改变而改变 DOM 结构时,要放在Vue.nextTick()中.
  3. 操作数组中的 ref 时,注意 ref 的 VueComponent 节点渲染也是数组,要取数组的第一个,即后面要加[0].
1
this.$nextTick(() => this.$refs[`sub${index}`][0].init());

数据劫持

Vue2 中无法使用Object.defineproperty给数据添加getter/setter属性,但是这个是 Object 的方法,不能操作数组.就要对数组方法重写.即数据劫持.
要重写的是改变数组的方法,一共 7 个.push,pop,shift,unshift,sort,splice,reserve.

生命周期

created: DOM 创建但是未挂载,所以这里访问不了 DOM,一般用作调用 api.Vue3 中的 setup 语法题里也获取不到 DOM.
mounted: 页面挂载后,这里就可以拿到 DOM 了.

动态参数

v-指令后面可以跟一个动态参数,用方括号[]包裹.里面是一个 js 表达式的结果.

1
2
3
4
//bind后的动态参数表达响应式的绑定这个值
<a v-bind:[attr] = "url"></a>
//v-on后面的参数也就是个动态的事件名
<a v-on:[eventName]="doSometing"></a>

约束

动态参数预期要一个字符串,异常情况下值为null.这个null会被用于移除绑定.
空格和引号在方括号里是无效的.可以用计算属性替代这种写法.
这里不要用大写,因为会被浏览器强制转换.

动态 class 与 style

v-bind:class接收一个对象以动态切换 class.

1
2
3
4
5
6
<div v=bind:class="{active: idActice, 'text-danger': hasError }"></div>

data : {
isActive: true,
hasError: false
}

计算属性

计算属性所计算的值不用在data中声明.可以说是在computed中声明的一个参数.利用this中的参数去处理之后得到这个值.处理完要return出去.

原理

动态 class 和计算属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div :class="classObject"></div>

data: {
isActive: true,
error: null
}
computed: {
classObiect(){
return {
active: this.isActive && !this.error,
'text-danger': this.error && this.error.type === 'fatal'
}
}
}

数组语法

1
2
3
4
5
6
7
8
9
<div :class="[activeClass, errorClass]"></div>

data: {
activeClass: 'active',
errorClass: 'text-danger'
}

//或者三元表达式
<div :class="[isActive ? activeClass : '', errorClass]"></div>

数组语法加对象语法

1
<div :class="[{ active: isActive }, errorClass]"></div>

绑定内联样式

对象语法

1
2
3
4
5
6
<div :style="{ color: activeColor, fontSize: fontSize + 'px' }"></div>

data: {
activeColor: 'red',
fontSize: 30
}

数组语法

1
<div :style="[activeColor, baseStyle]"></div>

v-for

v-for第一个参数是 value,第二个可以是键名name,第三个可以是索引index.

key

key 的特殊属性用于虚拟 dom 算法中对比新旧 nodes,辨识 VNodes.
如果不使用 key,Vue 会就地复用相同类型元素.可能会造成组件没有重新渲染.
使用 key,它会基于 key 的变化重新排列元素顺序,并会移除 key 不存在的元素.
下列场景适合使用 key:

  • 完整触发组件的生命周期钩子
  • 触发过渡
1
2
3
<transition>
<span :key="text">{{text}}</span>
</transition>

当 text 发生改变时, 总是会被替换而不是被修改,因此会触发过渡。

搭配计算属性

1
2
3
4
5
6
7
8
9
10
<li v-for="n in evenNumbers">{{ n }}</li>

data: {
numbers: [1,2,3,4,5]
},
computed: {
eventNumbers(){
return this.numbers.filter(number => number %2 === 0)
}
}

多维数组循环使用方法代替计算属性

1
2
3
4
5
6
7
8
9
10
11
12
<ul v-for"set in sets">
<li v-for="n in event(set)">{{ n }}</li>
</ul>

data: {
sets: [[1,2,3,4,5],[6,7,8,9,10]]
},
methods: {
even(numbers){
return numbers.filter(number => number%2 === 0)
}
}

v-for 和 v-if 搭配

不推荐二者在同一个元素使用,而且v-for优先级比v-if高.
但是如果想要部分渲染节点,不符合条件的不渲染可以用 v-if.

v-on

v-on可以传事件名,接参数,也可以访问原始 DOM 事件.

1
2
3
4
5
<button @click="greet"></button>

<button @click="alert(msg)"></button>

<button @click="warn('msg', $event)"></button>

v-model

在自定义事件上的 v-model 可以拆分为

1
<input v-bind:value="search" v-on:input="search=$event.target.value" />

在组件上的v-model可以拆分为

1
2
3
4
5
6
7
8
<custom-input
v-bind:value="search"
v-on:input="search = $event"
><custom-input>
//组件代码应为
<input v-bind:value="value"
v-on:input="$emit('input',$event.target.value)"
/>

computed VS methods VS watch

点击 button,value改变,watch 监听这个value,变化后在 watch 中导致 watchValue 变化.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div>
<ul>
<li>{{computedValue}}</li>
<li>{{computedValue}}</li>
<li>{{computedValue}}</li>
</ul>
<ul>
<li>{{methodsValue}}</li>
<li>{{methodsValue}}</li>
<li>{{methodsValue}}</li>
</ul>
<ul>
<li>{{watchValue}}</li>
<li>{{watchValue}}</li>
<li>{{watchValue}}</li>
</ul>
<button @click="changeValue">change</div>
</div>
</template>
<script>
data(){
return {
value: 1,
watchValue:1
}
},
watch:{
value(val){
console.log("watch");
//刚加载并未执行,因为没有发生变化.
//再次点击change,只打印一次
//如果要加载就执行,可添加immediate,函数就变成handler
this.watchValue = val + 1
}
},
computed: {
computedValue(){
console.log('computedValue'); //打印1次,有缓存,不变化不重复计算
return this.value + 1
}
},
methods:{
methodsValue(){
console.log('methodsValue'); //打印3次
return this.value + 1
},
changeValue(){
this.value = 2
}
}
}
</script>

watch

这里 watch 的一个特点是,最初绑定的时候是不会执行的,要等到 firstName 改变时才执行监听计算。那我们想要一开始就让他最初绑定的时候就执行改怎么办呢?我们需要修改一下我们的 watch 写法,修改过后的 watch 代码如下:

1
2
3
4
5
6
7
8
9
10
11

watch: {
firstName: {
handler(newName, oldName) {
this.fullName = newName + ' ' + this.lastName;
},
// 代表在wacth里声明了firstName这个方法之后立即先去执行handler方法
immediate: true
}
}

注意到 handler 了吗,我们给 firstName 绑定了一个 handler 方法,之前我们写的 watch 方法其实默认写的就是这个 handler,Vue.js 会去处理这个逻辑,最终编译出来其实就是这个 handler。
而 immediate:true 代表如果在 wacth 里声明了 firstName 之后,就会立即先去执行里面的 handler 方法,如果为 false 就跟我们以前的效果一样,不会在绑定的时候就执行。

箭头函数不能在 watch 中写

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 错误写法
watch: {
studentId: (newData, oldData) => {
this.disabled = !newData
console.log(this, 'this')
}
},

// 正确写法
watch: {
studentId: function (newData, oldData) {
this.disabled = !newData
console.log(this, 'this')
}
}

箭头函数会改变 this 的指向,在 Vue 组件中,最好不要随便使用箭头函数。特别是 watch 以及生命周期中。!文档也有相应的提示
image.png

computed 和 watch 区别

计算属性是根据组件数据派生出新数据.使用方式是设置一个函数,返回计算后的结果.具备缓存性质,如果依赖项不变,是不会重新计算的.
监听器 watch 可以监听某个响应式数据的变化并执行副作用.使用方式是传递一个函数,并执行副作用.一般用于请求接口.
场景上:
计算属性一般是在模板中表达式过于复杂就可以写在计算属性中.
监听器是在状态变化后做一些额外的 DOM 操作或者异步请求.

动态组件

通过componentis属性.

1
<component :is="currentTabComponent"></component>

currentTabComponent可以是已注册组件的名字或一个组件的选项对象.

is的其他用处

比如在table中,加入自定义组件会失效.

1
2
3
4
5
6
7
8
//失效
<table>
<blog-post-row></blog-post-row>
</table>
//生效
<table>
<tr is="blog-post-row"></tr>
</table>

另外,如果从以下条件使用模板,不存在限制.
字符串(例如: template:"...")
单文件组件(.vue)
<script type="text/x-template">

非 prop 的 Attribute

指传向一个组件,但是该组件并没有相应 prop 定义的 attribute.

禁用 Attribute 继承

如果不希望组件的根元素继承 attribute,可以设置inheritAttrs: false.
禁用继承和$attrs可以手动决定这些attribute会被赋予那个元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<label>
{{ label }}
<input
v-bind="$attrs"
v-bind:value="value"
v-on:input="$emit('input', $event.target.value)"
/>
</label>
</template>
<script>
export defalut {
inheritAttrs: false,
name: 'base-input',
props:['label','value'],
data(){}
</script>

注意inheritAttrs: false选项不会影响styleclass的绑定.

这个模式允许使用基础组件的时候更像是使用原始的 HTML 元素,而不会担心哪个元素是真正的根元素.

1
2
3
4
5
6
<base-input
label="Username:"
v-model="username"
required
placeholder="Enter your name"
></base-input>

$listeners

Vue 提供的一个属性,是个对象.包含了作用在这个组件上的所有监听器.
在一个组件的根元素上无法监听到原生事件时可以使用.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<label>
{{ label }}
<input v-bind="$attrs" v-bind:value="value" v-on="inputListener" />
</label>
</template>
<script>
export default {
name: 'base-input',
inheritAttrs:false,
props: ['label', 'value'],
computed: {
inputListener(){
var vm = this
return Object.assign({},this.$listeners,
{input: function(event){
vm.$emit('input', event.target.value)
}
})
}
}
</script>

.sync修饰符

.sync相当于update:myPropsName的模式.
比如更新 title

1
2
3
4
5
6
7
8
this.$emit('update:title', newTitle)

<text-docunment
v-bind:title="doc.title"
v-on:update:title="doc.title = $event"
></text-document>

<text-document :title.sync="doc.title"></text-document>

注意带有.sync修饰符的v-bind不能和表达式一起使用(例如v-bind:title.sync="doc.title + '!'"是无效的).取而代之的是只能使用想要绑定的 property 名,类似v-model.

多个 prop

1
<text-document v-bind:sync="doc"></text-document>

插槽

规则

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的.

v-slot 只能添加在<template>

作用域插槽

如果想在插槽中访问到子组件的数据,就得用作用域插槽.

1
2
3
4
5
6
7
8
9
10
11
12
13
//把要访问的数据绑定到slot上 //子组件
<span>
<slot v-bind:user="user">
{{user.lastName}}
</slot>
</span>

//上面的user数据绑定到了user上.被称为插槽prop //父级作用域中使用v-slot访问
<current-user>
<template v-slot:default="slotProps">
{{slotProps.user.firstName}}
</template>
</current-user>

解构插槽 Prop

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//上面示例中,父级的slotProps可以解构
<current-user v-slot="{ user }">
{{user.firstName}}
</current-user>

//多个prop可以重命名
<current-user v-slot="{ user: person }">
{{person.firstName}}
</current-user>

//设定初始值
<current-user v-slot="{ user = { firstName: 'Tom' } }">
{{user.firstName}}
</current-user>

动态插槽

1
2
3
4
<base-layout>
<template v-slot:[slotName]>
</template>
</base-layout>

动态组件&异步组件

动态组件上使用 keep-alive

is 动态切换时会重新渲染,如果不需要重新渲染可以用keep-alive缓存下来.

1
2
3
<keep-alive>
<component :is="currentTabcomponent"></component>
</keep-alive>

异步组件

就是异步导入,返回 Promise

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//标准写法
Vue.component("template", function (resolve, reject) {
setTimeout(function () {
resolve({ template: "<div>I am async!</div>" });
}, 1000);
});
//require语法
Vue.component("template", function (resolve) {
require(["./my-component"], resolve);
});
//import写法
Vue.component("template", () => import("./my-component"));
//局部注册
new Vue({
components: {
"my-component": () => import("./my-component"),
},
});

加载状态

1
2
3
4
5
6
7
8
9
10
11
const AsyncComponent = () => ({
component: import("./MyComponent.vue"),
//异步组件加载时所用的组件
loading: LoadingComponent,
//加载失败使用的组件
error: ErrorComponent,
//展示加载时组件的延时时间
delay: 200,
//超时时间
timeout: 3000,
});

处理边界情况

也就是操作其他组件内部或者手动操作 DOM 的情况.

根实例

1
2
3
this.$root.foo;
//写入
this.$root.foo = 2;

父组件实例

this.$parent.xxx

子组件实例或子元素

1
2
3
4
5
6
7
8
9
10
11
12
13
<base-input ref="usernameInput"></base-input>

//this.$refs访问这个实例
this.$refs.usernameInput

<input ref="input">

//通过其父级组件定义方法:
methods: {
focus(){
this.$refs.input.focus()
}
}

$refs只会在组件渲染完成之后生效.并且不是响应式的.仅作为一个操作子组件的应急方案.所以应该避免在模板或计算属性中访问$refs.

依赖注入

如果层级过多,使用$parents不方便.就要使用依赖注入,即provideinject.

1
2
3
4
5
6
7
8
9
//父级组件中声明一个要给子组件的参数,放到provide中
provide: function(){
return {
getMap: this.getMap
}
}

//在后代组件中,使用inject接收
inject: ['getMap']

可以把依赖注入看做一部分”大范围有效的 prop”.

程序化的事件监听器

$on(eventName, eventHandler)侦听一个事件
$once(eventName, eventHandler)一次性侦听一个事件
$off(eventName, eventHandler)停止侦听一个事件
用法示例:

1
2
3
4
5
6
7
8
9
10
//声明周期结束前销毁方法
mounted: function(){
var picker = new Pikaday({
field: this.$refs.input,
format: 'YYYY-MM-DD'
})
this.$once('hook:beforeDestroy', function(){
picker.destroy()
})
}

循环利用

递归组件

组件是可以在他们自己的模板中调用自身的.不过只能通过name来做这件事.为了避免无限循环,确保递归调用是条件性的(例如使用一个最终会得到falsev-if.

组件之间的循环引用

当首先组件 A 依赖组件 B,,然后 B 又依赖 A,如此往复.形成了循环.那么需要做一个点,A 需要 B,但不需要先解析 B.

1
2
3
4
5
6
7
8
9
10
11
12
13
//tree-folder
<p>
<span>{{ folder.name }}</span>
<tree-folder-contents : children="folder.children"/>
</p>

//tree-folder-content
<ul>
<li v-for="child in children">
<tree-folder v-if="child.children" :folder="child"/>
<span v-else>{{ child.name }}</span>
</li>
</ul>

处理方法

1
2
3
4
//在beforeCreate时注册它
beforeCreate(){
this.$options.components.TreeFolderContents = require('./tree-folder-contents.vue').default
}

或者在本地注册时,使用异步导入,

1
2
3
components: {
TreeFolderContents: () => import("./tree-folder-contents.vue");
}

mixin

递归合并

当组件和混入对象含有同名选项时,这些选项将以恰当的方式进行”合并”.
数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
var mixin = {
data() {
return {
message: "hello",
foo: "abc",
};
},
};
new Vue({
mixins: [mixin],
data() {
return {
message: "goodbye",
bar: "def",
};
},
created: function () {
console.log(this.$data);
//=>{ message:"goodbye', foo: 'abc', bar: 'def' }
},
});

同名钩子函数合并为一个数组,混入对象的钩子在组件自身的钩子之前调用.
如果createdmixin中有同名方法,两个生命周期都会执行,执行的方法按照页面的方法执行.
值为对象的选项,如methods,componentsdirectives,将被合并为同一个对象.两个对象键名冲突时,取组件对象的键值对.

全局混入

1
2
3
4
5
6
7
8
9
10
11
12
Vue.mixin({
created() {
var myOption = this.$options.myOption;
if (myOption) {
console.log(myOption);
}
},
});

new Vue({
myOption: "hello!",
});

自定义指令

钩子函数

一个指令定义对象可以提供以下钩子函数(均为可选):

  • bind:只调用一次,指令第一次绑定到元素时调用.在这里可以进行一次性的初始化设置.
  • inserted被绑定元素插入父节点时调用(仅保证父节点存在,但不一定已被插入文档中)
  • update所在组件的 VNode 更新时调用.但是可能发生在其子 VNode 更新之前.指令的值可能发生了改变,也可能没有.
  • componentUpdated指令所在组件的 VNode 及其子 VNode 全部更新后调用.
  • unbind只调用一次,指令与元素解绑时调用.

钩子函数参数

  • el指令所绑定的元素,可以用来直接操作 DOM
  • binding一个对象.包含以下 property:
  1. name指令名,不包含v-前缀.
  2. value指令的绑定值.例如v-my-directive="1+1"中,绑定值为 2.
  3. oldValue指令绑定的前一个值.仅在updatecomponentUpdated钩子中可用.无论值是否改变都可用
  4. expression字符串形式的指令表达式.例如v-my-directive="1+1"中,表达式为"1+1".
  5. arg传给指令的参数.可选.例如v-my-directive:foo中参数为”foo”.
  6. modifiers一个包含修饰符的对象.例如v-my-directive.foo.bar,修饰符对象为{ foo: true, bar: true }
  7. vnodeVue 编译生成的虚拟节点.
  8. oldVnode上一个虚拟节点,仅在updatecomponentUpdated钩子可用.

    除了el之外,其他参数都应该是只读的,切勿进行修改.如果需要在钩子之间共享数据,建议通过元素的dataset来进行.

动态指令参数

用法示例
创建一个自定义指令,用来通过固定布局将元素固定在页面上.

1
2
3
4
<div id="baseexample">
<p>Scroll down the page</p>
<p v-pin="200">Stick me 200px from the top of the page</p>
</div>
1
2
3
4
5
6
7
8
9
Vue.directive("pin", {
bind: function (el, binding, vnode) {
el.style.position = "fixed";
el.style.top = binding.value + "px";
},
});
new Vue({
el: "#baseexample",
});

这会把该元素固定到距顶部 200px 位置.如果需要固定在左侧,可以使用动态参数实现.

1
2
3
4
<div id="example">
<h3>Scroll down inside this section</h3>
<p v-pin:[direction]="200">I am pinned onto</p>
</div>

对象字面量

1
2
3
4
5
<div v-demo="{ color:'white', text: 'hello!' }"></div>

Vue.directive('demo',function(el, binding){
console.log(binding.value.color) //=> 'white'
console.log(binding.value.text) //=> 'hello!'

渲染函数 & JSX

通过 render 函数去解决重复而冗长的代码.

插件