<span id="mktg5"></span>

<i id="mktg5"><meter id="mktg5"></meter></i>

        <label id="mktg5"><meter id="mktg5"></meter></label>
        最新文章專題視頻專題問答1問答10問答100問答1000問答2000關鍵字專題1關鍵字專題50關鍵字專題500關鍵字專題1500TAG最新視頻文章推薦1 推薦3 推薦5 推薦7 推薦9 推薦11 推薦13 推薦15 推薦17 推薦19 推薦21 推薦23 推薦25 推薦27 推薦29 推薦31 推薦33 推薦35 推薦37視頻文章20視頻文章30視頻文章40視頻文章50視頻文章60 視頻文章70視頻文章80視頻文章90視頻文章100視頻文章120視頻文章140 視頻2關鍵字專題關鍵字專題tag2tag3文章專題文章專題2文章索引1文章索引2文章索引3文章索引4文章索引5123456789101112131415文章專題3
        問答文章1 問答文章501 問答文章1001 問答文章1501 問答文章2001 問答文章2501 問答文章3001 問答文章3501 問答文章4001 問答文章4501 問答文章5001 問答文章5501 問答文章6001 問答文章6501 問答文章7001 問答文章7501 問答文章8001 問答文章8501 問答文章9001 問答文章9501
        當前位置: 首頁 - 科技 - 知識百科 - 正文

        Vue雙向數據綁定源碼分析

        來源:懂視網 責編:小采 時間:2020-11-27 20:04:07
        文檔

        Vue雙向數據綁定源碼分析

        Vue雙向數據綁定源碼分析:雖然工作中一直使用Vue作為基礎庫,但是對于其實現機理僅限于道聽途說,這樣對長期的技術發展很不利。所以最近攻讀了其源碼的一部分,先把雙向數據綁定這一塊的內容給整理一下,也算是一種學習的反芻。本篇文章的Vue源碼版本為v2.2.0開發版。Vue源碼的整體架
        推薦度:
        導讀Vue雙向數據綁定源碼分析:雖然工作中一直使用Vue作為基礎庫,但是對于其實現機理僅限于道聽途說,這樣對長期的技術發展很不利。所以最近攻讀了其源碼的一部分,先把雙向數據綁定這一塊的內容給整理一下,也算是一種學習的反芻。本篇文章的Vue源碼版本為v2.2.0開發版。Vue源碼的整體架

        雖然工作中一直使用Vue作為基礎庫,但是對于其實現機理僅限于道聽途說,這樣對長期的技術發展很不利。所以最近攻讀了其源碼的一部分,先把雙向數據綁定這一塊的內容給整理一下,也算是一種學習的反芻。

        本篇文章的Vue源碼版本為v2.2.0開發版。

        Vue源碼的整體架構無非是初始化Vue對象,掛載數據data/props等,在不同的時期觸發不同的事件鉤子,如created() / mounted() / update()等,后面專門整理各個模塊的文章。這里先講雙向數據綁定的部分,也是最主要的部分。

        設計思想:觀察者模式

        Vue的雙向數據綁定的設計思想為觀察者模式,為了方便,下文中將被觀察的對象稱為觀察者,將觀察者對象觸發更新的稱為訂閱者。主要涉及到的概念有:

        1、Dep對象:Dependency依賴的簡寫,包含有三個主要屬性id, subs, target和四個主要函數addSub, removeSub, depend, notify,是觀察者的依賴集合,負責在數據發生改變時,使用notify()觸發保存在subs下的訂閱列表,依次更新數據和DOM。

      1. id: 每個觀察者(依賴對象)的唯一標識。

      2. subs: 觀察者對象的訂閱者列表。

      3. target: 全局唯一的訂閱者對象,因為只能同時計算和更新一個訂閱者的值。

      4. addSub(): 使用`push()`方法添加一個訂閱者。

      5. removeSub(): 使用`splice()`方法移除一個訂閱者。

      6. depend(): 將自己添加到當前訂閱者對象的依賴列表。

      7. notify(): 在數據被更新時,會遍歷subs對象,觸發每一個訂閱者的更新。

      8. 2、Observer對象:即觀察者,包含兩個主要屬性value, dep。做法是使用getter/setter方法覆蓋默認的取值和賦值操作,將對象封裝為響應式對象,每一次調用時更新依賴列表,更新值時觸發訂閱者。綁定在對象的__ob__原型鏈屬性上。

      9. value: 原始值。

      10. dep: 依賴列表。

      11. 源碼實戰解析

        有過Vue開發基礎的應該都了解其怎么初始化一個Vue對象:

        new Vue({
         el: '#container',
         data: {
         count: 100
         },
         ...
        });

        那么我們就從這個count說起,看它是怎么完成雙向數據綁定的。

        下面的代碼片段中英文注釋為尤雨溪所寫,中文注釋為我所寫,英文注釋更能代表開發者的清晰思路。

        首先從全局的初始化函數調用:initMixin(Vue$3); ,這里的Vue$3對象就是全局的Vue對象,在此之前已經掛載了Vue的各種基本數據和函數。這個函數體就是初始化我們上面聲明Vue語句的過程化邏輯,取主體代碼來看:

        // 這里的options就是上面聲明Vue對象的json對象
        Vue.prototype._init = function (options) {
         ...
         var vm = this;
         ...
         initLifecycle(vm);
         initEvents(vm);
         initRender(vm);
         callHook(vm, 'beforeCreate');
         // 這里就是我們接下來要跟進的初始化Vue參數
         initState(vm);
         initInjections(vm);
         callHook(vm, 'created');
         ...
         };

        這里主要完成了初始化事件、渲染、參數、注入等過程,并不斷調用事件鉤子的回調函數。下面來到如何初始化參數:

        function initState (vm) {
         vm._watchers = [];
         var opts = vm.$options;
         if (opts.props) { initProps(vm, opts.props); }
         if (opts.methods) { initMethods(vm, opts.methods); }
         // 我們的count在這里初始化
         if (opts.data) {
         initData(vm);
         } else {
         observe(vm._data = {}, true /* asRootData */);
         }
         if (opts.computed) { initComputed(vm, opts.computed); }
         if (opts.watch) { initWatch(vm, opts.watch); }
        }

        這里依次檢測參數中包含的props/methods/data/computed/watch并進入不同的函數進行初始化,這里我們只關心initData:

        function initData (vm) {
         var data = vm.$options.data;
         data = vm._data = typeof data === 'function'
         ? data.call(vm)
         : data || {};
         if (!isPlainObject(data)) {
         data = {};
         }
         ...
         // observe data
         observe(data, true /* asRootData */);

        可以看到Vue的data參數支持對象和回調函數,但最終返回的一定是對象,否則使用空對象。接下來就是重頭戲了,我們如何將data參數設置為響應式的:

        /**
         * Attempt to create an observer instance for a value,
         * returns the new observer if successfully observed,
         * or the existing observer if the value already has one.
         */
        function observe (value, asRootData) {
         if (!isObject(value)) {
         return
         }
         var ob;
         if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
         ob = value.__ob__;
         } else if (
         /* 為了防止value不是單純的對象而是Regexp或者函數之類的,或者是vm實例再或者是不可擴展的 */
         observerState.shouldConvert &&
         !isServerRendering() &&
         (Array.isArray(value) || isPlainObject(value)) &&
         Object.isExtensible(value) &&
         !value._isVue
         ) {
         ob = new Observer(value);
         }
         if (asRootData && ob) {
         ob.vmCount++;
         }
         return ob
        }

        這里的英文注釋非常清晰,就是為了給該對象新建一個觀察者類,如果存在則返回已存在的(比如互相引用或依賴重復),可以看到這個觀察者列表放置在對象的__ob__屬性下。下面我們看下這個Observer觀察者類:

        /**
         * Observer class that are attached to each observed
         * object. Once attached, the observer converts target
         * object's property keys into getter/setters that
         * collect dependencies and dispatches updates.
         */
        var Observer = function Observer (value) {
         this.value = value;
         this.dep = new Dep();
         this.vmCount = 0;
         // def函數是defineProperty的簡單封裝
         def(value, '__ob__', this);
         if (Array.isArray(value)) {
         // 在es5及更低版本的js里,無法完美繼承數組,這里檢測并選取合適的函數
         // protoAugment函數使用原型鏈繼承,copyAugment函數使用原型鏈定義(即對每個數組defineProperty)
         var augment = hasProto
         ? protoAugment
         : copyAugment;
         augment(value, arrayMethods, arrayKeys);
         this.observeArray(value);
         } else {
         this.walk(value);
         }
        };

        在Observer類的注釋里也清楚的說明,它會被關聯到每一個被檢測的對象,使用getter/setter修改其默認讀寫,用于收集依賴和發布更新。其中出現了三個我們需要關心的東西Dep類/observeArray/walk,我們先看observeArray的源碼:

        /**
         * Observe a list of Array items.
         */
        Observer.prototype.observeArray = function observeArray (items) {
         for (var i = 0, l = items.length; i < l; i++) {
         observe(items[i]);
         }
        };

        它不過是在Observer類和observe方法中間的一層遞歸,因為我們觀察的只能是對象,而不能是數字、字符串或者數組(數組的觀察比較特殊,事實上是重構了方法來觸發更新,后面會講到)。那我們接下來看下Dep類是做什么用的:

        /**
         * A dep is an observable that can have multiple
         * directives subscribing to it.
         */
        var Dep = function Dep () {
         this.id = uid$1++;
         this.subs = [];
        };

        注釋里告訴我們Dep類是一個會被多個指令訂閱的可被觀察的對象,這里的指令就是我們在html代碼里書寫的東西,如:class={active: hasActive}或{{ count }} {{ count * price }} ,而他們就會訂閱hasActive/count/price這些對象,而這些訂閱他們的對象就會被放置在Dep.subs列表中。每一次新建Dep對象,就會全局uid遞增,然后傳給該Dep對象,保證唯一性id。

        我們接著看剛才的walk函數做了什么:

        /**
         * Walk through each property and convert them into
         * getter/setters. This method should only be called when
         * value type is Object.
         */
        Observer.prototype.walk = function walk (obj) {
         var keys = Object.keys(obj);
         for (var i = 0; i < keys.length; i++) {
         defineReactive$$1(obj, keys[i], obj[keys[i]]);
         }
        };

        看來和名字一樣,它只是走了一遍,那我們來看下defineReactive$$1做了什么:

        /**
         * Define a reactive property on an Object.
         */
        function defineReactive$$1 (obj, key, val, customSetter) {
         var dep = new Dep();
        
         var property = Object.getOwnPropertyDescriptor(obj, key);
         if (property && property.configurable === false) {
         return
         }
        
         // cater for pre-defined getter/setters
         var getter = property && property.get;
         var setter = property && property.set;
        
         var childOb = observe(val);
         Object.defineProperty(obj, key, {
         enumerable: true,
         configurable: true,
         get: function reactiveGetter () {
         var value = getter ? getter.call(obj) : val;
         if (Dep.target) {
         dep.depend();
         if (childOb) {
         childOb.dep.depend();
         }
         if (Array.isArray(value)) {
         dependArray(value);
         }
         }
         return value
         },
         set: function reactiveSetter (newVal) {
         var value = getter ? getter.call(obj) : val;
         // 臟檢查,排除了NaN !== NaN的影響
         if (newVal === value || (newVal !== newVal && value !== value)) {
         return
         }
         if (setter) {
         setter.call(obj, newVal);
         } else {
         val = newVal;
         }
         childOb = observe(newVal);
         dep.notify();
         }
         });
        }

        終于找到重頭戲了,這里真正使用了getter/setter代理了對象的默認讀寫。我們首先新建一個Dep對象,利用閉包準備收集依賴,然后我們使用observe觀察該對象,注意此時與上面相比少了一個asRootData = true的參數。

        我們先來看取值的代理get,這里用到了Dep.target屬性和depend()方法,我們來看看它是做什么的:

        // the current target watcher being evaluated.
        // this is globally unique because there could be only one
        // watcher being evaluated at any time.
        Dep.target = null;
        
        Dep.prototype.depend = function depend () {
         if (Dep.target) {
         Dep.target.addDep(this);
         }
        };
        
        Dep.prototype.notify = function notify () {
         // stablize the subscriber list first
         var subs = this.subs.slice();
         for (var i = 0, l = subs.length; i < l; i++) {
         subs[i].update();
         }
        };

        注釋看的出來Dep.target是全局唯一的watcher對象,也就是當前正在指令計算的訂閱者,它會在計算時賦值成一個watcher對象,計算完成后賦值為null。而depend是用于對該訂閱者添加依賴,告訴它你的值依賴于我,每次更新時應該來找我。另外還有notify()的函數,用于遍歷所有的依賴,通知他們更新數據。

        這里多看一下addDep()的源碼:

        /**
         * Add a dependency to this directive.
         */
        Watcher.prototype.addDep = function addDep (dep) {
         var id = dep.id;
         if (!this.newDepIds.has(id)) {
         this.newDepIds.add(id);
         this.newDeps.push(dep);
         if (!this.depIds.has(id)) {
         // 使用push()方法添加一個訂閱者
         dep.addSub(this);
         }
         }
        };

        可以看到它有去重的機制,當重復依賴時保證相同ID的依賴只有一個。訂閱者包含3個屬性newDepIds/newDeps/depIds分別存儲依賴信息,如果之前就有了這個依賴,那么反過來將該訂閱者加入到這個依賴關系中去。

        接著看get方法中的dependArray() :

        /**
         * Collect dependencies on array elements when the array is touched, since
         * we cannot intercept array element access like property getters.
         */
        function dependArray (value) {
         for (var e = (void 0), i = 0, l = value.length; i < l; i++) {
         e = value[i];
         e && e.__ob__ && e.__ob__.dep.depend();
         if (Array.isArray(e)) {
         dependArray(e);
         }
         }
        }

        可以看到我們不能像對象一樣監聽數組的變化,所以如果獲取一個數組的值,那么就需要將數組中所有的對象的觀察者列表都加入到依賴中去。

        這樣get方法讀取值就代理完成了,接下來我們看set方法代理賦值的實現,我們先獲取原始值,然后與新賦的值進行比較,也叫臟檢查,如果數據發生了改變,則對該數據進行重新建立觀察者,并通知所有的訂閱者更新。

        接下來我們看下數組的更新檢測是如何實現的:

        /*
         * not type checking this file because flow doesn't play well with
         * dynamically accessing methods on Array prototype
         */
        var arrayProto = Array.prototype;
        var arrayMethods = Object.create(arrayProto);
        ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
         // cache original method
         var original = arrayProto[method];
         def(arrayMethods, method, function mutator () {
         var arguments$1 = arguments;
         // avoid leaking arguments:
         // http://jsperf.com/closure-with-arguments
         var i = arguments.length;
         var args = new Array(i);
         while (i--) {
         args[i] = arguments$1[i];
         }
         var result = original.apply(this, args);
         var ob = this.__ob__;
         var inserted;
         switch (method) {
         case 'push':
         inserted = args;
         break
         case 'unshift':
         inserted = args;
         break
         case 'splice':
         inserted = args.slice(2);
         break
         }
         if (inserted) { ob.observeArray(inserted); }
         // notify change
         ob.dep.notify();
         return result
         });
        });

        看的出來我們模擬了一個數組對象,代理了push/pop/shift/unshift/splice/sort/reverse方法,用于檢測數組的變化,并通知所有訂閱者更新。如果有新建元素,會補充監聽新對象。

        這就是從代碼上解釋為什么Vue不支持數組下標修改和長度修改的原因,至于為什么這么設計,我后面會再次更新或再開篇文章,講一些通用的設計問題以及Js機制和缺陷。

        總結

        從上面的代碼中我們可以一步步由深到淺的看到Vue是如何設計出雙向數據綁定的,最主要的兩點:

      12. 使用getter/setter代理值的讀取和賦值,使得我們可以控制數據的流向。

      13. 使用觀察者模式設計,實現了指令和數據的依賴關系以及觸發更新。

      14. 對于數組,代理會修改原數組對象的方法,并觸發更新。

      15. 明白了這些原理,其實你也可以實現一個簡單的數據綁定,造一個小輪子,當然,Vue的強大之處不止于此,我們后面再來聊一聊它的組件和渲染,看它是怎么一步一步將我們從DOM對象的魔爪里拯救出來的。

        聲明:本網頁內容旨在傳播知識,若有侵權等問題請及時與本網聯系,我們將在第一時間刪除處理。TEL:177 7030 7066 E-MAIL:11247931@qq.com

        文檔

        Vue雙向數據綁定源碼分析

        Vue雙向數據綁定源碼分析:雖然工作中一直使用Vue作為基礎庫,但是對于其實現機理僅限于道聽途說,這樣對長期的技術發展很不利。所以最近攻讀了其源碼的一部分,先把雙向數據綁定這一塊的內容給整理一下,也算是一種學習的反芻。本篇文章的Vue源碼版本為v2.2.0開發版。Vue源碼的整體架
        推薦度:
        標簽: 數據 VUE 解析
        • 熱門焦點

        最新推薦

        猜你喜歡

        熱門推薦

        專題
        Top
        主站蜘蛛池模板: 在线观看人成网站深夜免费| 91短视频免费在线观看| 男女啪啪永久免费观看网站| 亚洲乱码在线观看| 午夜神器成在线人成在线人免费| 亚洲成AV人在线观看天堂无码| 中国内地毛片免费高清| 亚洲狠狠婷婷综合久久久久| 好紧我太爽了视频免费国产| 久久综合亚洲色HEZYO社区 | a在线视频免费观看| 国产成人麻豆亚洲综合无码精品| 免费无遮挡无遮羞在线看| 亚洲一区二区三区无码影院| 成人自慰女黄网站免费大全 | 无码日韩精品一区二区免费| 亚洲AV无码专区在线亚| 在线免费观看污网站| 一级看片免费视频囗交| 亚洲AV无码专区国产乱码电影| 国产成人免费ā片在线观看老同学| 亚洲AV永久无码精品水牛影视| sihu国产精品永久免费| 337p日本欧洲亚洲大胆色噜噜 | 中文字幕无线码免费人妻| 久久精品国产亚洲av麻豆| 四虎成年永久免费网站| 亚洲av最新在线观看网址| 亚洲色偷偷综合亚洲AV伊人| 在线a免费观看最新网站| 国产精品亚洲专区无码牛牛| 亚洲午夜久久久久久久久电影网| 99视频在线免费观看| 亚洲乱码一二三四五六区| 亚洲AV日韩精品一区二区三区| 免费人成视频在线播放| 亚洲精品无码不卡| 日本特黄特色免费大片| 暖暖免费在线中文日本| 亚洲av无码一区二区三区人妖 | 午夜无遮挡羞羞漫画免费|