Skip to content

Commit

Permalink
Vue双向绑定原理
Browse files Browse the repository at this point in the history
  • Loading branch information
yzsunlei committed Dec 17, 2019
1 parent 023949c commit d31e37a
Showing 1 changed file with 69 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
## 简单应用
* 我们先来看一个简单的应用示例:
我们先来看一个简单的应用示例:

```vuejs
<div id="app">
Expand All @@ -16,93 +16,93 @@
</script>
```

* 上面的示例具有的功能就是初始时,'hello world'字符串会显示在input输入框中和div文本中,当手动输入值后,div文本的值也相应的改变。
上面的示例具有的功能就是初始时,'hello world'字符串会显示在input输入框中和div文本中,当手动输入值后,div文本的值也相应的改变。

* 我们来简单理一下实现思路:
我们来简单理一下实现思路:

> 1、input输入框以及div文本和data中的数据进行绑定
> 2、input输入框内容变化时,data中的对应数据同步变化,即 view => model
> 3、data中数据变化时,对应的div文本内容同步变化 即 model => view
> 3、data中数据变化时,对应的div文本内容同步变化,即 model => view
## 原理介绍
* Vue.js是通过数据劫持以及结合发布者-订阅者来实现双向绑定的,数据劫持是利用ES5的Object.defineProperty(obj, key, val)来
劫持各个属性的的setter以及getter,在数据变动时发布消息给订阅者,从而触发相应的回调来更新视图。
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:

```javascript
function Observer(obj, key, value){
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.keys(value).forEach(function(key) {
new Observer(value, key, value[key])
})
};

Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function(){
get: function() {
if (Dep.target) {
dep.addSub(Dep.target);
};
return value;
},
set: function(newVal){
set: function(newVal) {
value = newVal;
dep.notify();
}
})
}
```
* 递归的为obj的每个属性添加getter和setter。在getter中,我们把watcher添加到dep中。setter中,触发watcher执行回调。

递归的为对象obj的每个属性添加getter和setter。在getter中,我们把watcher添加到dep中。在setter中,触发watcher执行回调。

### 第二部分是Watcher:

```javascript
function Watcher(fn){
this.update = function(){
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。

fn是数据变化后要执行的回调函数,一般是获取数据渲染模板。默认执行一遍update方法是为了在渲染模板过程中,调用数据对象的getter时建立两者之间的关系。因为同一时刻只有一个watcher处于激活状态,把当前watcher绑定在Dep.target(方便在Observer内获取)。回调结束后,销毁Dep.target。

### 第三部分是Dep:

```javascript
function Dep(){
function Dep() {
this.subs = [];

this.addSub = function (watcher) {
this.subs.push(watcher);
}

this.notify = function(){
this.subs.forEach(function(watcher){
this.notify = function() {
this.subs.forEach(function(watcher) {
watcher.update();
});
}
}
```
* 内部一个存放watcher的数组subs。addSub用于向数组中添加watcher(getter时)。notify用于触发watcher的更新(setter时)。

* 以上我们就完成了简易的双向绑定的功能,我们用一下看是不是能达到上面简单应用同样的效果。
内部一个存放watcher的数组subs。addSub用于向数组中添加watcher(getter时)。notify用于触发watcher的更新(setter时)。

以上我们就完成了简易的双向绑定的功能,我们用一下看是不是能达到上面简单应用同样的效果。

```html
<div id="app">
<input id="input" type="text" v-model="text">
Expand All @@ -123,26 +123,24 @@ function Dep(){
})
</script>
```
* 当然上面这是最简单的双向绑定功能,Vue中还实现了对数组、对象的双向绑定,下面我们来看看Vue中的实现。

当然上面这是最简单的双向绑定功能,Vue中还实现了对数组、对象的双向绑定,下面我们来看看Vue中的实现。

## Vue中的双向绑定

* 看Vue的实现源码前,我们先来看下下面这张图,经典的Vue双向绑定原理示意图:
看Vue的实现源码前,我们先来看下下面这张图,经典的Vue双向绑定原理示意图(图片来自于网络)

![Vue双向绑定示意图](./images/1.jpg)

* 简单解析如下:
简单解析如下:

> 1、实现一个数据监听器Obverser,对data中的数据进行监听,若有变化,通知相应的订阅者。
> 2、实现一个指令解析器Compile,对于每个元素上的指令进行解析,根据指令替换数据,更新视图。
> 3、实现一个Watcher,用来连接Obverser和Compile, 并为每个属性绑定相应的订阅者,当数据发生变化时,执行相应的回调函数,从而更新视图。
> 4、构造函数 (new Vue({}))
### Vue中的Observer:
* 首先是Observer对象,源码位置`src/core/observer/index.js`
首先是Observer对象,源码位置`src/core/observer/index.js`

```vuejs
export class Observer {
value: any;
Expand Down Expand Up @@ -183,7 +181,9 @@ export class Observer {
}
}
```
* 整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。

整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。

```vuejs
export function defineReactive (
obj: Object,
Expand Down Expand Up @@ -252,10 +252,9 @@ export function defineReactive (
})
}
```
* `defineReactive`这个方法里面,是具体的为对象的属性添加getter、setter的地方。它会为每个值创建一个dep,如果用户为这个值传入getter和setter,则暂时保存。
之后通过Object.defineProperty,重新添加装饰器。在getter中,dep.depend其实做了两件事,一是向Dep.target内部的deps添加dep,
二是将Dep.target添加到dep内部的subs,也就是建立它们之间的联系。在setter中,如果新旧值相同,直接返回,
不同则调用dep.notify来更新与之相关的watcher。

defineReactive这个方法里面,是具体的为对象的属性添加getter、setter的地方。它会为每个值创建一个dep,如果用户为这个值传入getter和setter,则暂时保存。之后通过Object.defineProperty,重新添加装饰器。在getter中,dep.depend其实做了两件事,一是向Dep.target内部的deps添加dep,二是将Dep.target添加到dep内部的subs,也就是建立它们之间的联系。在setter中,如果新旧值相同,直接返回,不同则调用dep.notify来更新与之相关的watcher。

```vuejs
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是对象就跳过
Expand All @@ -282,12 +281,12 @@ export function observe (value: any, asRootData: ?boolean): Observer | void {
return ob
}
```
* `observe`这个方法用于观察一个对象,返回与对象相关的Observer对象,如果没有则为value创建一个对应的Observer。

* 好的,我们再回到Observer,如果传入的是对象,我们就调用walk,该方法就是遍历对象,对每个值执行defineReactive。
`observe`这个方法用于观察一个对象,返回与对象相关的Observer对象,如果没有则为value创建一个对应的Observer。

好的,我们再回到Observer,如果传入的是对象,我们就调用walk,该方法就是遍历对象,对每个值执行defineReactive。

* 对于传入的对象是数组的情况,其实会有一些特殊的处理,因为数组本身只引用了一个地址,所以对数组进行push、splice、sort等操作,我们是无法监听的。
所以,Vue中改写value的__proto__(如果有),或在value上重新定义这些方法。augment在环境支持__proto__时是protoAugment,不支持时是copyAugment。
对于传入的对象是数组的情况,其实会有一些特殊的处理,因为数组本身只引用了一个地址,所以对数组进行push、splice、sort等操作,我们是无法监听的。所以,Vue中改写value的__proto__(如果有),或在value上重新定义这些方法。augment在环境支持__proto__时是protoAugment,不支持时是copyAugment。

```vuejs
// augment在环境支持__proto__时
Expand All @@ -302,10 +301,10 @@ function copyAugment (target: Object, src: Object, keys: Array<string>) {
}
}
```
* `augment`在环境支持`__proto__`时,就很简单,调用`protoAugment`其实就是执行了`value.__proto__ = arrayMethods`
`augment`在环境支持`__proto__`时,调用`copyAugment`中循环把`arrayMethods`上的`arrayKeys`方法添加到`value`上。

* 那这里我们就要看看`arrayMethods`方法了。`arrayMethods`其实是改写了数组方法的新对象。`arrayKeys``arrayMethods`中的方法列表。
`augment`在环境支持`__proto__`时,就很简单,调用`protoAugment`其实就是执行了`value.__proto__ = arrayMethods``augment`在环境支持`__proto__`时,调用`copyAugment`中循环把`arrayMethods`上的`arrayKeys`方法添加到`value`上。

那这里我们就要看看`arrayMethods`方法了。`arrayMethods`其实是改写了数组方法的新对象。`arrayKeys``arrayMethods`中的方法列表。

```vuejs
const arrayProto = Array.prototype
Expand Down Expand Up @@ -345,15 +344,13 @@ methodsToPatch.forEach(function (method) {
})
```

* 实际上还是调用数组相应的方法来操作value,只不过操作之后,添加了相关watcher的更新。
调用`push``unshift``splice`三个方法参数大于2时,要重新调用ob.observeArray,因为这三种情况都是像数组中添加新的元素,
所以需要重新观察每个子元素。最后在通知变化。
实际上还是调用数组相应的方法来操作value,只不过操作之后,添加了相关watcher的更新。调用`push``unshift``splice`三个方法参数大于2时,要重新调用ob.observeArray,因为这三种情况都是像数组中添加新的元素,所以需要重新观察每个子元素。最后在通知变化。

* Vue中的Observer就讲到这里了。实际上还有两个函数`set``del`没有讲解,其实就是在添加或删除数组元素、对象属性时进行getter、
setter的绑定以及通知变化,具体可以去看源码。
Vue中的Observer就讲到这里了。实际上还有两个函数`set``del`没有讲解,其实就是在添加或删除数组元素、对象属性时进行getter、setter的绑定以及通知变化,具体可以去看源码。

### Vue中的Dep:
* 看完Vue中的Observer,然后我们来看看Vue中Dep,源码位置:`src/core/observer/dep.js`
看完Vue中的Observer,然后我们来看看Vue中Dep,源码位置:`src/core/observer/dep.js`

```vuejs
let uid = 0
export default class Dep {
Expand Down Expand Up @@ -396,13 +393,14 @@ export default class Dep {
}
}
```
* Dep类就比较简单,内部有一个id和一个subs,id用于作为dep对象的唯一标识,subs就是保存watcher的数组。
相比于上面我们自己实现的demo应用,这里多了removeSub和depend。removeSub是从数组中移除某个watcher,depend是调用了watcher的addDep。

* 好,Vue中的Dep只能说这么多了。
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`
最后我们再来看看Vue中的Watcher,源码位置:`src/core/observer/watcher.js`

```vuejs
// 注,我删除了源码中一些不太重要或与双向绑定关系不太大的逻辑,删除的代码用// ... 表示
let uid = 0
Expand Down Expand Up @@ -542,38 +540,35 @@ export default class Watcher {
}
}
```
* 创建Watcher对象时,有两个比较重要的参数,一个是expOrFn,一个是cb。

* 在Watcher创建时,会调用this.get,里面会执行根据expOrFn解析出来的getter。在这个getter中,我们或渲染页面,
或获取某个数据的值。总之,会调用相关data的getter,来建立数据的双向绑定。
创建Watcher对象时,有两个比较重要的参数,一个是expOrFn,一个是cb。

在Watcher创建时,会调用this.get,里面会执行根据expOrFn解析出来的getter。在这个getter中,我们或渲染页面,或获取某个数据的值。总之,会调用相关data的getter,来建立数据的双向绑定。

当相关的数据改变时,会调用watcher的update方法,进而调用run方法。我们看到,run中还会调用this.get来获取修改之后的value值。

* 当相关的数据改变时,会调用watcher的update方法,进而调用run方法。我们看到,run中还会调用this.get来获取修改之后的value值。
其实Watcher有两种主要用途:一种是更新模板,另一种就是监听某个值的变化。

* 其实Watcher有两种主要用途:一种是更新模板,另一种就是监听某个值的变化。
模板更新的情况:在Vue声明周期挂载元素时,我们是通过创建Watcher对象,然后调用updateComponent来更新渲染模板的。

* 模板更新的情况:在Vue声明周期挂载元素时,我们是通过创建Watcher对象,然后调用updateComponent来更新渲染模板的。
```vuejs
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并执行响应的回调。
在创建Watcher会调用this.get,也就是这里的updateComponent。在render的过程中,会调用data的getter方法,以此来建立数据的双向绑定,当数据改变时,会重新触发updateComponent。

* 好,Vue中的Watcher就说这么多了。其实上面注释的代码中还有`cleanupDeps`清除依赖逻辑、`teardown`销毁Watcher逻辑等,留给大家自己去看源码吧。
数据监听的情况:另一个用途就是我们的computed、watch等,即监听数据的变化来执行响应的操作。此时this.get返回的是要监听数据的值。初始化过程中,调用this.get会拿到初始值保存为this.value,监听的数据改变后,会再次调用this.get并拿到修改之后的值,将旧值和新值传给cb并执行响应的回调。

好,Vue中的Watcher就说这么多了。其实上面注释的代码中还有`cleanupDeps`清除依赖逻辑、`teardown`销毁Watcher逻辑等,留给大家自己去看源码吧。

## 总结一下
* Vue中双向绑定,简单来说就是Observer、Watcher、Dep三部分。下面我们再梳理一下整个过程:
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://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://juejin.im/post/5acd0c8a6fb9a028da7cdfaf)
* [https://www.cnblogs.com/zhenfei-jiang/p/7542900.html](https://www.cnblogs.com/zhenfei-jiang/p/7542900.html)
Expand Down

0 comments on commit d31e37a

Please sign in to comment.