From d31e37a9838ef4dc2e7bc2d3ea8b6a21dc577cba Mon Sep 17 00:00:00 2001 From: yzsunlei Date: Tue, 17 Dec 2019 21:46:59 +0800 Subject: [PATCH] =?UTF-8?q?Vue=E5=8F=8C=E5=90=91=E7=BB=91=E5=AE=9A?= =?UTF-8?q?=E5=8E=9F=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...42\351\230\205\346\234\272\345\210\266.md" | 143 +++++++++--------- 1 file changed, 69 insertions(+), 74 deletions(-) diff --git "a/1.3.Vue\345\217\214\345\220\221\347\273\221\345\256\232\345\216\237\347\220\206-Object.defineProperty\346\225\260\346\215\256\345\212\253\346\214\201\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205\346\234\272\345\210\266.md" "b/1.3.Vue\345\217\214\345\220\221\347\273\221\345\256\232\345\216\237\347\220\206-Object.defineProperty\346\225\260\346\215\256\345\212\253\346\214\201\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205\346\234\272\345\210\266.md" index 64a3cc5..b8bb906 100644 --- "a/1.3.Vue\345\217\214\345\220\221\347\273\221\345\256\232\345\216\237\347\220\206-Object.defineProperty\346\225\260\346\215\256\345\212\253\346\214\201\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205\346\234\272\345\210\266.md" +++ "b/1.3.Vue\345\217\214\345\220\221\347\273\221\345\256\232\345\216\237\347\220\206-Object.defineProperty\346\225\260\346\215\256\345\212\253\346\214\201\345\222\214\345\217\221\345\270\203\350\256\242\351\230\205\346\234\272\345\210\266.md" @@ -1,5 +1,5 @@ ## 简单应用 -* 我们先来看一个简单的应用示例: +我们先来看一个简单的应用示例: ```vuejs
@@ -16,63 +16,61 @@ ``` -* 上面的示例具有的功能就是初始时,'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; @@ -80,29 +78,31 @@ function Watcher(fn){ 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
@@ -123,26 +123,24 @@ function Dep(){ }) ``` -* 当然上面这是最简单的双向绑定功能,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; @@ -183,7 +181,9 @@ export class Observer { } } ``` -* 整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。 + +整体上,value分为对象或数组两种情况来处理。这里我们先来看看defineReactive和observe这两个比较重要的函数。 + ```vuejs export function defineReactive ( obj: Object, @@ -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 { // 如果不是对象就跳过 @@ -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__时 @@ -302,10 +301,10 @@ function copyAugment (target: Object, src: Object, keys: Array) { } } ``` -* `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 @@ -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 { @@ -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 @@ -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)