是什麼
是指在資料狀态發生變化時,讓對應的重新DOM渲染。這裡我們首先了解對象(Object)變化時,怎麼通知到外界的。
(1)簡單來看,第一次渲染的時候資料傳遞就是這樣一個過程
![](https://img.laitimes.com/img/9ZDMuAjOiMmIsIjOiQnIsIyZuBnLiR2N1YmMyQTZkNmYzQDNhJ2M1QzY0MDOxgjZhZTMihzLc52YucWbp5GZzNmLn9Gbi1yZtl2Lc9CX6MHc0RHaiojIsJye.png)
這裡的外界,指的是要渲染的模闆,或者要暴露出去的watch監聽方法等。
(2)當資料發生變化時,要讓外界知道資料的變化。是以資料和外界的關系應當如下。
(3)然後就是怎麼通知的問題了,這就是我們這次要将的Object變化偵測,就是要監聽資料的變化,并且通知到外界。這個監聽器我們暫且定義為watcher。watcher監聽着資料的變化,并負責通知外界。
(4)接下來,就是研究怎麼去監聽資料的變化了。
Vue2.0中使用的監聽方法是,使用Object.defineProperty()給對象設定setter和getter方法,getter和setter方法監聽資料的變化。
簡單實作簡單監聽的Demo:
function defindReactive(data, key, val) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
console.log('get data')
return val;
},
set: function (newVal) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
}
});
}
let numData = {num: 1};
let key = 'num';
// 設定監聽
defindReactive(numData, key, 2);
numData.num = 3
console.log(numData.num)
// 輸出
set data
get data
3
Object.defineProperty(obj, prop, descriptor);
具體參考:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty
可以看出在設定好監聽後,我們再對對象設定或讀取時,都可以觸發自定義的操作。
(5)能監聽到資料變化後,就要在資料變化時通知到使用該資料的地方,如某個對象的name字段值被修改了,就需要通知頁面上展示字段name的地方修改值。這裡展示name的地方也可以稱為該資料的依賴。是以我們在監聽到資料變化後,要做的主要工作就有:1. 收集依賴。 2. 通知依賴變化。
為了減少耦合,我們單獨定義一個依賴收集器Dep。
我們首先定義一個依賴收集器:
// 依賴收集器
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)
}
}
// 通知
notify() {
// 複制數組
const subs = this.subs.slice()
for (let i = 0; i < subs.length; i++) {
subs[i].update()
}
}
}
function remove(arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if(index > -1) {
arr.splice(index, 1)
}
}
}
然後在資料getter時收集依賴,在setter時觸發依賴:
// 資料監聽和依賴收集器結合
function defindReactive(data, key, val) {
let dep = new Dep();
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function () {
// 收集依賴
dep.depend()
return val;
},
set: function (newVal) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
// 通知依賴發生變化
dep.notify();
}
});
}
(6)對于Object,我們希望把對象所有的屬性以及後代屬性都監聽到,是以我們還需要定義一個類,把資料内所有的屬性的都轉換成getter和setter形式。我們把它定義為Observer。
class Observer {
constructor(value) {
this.value = value
if (!Array.isArray(value)) {
this.walk(value);
}
}
walk(obj) {
for (let key in obj) {
defineReactive(obj, key, obj[key]);
console.log('value', obj[key]);
console.log('key', key);
}
}
}
簡單測試一下:
let user = {
name: 'hello',
age: 223
}
let ob = new Observer(user)
// 列印輸出
value hello
key name
value 223
key age
再改造一下defineReactive:
// 資料監聽和依賴收集器結合
function defindReactive(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) {
console.log('set data')
if (val === newVal) {
return
}
val = newVal
// 通知依賴發生變化
dep.notify();
}
});
}
(7)最後實作Watcher的工作:
const bailRE = /[^\w.$]/
// 比對path是否滿足 a.b.c這樣的格式。傳回一個函數
export default function parsePath(path) {
if (bailRE.test(path)) {
return
}
const segments = path.split('.')
return function(obj) {
for (let i = 0; i < segments.length; i++) {
if (!obj) return
obj = obj[segments[i]]
}
}
}
class watcher {
constructor(vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
get() {
window.target = this
let value = this.getter.call(this.vm, this.vm);
window.target = undefined
return value
}
set() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
代碼中的window.target用來表示目前依賴。
Object偵測的簡單邏輯圖