最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

通過源碼分析Vue的雙向數(shù)據(jù)綁定詳解

 更新時(shí)間:2017年09月24日 14:47:16   作者:趙帥強(qiáng)  
使用vue也好有一段時(shí)間了,雖然對(duì)其雙向綁定原理也有了解個(gè)大概,但也沒好好探究下其原理實(shí)現(xiàn),下面這篇文章通過源碼主要分析了Vue的雙向數(shù)據(jù)綁定,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考借鑒,下面來一起看看吧。

前言

雖然工作中一直使用Vue作為基礎(chǔ)庫,但是對(duì)于其實(shí)現(xiàn)機(jī)理僅限于道聽途說,這樣對(duì)長期的技術(shù)發(fā)展很不利。所以最近攻讀了其源碼的一部分,先把雙向數(shù)據(jù)綁定這一塊的內(nèi)容給整理一下,也算是一種學(xué)習(xí)的反芻。

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

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

設(shè)計(jì)思想:觀察者模式

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

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

  • id: 每個(gè)觀察者(依賴對(duì)象)的唯一標(biāo)識(shí)。
  • subs: 觀察者對(duì)象的訂閱者列表。
  • target: 全局唯一的訂閱者對(duì)象,因?yàn)橹荒芡瑫r(shí)計(jì)算和更新一個(gè)訂閱者的值。
  • addSub(): 使用`push()`方法添加一個(gè)訂閱者。
  • removeSub(): 使用`splice()`方法移除一個(gè)訂閱者。
  • depend(): 將自己添加到當(dāng)前訂閱者對(duì)象的依賴列表。
  • notify(): 在數(shù)據(jù)被更新時(shí),會(huì)遍歷subs對(duì)象,觸發(fā)每一個(gè)訂閱者的更新。

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

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

源碼實(shí)戰(zhàn)解析

有過Vue開發(fā)基礎(chǔ)的應(yīng)該都了解其怎么初始化一個(gè)Vue對(duì)象:

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

那么我們就從這個(gè)count說起,看它是怎么完成雙向數(shù)據(jù)綁定的。

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

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

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

這里主要完成了初始化事件、渲染、參數(shù)、注入等過程,并不斷調(diào)用事件鉤子的回調(diào)函數(shù)。下面來到如何初始化參數(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); }
}

這里依次檢測參數(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ì)象。接下來就是重頭戲了,我們?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ù)之類的,或者是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è)觀察者類,如果存在則返回已存在的(比如互相引用或依賴重復(fù)),可以看到這個(gè)觀察者列表放置在對(duì)象的__ob__屬性下。下面我們看下這個(gè)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函數(shù)是defineProperty的簡單封裝
 def(value, '__ob__', this);
 if (Array.isArray(value)) {
 // 在es5及更低版本的js里,無法完美繼承數(shù)組,這里檢測并選取合適的函數(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類的注釋里也清楚的說明,它會(huì)被關(guān)聯(lián)到每一個(gè)被檢測的對(duì)象,使用getter/setter修改其默認(rèn)讀寫,用于收集依賴和發(fā)布更新。其中出現(xiàn)了三個(gè)我們需要關(guān)心的東西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方法中間的一層遞歸,因?yàn)槲覀冇^察的只能是對(duì)象,而不能是數(shù)字、字符串或者數(shù)組(數(shù)組的觀察比較特殊,事實(shí)上是重構(gòu)了方法來觸發(fā)更新,后面會(huì)講到)。那我們接下來看下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類是一個(gè)會(huì)被多個(gè)指令訂閱的可被觀察的對(duì)象,這里的指令就是我們在html代碼里書寫的東西,如: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]]);
 }
};

看來和名字一樣,它只是走了一遍,那我們來看下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)讀寫。我們首先新建一個(gè)Dep對(duì)象,利用閉包準(zhǔn)備收集依賴,然后我們使用observe觀察該對(duì)象,注意此時(shí)與上面相比少了一個(gè)asRootData = true的參數(shù)。

我們先來看取值的代理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對(duì)象,也就是當(dāng)前正在指令計(jì)算的訂閱者,它會(huì)在計(jì)算時(shí)賦值成一個(gè)watcher對(duì)象,計(jì)算完成后賦值為null。而depend是用于對(duì)該訂閱者添加依賴,告訴它你的值依賴于我,每次更新時(shí)應(yīng)該來找我。另外還有notify()的函數(shù),用于遍歷所有的依賴,通知他們更新數(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ù)依賴時(shí)保證相同ID的依賴只有一個(gè)。訂閱者包含3個(gè)屬性newDepIds/newDeps/depIds分別存儲(chǔ)依賴信息,如果之前就有了這個(gè)依賴,那么反過來將該訂閱者加入到這個(gè)依賴關(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)聽數(shù)組的變化,所以如果獲取一個(gè)數(shù)組的值,那么就需要將數(shù)組中所有的對(duì)象的觀察者列表都加入到依賴中去。

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

接下來我們看下數(shù)組的更新檢測是如何實(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
 });
});

看的出來我們模擬了一個(gè)數(shù)組對(duì)象,代理了push/pop/shift/unshift/splice/sort/reverse方法,用于檢測數(shù)組的變化,并通知所有訂閱者更新。如果有新建元素,會(huì)補(bǔ)充監(jiān)聽新對(duì)象。

這就是從代碼上解釋為什么Vue不支持?jǐn)?shù)組下標(biāo)修改和長度修改的原因,至于為什么這么設(shè)計(jì),我后面會(huì)再次更新或再開篇文章,講一些通用的設(shè)計(jì)問題以及Js機(jī)制和缺陷。

總結(jié)

從上面的代碼中我們可以一步步由深到淺的看到Vue是如何設(shè)計(jì)出雙向數(shù)據(jù)綁定的,最主要的兩點(diǎn):

  • 使用getter/setter代理值的讀取和賦值,使得我們可以控制數(shù)據(jù)的流向。
  • 使用觀察者模式設(shè)計(jì),實(shí)現(xiàn)了指令和數(shù)據(jù)的依賴關(guān)系以及觸發(fā)更新。
  • 對(duì)于數(shù)組,代理會(huì)修改原數(shù)組對(duì)象的方法,并觸發(fā)更新。

明白了這些原理,其實(shí)你也可以實(shí)現(xiàn)一個(gè)簡單的數(shù)據(jù)綁定,造一個(gè)小輪子,當(dāng)然,Vue的強(qiáng)大之處不止于此,我們后面再來聊一聊它的組件和渲染,看它是怎么一步一步將我們從DOM對(duì)象的魔爪里拯救出來的。

好了,以上就是這篇文章的全部內(nèi)容了,希望本文的內(nèi)容對(duì)大家的學(xué)習(xí)或者工作能帶來一定的幫助,如果有疑問大家可以留言交流,謝謝大家對(duì)腳本之家的支持。

參考資料

數(shù)據(jù)的響應(yīng)化:https://github.com/Ma63d/vue-...
Vue v2.2.0 源代碼文件
es6 Proxy: http://es6.ruanyifeng.com/#do...

相關(guān)文章

  • vue模仿網(wǎng)易云音樂的單頁面應(yīng)用

    vue模仿網(wǎng)易云音樂的單頁面應(yīng)用

    這篇文章主要介紹了vue仿網(wǎng)易云音樂的單頁面應(yīng)用實(shí)例代碼,非常不錯(cuò),具有一定的參考借鑒價(jià)值,需要的朋友可以參考下
    2019-04-04
  • vue中拆分組件的實(shí)戰(zhàn)案例

    vue中拆分組件的實(shí)戰(zhàn)案例

    最近在學(xué)vue,試著寫個(gè)單頁應(yīng)用,在寫組件,拆分組件時(shí)遇到一些困惑,下面這篇文章主要給大家介紹了關(guān)于vue中拆分組件的相關(guān)資料,文中通過實(shí)例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-12-12
  • 詳解Vue中過度動(dòng)畫效果應(yīng)用

    詳解Vue中過度動(dòng)畫效果應(yīng)用

    這篇文章主要介紹了詳解Vue中過度動(dòng)畫效果應(yīng)用,小編覺得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過來看看吧
    2017-05-05
  • vue中前端代理跨域問題實(shí)例總結(jié)

    vue中前端代理跨域問題實(shí)例總結(jié)

    前后端分離進(jìn)行項(xiàng)目開發(fā),跨域問題不可避免,下面這篇文章主要給大家介紹了關(guān)于vue中前端代理跨域問題的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),需要的朋友可以參考下
    2022-04-04
  • vue-admin-box第一步npm?install時(shí)報(bào)錯(cuò)的處理

    vue-admin-box第一步npm?install時(shí)報(bào)錯(cuò)的處理

    這篇文章主要介紹了vue-admin-box第一步npm?install時(shí)報(bào)錯(cuò)的處理方案,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-10-10
  • vue中的文本空格占位符說明

    vue中的文本空格占位符說明

    這篇文章主要介紹了vue中的文本空格占位符說明,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2022-06-06
  • 在Vue開發(fā)過程中解決和預(yù)防內(nèi)存泄漏問題的方法詳解

    在Vue開發(fā)過程中解決和預(yù)防內(nèi)存泄漏問題的方法詳解

    Vue作為一款流行的前端框架,已經(jīng)在許多項(xiàng)目中得到廣泛應(yīng)用,然而,隨著我們在Vue中構(gòu)建更大規(guī)模的應(yīng)用程序,我們可能會(huì)遇到一個(gè)嚴(yán)重的問題,那就是內(nèi)存泄漏,因此,我們需要認(rèn)識(shí)到在Vue開發(fā)過程中,內(nèi)存泄漏問題的重要性,本文將給大家介紹如何解決和預(yù)防內(nèi)存泄漏問題
    2023-10-10
  • vue elementui異步給dom賦值無效問題

    vue elementui異步給dom賦值無效問題

    這篇文章主要介紹了vue elementui異步給dom賦值無效問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教
    2024-06-06
  • 基于vue中css預(yù)加載使用sass的配置方式詳解

    基于vue中css預(yù)加載使用sass的配置方式詳解

    下面小編就為大家分享一篇基于vue中css預(yù)加載使用sass的配置方式詳解,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。一起跟隨小編過來看看吧
    2018-03-03
  • vue實(shí)現(xiàn)web前端登錄頁數(shù)字驗(yàn)證碼

    vue實(shí)現(xiàn)web前端登錄頁數(shù)字驗(yàn)證碼

    這篇文章主要為大家詳細(xì)介紹了vue實(shí)現(xiàn)web前端登錄頁數(shù)字驗(yàn)證碼,文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2022-06-06

最新評(píng)論

谢通门县| 沙河市| 南康市| 榕江县| 宁德市| 南通市| 新化县| 临安市| 井陉县| 北碚区| 祁连县| 收藏| 淮阳县| 额济纳旗| 根河市| 临沂市| 德江县| 图木舒克市| 汝州市| 宁夏| 石屏县| 泽普县| 衡水市| 宜兰县| 涡阳县| 水富县| 斗六市| 普格县| 荆州市| 诸城市| 通州市| 马公市| 京山县| 黄骅市| 通许县| 阜新| 西宁市| 乌兰浩特市| 明溪县| 甘孜县| 玉屏|