Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 Javascript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样非常重要,这样你可以回避一些常见的问题。

原理图剖析

仔细阅读这张官方原理图,大概可以剖析为以下几个步骤:

  • 编译组件:对特殊标记的部分(比如双大括号部分)进行替换为相应的数据值。
  • 收集依赖:对于编译阶段依赖的数据进行监听(这个都是通过 watcher 对象实现的)
  • 通知更新:当步骤2中监听的数据发生变化时,会通知 watcher 进行重新计算,触发关联视图更新。

可以简单理解为一个发布订阅系统,当然这里的过程介绍比较通俗,单纯是为了理解而解释的,实际流程其实还是很复杂的。如果想结合源码深入了解的,建议去阅读这篇文章:
Vue 源码解析:深入响应式原理

关于Vuejs模板

上一篇中提到,Vue实例在初始化的时候会将目标节点 div#app 内容进行替换,是在定义了 template 属性或者 render 函数的前提下。否则会对把 div#app 内容当做模板进行编译输出。一般情况下,很少使用 template 属性来定义模板,因为实际开发过程中,我们的模板还是很复杂的,单纯通过 template 属性定义,我们的代码会显得非常臃肿,不便于阅读。所以,我们通常使用的是将模板内容写在过载目标元素的内部,稍后将采用这种方法做代码演示。

当然对于复杂项目,更多的是使用 单文件组件 (这将在以后专题文章中介绍)。

百闻不如一见

还记得上一篇中讲述的如何实现一个简单的 Vue 应用,并通过不同的 API 实现了目标元素的渲染吧。不记得也不要紧,这里还是从最基本的代码开始。下面我们会动态的实现一个列表的渲染,并通过修改数据,来触发视图的更新,以此来感受下响应式系统的快感。先来看一段代码:

Demo1:data

1
2
3
4
5
6
7
8
<div id="app">
<button v-on:click="addItem">添加</button>
<ul>
<li v-for="(item, index) in list" v-bind:key="index">
<a v-bind:href="item.url">{{ item.name }}</a>
</li>
</ul>
</div>
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
var app = new Vue({
el: "#app",
data () {
return {
count: 1,
list: [
{
name: 'Vuejs官网',
url: 'https://cn.vuejs.org'
},
{
name: 'Github',
url: 'https://github.com'
},
{
name: 'Yuga博客1',
url: 'https://yugasun.com'
}
]
}
},
methods: {
addItem () {
this.count++
this.list.push({
name: 'Yuga博客' + this.count,
url: 'https://yugasun.com'
})
}
}
})

上面 Vue 实例在初始化的时候,属性 data 中定义了一个站点列表,然后模板中通过 v-for 指令进行列表渲染,同时也使用了 v-bind 指令来进行属性绑定,并且通过 v-on 指令来监听按钮的 click 事件来执行 addItem 方法。当点击添加按钮,就会向 list push一条数据,视图会再次更新。(关于指令相关文章将在下一篇中讲到,感兴趣的同学可以先到官网学习了解。)

Demo2:computed

在实际开发过程中,我们的接口请求到的数据往往会差强人意,这时就需要我们进行一些转化,来生成我们想要的数据结构,当然最直接的方式就是每次请求完数据就通过固定函数处理一遍,但是这个得手动执行。此时 computed 计算属性就可以用上了。

我们知道除了 data 中定义的数据可以再模板中使用外,computed 属性也可以。只不过 computed 中的属性是需要先进行计算,然后再返回想要的数据的。当我们输出某个属性,必须依赖另外一个 data 中的属性来动态计算获得的,此时使用 computed 就非常简单了。代码如下:

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
var app = new Vue({
el: "#app",
data () {
return {
count: 1,
// 实际开发中往往是通过接口请求获取
requestList: [
'Vuejs官网-https://cn.vuejs.org',
'Github-https://github.com',
'Yuga博客1-https://yugasun.com'
]
}
},
computed: {
list: function() {
var list = [];
this.requestList.map(function(item, index) {
var tempArr = item.split('-');
list.push({
name: tempArr[0],
url: tempArr[1]
});
})
return list;
}
},
methods: {
addItem () {
this.count++
this.requestList.push('Yuga博客' + this.count + '-https://yugasun.com')
}
}
})

关于计算属性 - computed

关于计算属性,其实有个细节是很多同学没有注意到的,计算属性实际上是可以修改的!那么如何才能修改呢?

其实计算属性不仅可以定义为一个函数,也可以定义为一个含有 get/set 属性的对象。当我们定义为一个函数是,Vue 内部会默认将这个函数赋值给 get 属性,一般 set 是未定义的。当我们定义 set 属性后,就可以对它进行修改了。来看下面一段代码:

1
2
3
4
<div id="app">
<button v-on:click="changeName">改变姓名</button>
<h2>{{ username }}</h2>
</div>
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
var app = new Vue({
el: "#app",
data () {
return {
firstName: 'Yuga',
lastName: 'Sun'
}
},
computed: {
username: {
get: function() {
return this.firstName + ' ' + this.lastName;
},
set: function(newVal) {
var names = newVal.split(' ');
this.firstName = names[0];
this.lastName = names[1];
}
}
},
methods: {
changeName () {
if (this.username === 'Yuga Sun') {
this.username = 'Summer Wu';
} else {
this.username = 'Yuga Sun';
}
}
}
})

当进行赋值操作 this.username = 'Summer Wu' 时,计算属性 username 的 set 函数就会被调用,同时也对 firstName 和 lastName 进行了相应的更新。这里看似是直接进行赋值操作,其实也是通过间接修改 firstName 和 lastName 来实现 username 的更新的。因为 计算属性是基于它依赖的值进行缓存的,如果它依赖的值没有发生改变,它自己就没法发生改变。

关于侦听器 - watch

创建 Vue 应用时,我们还提到过 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
var app = new Vue({
el: "#app",
data () {
return {
firstName: 'Yuga',
lastName: 'Sun',
username: 'Yuga Sun'
}
},
watch: {
firstName: function(val, oldVal) {
this.username = val + ' ' + this.lastName;
},
lastName: function (val, oldVal) {
this.username = this.firstName + ' ' + val;
}
},
methods: {
changeName () {
if (this.username === 'Yuga Sun') {
this.firstName = 'Summer';
this.lastName = 'Wu';
} else {
this.firstName = 'Yuga';
this.lastName = 'Sun';
}
}
}
})

以上就是最基础用法,往往有些时候我们的监听属性并没有那么简单。往往是一个对象,这时当我们修改该对象的属性时,如何实现监听呢?先看下面代码:

1
2
3
4
<div id="app">
<button v-on:click="changeName">改变姓名</button>
<h4>{{ username }}</h4>
</div>
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
var app = new Vue({
el: "#app",
data () {
return {
userinfo: {
firstName: 'Yuga',
lastName: 'Sun',
},
username: 'Yuga Sun'
}
},
watch: {
userinfo: function (val, oldVal) {
this.username = val.firstName + ' ' + val.lastName;
}
},
methods: {
changeName () {
if (this.username === 'Yuga Sun') {
this.userinfo.firstName = 'Summer'
this.userinfo.lastName = 'Wu'
} else {
this.userinfo.firstName = 'Yuga'
this.userinfo.lastName = 'Sun'
}
}
}
})

此时无论我们如何点击按钮,都无法改变 username 的值,因为 watch 侦听器默认只是侦听该对象本身的赋值操作,也就是直接对 this.userinfo 进行赋值操作时的变化,并未对其内部属性进行侦听。实际上对于侦听的值是可以为一个对象的,它还有个 deep 属性,用来设置是否侦听内部属性的变化,而回调函数是通过 handler 来设置的。我们再次修改代码如下:

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
var app = new Vue({
el: "#app",
data () {
return {
userinfo: {
firstName: 'Yuga',
lastName: 'Sun',
},
username: 'Yuga Sun'
}
},
watch: {
userinfo: {
deep: true,
handler: function (val, oldVal) {
this.username = val.firstName + ' ' + val.lastName;
}
}
},
methods: {
changeName () {
if (this.username === 'Yuga Sun') {
this.userinfo.firstName = 'Summer'
this.userinfo.lastName = 'Wu'
} else {
this.userinfo.firstName = 'Yuga'
this.userinfo.lastName = 'Sun'
}
}
}
})

点击按钮,你会发现 username 可以根据按钮点击更新了,这个属性在实际项目中非常实用,因为往往我们修改数据时,并不是整体赋值,大部分时候都是局部修改属性的,所以这个时候就需要通过设置 deep 属性为 true,来达到我们的侦听目的。

问题来了,当侦听对象包含很多属性,而我们只是需要监听其中的一个或某几个属性,这时如果我们通过这种方式侦听所有内部属性的变化,自然就会造成内存的浪费。那么能不能只侦听单一内部属性的变化呢?答案是肯定的。

其实在 watch 定义的时候,键是可以为需要观察的表达式 的,表达式 就是说明,我们是可以写成对象属性访问表达式的。比如我们只需要侦听姓氏的修改,我们可以写成 userinfo.lastName。再来看下面代码:

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
var app = new Vue({
el: "#app",
data () {
return {
userinfo: {
firstName: 'Yuga',
lastName: 'Sun',
},
username: 'Yuga Sun'
}
},
watch: {
'userinfo.lastName': function (val, oldVal) {
this.username = this.userinfo.firstName + ' ' + val;
}
},
methods: {
changeName () {
if (this.username === 'Yuga Sun') {
this.userinfo.lastName = 'Wu'
} else {
this.userinfo.lastName = 'Sun'
}
}
}
})

这样我们就可以根据项目实际情况,灵活的使用侦听器来侦听我们所关注的属性了,赶紧动手试一试吧~

源码在此

Vuejs - 深入浅出响应式系统的更多相关文章

  1. [vuejs] 深入响应式原理

    深入响应式原理 现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统.数据模型仅仅是普通的 JavaScript 对象.而当你修改它们时,视图会进行更新.这使得状态管理非常简单直接 ...

  2. vue原理探索--响应式系统

    Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」. 首先看一下 Object.defi ...

  3. Vue 及框架响应式系统原理

    个人bolg地址 全局概览 Vue运行内部运行机制 总览图: 初始化及挂载 在 new Vue()之后. Vue 会调用 _init 函数进行初始化,也就是这里的 init 过程,它会初始化生命周期. ...

  4. Vue的响应式系统

    Vue的响应式系统 我们第一次使用Vue的时候,会感觉有些神奇,举个例子: <div id="app"> <div>价格:¥{{price}}</di ...

  5. 【js】vue 2.5.1 源码学习 (七) 初始化之 initState 响应式系统基本思路

    大体思路(六) 本节内容: 一.生命周期的钩子函数的实现 ==> callHook(vm , 'beforeCreate') beforeCreate 实例创建之后 事件数据还未创建 二.初始化 ...

  6. 你是如何理解Vue的响应式系统的

    1.响应式系统简述: 任何一个 Vue Component 都有一个与之对应的 Watcher 实例. Vue 的 data 上的属性会被添加 getter 和 setter 属性. 当 Vue Co ...

  7. 前端必读:Vue响应式系统大PK

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...

  8. 前端必读:Vue响应式系统大PK(下)

    转载请注明出处:葡萄城官网,葡萄城为开发者提供专业的开发工具.解决方案和服务,赋能开发者. 原文参考:https://www.sitepoint.com/vue-3-reactivity-system ...

  9. 一些vue 响应式系统的底层的细节

    当你把一个普通的 JavaScript 对象传给 Vue 实例的 data 选项,Vue 将遍历此对象所有的属性,并使用 Object.defineProperty 把这些属性全部转为 getter/ ...

随机推荐

  1. win7 64位在线编辑dsoframer控件的安装和使用配置

    经历了两天的折磨,查阅了网上的资料,按网上的操作试了n种方法结果还是不行,开始以为是dsoframer 是32位控件问题,结果不是(经历了更改解决方案cpu,发布基于x86的网站:以为是操作系统问题, ...

  2. stm32的两种固件下载模式:JTAG和SWD

    一.JTAG模式 这种模式一般有10pin的.14pin的和20pin的,尽管引脚数和引脚的排列顺序不同,但是其中有一些引脚是一样的.值得注意的是,不同的IC公司会自己定义自家产品专属的Jtag头,来 ...

  3. 【bzoj1093】[ZJOI2007]最大半连通子图 Tarjan+拓扑排序+dp

    题目描述 一个有向图G=(V,E)称为半连通的(Semi-Connected),如果满足:对于u,v∈V,满足u→v或v→u,即对于图中任意两点u,v,存在一条u到v的有向路径或者从v到u的有向路径. ...

  4. Go语言【第八篇】:Go语言变量作用域

    Go语言变量作用域 作用域为已声明标识符所表示的常量.类型.变量.函数或包在源代码中的作用范围,Go语言中变量可以在三个地方声明: 函数内定义的变量称为局部变量: 函数外定义的变量称为全局变量: 函数 ...

  5. CodeForces - 704C

    题目大意:给你一个逻辑表达式,然后让你计算表达式为真的取值情况数,表达式由一系列的 a 或者 a|b 亦或起来,每个变量最多出现两次(包括反变量) 嘴炮开始:每个变量最多出现两次,那么跟它相关联的变量 ...

  6. HDOJ(HDU).1003 Max Sum (DP)

    HDOJ(HDU).1003 Max Sum (DP) 点我挑战题目 算法学习-–动态规划初探 题意分析 给出一段数字序列,求出最大连续子段和.典型的动态规划问题. 用数组a表示存储的数字序列,sum ...

  7. MyBatis之二级缓存

    二级缓存与一级缓存区别:二级缓存的范围更大,多个sqlSession可以共享一个UserMapper的二级缓存区域. 每一个mapper都有一个自己的二缓存区域(按namespace区分),两个map ...

  8. 阅读android源码了解 android 加载so的流程

    参考原文:http://bbs.pediy.com/thread-217656.htm Android安全–linker加载so流程,在.init下断点: http://www.blogfshare. ...

  9. 背景建模技术(六):帧处理(FrameProcessor)模块

    前面几篇文章简单介绍了BgsLibrary的入口函数.视频分析和视频捕获模块,本文将简单介绍帧处理模块,即对每一帧进行处理的函数,也就是真正调用背景建模算法的接口处. 下面贴出源码供大家分析: #in ...

  10. STL之一:字符串用法详解

    转载于:http://blog.csdn.net/longshengguoji/article/details/8539471 字符串是程序设计中最复杂的变成内容之一.STL string类提供了强大 ...