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 相关文章推荐
取选中的radio的值
Jan 11 Javascript
JavaScript常用本地对象小结
Mar 28 Javascript
jQuery查看选中对象HTML代码的方法
Jun 17 Javascript
Angular 4 指令快速入门教程
Jun 07 Javascript
vue+axios 前端实现登录拦截的两种方式(路由拦截、http拦截)
Oct 24 Javascript
使用vuepress搭建静态博客的示例代码
Feb 14 Javascript
微信小程序云开发 生成带参小程序码流程
May 18 Javascript
layui+SSM的数据表的增删改实例(利用弹框添加、修改)
Sep 27 Javascript
vue路由拦截器和请求拦截器知识点总结
Nov 08 Javascript
vue中使用v-for时为什么不能用index作为key
Apr 04 Javascript
解决React在安装antd之后出现的Can't resolve './locale'问题(推荐)
May 03 Javascript
IDEA配置jQuery, $符号不再显示黄色波浪线的问题
Oct 09 jQuery
在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
冰滴咖啡制作步骤
2021/03/03 冲泡冲煮
php zlib压缩和解压缩swf文件的代码
2008/12/30 PHP
php数组对百万数据进行排除重复数据的实现代码
2010/06/08 PHP
使用php+Ajax实现唯一校验实现代码[简单应用]
2011/11/29 PHP
PHP学习笔记之字符串编码的转换和判断
2014/05/22 PHP
ucenter通信原理分析
2015/01/09 PHP
Session 失效的原因汇总及解决丢失办法
2015/09/30 PHP
php正则去除网页中所有的html,js,css,注释的实现方法
2016/11/03 PHP
Jquery实现图片预加载与延时加载的方法
2014/12/22 Javascript
基于Jquery制作图片文字排版预览效果附源码下载
2015/11/18 Javascript
js 动态添加元素(div、li、img等)及设置属性的方法
2016/07/19 Javascript
jquery操作checkbox火狐下第二次无法勾选的解决方法
2016/10/10 Javascript
webpack常用配置项配置文件介绍
2016/11/07 Javascript
JS获取浮动(float)元素的style.left值为空的快速解决办法
2017/02/19 Javascript
JS正则表达式完美实现身份证校验功能
2017/10/18 Javascript
JS实现根据详细地址获取经纬度功能示例
2019/04/16 Javascript
jquery实现点击弹出对话框
2020/02/08 jQuery
JavaScript实现移动端带transition动画的轮播效果
2020/03/24 Javascript
ant-design-vue中的select选择器,对输入值的进行筛选操作
2020/10/24 Javascript
Python中import机制详解
2017/11/14 Python
Python简单生成随机数的方法示例
2018/03/31 Python
django框架自定义用户表操作示例
2018/08/07 Python
PyCharm+PySpark远程调试的环境配置的方法
2018/11/29 Python
python hashlib加密实现代码
2019/10/17 Python
Python爬虫爬取煎蛋网图片代码实例
2019/12/16 Python
Cython编译python为so 代码加密示例
2019/12/23 Python
Python文件操作及内置函数flush原理解析
2020/10/13 Python
swtich是否能作用在byte上,是否能作用在long上,是否能作用在String上?
2013/03/30 面试题
移动通信行业实习自我鉴定
2013/09/28 职场文书
小学教师师德反思
2014/02/03 职场文书
绘画专业自荐信范文
2014/02/23 职场文书
幼儿园教师教育感言
2014/02/28 职场文书
思想作风建设心得体会
2014/10/22 职场文书
2014年六五普法工作总结
2014/11/25 职场文书
公司给客户的感谢信
2015/01/23 职场文书
大学入学感言
2015/08/01 职场文书