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 相关文章推荐
javascript 单选框,多选框美化代码
Aug 01 Javascript
javascript的offset、client、scroll使用方法详解
Dec 25 Javascript
A标签中通过href和onclick传递的this对象实现思路
Apr 19 Javascript
node.js操作mongoDB数据库示例分享
Nov 26 Javascript
JavaScript 七大技巧(二)
Dec 13 Javascript
js 声明数组和向数组中添加对象变量的简单实例
Jul 28 Javascript
electron + vue项目实现打印小票功能及实现代码
Nov 25 Javascript
一个因@click.stop引发的bug的解决
Jan 08 Javascript
jquery ajax 请求小技巧实例分析
Nov 11 jQuery
微信小程序canvas开发水果老虎机的思路详解
Feb 07 Javascript
基于javascript实现碰撞检测
Mar 12 Javascript
Vue toFixed保留两位小数的3种方式
Oct 23 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
一个连接两个不同MYSQL数据库的PHP程序
2006/10/09 PHP
php结合ajax实现赞、顶、踩功能实例
2014/05/12 PHP
thinkPHP中配置的读取与C方法详解
2016/12/05 PHP
PHP设计模式之委托模式定义与用法简单示例
2018/08/13 PHP
精心挑选的15款优秀jQuery 本特效插件和教程
2012/08/06 Javascript
javascript解决innerText浏览器兼容问题思路代码
2013/05/17 Javascript
文档对象模型DOM通俗讲解
2013/11/01 Javascript
jquery获取一组checkbox的值(实例代码)
2013/11/04 Javascript
javascript禁用Tab键脚本实例
2013/11/22 Javascript
javascript生成随机数的方法
2014/05/16 Javascript
js中window.open的参数及注意注意事项
2016/07/06 Javascript
Javascript使用function创建类的两种方法(推荐)
2016/11/19 Javascript
详解node HTTP请求客户端 - Request
2017/05/05 Javascript
jQuery EasyUI的TreeGrid查询功能实现方法
2017/08/08 jQuery
Redux 和 Mobx的选择问题:让你不再困惑!
2017/09/18 Javascript
对Vue.js之事件的绑定(v-on: 或者 @ )详解
2018/09/15 Javascript
vue(2.x,3.0)配置跨域代理
2019/11/27 Javascript
利用PHP实现递归删除链表元素的方法示例
2020/10/23 Javascript
vue中使用腾讯云Im的示例
2020/10/23 Javascript
关于Python面向对象编程的知识点总结
2017/02/14 Python
Python获取指定文件夹下的文件名的方法
2018/02/06 Python
python 日志增量抓取实现方法
2018/04/28 Python
python制作抖音代码舞
2019/04/07 Python
Python解决pip install时出现的Could not fetch URL问题
2019/08/01 Python
Python爬虫如何应对Cloudflare邮箱加密
2020/06/24 Python
Python爬取数据并实现可视化代码解析
2020/08/12 Python
python的数学算法函数及公式用法
2020/11/18 Python
python mongo 向数据中的数组类型新增数据操作
2020/12/05 Python
python中的时区问题
2021/01/14 Python
Python tkinter之ComboBox(下拉框)的使用简介
2021/02/05 Python
Joules美国官网:出色的英国风格
2017/10/30 全球购物
高校教师思想汇报
2014/01/11 职场文书
毕业评语大全
2014/05/04 职场文书
设计专业自荐信
2014/06/19 职场文书
酒店管理失职检讨书
2014/09/16 职场文书
教师见习报告范文
2014/11/03 职场文书