Vue.js每天必学之内部响应式原理探究


Posted in Javascript onSeptember 07, 2016

深入响应式原理

大部分的基础内容我们已经讲到了,现在讲点底层内容。Vue.js 最显著的一个功能是响应系统 —— 模型只是普通对象,修改它则更新视图。这让状态管理非常简单且直观,不过理解它的原理也很重要,可以避免一些常见问题。下面我们开始深挖 Vue.js 响应系统的底层细节。

如何追踪变化

把一个普通对象传给 Vue 实例作为它的 data 选项,Vue.js 将遍历它的属性,用 Object.defineProperty 将它们转为 getter/setter。这是 ES5 特性,不能打补丁实现,这便是为什么 Vue.js 不支持 IE8 及更低版本。

用户看不到 getter/setters,但是在内部它们让 Vue.js 追踪依赖,在属性被访问和修改时通知变化。一个问题是在浏览器控制台打印数据对象时 getter/setter 的格式化不同,使用 vm.$log() 实例方法可以得到更友好的输出。

模板中每个指令/数据绑定都有一个对应的 watcher 对象,在计算过程中它把属性记录为依赖。之后当依赖的 setter 被调用时,会触发 watcher 重新计算 ,也就会导致它的关联指令更新 DOM。

Vue.js每天必学之内部响应式原理探究

变化检测问题

受 ES5 的限制,Vue.js 不能检测到对象属性的添加或删除。因为 Vue.js 在初始化实例时将属性转为 getter/setter,所以属性必须在 data 对象上才能让 Vue.js 转换它,才能让它是响应的。例如:

var data = { a: 1 }
var vm = new Vue({
 data: data
})
// `vm.a` 和 `data.a` 现在是响应的

vm.b = 2
// `vm.b` 不是响应的

data.b = 2
// `data.b` 不是响应的

不过,有办法在实例创建之后添加属性并且让它是响应的。

对于 Vue 实例,可以使用 $set(key, value) 实例方法:

vm.$set('b', 2)
// `vm.b` 和 `data.b` 现在是响应的

对于普通数据对象,可以使用全局方法 Vue.set(object, key, value):

Vue.set(data, 'c', 3)
// `vm.c` 和 `data.c` 现在是响应的

有时你想向已有对象上添加一些属性,例如使用 Object.assign() 或 _.extend() 添加属性。但是,添加到对象上的新属性不会触发更新。这时可以创建一个新的对象,包含原对象的属性和新的属性:

// 不使用 `Object.assign(this.someObject, { a: 1, b: 2 })`
this.someObject = Object.assign({}, this.someObject, { a: 1, b: 2 })

也有一些数组相关的问题,之前已经在列表渲染中讲过。

初始化数据

尽管 Vue.js 提供了 API 动态地添加响应属性,还是推荐在 data 对象上声明所有的响应属性。

不这么做:

var vm = new Vue({
 template: '<div>{{msg}}</div>'
})
// 然后添加 `msg`
vm.$set('msg', 'Hello!')

这么做:

var vm = new Vue({
 data: {
 // 以一个空值声明 `msg`
 msg: ''
 },
 template: '<div>{{msg}}</div>'
})
// 然后设置 `msg`
vm.msg = 'Hello!'

这么做有两个原因:
 1.data 对象就像组件状态的模式(schema)。在它上面声明所有的属性让组件代码更易于理解。

 2.添加一个顶级响应属性会强制所有的 watcher 重新计算,因为它之前不存在,没有 watcher 追踪它。这么做性能通常是可以接受的(特别是对比 Angular 的脏检查),但是可以在初始化时避免。 

异步更新队列

Vue.js 默认异步更新 DOM。每当观察到数据变化时,Vue 就开始一个队列,将同一事件循环内所有的数据变化缓存起来。如果一个 watcher 被多次触发,只会推入一次到队列中。等到下一次事件循环,Vue 将清空队列,只进行必要的 DOM 更新。在内部异步队列优先使用 MutationObserver,如果不支持则使用 setTimeout(fn, 0)。

例如,设置了 vm.someData = 'new value',DOM 不会立即更新,而是在下一次事件循环清空队列时更新。我们基本不用关心这个过程,但是如果想在 DOM 状态更新后做点什么,这会有帮助。尽管 Vue.js 鼓励开发者沿着数据驱动的思路,避免直接修改 DOM,但是有时确实要这么做。为了在数据变化之后等待 Vue.js 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback) 。回调在 DOM 更新完成后调用。例如:

<div id="example">{{msg}}</div>

var vm = new Vue({
 el: '#example',
 data: {
 msg: '123'
 }
})
vm.msg = 'new message' // 修改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
 vm.$el.textContent === 'new message' // true
})

vm.$nextTick() 这个实例方法比较方便,因为它不需要全局 Vue,它的回调的 this 自动绑定到当前 Vue 实例:

Vue.component('example', {
 template: '<span>{{msg}}</span>',
 data: function () {
 return {
 msg: 'not updated'
 }
 },
 methods: {
 updateMessage: function () {
 this.msg = 'updated'
 console.log(this.$el.textContent) // => 'not updated'
 this.$nextTick(function () {
 console.log(this.$el.textContent) // => 'updated'
 })
 }
 }
})

计算属性的奥秘

你应该注意到 Vue.js 的计算属性不是简单的 getter。计算属性持续追踪它的响应依赖。在计算一个计算属性时,Vue.js 更新它的依赖列表并缓存结果,只有当其中一个依赖发生了变化,缓存的结果才无效。因此,只要依赖不发生变化,访问计算属性会直接返回缓存的结果,而不是调用 getter。

为什么要缓存呢?假设我们有一个高耗计算属性 A,它要遍历一个巨型数组并做大量的计算。然后,可能有其它的计算属性依赖 A。如果没有缓存,我们将调用 A 的 getter 许多次,超过必要次数。

由于计算属性被缓存了,在访问它时 getter 不总是被调用。考虑下例:

var vm = new Vue({
 data: {
 msg: 'hi'
 },
 computed: {
 example: function () {
 return Date.now() + this.msg
 }
 }
})

计算属性 example 只有一个依赖:vm.msg。Date.now() 不是 响应依赖,因为它跟 Vue 的数据观察系统无关。因而,在访问 vm.example 时将发现时间戳不变,除非 vm.msg 变了。

有时希望 getter 不改变原有的行为,每次访问 vm.example 时都调用 getter。这时可以为指定的计算属性关闭缓存:

computed: {
 example: {
 cache: false,
 get: function () {
 return Date.now() + this.msg
 }
 }
}

现在每次访问 vm.example 时,时间戳都是新的。但是,只是在 JavaScript 中访问是这样的;数据绑定仍是依赖驱动的。如果在模块中这样绑定计算属性 {{example}},只有响应依赖发生变化时才更新 DOM。

本文已被整理到了《Vue.js前端组件学习教程》,欢迎大家学习阅读。

关于vue.js组件的教程,请大家点击专题vue.js组件学习教程进行学习。

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
解决FireFox下[使用event很麻烦]的问题
Nov 26 Javascript
validator验证控件使用代码
Nov 23 Javascript
jQuery打印图片pdf、txt示例代码
Jul 22 Javascript
jQuery实现宽屏图片轮播实例教程
Nov 24 Javascript
javascript cookie用法基础教程(概念,设置,读取及删除)
Sep 20 Javascript
JS控制div跳转到指定的位置的几种解决方案总结
Nov 05 Javascript
JavaScript实现移动端页面按手机屏幕分辨率自动缩放的最强代码
Aug 18 Javascript
微信小程序网络请求封装示例
Jul 24 Javascript
使用electron实现百度网盘悬浮窗口功能的示例代码
Oct 24 Javascript
jQuery高级编程之js对象、json与ajax用法实例分析
Nov 01 jQuery
详解用js代码触发dom事件的实现方案
Jun 10 Javascript
Vue中的nextTick作用和几个简单的使用场景
Jan 25 Vue.js
在JavaScript中调用Java类和接口的方法
Sep 07 #Javascript
Vue.js每天必学之指令系统与自定义指令
Sep 07 #Javascript
Vue.js每天必学之过滤器与自定义过滤器
Sep 07 #Javascript
详解AngularJS中ng-src指令的使用
Sep 07 #Javascript
AngularJS实现按钮提示与点击变色效果
Sep 07 #Javascript
JS实现简单易用的手机端浮动窗口显示效果
Sep 07 #Javascript
利用Angularjs实现幻灯片效果
Sep 07 #Javascript
You might like
请php正则走开
2008/03/15 PHP
PHP扩展程序实现守护进程
2015/04/16 PHP
WordPress中用于创建以及获取侧边栏的PHP函数讲解
2015/12/29 PHP
用javascript getComputedStyle获取和设置style的原理
2008/10/10 Javascript
jQuery 性能优化手册 推荐
2010/02/23 Javascript
js split 的用法和定义 js split分割字符串成数组的实例代码
2012/05/13 Javascript
JavaScript数组常用操作技巧汇总
2014/11/17 Javascript
JS动画效果打开、关闭层的实现方法
2015/05/09 Javascript
详解javascript的变量与标识符
2016/01/04 Javascript
jQuery+PHP+MySQL实现无限级联下拉框效果
2016/02/19 Javascript
js日期插件dateHelp获取本月、三个月、今年的日期
2016/03/07 Javascript
javaScript事件机制兼容【详细整理】
2016/07/23 Javascript
jQuery EasyUI tree 使用拖拽时遇到的错误小结
2016/10/10 Javascript
Vuejs实现带样式的单文件组件新方法
2017/05/02 Javascript
jQuery操作DOM_动力节点Java学院整理
2017/07/04 jQuery
JS实现区分中英文并统计字符个数的方法示例
2018/06/09 Javascript
JS简单数组排序操作示例【sort方法】
2019/05/17 Javascript
js实现盒子滚动动画效果
2020/08/09 Javascript
[33:39]DOTA2上海特级锦标赛C组小组赛#2 LGD VS Newbee第二局
2016/02/27 DOTA
100行python代码实现跳一跳辅助程序
2018/01/15 Python
Django项目开发中cookies和session的常用操作分析
2018/07/03 Python
windows7 32、64位下python爬虫框架scrapy环境的搭建方法
2018/11/29 Python
python 实现手机自动拨打电话的方法(通话压力测试)
2019/08/08 Python
Python3常用内置方法代码实例
2019/11/18 Python
Python实现自动访问网页的例子
2020/02/21 Python
Django自带用户认证系统使用方法解析
2020/11/12 Python
HTML5 新表单类型示例代码
2018/03/20 HTML / CSS
使用html2canvas.js实现页面截图并显示或上传的示例代码
2018/12/18 HTML / CSS
维多利亚的秘密阿联酋官网:Victoria’s Secret阿联酋
2019/12/07 全球购物
Unix如何在一行中运行多个命令
2015/05/29 面试题
高二物理教学反思
2014/02/08 职场文书
厨房管理计划书
2014/04/27 职场文书
关于读书的演讲稿800字
2014/08/27 职场文书
创先争优活动心得体会
2014/09/04 职场文书
员工辞退通知书
2015/04/17 职场文书
解决MySQL存储时间出现不一致的问题
2021/04/28 MySQL