通过图带你深入了解vue的响应式原理


Posted in Javascript onJune 21, 2019

前言

如果自己去实现数据驱动的模式,如何解决一下几个问题:

  • 通过什么手段去知道我的数据变了?
  • 通过什么东西去同步更新视图?

数据劫持——obvserver

我们需要知道数据的获取和改变,数据劫持是最基础的手段。在Obeserver中,我们可以看到代码如下:

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// ...
},
set: function reactiveSetter (newVal) {
// ...
}
})

通过Object.defineProperty这个方法,我们可以在数据发生改变或者获取的时候,插入一些自定义操作。同理,vue也是在这个方法中做依赖收集和派发更新的。

绑定和更新视图——watcher

从初始化开始,我们渲染视图的时候,便会生成一个watcher,他是监视视图中参数变化以及更新视图的。代码如下:

// 在mount的生命钩子中
new Watcher(vm, updateComponent, noop, {
before () {
if (vm._isMounted && !vm._isDestroyed) {
callHook(vm, 'beforeUpdate')
}
}
}, true /* isRenderWatcher */)

当然,我们可以保留疑问:

  • watcher是怎么去更新视图的
  • 数据又是怎么和watcher联动起来的

具体的绑定和更新的流程,我们到后续的依赖收集中讲解。

我们先来讲讲响应式系统中涉及到的设计模式。

发布订阅模式

在发布订阅模式中,发布者和订阅者之间多了一个发布通道;一方面从发布者接收事件,另一方面向订阅者发布事件;订阅者需要从事件通道订阅事件

以此避免发布者和订阅者之间产生依赖关系

通过图带你深入了解vue的响应式原理

vue的响应式流程

vue的响应式系统借鉴了数据劫持和发布订阅模式。

通过图带你深入了解vue的响应式原理

Vue用Dep作为一个中间者,解藕了Observer和Watcher之间的关系,使得两者的职能更加明确。

那具体是如何来完成依赖收集和订阅更新的呢?

依赖收集过程

依赖收集的流程

举个例子

<div id="app">
{{ message }}
{{ message1 }}
<input type="text" v-model="message">
<div @click="changeMessage">改变message</div> 
</div>
var app = new Vue({
el: '#app',
data: {
message: '1',
message1: '2',
},
methods: {
changeMessage() {
this.message = '2'
}
},
watch: {
message: function(val) {
this.message1 = val
}
}
})

依赖收集流程图:

通过图带你深入了解vue的响应式原理

如何看懂这个依赖收集流程?关键在watcher代码中:

get () {
pushTarget(this)
let value
const vm = this.vm
try {
value = this.getter.call(vm, vm)
} catch (e) {
// 省略
} finally {
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
}
return value
}

调用的这个this.getter有两种,一种是key值的getter方法,还有一种是expOrFn,比如mounted中传入的updateComponent。

如何防止重复收集

我们不妨想想什么才算是重复收集了?

笔者想到一种情况:就是dep数组中,出现了多个一样的watcher。

比如renderWatch就容易被重复收集,因为我们在html模版中,会重复使用data中的某个变量。那他是如何去重的呢?

1、只有watch在执行get时,触发的取数操作,才会被收集

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
dep.depend()
// ...
}
return value
},
set: function reactiveSetter (newVal) {
// ...
dep.notify()
}
})

当只有Dep.target这个存在的时候才进行依赖收集。Dep.target这个值只有在watcher执行get方法的时候才会存在。

2、在dep.depend的时候会判断watch的id

depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
addDep (dep: Dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
dep.addSub(this)
}
}
}

我们会发现,在depend过程中,会有一个newDepIds去记录已经存入的dep的id,当一个watcher已经被该dep存过时,便不再会进行依赖收集操作。

派发更新过程

收集流程讲完了,不妨在听听更新流程。

订阅更新的流程

老例子

<div id="app">
{{ message }}
{{ message1 }}
<input type="text" v-model="message">
<div @click="changeMessage">改变message</div> 
</div>
var app = new Vue({
el: '#app',
data: {
message: '1',
message1: '2',
},
methods: {
changeMessage() {
this.message = '3'
}
},
watch: {
message: function(val) {
this.message1 = val
}
}
})

依赖收集的最终结果:

通过图带你深入了解vue的响应式原理

当触发click事件的时候,便会触发订阅更新流程。

订阅更新流程图:

通过图带你深入了解vue的响应式原理

当renderWatch执行更新的时候,回去调用beforeUpdate生命钩子,然后执行patch方法,进行视图的变更。

如何防止重复更新

如何去防止重复更新呢?renderWatch会被很多dep进行收集,如果视图多次渲染,会造成性能问题。

其实问题的关在在于——queueWatcher

在queueWatcher中有两个操作:去重和异步更新。

function queueWatcher (watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
queue.push(watcher)
// ...
if (!waiting) {
waiting = true
// ...
nextTick(flushSchedulerQueue)
}
}
}

其实queueWatcher很简单,将所有watch收集到一个数组当中,然后去重。

这样至少可以避免renderWatch频繁更新。

比如上述例子中的,message和message1都有一个renderWatch,但是只会执行一次。

异步更新也可以保证当一个事件结束之后,才会触发视图层的更新,也能防止renderWatch重复更新

结尾

文章讲述了响应式流程的原因,代码细节并未深入,

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

Javascript 相关文章推荐
用js的document.write输出的广告无阻塞加载的方法
Jun 05 Javascript
Node.js中的模块机制学习笔记
Nov 04 Javascript
jQuery的deferred对象详解
Nov 12 Javascript
PhantomJS快速入门教程(服务器端的 JavaScript API 的 WebKit)
Aug 06 Javascript
KnockoutJS 3.X API 第四章之数据控制流component绑定
Oct 10 Javascript
javascript 定时器工作原理分析
Dec 03 Javascript
关于Node.js中Buffer的一些你可能不知道的用法
Mar 28 Javascript
使用JS实现气泡跟随鼠标移动的动画效果
Sep 16 Javascript
基于vue和react的spa进行按需加载的实现方法
Sep 29 Javascript
element-ui的回调函数Events的用法详解
Oct 16 Javascript
vue draggable resizable gorkys与v-chart使用与总结
Sep 05 Javascript
html5中sharedWorker实现多页面通信的示例代码
May 07 Javascript
10种JavaScript最常见的错误(小结)
Jun 21 #Javascript
微信小程序开发注意指南和优化实践(小结)
Jun 21 #Javascript
使用Vue开发自己的Chrome扩展程序过程详解
Jun 21 #Javascript
如何测量vue应用运行时的性能
Jun 21 #Javascript
Vue组件之高德地图地址选择功能的实例代码
Jun 21 #Javascript
如何提升vue.js中大型数据的性能
Jun 21 #Javascript
配置node服务器并且链接微信公众号接口配置步骤详解
Jun 21 #Javascript
You might like
php 取得瑞年与平年的天数的代码
2009/08/10 PHP
php strcmp使用说明
2010/04/22 PHP
php采集文章中的图片获取替换到本地(实现代码)
2013/07/08 PHP
PHP7常量数组用法分析
2016/09/26 PHP
PHP编程计算两个时间段是否有交集的实现方法(不算边界重叠)
2017/05/30 PHP
PHP设计模式之模板方法模式实例浅析
2018/12/20 PHP
php 根据URL下载远程图片、压缩包、pdf等文件到本地
2019/07/26 PHP
PHP中非常有用却鲜有人知的函数集锦
2019/08/17 PHP
jQuery的运行机制和设计理念分析
2011/04/05 Javascript
再JavaScript的jQuery库中编写动画效果的指南
2015/08/13 Javascript
纯javascript模仿微信打飞机小游戏
2015/08/20 Javascript
使用Node.js处理前端代码文件的编码问题
2016/02/16 Javascript
JS实现星星评分功能实例代码(两种方法)
2016/06/09 Javascript
JavaScript实现时间倒计时跳转(推荐)
2016/06/28 Javascript
Node.js之网络通讯模块实现浅析
2017/04/01 Javascript
从零开始学习Node.js系列教程之SQLite3和MongoDB用法分析
2017/04/13 Javascript
利用JQuery操作iframe父页面、子页面的元素和方法汇总
2017/09/10 jQuery
Laravel整合Bootstrap 4的完整方案(推荐)
2018/01/25 Javascript
微信小程序实现美团菜单
2018/06/06 Javascript
jQuery使用each遍历循环的方法
2018/09/19 jQuery
vue-cli3 项目从搭建优化到docker部署的方法
2019/01/28 Javascript
Python while、for、生成器、列表推导等语句的执行效率测试
2015/06/03 Python
Python的Tornado框架实现异步非阻塞访问数据库的示例
2016/06/30 Python
Django模型修改及数据迁移实现解析
2019/08/01 Python
Python字典深浅拷贝与循环方式方法详解
2020/02/09 Python
python实现图书馆抢座(自动预约)功能的示例代码
2020/09/29 Python
Notino法国:购买香水和化妆品
2019/04/15 全球购物
银行实习生自我鉴定范文
2013/09/19 职场文书
进修护士自我鉴定
2013/10/14 职场文书
生物学学生自我评价
2014/01/17 职场文书
抽样调查项目计划书
2014/04/24 职场文书
旅游活动总结
2014/08/27 职场文书
仲裁协议书
2014/09/26 职场文书
学生病假条范文
2015/08/17 职场文书
python实现求纯色彩图像的边框
2021/04/08 Python
Nginx反向代理学习实例教程
2021/10/24 Servers