通过图带你深入了解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 相关文章推荐
特殊字符、常规符号及其代码对照表
Jun 26 Javascript
jquery中ajax处理跨域的三大方式
Jan 05 Javascript
深入浅析JavaScript中的constructor
Apr 19 Javascript
Ionic快速安装教程
Jun 03 Javascript
javascript设计模式之module(模块)模式
Aug 19 Javascript
vue 封装自定义组件之tabal列表编辑单元格组件实例代码
Sep 07 Javascript
vue-cli开发环境实现跨域请求的方法
Apr 07 Javascript
jQuery发请求传输中文参数乱码问题的解决方案
May 22 jQuery
js获取浏览器地址(获取第1个斜杠后的内容)
Sep 03 Javascript
Vue 使用Props属性实现父子组件的动态传值详解
Nov 13 Javascript
vue 动态设置img的src地址无效,npm run build 后找不到文件的解决
Jul 26 Javascript
JavaScript 事件捕获冒泡与捕获详情
Nov 11 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中register_globals参数为OFF和ON的区别(register_globals 使用详解)
2012/02/05 PHP
PHP的Socket通信之UDP通信实例
2015/07/02 PHP
JQuery中对服务器控件 DropdownList, RadioButtonList, CheckboxList的操作总结
2011/06/28 Javascript
jQuery常见开发技巧详细整理
2013/01/02 Javascript
js 程序执行与顺序实现详解
2013/05/13 Javascript
JS分页控件 可用于无刷新分页
2013/07/23 Javascript
如何实现修改密码时密码框显示保存到cookie的密码
2013/12/10 Javascript
JavaScript获取鼠标移动时的坐标(兼容IE8、chome谷歌、Firefox)
2014/09/13 Javascript
JavaScript中常见获取元素的方法汇总
2015/03/04 Javascript
Node.js 文件夹目录结构创建实例代码
2016/07/08 Javascript
微信支付 JS API支付接口详解
2016/07/11 Javascript
JavaScript数据操作_浅谈原始值和引用值的操作本质
2016/08/23 Javascript
javascript判断回文数详解及实现代码
2017/02/03 Javascript
JavaScript获取tr td 的三种方式全面总结(推荐)
2017/08/15 Javascript
vue+iview 兼容IE11浏览器的实现方法
2019/01/07 Javascript
sortable+element 实现表格行拖拽的方法示例
2019/06/07 Javascript
jqGrid表格底部汇总、合计行footerrow处理
2019/08/21 Javascript
node.js中process进程的概念和child_process子进程模块的使用方法示例
2020/02/11 Javascript
[02:50]【扭转乾坤,只此一招】DOTA2永雾林渊版本开启新篇章
2020/12/22 DOTA
Python学习笔记之常用函数及说明
2014/05/23 Python
python进程类subprocess的一些操作方法例子
2014/11/22 Python
Python实现邮件的批量发送的示例代码
2018/01/23 Python
python3对拉勾数据进行可视化分析的方法详解
2019/04/03 Python
python获取微信企业号打卡数据并生成windows计划任务
2019/04/30 Python
Pytorch mask_select 函数的用法详解
2020/02/18 Python
python中random.randint和random.randrange的区别详解
2020/09/20 Python
python 发送get请求接口详解
2020/11/17 Python
计算机网络专业推荐信
2013/11/24 职场文书
运动会解说词50字
2014/01/18 职场文书
人事专员职责
2014/02/22 职场文书
气象学专业个人求职信
2014/03/15 职场文书
专项资金申请报告
2015/05/15 职场文书
2015年车间主任工作总结
2015/05/21 职场文书
golang json数组拼接的实例
2021/04/28 Golang
Python卷积神经网络图片分类框架详解分析
2021/11/07 Python
el-form每行显示两列底部按钮居中效果的实现
2022/08/05 HTML / CSS