又到了學(xué)習(xí)源碼的時間??。
我們都知道vue是一個mvvm框架击敌,數(shù)據(jù)與視圖雙向綁定,所有入門vue的同學(xué)拴事,實現(xiàn)的第一個demo應(yīng)該都是??
<div id="app">
<span>{{ msg }}</span>
</div>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
msg: 'hello, world!'
}
}) // 此時瀏覽器打印 hello,world!
vm.msg = 'change msg' // 添加這句之后沃斤, 瀏覽器打印'change msg'
</script>
這種數(shù)據(jù)響應(yīng)式綁定是如何實現(xiàn)的呢? 稍有經(jīng)驗的同學(xué)知道刃宵,是發(fā)布訂閱
的設(shè)計模式實現(xiàn)的衡瓶,還知道實現(xiàn)這個模式的關(guān)鍵代碼是Object.defineProperty()
。再往下問牲证,可能有人就不知道了哮针。 不知道咱們就學(xué),學(xué)無止境好伐~??从隆。開始正文 ??
準(zhǔn)備工作
- 了解vue響應(yīng)式原理
- 了解發(fā)布訂閱設(shè)計模式
核心代碼
- Observer類诚撵, 代碼注釋中的序號我會一一解釋,如果解釋不對键闺,請指正!??
/**
* Observer class that is attached to each observed
* object. Once attached, the observer converts the target
* object's property keys into getter/setters that
* collect dependencies and dispatch updates.
*/
var Observer = function Observer (value) {
this.value = value;
this.dep = new Dep(); // 1. 依賴對象
this.vmCount = 0;
def(value, '__ob__', this); // 2.def方法
if (Array.isArray(value)) {
if (hasProto) { // 3. hasProto變量
protoAugment(value, arrayMethods); // 4.arrayMethods變量澈驼,5. protoAugment方法
} else {
copyAugment(value, arrayMethods, arrayKeys); //6.copyAugment方法
}
this.observeArray(value); // 7.observeArray方法
} else {
this.walk(value); // 8.walk方法
}
};
首先我們看一下代碼開始的官方解釋辛燥,我理解的意思是:
當(dāng)目標(biāo)對象被追蹤,觀察者這個方法會將目標(biāo)對象的屬性key轉(zhuǎn)換為getter和setter方法缝其,用來收集依賴和捕獲更新
一個截圖你就明白了~
我在
data
中隨便寫了一個對象挎塌,打印出來之后可以看到,這個對象中多了get xxx
set xxx
這種方法内边。這些方法就是觀察者
給目標(biāo)對象添加的
- 依次解釋注釋部分
- Dep() 依賴類
var uid = 0;
/**
* A dep is an observable that can have multiple
* directives subscribing to it.
*/
var Dep = function Dep () {
this.id = uid++;
this.subs = [];
};
官方解釋我覺得非沉穸迹晦澀,我理解的意思就是 dep對象上有多個方法漠其。這個對象的作用是為了去檢測數(shù)組的變化嘴高,因為Array類型的變量沒有g(shù)etter setter方法,只能通過
__ob__
屬性中的dep來收集依賴捕獲更新和屎。
- def方法
/**
* Define a property.
*/
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
});
}
對原生
Object.defineProperty
進(jìn)行了封裝
hasProto
var hasProto = '__proto__' in {};
判斷一個空對象中是否有
__proto__
屬性拴驮。這段代碼我最初以為是判斷當(dāng)前環(huán)境,但是我嘗試了在node環(huán)境下打印空對象柴信,發(fā)現(xiàn)里面也有__proto__
屬性套啤。在網(wǎng)上查了也沒有結(jié)果,如果你知道的話随常,請麻煩在評論區(qū)幫我解惑潜沦!
arrayMethods
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
arrayMethods
是Array
的子類萄涯。這樣做是為了獲取Array
的方法,比如push
,slice
等等所有方法唆鸡。
protoAugment()
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src) {
/* eslint-disable no-proto */
target.__proto__ = src;
/* eslint-enable no-proto */
}
將
arrayMethods
賦值給目標(biāo)對象窃判。__proto__
這個屬性是所有對象都有的,是瀏覽器實現(xiàn)的喇闸,方便我們查看原型鏈袄琳,MDN上建議這個屬性作為可讀屬性,最好不要直接使用燃乍。
copyAugment
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
function copyAugment (target, src, keys) {
for (var i = 0, l = keys.length; i < l; i++) {
var key = keys[i];
def(target, key, src[key]);
}
}
這段代碼是把
arrayMethods
中的方法依次添加到目標(biāo)對象中
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]);
}
};
如果目標(biāo)是數(shù)組對象唆樊,遍歷這個數(shù)組,給每個對象注冊觀察者對象(也就是watcher)刻蟹。
walk()
/**
* Walk through all properties 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]);
}
};
如果目標(biāo)是純對象逗旁, 就給其中的每個屬性添加getter/setter方法
defineReactive$$1()
/**
* Define a reactive property on an Object.
*/
function defineReactive$$1 (
obj,
key,
val,
customSetter,
shallow
) {
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;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
var childOb = !shallow && 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;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) { return }
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify(); // 通知watcher改變, 響應(yīng)式原理
}
});
}
走到了這一步舆瘪,才是真正實現(xiàn)了響應(yīng)式片效。核心是
dep.notify()
整體解析
有一部分代碼我沒有貼出來,如果感興趣可以去vue源碼中查看英古。
我理解的vue響應(yīng)式的思路大致是
- 首先給目標(biāo)加上
__ob__
屬性淀衣,其值是目標(biāo)本身的值以及dep依賴對象和vmcount - 判斷目標(biāo)是否為數(shù)組,因為數(shù)組變化是無法檢測到的召调,所以特例一個情況膨桥。
- 如果目標(biāo)是數(shù)組的話,先把數(shù)組中會改變原數(shù)組的方法取出來唠叛,賦給目標(biāo)只嚣,如果目標(biāo)觸發(fā)了這些方法,說明原數(shù)組改變了艺沼,這樣能側(cè)面反應(yīng)出數(shù)據(jù)是否改變册舞。源碼中不僅僅是如此,還給數(shù)組中的每個值注冊了watcher障般,如果這些值改變了调鲸,也會通知watcher
- 如果目標(biāo)是對象,給目標(biāo)綁定getter/setter剩拢。對象的值改變會觸發(fā)
notify()
线得,通知watcher
改變,引起視圖改變
總結(jié)
目前寫下這篇博客徐伐,僅僅是我在閱讀源碼之后寫的贯钩,其中肯定有很多理解不正確的地方,后續(xù)在網(wǎng)上學(xué)習(xí)之后,我會改正角雷。其實我覺得vue實現(xiàn)響應(yīng)式最關(guān)鍵的是Dep對象祸穷, 其中的notify()
通知watcher
,才實現(xiàn)了響應(yīng)式勺三,但是由于我對Dep對象的理解不深雷滚,所以暫時沒有寫下相關(guān)的代碼,繼續(xù)學(xué)習(xí)吗坚,等我深刻理解之后再回來補(bǔ)充??
路漫漫其修遠(yuǎn)兮祈远,吾將上下而求索。共勉商源。