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 相关文章推荐
Cookie 注入是怎样产生的
Apr 08 Javascript
另一个javascript小测验(代码集合)
Jul 27 Javascript
jQuery获取CSS样式中的颜色值的问题,不同浏览器格式不同的解决办法
May 13 Javascript
基于jquery实现的树形菜单效果代码
Sep 06 Javascript
js省市联动效果完整实例代码
Dec 09 Javascript
jQuery UI库中dialog对话框功能使用全解析
Apr 23 Javascript
全面了解JavaScript对象进阶
Jul 19 Javascript
原生JS查找元素的方法(推荐)
Nov 22 Javascript
vue之数据交互实例代码
Jun 16 Javascript
vue的事件绑定与方法详解
Aug 16 Javascript
JS函数节流和防抖之间的区分和实现详解
Jan 11 Javascript
微信小程序如何实现全局重新加载
Jun 05 Javascript
在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
Win9x/ME下Apache+PHP安装配置
2006/10/09 PHP
安装APACHE
2007/01/15 PHP
基于PHP文件操作的详解
2013/06/05 PHP
PHP+MYSQL会员系统的开发实例教程
2014/08/23 PHP
PHP实现网页内容html标签补全和过滤的方法小结【2种方法】
2017/04/27 PHP
浅谈PHP5.6 与 PHP7.0 区别
2019/10/09 PHP
jQuery调用AJAX时Get和post公用的乱码解决方法实例说明
2013/06/04 Javascript
Jquery中LigerUi的弹出编辑框(实现方法)
2013/07/09 Javascript
JavaScript中的eval()函数详解
2013/08/22 Javascript
JQuery异步加载无限下拉框级联功能实现示例
2014/02/19 Javascript
值得分享的bootstrap table实例
2016/09/22 Javascript
jQuery插件开发发送短信倒计时功能代码
2017/05/09 jQuery
详解Webpack-dev-server的proxy用法
2018/09/08 Javascript
JavaScript使用小插件实现倒计时的方法讲解
2019/03/11 Javascript
JavaScript实现星级评价效果
2019/05/17 Javascript
使用p5.js临摹动态图片
2019/11/04 Javascript
vue data变量相互赋值后被实时同步的解决步骤
2020/08/05 Javascript
python数字图像处理之骨架提取与分水岭算法
2018/04/27 Python
基于Python列表解析(列表推导式)
2018/06/23 Python
python实现获取单向链表倒数第k个结点的值示例
2019/10/24 Python
通过 Python 和 OpenCV 实现目标数量监控
2020/01/05 Python
python中列表的含义及用法
2020/05/26 Python
HTML5之SVG 2D入门5—颜色的表示及定义方式
2013/01/30 HTML / CSS
馥绿德雅美国官方网站:Rene Furterer头皮护理专家
2019/05/01 全球购物
垃圾回收的优点和原理。并考虑2种回收机制
2016/10/16 面试题
类、抽象类、接口的差异
2016/06/13 面试题
产品促销活动策划书
2014/01/15 职场文书
校园奶茶店创业计划书
2014/01/23 职场文书
优秀的个人求职信范文
2014/05/09 职场文书
六五普法宣传标语
2014/10/06 职场文书
2015年见习期工作总结
2014/12/12 职场文书
冬季作息时间调整通知
2015/04/24 职场文书
党员干部公开承诺书范文
2015/04/27 职场文书
2019思想汇报范文
2019/05/21 职场文书
python 制作一个gui界面的翻译工具
2021/05/14 Python
win10忘记pin密码登录不了怎么办?win10忘记pin密码登不进去的解决方法
2022/07/07 数码科技