亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb

首頁 > 編程 > JavaScript > 正文

通過源碼分析Vue的雙向數據綁定詳解

2019-11-19 15:19:08
字體:
來源:轉載
供稿:網友

前言

雖然工作中一直使用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。

  • id: 每個觀察者(依賴對象)的唯一標識。
  • subs: 觀察者對象的訂閱者列表。
  • target: 全局唯一的訂閱者對象,因為只能同時計算和更新一個訂閱者的值。
  • addSub(): 使用`push()`方法添加一個訂閱者。
  • removeSub(): 使用`splice()`方法移除一個訂閱者。
  • depend(): 將自己添加到當前訂閱者對象的依賴列表。
  • notify(): 在數據被更新時,會遍歷subs對象,觸發每一個訂閱者的更新。

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

  • value: 原始值。
  • dep: 依賴列表。

源碼實戰解析

有過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是如何設計出雙向數據綁定的,最主要的兩點:

  • 使用getter/setter代理值的讀取和賦值,使得我們可以控制數據的流向。
  • 使用觀察者模式設計,實現了指令和數據的依賴關系以及觸發更新。
  • 對于數組,代理會修改原數組對象的方法,并觸發更新。

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

好了,以上就是這篇文章的全部內容了,希望本文的內容對大家的學習或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對武林網的支持。

參考資料

數據的響應化:https://github.com/Ma63d/vue-...
Vue v2.2.0 源代碼文件
es6 Proxy: http://es6.ruanyifeng.com/#do...

發表評論 共有條評論
用戶名: 密碼:
驗證碼: 匿名發表
亚洲香蕉成人av网站在线观看_欧美精品成人91久久久久久久_久久久久久久久久久亚洲_热久久视久久精品18亚洲精品_国产精自产拍久久久久久_亚洲色图国产精品_91精品国产网站_中文字幕欧美日韩精品_国产精品久久久久久亚洲调教_国产精品久久一区_性夜试看影院91社区_97在线观看视频国产_68精品久久久久久欧美_欧美精品在线观看_国产精品一区二区久久精品_欧美老女人bb
国产v综合ⅴ日韩v欧美大片| 久久天天躁狠狠躁老女人| 久久久精品国产一区二区| 伊人伊成久久人综合网站| 91免费精品国偷自产在线| 黄色一区二区在线观看| 国产日韩欧美黄色| 91tv亚洲精品香蕉国产一区7ujn| 亚洲精品网站在线播放gif| 国产偷国产偷亚洲清高网站| 欧美一级淫片aaaaaaa视频| 欧美成人精品h版在线观看| 高清在线视频日韩欧美| 国产精品黄色影片导航在线观看| 国产精品激情av在线播放| 国产成人精品电影| 久久久久久久久久久国产| 欧美又大又粗又长| 欧美激情在线有限公司| 中文字幕亚洲一区二区三区五十路| 日韩精品视频观看| 91精品在线观| 亚洲区中文字幕| 九九综合九九综合| 亚洲视频电影图片偷拍一区| 国产美女久久精品| 亚洲精品www久久久久久广东| 亚洲伊人成综合成人网| 亚洲男女性事视频| 91精品国产色综合久久不卡98口| 欧美—级a级欧美特级ar全黄| 国产视频精品久久久| www.xxxx精品| 91亚洲国产精品| xxxx欧美18另类的高清| 久热在线中文字幕色999舞| 欧美在线一区二区三区四| 亚洲欧美日韩国产中文专区| 欧美精品性视频| 91中文在线观看| 国产精品扒开腿做爽爽爽视频| 97香蕉超级碰碰久久免费软件| 国产精品都在这里| 97视频在线观看播放| 国产精品亚洲美女av网站| 美日韩精品免费观看视频| 成人激情视频在线播放| 欧美性极品xxxx做受| 亚洲精品自拍第一页| 亚洲美女在线看| 96国产粉嫩美女| 久久99亚洲精品| 国产日韩精品在线播放| 欧美日韩中文字幕| 国产91ⅴ在线精品免费观看| 北条麻妃99精品青青久久| 欧美—级高清免费播放| 亚洲福利视频二区| 欧美亚洲成人网| 久久精品视频在线观看| 91久久精品一区| 亚洲欧美精品伊人久久| 中文字幕av一区二区| 久久精品一区中文字幕| 亚洲资源在线看| 精品视频偷偷看在线观看| 亚洲电影天堂av| 日韩成人中文电影| 久久精品成人欧美大片古装| 日韩美女视频在线观看| 精品网站999www| 久久视频免费在线播放| 国产偷国产偷亚洲清高网站| 欧美精品一区二区免费| 欧美日韩亚洲精品一区二区三区| 午夜精品美女自拍福到在线| 国产精品91久久| 97精品伊人久久久大香线蕉| 国产精品网站入口| 国精产品一区一区三区有限在线| 亚洲性av网站| 国产精品成人国产乱一区| 久久精品99久久久香蕉| 国产精品久久久久不卡| 国产在线精品一区免费香蕉| 91香蕉国产在线观看| 中文字幕亚洲欧美日韩高清| 欧美亚洲视频一区二区| 欧美成人免费在线观看| 亚洲变态欧美另类捆绑| 日韩成人激情影院| 久久久精品一区二区三区| 韩日欧美一区二区| 亚洲国产精彩中文乱码av在线播放| 欧美一级高清免费| 精品国产91久久久久久老师| xxxx欧美18另类的高清| 国产精品视频免费在线观看| 日本在线精品视频| 日本精品免费观看| 亚洲精品xxxx| 亚洲国产精彩中文乱码av| 国产日韩欧美一二三区| 日韩一二三在线视频播| 亚洲直播在线一区| 国产日韩中文字幕在线| 欧美猛男性生活免费| 亚洲成**性毛茸茸| 777午夜精品福利在线观看| 亚洲性生活视频在线观看| 欧美怡红院视频一区二区三区| 欧美激情欧美狂野欧美精品| 国产欧美日韩中文字幕| 国模精品视频一区二区| 人妖精品videosex性欧美| 国产精品视频久久| 亚洲精品在线观看www| 欧美高清视频在线观看| 九色成人免费视频| 成人免费看黄网站| 欧美性极品少妇精品网站| 欧美激情2020午夜免费观看| 国产精品福利久久久| 亚洲自拍偷拍网址| 国产精品久久久久久久天堂| 国产精品黄色av| 最近中文字幕2019免费| 日韩av在线一区二区| 久久视频免费在线播放| 欧美成人精品一区| 欧美极品欧美精品欧美视频| 一区二区三区 在线观看视| 亚洲国产成人在线视频| 韩剧1988在线观看免费完整版| 高跟丝袜欧美一区| 色多多国产成人永久免费网站| 欧美一区二区三区精品电影| 欧美性一区二区三区| 91沈先生作品| 国产日韩精品在线播放| 亚洲片在线资源| 日韩色av导航| 国产丝袜一区视频在线观看| 国产亚洲视频在线| 亚洲va国产va天堂va久久| 亚洲精品久久久久国产| 2019最新中文字幕| 欧美成人免费大片| 91视频九色网站| 欧美一区二区三区四区在线| 超碰精品一区二区三区乱码| 欧美激情亚洲国产| 欧美成人性色生活仑片| 亚洲精品一区中文| 国产一区二区色| 全色精品综合影院| 国产激情久久久久| 国产专区精品视频| 在线成人免费网站| 成人黄色免费在线观看| 国产精品999999| 国产日韩欧美91| 91免费视频国产| 国产精品精品国产|