[Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅
有一段时间没有更新技术博文了,因为这段时间埋下头来看Vue源码了。本文我们一起通过学习双向绑定原理来分析Vue源码。预计接下来会围绕Vue源码来整理一些文章,如下。
- 一起来学Vue双向绑定原理-数据劫持和发布订阅
- 一起来学Vue模板编译原理(一)-Template生成AST
- 一起来学Vue模板编译原理(二)-AST生成Render字符串
- 一起来学Vue虚拟DOM解析-Virtual Dom实现和Dom-diff算法
这些文章统一放在我的git仓库:https://github.com/yzsunlei/javascript-series-code-analyzing。觉得有用记得star收藏。
简单应用
我们先来看一个简单的应用示例:
<div id="app">
<input id="input" type="text" v-model="text">
<div id="text">输入的值为:{{text}}</div>
</div>
<script>
var vm = new Vue({
el: '#app',
data: {
text: 'hello world'
}
})
</script>
上面的示例具有的功能就是初始时,'hello world'字符串会显示在input输入框中和div文本中,当手动输入值后,div文本的值也相应的改变。
我们来简单理一下实现思路:
- 1、input输入框以及div文本和data中的数据进行绑定
- 2、input输入框内容变化时,data中的对应数据同步变化,即 view => model
- 3、data中数据变化时,对应的div文本内容同步变化,即 model => view
原理介绍
Vue.js是通过数据劫持以及结合发布者-订阅者来实现双向绑定的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
双向数据绑定,简单点来说分为三个部分:
- 1、Observer:观察者,这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的watcher。
- 2、Watcher:订阅者,当监听的数据值修改时,执行响应的回调函数(Vue里面的更新模板内容)。
- 3、Dep:订阅管理器,连接Observer和Watcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存与该Observer相关的Watcher。
DEMO实现双向绑定
下面我们来一步步的实现双向数据绑定。
第一部分是Observer:
function Observer(obj, key, value) {
var dep = new Dep();
if (Object.prototype.toString.call(value) == '[object Object]') {
Object.keys(value).forEach(function(key) {
new Observer(value, key, value[key])
})
};
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function() {
if (Dep.target) {
dep.addSub(Dep.target);
};
return value;
},
set: function(newVal) {
value = newVal;
dep.notify();
}
})
}
递归的为对象obj的每个属性添加getter和setter。在getter中,我们把watcher添加到dep中。在setter中,触发watcher执行回调。
第二部分是Watcher:
function Watcher(fn) {
this.update = function() {
Dep.target = this;
fn();
Dep.target = null;
}
this.update();
}
fn是数据变化后要执行的回调函数,一般是获取数据渲染模板。默认执行一遍update方法是为了在渲染模板过程中,调用数据对象的getter时建立两者之间的关系。因为同一时刻只有一个watcher处于激活状态,把当前watcher绑定在Dep.target(方便在Observer内获取)。回调结束后,销毁Dep.target。
第三部分是Dep:
function Dep() {
this.subs = [];
this.addSub = function (watcher) {
this.subs.push(watcher);
}
this.notify = function() {
this.subs.forEach(function(watcher) {
watcher.update();
});
}
}
内部一个存放watcher的数组subs。addSub用于向数组中添加watcher(getter时)。notify用于触发watcher的更新(setter时)。
以上我们就完成了简易的双向绑定的功能,我们用一下看是不是能达到上面简单应用同样的效果。
<div id="app">
<input id="input" type="text" v-model="text">
<div id="text">输入的值为:{{text}}</div>
</div>
<script type="text/javascript">
var obj = {
text: 'hello world'
}
Object.keys(obj).forEach(function(key){
new Observer(obj, key, obj[key])
});
new Watcher(function(){
document.querySelector("#text").innerHTML = "输入的值为:" + obj.text;
})
document.querySelector("#input").addEventListener('input', function(e) {
obj.text = e.target.value;
})
</script>
当然上面这是最简单的双向绑定功能,Vue中还实现了对数组、对象的双向绑定,下面我们来看看Vue中的实现。
Vue中的双向绑定
看Vue的实现源码前,我们先来看下下面这张图,经典的Vue双向绑定原理示意图(图片来自于网络):
简单解析如下:
- 1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。
- 2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。
- 3、实现一个Watcher,用来连接Obverser和Compile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
Vue中的Observer:
首先是Observer对象,源码位置src/core/observer/index.js
export class Observer {
value: any;
dep: Dep;
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
// 添加__ob__来标示value有对应的Observer
def(value, '__ob__', this)
if (Array.isArray(value)) { // 处理数组
if (hasProto) { // 实现是'__proto__' in {}
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else { // 处理对象
this.walk(value)
}
}
// 给对象每个属性添加getter/setters
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i]) // 重点
}
}
// 循环观察数组的每一项
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i]) // 重点
}
}
}
整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
const property = Object.getOwnPropertyDescriptor(obj, key)
// 带有不可配置的属性直接跳过
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
// 保存对象属性上自有的getter和setter
const getter = property && property.get
const setter = property && property.set
// 如果属性上之前没有定义getter,并且没有传入初始val值,就把属性原有的值赋值给val
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
// 给属性设置getter
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 给每个属性创建一个dep
dep.depend()
if (childOb) {
childOb.dep.depend()
// 如果是数组,就递归创建
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
// 给属性设置setter
const value = getter ? getter.call(obj) : val
// 值未变化,就跳过
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter() // 非生产环境自定义调试用,这里忽略
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
childOb = !shallow && observe(newVal)
// 值发生变化进行通知
dep.notify()
}
})
}
defineReactive这个方法里面,是具体的为对象的属性添加getter、setter的地方。它会为每个值创建一个dep,如果用户为这个值传入getter和setter,则暂时保存。之后通过Object.defineProperty,重新添加装饰器。在getter中,dep.depend其实做了两件事,一是向Dep.target内部的deps添加dep,二是将Dep.target添加到dep内部的subs,也就是建立它们之间的联系。在setter中,如果新旧值相同,直接返回,不同则调用dep.notify来更新与之相关的watcher。
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象就跳过
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
// 如果已有observer,就直接返回,上面讲到过会用`__ob__`属性来记录
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 如果没有,就创建一个
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
observe
这个方法用于观察一个对象,返回与对象相关的Observer对象,如果没有则为value创建一个对应的Observer。
好的,我们再回到Observer,如果传入的是对象,我们就调用walk,该方法就是遍历对象,对每个值执行defineReactive。
对于传入的对象是数组的情况,其实会有一些特殊的处理,因为数组本身只引用了一个地址,所以对数组进行push、splice、sort等操作,我们是无法监听的。所以,Vue中改写value的__proto__(如果有),或在value上重新定义这些方法。augment在环境支持__proto__时是protoAugment,不支持时是copyAugment。
// augment在环境支持__proto__时
function protoAugment (target, src: Object) {
target.__proto__ = src
}
// augment在环境不支持__proto__时
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
augment
在环境支持__proto__
时,就很简单,调用protoAugment
其实就是执行了value.__proto__ = arrayMethods
。augment
在环境支持__proto__
时,调用copyAugment
中循环把arrayMethods
上的arrayKeys
方法添加到value
上。
那这里我们就要看看arrayMethods
方法了。arrayMethods
其实是改写了数组方法的新对象。arrayKeys
是arrayMethods
中的方法列表。
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
methodsToPatch.forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 是push、unshift、splice时,重新观察数组,因为这三个方法都是像数组中添加新的元素
if (inserted) ob.observeArray(inserted)
// 通知变化
ob.dep.notify()
return result
})
})
实际上还是调用数组相应的方法来操作value,只不过操作之后,添加了相关watcher的更新。调用push
、unshift
、splice
三个方法参数大于2时,要重新调用ob.observeArray,因为这三种情况都是像数组中添加新的元素,所以需要重新观察每个子元素。最后在通知变化。
Vue中的Observer就讲到这里了。实际上还有两个函数set
、del
没有讲解,其实就是在添加或删除数组元素、对象属性时进行getter、setter的绑定以及通知变化,具体可以去看源码。
Vue中的Dep:
看完Vue中的Observer,然后我们来看看Vue中Dep,源码位置:src/core/observer/dep.js
。
let uid = 0
export default class Dep {
static target: ?Watcher;
id: number;
subs: Array<Watcher>;
constructor () {
this.id = uid++
this.subs = []
}
// 添加订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
// 移除订阅者
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
// 添加到订阅管理器
depend () {
if (Dep.target) {
Dep.target.addDep(this)
}
}
// 通知变化
notify () {
const subs = this.subs.slice()
if (process.env.NODE_ENV !== 'production' && !config.async) {
subs.sort((a, b) => a.id - b.id)
}
// 遍历所有的订阅者,通知更新
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
Dep类就比较简单,内部有一个id和一个subs,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。相比于上面我们自己实现的demo应用,这里多了removeSub和depend。removeSub是从数组中移除某个watcher,depend是调用了watcher的addDep。
好,Vue中的Dep只能说这么多了。
Vue中的Watcher:
最后我们再来看看Vue中的Watcher,源码位置:src/core/observer/watcher.js
。
// 注,我删除了源码中一些不太重要或与双向绑定关系不太大的逻辑,删除的代码用// ... 表示
let uid = 0
export default class Watcher {
vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;
newDeps: Array<Dep>;
depIds: SimpleSet;
newDepIds: SimpleSet;
before: ?Function;
getter: Function;
value: any;
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// ...
this.cb = cb
this.id = ++uid
// ...
this.expression = process.env.NODE_ENV !== 'production'
? expOrFn.toString()
: ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
// ...
if (this.deep) {
traverse(value)
}
popTarget()
this.cleanupDeps()
return value
}
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)
}
}
}
cleanupDeps () {
// ...
}
update () {
// 更新三种模式吧,lazy延迟更新,sync同步更新直接执行,默认异步更新添加到处理队列
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
run () {
// 触发更新,在这里调用cb函数
if (this.active) {
const value = this.get()
if (
value !== this.value ||
isObject(value) ||
this.deep
) {
const oldValue = this.value
this.value = value
if (this.user) {
try {
this.cb.call(this.vm, value, oldValue)
} catch (e) {
handleError(e, this.vm, `callback for watcher "${this.expression}"`)
}
} else {
this.cb.call(this.vm, value, oldValue)
}
}
}
}
evaluate () {
// ...
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
teardown () {
// ...
}
}
创建Watcher对象时,有两个比较重要的参数,一个是expOrFn,一个是cb。
在Watcher创建时,会调用this.get,里面会执行根据expOrFn解析出来的getter。在这个getter中,我们或渲染页面,或获取某个数据的值。总之,会调用相关data的getter,来建立数据的双向绑定。
当相关的数据改变时,会调用watcher的update方法,进而调用run方法。我们看到,run中还会调用this.get来获取修改之后的value值。
其实Watcher有两种主要用途:一种是更新模板,另一种就是监听某个值的变化。
模板更新的情况:在Vue声明周期挂载元素时,我们是通过创建Watcher对象,然后调用updateComponent来更新渲染模板的。
vm._watcher = new Watcher(vm, updateComponent, noop)
在创建Watcher会调用this.get,也就是这里的updateComponent。在render的过程中,会调用data的getter方法,以此来建立数据的双向绑定,当数据改变时,会重新触发updateComponent。
数据监听的情况:另一个用途就是我们的computed、watch等,即监听数据的变化来执行响应的操作。此时this.get返回的是要监听数据的值。初始化过程中,调用this.get会拿到初始值保存为this.value,监听的数据改变后,会再次调用this.get并拿到修改之后的值,将旧值和新值传给cb并执行响应的回调。
好,Vue中的Watcher就说这么多了。其实上面注释的代码中还有cleanupDeps
清除依赖逻辑、teardown
销毁Watcher逻辑等,留给大家自己去看源码吧。
总结一下
Vue中双向绑定,简单来说就是Observer、Watcher、Dep三部分。下面我们再梳理一下整个过程:
首先我们为每个vue属性用Object.defineProperty()实现数据劫持,为每个属性分配一个订阅者集合的管理数组dep;
然后在编译的时候在该属性的数组dep中添加订阅者,Vue中的v-model会添加一个订阅者,{{}}也会,v-bind也会;
最后修改值就会为该属性赋值,触发该属性的set方法,在set方法内通知订阅者数组dep,订阅者数组循环调用各订阅者的update方法更新视图。
相关
- https://github.com/liutao/vue2.0-source/blob/master/%E5%8F%8C%E5%90%91%E6%95%B0%E6%8D%AE%E7%BB%91%E5%AE%9A.md
- https://juejin.im/post/5acd0c8a6fb9a028da7cdfaf
- https://www.cnblogs.com/zhenfei-jiang/p/7542900.html
- https://segmentfault.com/a/1190000014274840
- https://segmentfault.com/a/1190000014616977
[Vue源码]一起来学Vue双向绑定原理-数据劫持和发布订阅的更多相关文章
- [Vue源码]一起来学Vue模板编译原理(一)-Template生成AST
本文我们一起通过学习Vue模板编译原理(一)-Template生成AST来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学Vu ...
- [Vue源码]一起来学Vue模板编译原理(二)-AST生成Render字符串
本文我们一起通过学习Vue模板编译原理(二)-AST生成Render字符串来分析Vue源码.预计接下来会围绕Vue源码来整理一些文章,如下. 一起来学Vue双向绑定原理-数据劫持和发布订阅 一起来学V ...
- Vue源码分析(一) : new Vue() 做了什么
Vue源码分析(一) : new Vue() 做了什么 author: @TiffanysBear 在了解new Vue做了什么之前,我们先对Vue源码做一些基础的了解,如果你已经对基础的源码目录设计 ...
- Vue 源码解读(3)—— 响应式原理
前言 上一篇文章 Vue 源码解读(2)-- Vue 初始化过程 详细讲解了 Vue 的初始化过程,明白了 new Vue(options) 都做了什么,其中关于 数据响应式 的实现用一句话简单的带过 ...
- Vue源码学习(零):内部原理解析
本篇文章是在阅读<剖析 Vue.js 内部运行机制>小册子后总结所得,想要了解详细内容,请参考原文:https://juejin.im/book/5a36661851882538e2259 ...
- 从Vue源码中我学到了几点精妙方法
话不多说,赶快试试这几个精妙方法吧!在工作中肯定会用得到. 立即执行函数 页面加载完成后只执行一次的设置函数. (function (a, b) { console.log(a, b); // 1,2 ...
- vue源码分析之new Vue过程
实例化构造函数 从这里可以看出new Vue实际上是使vue构造函数实例化,然后调用_init方法 _init方法,该方法在 src/core/instance/init.js 中定义 Vue.pro ...
- Vue源码思维导图------------Vue选项的合并之$options
本节将看下初始化中的$options: Vue.prototype._init = function (options?: Object) { const vm: Component = this / ...
- Vue源码解析---数据的双向绑定
本文主要抽离Vue源码中数据双向绑定的核心代码,解析Vue是如何实现数据的双向绑定 核心思想是ES5的Object.defineProperty()和发布-订阅模式 整体结构 改造Vue实例中的dat ...
随机推荐
- 批量获取mysql数据库实例指定参数的值
需求:需要对比所有mysql数据库实例上面的指定参数配置情况,同时需要需要能看到如ip,端口,master or slave,毕竟主和从参数不一样还是有可能的. 说明:必须要有个数据库存储所有是数据库 ...
- linux-crond_计划任务
定时计划任务 主要文件介绍: [root@nginx ~]# ll /etc/cron* -d drwxr-xr-x. 2 root root 21 7月 11 20:28 /etc/cron.d d ...
- 理解LDAP与LDAP注入
0x01 LDAP简介 LDAP,轻量目录访问协议 |dn :一条记录的位置||dc :一条记录所属区域||ou :一条记录所属组织||cn/uid:一条记录的名字/ID| 此处我更喜欢把LDAP和 ...
- Phalanx HDU - 2859 dp
#include<cstdio> #include<cstring> #include<algorithm> #include<iostream> us ...
- Wannafly Camp 2020 Day 1H 最大公约数 - 质因数分解,高精度
把每个质因子扒出来乱搞一下 #include <bits/stdc++.h> using namespace std; int g[505][505]; int isp[505]; str ...
- java中锁与@Transactional同时使用导致锁失效的问题
示例代码 @Transactional public void update(int id) { boolean lock = redisLock.lock(id); if (!lock) { thr ...
- 二分-C - Pie
C - Pie My birthday is coming up and traditionally I'm serving pie. Not just one pie, no, I have a n ...
- 共享v2射线局域网http代理方法
问题描述 默认v节点大部分是socks代理,实际使用过程中存在以下问题: 部分浏览器无法支持socks需要走http代理. 局域网内其他设备(手机.PS4等)需要配置代理. 解决方法 1.在PC托盘图 ...
- jdbc中SQL语句拼接java变量
例如:String sql = "select * from user where username='" + username + "' and password =' ...
- HandlerMethod解析
api写HandlerMethod的作用: Encapsulates information about a handler method consisting of a method and a b ...