相信每位前端人都被問過Vue
雙向數據綁定的原理是什麼吧?應該也很快能答出來是經過Object.defineProperty讓數據的每一個屬性變成getter/setter
實現的,但這僅僅只回答了一半,由於Object
和Array
的實現方式是不同的,這也是爲何標題是Object
篇的緣由。(建議先看總結,再一步步看實現過程)javascript
首先了解一下下面的概念:前端
這個概念就通俗點說了,想詳細瞭解的可自行查閱資料java
// 給定一個數組 arr = [1, 2, 3], 想要一個新的數組每一項都加一
const arr = [1, 2, 3];
// 命令式 告訴瀏覽器循環數組,每個元素+1,而後push進新數組
let newArr1 = [];
for (let i = 0; i < arr.length; i++) {
newArr1.push(arr[i]+1);
}
console.log(newArr1) // 拿到新數組
// 聲明式 告訴瀏覽器新數組的每一項是舊數組對應的每一項加一
let result = arr.map(item => {
return item + 1;
})
console.log(result) // 新的數組
複製代碼
爲何要說這個呢?由於Vue.js是聲明式的,按API文檔的要求來寫Vue就知道要作什麼。 (回頭看好像偏題了,無論了就當鞏固知識吧😂)編程
在一個對象上定義一個新屬性,或者修改一個對象的現有屬性, 並返回這個對象, Vue.js是利用這個方法修改data
對象的屬性。數組
let name = 'test';
let obj = {};
Object.defineProperty(obj, 'name', {
configurable: true, // 可修改,可刪除
enumerable: true, //可枚舉
get: function() { // 讀值觸發
console.log('讀取數據');
return name;
},
set: function(newVal) { // 賦值觸發
if(name === newVal){
return;
}
console.log('從新賦值');
name = newVal;
}
})
console.log(obj.name);
obj.name = '賦值';
//打印出
// 讀取數據
// test
// 從新賦值
// 賦值
複製代碼
到這裏已經算是Vue的Object
雙向數據綁定原理了。瀏覽器
實現完整的Object
對象的雙向數據綁定,Vue作了那些操做呢?函數
經過上面的概念介紹就知道Object.defineProperty
是作數據監控的,獲取值的時候get
被觸發進行相應操做,設置數據時,set
被觸發這時就能知道數據是否被改變。那咱們是否是就很清楚知道能夠在數據被調用觸發get
函數的時候,去收集那些地方使用了對應的數據了呢?而後在設置的時候,觸發set
函數去通知get
收集好的依賴進行相應的操做呢?好了,下面就針對目前這個理解,對Object.defineProperty
進行封裝工具
function defineReactive(data, key, val) {
//let dep = [];
let dep = new Dep() // 修改
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
// 收集依賴
// dep.push(window.target) // window.target後面會定義,很6的操做,期待一下
dep.depend() // 修改
return val
},
set: function(newVal){
if(val === newVal){
return
}
// 觸發依賴
// for(let i=0; i<dep.length; i++){
// dep[i](newVal, val);
// }
dep.notify() // 修改
val = newVal
}
})
}
複製代碼
這裏就實現了在get
的時候,收集依賴保存到dep
這個數組中,當觸發set
的時候,就把dep
中的每一個依賴觸發。在源碼裏是把dep
封裝成一個類,來管理依賴的,下面就實現一下Dep
這個類吧。ui
export default class Dep {
constructor() {
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
removeSub(sub) {
remove(this.subs, sub)
}
depend() {
if(window.target){
this.addSub(window.target) // window.target是什麼?
}
}
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() // window.target的update方法
}
}
}
function remove (arr, item) {
if(arr.length){
const index = arr.indexOf(item)
if(index > -1){
return arr.splice(index, 1)
}
}
}
複製代碼
這樣咱們封裝的Dep
類就能夠收集依賴、刪除依賴、通知依賴,那咱們就要把這個Dep
類用上,對上面的defineReactive
進行修改一下。Dep收集到的依賴看代碼都知道是window.target
,當數據發生變化的時候,調用window.target
的update
方法進行響應更新。this
源碼裏有個Watcher
類,它的實例就是咱們收集的window.target
,下面先來看看Vue中的一個用法
vm.$watch('user.name', function(newVal, oldVal){
console.log('個人新名叫' + newVal); // 就是update函數
})
複製代碼
當Vue實例中的data.user.name
被修改時,會觸發function
的執行,也就是說須要把這個函數添加到data.user.name
的依賴中,怎麼收集呢?是否是調一下data.user.name
的get
方法就能夠了。那麼Watcher
要作的就是把本身的實例添加到對應屬性的Dep
中,同時也有通知去更新的能力,下面寫下Watcher
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn);
this.cb = cb;
this.value = this.get() // 獲取初始值
}
get() {
window.target = this // 把當前實例暴露給Dep,Dep就知道依賴是誰了
let value = this.getter.call(this.vm, this.vm) // 取一下值,觸發vm實例上對應屬性的get方法收集依賴
window.target = undefined // 用完給別人用
return value
}
update() {
const oldValue = this.value // 舊值
this.value = this.get() // 獲取新值
this.cb.call(this.vm, this.value, oldValue)
}
}
複製代碼
到這裏再回顧一下上面寫好的幾個程序,你會發現之間都是很巧妙的結合了,特別是Watcher
的實例,把本身給添加到Dep
中了,反正我本身是以爲這操做特6。這裏也說明了Vue中de$watch
是經過Watcher
實現的。
固然parsePath
還沒說是什麼,結合上面的例子和Watcher
應該知道parsePath
返回的是一個方法,而且被調用後返回一個值,也就是獲取值的功能,下面來實現一下
const bailRE = /[^\w.$]/
export function parsePath (path) {
if (bailRE.test(path)) {
return
}
const segments = path.spilt('.')
return function(obj){
for(let i = 0; i < segments.length; i++){
if(!obj) return
obj = obj[segments[i]]
}
return obj
}
}
複製代碼
Watcher中的this.getter.call(this.vm, this.vm)
把parsePath
的返回的函數指向this.vm
,並把this.vm
當參數傳過去取值。
Vue中data
的每個屬性都會被監測到,實際上咱們使用defineReactive
就能夠監測,若是一個data
有不少屬性,那是否是要調用不少次呢,那麼就有了Observer
這個工具類把每一個屬性變成getter/setter
,來碼上
export class Observer {
constructor(value) {
this.value = value
if(!Array.isArray(value)){
this.walk(value)
}
}
walk(obj) {
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
}
複製代碼
那麼new Observer(obj)
就能把obj
下的屬性都變成getter/setter
了,若是obj[key]
依然是一個對象呢?是否是要繼續new Observer(obj[key])
呀,那麼就是defineReactive
拿到obj[key]
時,須要進行判斷是否是對象,是的話就進行遞歸,那麼加上這一步操做
function defineReactive(data, key, val) {
if(typeof val === 'object') {
new Observer(val)
}
let dep = new Dep()
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend()
return val
},
set: function(newVal){
if(val === newVal){
return
}
dep.notify()
val = newVal
}
})
}
複製代碼
到這裏Vue中Object
是數據響應已經完成了,可是有缺陷你們都很清楚,就是給data
新增屬性或者刪除屬性時,沒法監測,上面的實現過程都是依賴現有屬性進行的,可是Vue提供$set
和$delete
去實現這兩個功能,相信弄懂上面的代碼,這兩個的實現就不難了。
對Vue中Object
的數據響應,我總結的一句話就是「定義getter/setter
備用,「用」:收集依賴,「變」:觸發依賴
Observer
和defineReactive
把屬性變成getter/setter
;Watcher
在getter
中把依賴收集到dep
;setter
告訴dep
數據變化了,dep
通知Watcher
去更新;