Vuejs进阶


title: Vue.js进阶

date: 2019-09-13 15:47:32

tags:  # 这里写的分类会自动汇集到 categories 页面上,分类可以多级

  • Vue.js # 一级分类
  • Vue.js进阶 # 二级分类

关于h

h 作为 createElement 的别名是 Vue 生态系统中的一个通用惯例.

响应式原理

官网解释

当你把一个普通的 JavaScript 对象传入 Vue 实例作为 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/setter。每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

官方解释总结:

  1. 任何一个 Vue 组件都有一个与之对应的Watcher实例。
  2. Vue 的data上的属性会被添加gettersetter属性。
  3. 当 Vue 组件的render函数被执行的时候,data上会被触碰(touch),即被读,getter方法会被调用,此时 Vue 会去记录此 Vue 组件所依赖的所有data。(这一过程被称为依赖收集)
  4. data被改动时(主要是用户操作),即被写,setter方法会被调用,此时 Vue 会去通知所有依赖于此data的组件去调用他们的render函数进行更新。

其他说法:

mvvm 用来初始化数据

observer用来对初始数据通过Object.defineProperty添加settergetter,当取数据(即调用 get)的时候添加订阅对象(watcher)到数组里, 当给数据赋值(即调用 set)的时候就能知道数据的变化,此时调用发布订阅中心的notify,从而遍历当前这个数据的订阅数组,执行里面所有的watcher,通知变化update

compiler 是用来把 data 编译到 dom 中。分三步:1.先把真实的 dom 移入到内存中fragment,2.编译:提取想要的元素节点v-model和文本节点;3.把编译好的 fragment 塞回到页面去。第二步骤中会对编译到 dom 中的data添加watcher,当data变化时,这里的watcher回调也能收到通知得到执行。

watcherobervercompiler之间通信的桥梁。

Object.defineProperty 的缺陷

  1. 无法监听数组变化。
  2. 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历,显然能劫持一个完整的对象是更好的选择。

使用 Proxy 实现 Vue 数据劫持

proxy 定义: Proxy 对象用于定义基本操作的自定义行为(如属性查找,赋值,枚举,函数调用等).

Proxy 第一个参数是目标对象,第二个参数是一个对象,其属性是当执行一个操作时定义代理的行为的函数。这时可以在第二个参数中加入一个 set 方法,这时可以监听到是哪个 key 做了改变。并且通过 Reflect 的 set 方法去模拟真实的 set 方法。

为什么说 Proxy 的性能比 Object.defineProperty 更好呢?

Object.defineProperty只能监听属性,而 Proxy 能监听整个对象,省去对非对象或数组类型的劫持,也能做到监听。

vue 是对对象每一个属性进行Object.defineProperty

第二点,Object.defineProperty不能监测到数组变化

总结

Proxy 返回的是一个新对象,我们可以只操作新的对象达到目的,而Object.defineProperty只能遍历对象属性直接修改;

Proxy 作为新标准将受到浏览器厂商重点持续的性能优化,也就是传说中的新标准的性能红利

当然,Proxy 的劣势就是兼容性问题,而且无法用polyfill实现

Proxy 基本语法

const obj = new Proxy(target, handler);

参数说明如下:

target: 被代理对象。

handler: 是一个对象,声明了代理 target 的一些操作。

obj: 是被代理完成之后返回的对象。

但是当外界每次对 obj 进行操作时,就会执行 handler 对象上的一些方法。handler 中常用的对象方法如下:

1
2
3
4
5
1. get(target, propKey, receiver)
2. set(target, propKey, value, receiver)
3. has(target, propKey)
4. construct(target, args):
5. apply(target, object, args)

常用(考)组件之keep-alive

作用: 缓存组件内部状态,避免重新渲染

注意: 和<transition>相似,<keep-alive>是一个抽象组件:自身不会渲染一个 DOM 元素,也不会出现在父组件链中.

用法:

缓存动态组件:

<keep-alive>包裹动态组件时,会缓存不活动的组件实例,而不是销毁它们,此种方式并无太大的实用意义。

1
2
3
4
5
6
7
8
9
10
<!-- 基本 -->
<keep-alive>
<component :is="view"></component>
</keep-alive>

<!-- 多个条件判断的子组件 -->
<keep-alive>
<comp-a v-if="a > 1"></comp-a>
<comp-b v-else></comp-b>
</keep-alive>

使用keep-alive可以将所有路径匹配到的路由组件都缓存起来,包括路由组件里面的组件,keep-alive大多数使用场景就是这种。

1
2
3
<keep-alive>
<router-view></router-view>
</keep-alive>

常用(考)API 之nextTick

作用: $nextTick是将回调推迟到下次 DOM 更新循环之后再执行,在修改数据之后使用$nextTick,则可以在回调中获取更新后的 DOM,

Vue 实现响应式并不是数据发生变化之后 DOM 立即变化,而是按一定的策略进行 DOM 的更新。

1
2
3
4
5
6
7
8
9
10
11
// 修改数据
vm.msg = "Hello";
// DOM 还没有更新
Vue.nextTick(function () {
// DOM 更新了
});

// 作为一个 Promise 使用 (2.1.0 起新增,详见接下来的提示)
Vue.nextTick().then(function () {
// DOM 更新了
});

为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。在组件内使用vm.$nextTick()实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 this 将自动绑定到当前的 Vue 实例上.因为 $nextTick()返回一个 Promise 对象,所以可以使用 ES6 语法.

常用(考)API 之 set

返回: 设置的值.

用法: 向响应式对象中添加一个属性,并确保这个新属性同样是响应式的,且触发视图更新。它必须用于向响应式对象上添加新属性,因为 Vue 无法探测普通的新增属性 .

注意对象不能是 Vue 实例,或者 Vue 实例的根数据对象。

常用(考)API 之 watch

一个对象,是需要观察的表达式对应回调函数。值也可以是方法名,或者包含选项的对象。

Vue 实例将会在实例化时调用 $watch(),遍历 watch 对象的每一个属性。

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
<div id="app">
<input type="text" v-model:value="childrens.name" />
<input type="text" v-model:value="lastName" />
</div>

<script type="text/javascript">
var vm = new Vue( {
el: '#app',
data: {
children: {
name: '小强',
age: 20,
sex: '男'
},
tdArray:["1","2"],
lastName:"张三"
},
watch:{
children:{
//如果childrens发生变化,函数就会执行
handler:function(val,oldval){
console.log(val.name)
},
deep:true//对象内部的属性监听,也叫深度监听
},
'children.name':function(val,oldval){
console.log(val+"aaa")
},//键路径必须加上引号
lastName:{function(val,oldval){
console.log(this.lastName)
},
immediate:true //立即以'childern.name'触发回调
}
},//以V-model绑定数据时使用的数据变化监测
} );
vm.$watch("lastName",function(val,oldval){
console.log(val)
})//主动调用$watch方法来进行数据监测
</script>
</body>

取消观察

vm.$watch 返回一个取消观察函数,用来停止触发回调:

1
2
3
var unwatch = vm.$watch("a", cb);
// 之后取消观察
unwatch();

注意,不应该使用箭头函数来定义watcher函数 (例如searchQuery: newValue => this.updateAutocomplete(newValue))。理由是箭头函数绑定了父级作用域的上下文,所以this将不会按照期望指向 Vue 实例,this.updateAutocomplete将是undefined

其他 API

Vue.extend(options)

用法: 使用基础 Vue 构造器创建一个子类.参数是一个包含组件选项的对象.

data 在Vue.extend中是函数

1
2
3
4
5
6
7
8
9
10
11
12
13
// 创建构造器
var Profile = Vue.extend({
template: "<p>{{firstName}} {{lastName}} aka {{alias}}</p>",
data: function () {
return {
firstName: "Walter",
lastName: "White",
alias: "Heisenberg",
};
},
});
// 创建 Profile 实例,并挂载到一个元素上。
new Profile().$mount("#mount-point");

Vue.set(target,propertyName/index,value)

返回值: 设置的值

用法: 向响应式添加新属性,并确保这个新属性同样是响应式的.且触发识图更新.它必须用于响应式对象上添加新属性,因为 Vue 无法探测新增的属性.

如果我们在创建实例以后,再在实例上绑定新属性,vue 是无法进行双向绑定的。

注意: 对象不能是 Vue 实例,或者 Vue 实例的根数据对象.

Vue.mixin

用法: 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例.插件作者可以使用混入,向组件注入自定义行为.不推荐在应用代码中使用.

1
2
3
4
5
6
Vue.mixin({
beforeCreate() {
//..逻辑
//这种方法会影响到每个组件的beforeCreate钩子函数
},
});

虽然文档不建议我们在应用中直接使用 mixin ,但是如果不滥用的话也是很有帮助的,比如可以全局混入封装好的 ajax 或者一些工具函数等等。

mixins应该是我们最常使用的扩展组件的方式了。如果多个组件中有相同的业务逻辑,就可以将这些逻辑剥离出来,通过mixins混入代码,比如上拉下拉加载数据这种逻辑等等。

另外需要注意的是mixins混入的钩子函数会先于组件内的钩子函数执行,并且在遇到同名选项的时候也会有选择性的进行合并.

Vue 页面优化(spa 首屏单页面)

  1. 压缩代码
  2. 框架和插件按需加载
  3. 框架和插件从 CDN 中引入
  4. 路由懒加载
  5. SSR 服务端渲染

函数化组件

先设置functional: true,表示该组件无状态无实例,不能使用this

使用上下文context进行替换

替换规律:

1
2
this.text-----context.props.text
this.$slots.default------context.children
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
<div id="app">
<my-component value="haha">
</my-component>
</div>
<script>
Vue.component('my-component',{
functional: true, //开启函数化,无实例,无状态.this失效
render: function(createElement,context){
//添加context参数
return createElement('button',{
on: {
click: function(){
console.log(context)
console.log(context.parent) //父组件
console.log(context.parent.msg)
console.log(context.props.value)
}
}
},'点击我') //第三个参数
},
props:['value']
)

var app = new Vue({
el: "#app",
data: {
msg: "我是父组件内容"
}
})
</script>

虚拟 dom

虚拟 DOM 到底是什么,说简单点,就是一个普通的 JavaScript 对象,包含了 tagpropschildren 三个属性。

虚拟 dom 优点

用 JS 对象模拟 DOM 节点的好处是,页面的更新可以先全部反映在 JS 对象(虚拟 DOM )上,操作内存中的 JS 对象的速度显然要更快,等更新完成后,再将最终的 JS 对象映射成真实的 DOM,交由浏览器去绘制。

1
2
3
<div id="app">
<p class="text">hello world!!!</p>
</div>

上面的 HTML 转换为虚拟 DOM 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
{
tag: 'div',
props: {
id: 'app'
},
chidren: [
{
tag: 'p',
props: {
className: 'text'
},
chidren: [
'hello world!!!'
]
}
]
}

该对象就是我们常说的虚拟 DOM 了,因为 DOM 是树形结构,所以使用 JavaScript 对象就能很简单的表示。而原生 DOM 因为浏览器厂商需要实现众多的规范(各种 HTML5 属性、DOM 事件),即使创建一个空的 div 也要付出昂贵的代价。虚拟 DOM 提升性能的点在于 DOM 发生变化的时候,通过 diff 算法比对 JavaScript 原生对象,计算出需要变更的 DOM,然后只对变化的 DOM 进行操作,而不是更新整个视图.

diff 算法

diff 算法用来比较两棵 Virtual DOM 树的差异,如果需要两棵树的完全比较,那么 diff 算法的时间复杂度为 O(n^3)。但是在前端当中,你很少会跨越层级地移动 DOM 元素,所以 Virtual DOM 只会对同一个层级的元素进行对比,如下图所示, div 只会和同一层级的 div 对比,第二层级的只会跟第二层级对比,这样算法复杂度就可以达到 O(n)。

Vue 中的虚拟 dom

Vue 通过建立一个虚拟 DOM 来追踪自己要如何改变真实 DOM。请仔细看这行代码:

1
return createElement("h1", this.blogTitle);

createElement 到底会返回什么呢?其实不是一个实际的 DOM 元素。它更准确的名字可能是 createNodeDescription,因为它所包含的信息会告诉 Vue 页面上需要渲染什么样的节点,包括及其子节点的描述信息。我们把这样的节点描述为“虚拟节点 (virtual node)”,也常简写它为“VNode”。“虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼。

render 函数(渲染函数)

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
<template id="hdom">
//template下只允许有一个子节点,所以用div包裹三个标题
<div>
<h1 v-if="level==1">
<slot></slot>
</h1>
<h2 v-if="level==2">
<slot></slot>
</h2>
<h3 v-if="level==3">
<slot></slot>
</h3>
</div>
</template>

<script>

Vue.component('child', {
//使用template命名的方法将大段代码写在html里
props: ['level'],
template: '#hdom'
})

Vue.component('child', {
//使用render函数替代template,节省大量代码
render: function (createElement) {
return createElement('h' + this.level, this.$slots.default);
},
props: ['level']
})
</script>

render 函数的第一个参数

在 render 函数的方法中,参数必须是createElement,它的类型是function

createElement的第一个参数必选.类型可以是String|Object|Function

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Vue.component("child", {
render: function (createElement) {
return createELement("h1");
//参数是字符串.返回<h1></h1>
return createElement({
template: `<div>鹅鹅鹅</div>`,
});
//参数是对象.返回<div>鹅鹅鹅</div>
var domFun = function () {
return {
template: `<div>鹅鹅鹅</div>`,
};
};
return createELement(domFun());
//参数是函数.返回<div>鹅鹅鹅</div>
},
});

render 函数的第二个参数

createElement的第二个参数可选.参数是数据对象,只能是Object

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
Vue.component('child',{
render: function(createElement){
return CreateElement({
template: `<div>鹅鹅鹅</div>`
},{
//添加class选项,其中为true的会添加到模板<div>中
'class':{
foo: true
baz: false
},
//添加style属性
style: {
color: 'red',
fontSize: '18px'
},
//正常的html属性
attrs: {
id: 'foo',
src: 'xxxx'
},
//原生DOM属性
domProps: {
innerHTML: '<span style="color:blue">我是蓝色</span>'
}
}

)
}
})

render 函数的第三个参数

createElement的第三个参数可选.参数可以是String|Array,代表子节点

1
2
3
4
5
6
7
8
Vue.component("child", {
render: function (createElement) {
return createElement("div", [
createElement("h1", "我是h1标题"),
createElement("h6", "我是h6标题"),
]);
},
});

在 render 函数中使用this.$slots

第三个参数存的是 VNode,也就是虚拟节点.组件树中的所有 VNode 必须是唯一的.

1
2
3
4
5
6
7
8
9
10
11
12
13
Vue.component("my-component", {
render: function (createElement) {
var header = this.$slots.header;
//返回的就是含有VNode的数组
var main = this.$slots.default;
var footer = this.$slots.footer;
return createElement("div", [
createElement("header", header),
createElement("main", main),
createElement("footer", footer),
]);
},
});

在 render 函数中使用 props 传数据

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
<div id="app">
<button @click="switchShow">点击切换</button>
<my-component :show="show"></my-component>
</div>

<script>
Vue.component('my-component',{
props: ['show'],
render: function(createElement){
var imgsrc
if(this.show){
imgsrc = 'img/001.jpg'
}else{
imgsrc = 'img/002.jpg'
}
return createElement('img',{
attrs: {
scr: imgsrc
}
})
}
})

var app = new Vue({
el: "#app",
data: {
show: true
},
methods: {
switchShow: function(){
this.show = !this.show
}
}
})
</script>

在 render 函数中使用v-model

v-model作用: 接收input的内容并绑定到后面的值上.

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
<div id="app">
<my-component v-bind:name="name" v-model="name"></my-component>
{{ name }}
</div>
<script>
Vue.component('my-component',{
render: function(createElement){
props:['name'],
var self = this //指的是当前的Vue实例
return createElement('input',{
domProps: {
//原生DOM
value: self.name
},
on: {
//这里添加事件
input: function(event){
//此处的this是window,所以需要声明self指代vue实例
self.$emit('input',event.target.value)
}
}
})
}
})

var app = new Vue({
el: "#app",
data: {
name: "Tom"
}
})
</script>

render 函数中使用作用域插槽

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div id="app">
<my-component>
<template scoped="prop">
{{ prop.text }}
{{ prop.msg }}
</template>
</my-component>
</div>
<script>
Vue.component('my-component',{
render: function(createElement){
return createElement('div',this.$scopedSlots.default({
text: '我是子组件传递的数据',
msg: 'scopetext'
}))
})

var app = new Vue({
el: "#app",
data: {

}
})
</script>