您好,登錄后才能下訂單哦!
前言
雖然工作中一直使用Vue作為基礎(chǔ)庫(kù),但是對(duì)于其實(shí)現(xiàn)機(jī)理僅限于道聽(tīng)途說(shuō),這樣對(duì)長(zhǎng)期的技術(shù)發(fā)展很不利。所以最近攻讀了其源碼的一部分,先把雙向數(shù)據(jù)綁定這一塊的內(nèi)容給整理一下,也算是一種學(xué)習(xí)的反芻。
本篇文章的Vue源碼版本為v2.2.0開(kāi)發(fā)版。
Vue源碼的整體架構(gòu)無(wú)非是初始化Vue對(duì)象,掛載數(shù)據(jù)data/props等,在不同的時(shí)期觸發(fā)不同的事件鉤子,如created() / mounted() / update()等,后面專(zhuān)門(mén)整理各個(gè)模塊的文章。這里先講雙向數(shù)據(jù)綁定的部分,也是最主要的部分。
設(shè)計(jì)思想:觀(guān)察者模式
Vue的雙向數(shù)據(jù)綁定的設(shè)計(jì)思想為觀(guān)察者模式,為了方便,下文中將被觀(guān)察的對(duì)象稱(chēng)為觀(guān)察者,將觀(guān)察者對(duì)象觸發(fā)更新的稱(chēng)為訂閱者。主要涉及到的概念有:
1、Dep對(duì)象:Dependency依賴(lài)的簡(jiǎn)寫(xiě),包含有三個(gè)主要屬性id, subs, target和四個(gè)主要函數(shù)addSub, removeSub, depend, notify,是觀(guān)察者的依賴(lài)集合,負(fù)責(zé)在數(shù)據(jù)發(fā)生改變時(shí),使用notify()觸發(fā)保存在subs下的訂閱列表,依次更新數(shù)據(jù)和DOM。
2、Observer對(duì)象:即觀(guān)察者,包含兩個(gè)主要屬性value, dep。做法是使用getter/setter方法覆蓋默認(rèn)的取值和賦值操作,將對(duì)象封裝為響應(yīng)式對(duì)象,每一次調(diào)用時(shí)更新依賴(lài)列表,更新值時(shí)觸發(fā)訂閱者。綁定在對(duì)象的__ob__原型鏈屬性上。
源碼實(shí)戰(zhàn)解析
有過(guò)Vue開(kāi)發(fā)基礎(chǔ)的應(yīng)該都了解其怎么初始化一個(gè)Vue對(duì)象:
new Vue({ el: '#container', data: { count: 100 }, ... });
那么我們就從這個(gè)count說(shuō)起,看它是怎么完成雙向數(shù)據(jù)綁定的。
下面的代碼片段中英文注釋為尤雨溪所寫(xiě),中文注釋為我所寫(xiě),英文注釋更能代表開(kāi)發(fā)者的清晰思路。
首先從全局的初始化函數(shù)調(diào)用:initMixin(Vue$3);
,這里的Vue$3對(duì)象就是全局的Vue對(duì)象,在此之前已經(jīng)掛載了Vue的各種基本數(shù)據(jù)和函數(shù)。這個(gè)函數(shù)體就是初始化我們上面聲明Vue語(yǔ)句的過(guò)程化邏輯,取主體代碼來(lái)看:
// 這里的options就是上面聲明Vue對(duì)象的json對(duì)象 Vue.prototype._init = function (options) { ... var vm = this; ... initLifecycle(vm); initEvents(vm); initRender(vm); callHook(vm, 'beforeCreate'); // 這里就是我們接下來(lái)要跟進(jìn)的初始化Vue參數(shù) initState(vm); initInjections(vm); callHook(vm, 'created'); ... };
這里主要完成了初始化事件、渲染、參數(shù)、注入等過(guò)程,并不斷調(diào)用事件鉤子的回調(diào)函數(shù)。下面來(lái)到如何初始化參數(shù):
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); } }
這里依次檢測(cè)參數(shù)中包含的props/methods/data/computed/watch并進(jìn)入不同的函數(shù)進(jìn)行初始化,這里我們只關(guān)心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參數(shù)支持對(duì)象和回調(diào)函數(shù),但最終返回的一定是對(duì)象,否則使用空對(duì)象。接下來(lái)就是重頭戲了,我們?nèi)绾螌ata參數(shù)設(shè)置為響應(yīng)式的:
/** * 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不是單純的對(duì)象而是Regexp或者函數(shù)之類(lèi)的,或者是vm實(shí)例再或者是不可擴(kuò)展的 */ observerState.shouldConvert && !isServerRendering() && (Array.isArray(value) || isPlainObject(value)) && Object.isExtensible(value) && !value._isVue ) { ob = new Observer(value); } if (asRootData && ob) { ob.vmCount++; } return ob }
這里的英文注釋非常清晰,就是為了給該對(duì)象新建一個(gè)觀(guān)察者類(lèi),如果存在則返回已存在的(比如互相引用或依賴(lài)重復(fù)),可以看到這個(gè)觀(guān)察者列表放置在對(duì)象的__ob__屬性下。下面我們看下這個(gè)Observer觀(guān)察者類(lèi):
/** * 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函數(shù)是defineProperty的簡(jiǎn)單封裝 def(value, '__ob__', this); if (Array.isArray(value)) { // 在es5及更低版本的js里,無(wú)法完美繼承數(shù)組,這里檢測(cè)并選取合適的函數(shù) // protoAugment函數(shù)使用原型鏈繼承,copyAugment函數(shù)使用原型鏈定義(即對(duì)每個(gè)數(shù)組defineProperty) var augment = hasProto ? protoAugment : copyAugment; augment(value, arrayMethods, arrayKeys); this.observeArray(value); } else { this.walk(value); } };
在Observer類(lèi)的注釋里也清楚的說(shuō)明,它會(huì)被關(guān)聯(lián)到每一個(gè)被檢測(cè)的對(duì)象,使用getter/setter修改其默認(rèn)讀寫(xiě),用于收集依賴(lài)和發(fā)布更新。其中出現(xiàn)了三個(gè)我們需要關(guān)心的東西Dep類(lèi)/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]); } };
它不過(guò)是在Observer類(lèi)和observe方法中間的一層遞歸,因?yàn)槲覀冇^(guān)察的只能是對(duì)象,而不能是數(shù)字、字符串或者數(shù)組(數(shù)組的觀(guān)察比較特殊,事實(shí)上是重構(gòu)了方法來(lái)觸發(fā)更新,后面會(huì)講到)。那我們接下來(lái)看下Dep類(lèi)是做什么用的:
/** * A dep is an observable that can have multiple * directives subscribing to it. */ var Dep = function Dep () { this.id = uid$1++; this.subs = []; };
注釋里告訴我們Dep類(lèi)是一個(gè)會(huì)被多個(gè)指令訂閱的可被觀(guān)察的對(duì)象,這里的指令就是我們?cè)趆tml代碼里書(shū)寫(xiě)的東西,如:class={active: hasActive}
或{{ count }} {{ count * price }}
,而他們就會(huì)訂閱hasActive/count/price這些對(duì)象,而這些訂閱他們的對(duì)象就會(huì)被放置在Dep.subs列表中。每一次新建Dep對(duì)象,就會(huì)全局uid遞增,然后傳給該Dep對(duì)象,保證唯一性id。
我們接著看剛才的walk函數(shù)做了什么:
/** * 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]]); } };
看來(lái)和名字一樣,它只是走了一遍,那我們來(lá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代理了對(duì)象的默認(rèn)讀寫(xiě)。我們首先新建一個(gè)Dep對(duì)象,利用閉包準(zhǔn)備收集依賴(lài),然后我們使用observe觀(guān)察該對(duì)象,注意此時(shí)與上面相比少了一個(gè)asRootData = true
的參數(shù)。
我們先來(lái)看取值的代理get,這里用到了Dep.target
屬性和depend()
方法,我們來(lái)看看它是做什么的:
// 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(); } };
注釋看的出來(lái)Dep.target
是全局唯一的watcher對(duì)象,也就是當(dāng)前正在指令計(jì)算的訂閱者,它會(huì)在計(jì)算時(shí)賦值成一個(gè)watcher對(duì)象,計(jì)算完成后賦值為null。而depend是用于對(duì)該訂閱者添加依賴(lài),告訴它你的值依賴(lài)于我,每次更新時(shí)應(yīng)該來(lái)找我。另外還有notify()
的函數(shù),用于遍歷所有的依賴(lài),通知他們更新數(shù)據(jù)。
這里多看一下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()方法添加一個(gè)訂閱者 dep.addSub(this); } } };
可以看到它有去重的機(jī)制,當(dāng)重復(fù)依賴(lài)時(shí)保證相同ID的依賴(lài)只有一個(gè)。訂閱者包含3個(gè)屬性newDepIds/newDeps/depIds分別存儲(chǔ)依賴(lài)信息,如果之前就有了這個(gè)依賴(lài),那么反過(guò)來(lái)將該訂閱者加入到這個(gè)依賴(lài)關(guān)系中去。
接著看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); } } }
可以看到我們不能像對(duì)象一樣監(jiān)聽(tīng)數(shù)組的變化,所以如果獲取一個(gè)數(shù)組的值,那么就需要將數(shù)組中所有的對(duì)象的觀(guān)察者列表都加入到依賴(lài)中去。
這樣get方法讀取值就代理完成了,接下來(lái)我們看set方法代理賦值的實(shí)現(xiàn),我們先獲取原始值,然后與新賦的值進(jìn)行比較,也叫臟檢查,如果數(shù)據(jù)發(fā)生了改變,則對(duì)該數(shù)據(jù)進(jìn)行重新建立觀(guān)察者,并通知所有的訂閱者更新。
接下來(lái)我們看下數(shù)組的更新檢測(cè)是如何實(shí)現(xiàn)的:
/* * 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 }); });
看的出來(lái)我們模擬了一個(gè)數(shù)組對(duì)象,代理了push/pop/shift/unshift/splice/sort/reverse方法,用于檢測(cè)數(shù)組的變化,并通知所有訂閱者更新。如果有新建元素,會(huì)補(bǔ)充監(jiān)聽(tīng)新對(duì)象。
這就是從代碼上解釋為什么Vue不支持?jǐn)?shù)組下標(biāo)修改和長(zhǎng)度修改的原因,至于為什么這么設(shè)計(jì),我后面會(huì)再次更新或再開(kāi)篇文章,講一些通用的設(shè)計(jì)問(wèn)題以及Js機(jī)制和缺陷。
總結(jié)
從上面的代碼中我們可以一步步由深到淺的看到Vue是如何設(shè)計(jì)出雙向數(shù)據(jù)綁定的,最主要的兩點(diǎn):
明白了這些原理,其實(shí)你也可以實(shí)現(xiàn)一個(gè)簡(jiǎn)單的數(shù)據(jù)綁定,造一個(gè)小輪子,當(dāng)然,Vue的強(qiáng)大之處不止于此,我們后面再來(lái)聊一聊它的組件和渲染,看它是怎么一步一步將我們從DOM對(duì)象的魔爪里拯救出來(lái)的。
好了,以上就是這篇文章的全部?jī)?nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來(lái)一定的幫助,如果有疑問(wèn)大家可以留言交流,謝謝大家對(duì)億速云的支持。
參考資料
數(shù)據(jù)的響應(yīng)化:https://github.com/Ma63d/vue-...
Vue v2.2.0 源代碼文件
es6 Proxy: http://es6.ruanyifeng.com/#do...
免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀(guān)點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。