1. 預期效果 當數據變動時,觸發自定義的回調函數。 2. 思路 對對象 object 的 setter 進行設置,使 setter 在賦值之後執行回調函數 callback()。 3.細節 3.1 設置 setter 和 getter JS提供了 [Object.defineProperty()] ...
1. 預期效果
當數據變動時,觸發自定義的回調函數。
2. 思路
對對象 object
的 setter
進行設置,使 setter
在賦值之後執行回調函數 callback()
。
3.細節
3.1 設置 setter 和 getter
JS提供了 [Object.defineProperty()](Object.defineProperty() - JavaScript | MDN (mozilla.org)) 這個API來定義對象屬性的設置,這些設置就包括了 getter
和 setter
。註意,在這些屬性中,如果一個描述符同時擁有 value
或 writable
和 get
或 set
鍵,則會產生一個異常。
Object.defineProperty(obj, "key", {
enumerable: false, // 是否可枚舉
configurable: false, // 是否可配置
writable: false, // 是否可寫
value: "static"
});
我們可以利用JS的 [閉包](閉包 - JavaScript | MDN (mozilla.org)),給 getter
和 setter
創造一個共同的環境,來保存和操作數據 value
和 callback
。同時,還可以在 setter
中檢測值的變化。
// task1.js
const defineReactive = function(data, key, value, cb) {
Object.defineProperty(data, key, {
enumerable: true,
configurable: true,
get() {
console.log('getter')
return value
},
set(newValue) {
if (newValue !== value) {
value = newValue
console.log('setter: value change')
cb(newValue)
}
}
});
}
const task = function() {
console.log('running task 1...')
const obj = {}
const callback = function(newVal) {
console.log('callback: new value is ' + newVal)
}
defineReactive(obj, 'a', 1, callback)
console.log(obj.a)
obj.a = 2
obj.a = 3
obj.a = 4
}
task()
至此我們監控了 value
,可以感知到它的變化並執行回調函數。
3.2 遞歸監聽對象的值
上面的 defineRective()
在 value
為對象的時候,當修改深層鍵值,則無法響應到。因此通過迴圈遞歸的方法來對每一個鍵值賦予響應式。這裡可以通過 observe()
和 Observer
類來實現這種遞歸:
// observe.js
import { Observer } from "./Observer.js"
// 為數據添加響應式特性
export default function(value) {
console.log('type of obj: ', typeof value)
if (typeof value !== 'object') {
// typeof 數組 = object
return
}
if (typeof value.__ob__ !== 'undefined') {
return value.__ob__
}
return new Observer(value)
}
// Observer.js
import { defineReactive } from './defineReactive.js'
import { def } from './util.js';
export class Observer {
constructor(obj) {
// 註意設置成不可枚舉,不然會在walk()中迴圈調用
def(obj, '__ob__', this, false)
this.walk(obj)
}
walk(obj) {
for (const key in obj) {
defineReactive(obj, key)
}
}
}
在這裡包裝了一個 def()
函數,用於配置對象屬性,把 __ob__
屬性設置成不可枚舉,因為 __ob__
類型指向自身,設置成不可枚舉可以放置遍歷對象時死迴圈
// util.js
export const def = function(obj, key, value, enumerable) {
Object.defineProperty(obj, key, {
value,
enumerable,
writable: true,
configurable: true
})
}
3.3 檢測數組
從需求出發,對於響應式,我們對數組和對象的要求不同,對於對象,我們一般要求檢測其成員的修改;對於數組,不僅要檢測元素的修改,還要檢測其增刪(比如網頁中的表格)
對由於數組沒有 key
,所以不能通過 defineReactive()
來設置響應式,同時為了滿足響應數組的增刪改,所以 Vue 的方法是,通過包裝 Array
的方法來實現響應式,當調用 push()
、poll()
、splice()
等方法時,會執行自己設置的響應式方法
使用 Object.create(obj)
方法可以 obj
對象為原型(prototype)創建一個對象,因此我們可以以數組原型 Array.prototype
為原型創建一個新的數組對象,在這個對象中響應式包裝原來的 push()
、pop()
、splice()
等數組
// array.js
import { def } from "./util.js"
export const arrayMethods = Object.create(Array.prototype)
const methodNameNeedChange = [
'pop',
'push',
'splice',
'shift',
'unshift',
'sort',
'reverse'
]
methodNameNeedChange.forEach(methodName => {
const original = Array.prototype[methodName]
def(arrayMethods, methodName, function() {
// 響應式處理
console.log('call ' + methodName)
const res = original.apply(this, arguments)
const args = [...arguments]
let inserted = []
const ob = this.__ob__
switch (methodName) {
case 'push':
case 'unshift':
inserted = args
case 'splice':
inserted = args.slice(2)
}
ob.observeArray(inserted)
return res
})
})
// Observer.js
import { arrayMethods } from './array.js'
import { defineReactive } from './defineReactive.js'
import observe from './observe.js'
import { def } from './util.js'
export class Observer {
constructor(obj) {
console.log('Observer', obj)
// 註意設置成不可枚舉,不然會在walk()中迴圈調用
def(obj, '__ob__', this, false)
if (Array.isArray(obj)) {
// 將數組方法設置為響應式
Object.setPrototypeOf(obj, arrayMethods)
this.observeArray(obj)
} else {
this.walk(obj)
}
}
// 遍歷對象成員並設置為響應式
walk(obj) {
for (const key in obj) {
defineReactive(obj, key)
}
}
// 遍曆數組成員並設置為響應式
observeArray(arr) {
for (let i = 0, l = arr.length; i < l; i++) {
observe(arr[i])
}
}
}
3.5 Watcher 和 Dep 類
設置多個觀察者檢測同一個數據
// Dep.js
var uid = 0
export default class Dep {
constructor() {
this.id = uid++
// console.log('construct Dep ' + this.id)
this.subs = []
}
addSub(sub) {
this.subs.push(sub)
}
depend() {
if (Dep.target) {
if (this.subs.some((sub) => { sub.id === Dep.target.id })) {
return
}
this.addSub(Dep.target)
}
}
notify() {
const s = this.subs.slice();
for (let i = 0, l = s.length; i < l; i++) {
s[i].update()
}
}
}
// Watcher.js
import Dep from "./Dep.js"
var uid = 0
export default class Watcher {
constructor(target, expression, callback) {
this.id = uid++
this.target = target
this.getter = parsePath(expression)
this.callback = callback
this.value = this.get()
}
get() {
Dep.target = this
const obj = this.target
let value
try {
value = this.getter(obj)
} finally {
Dep.target = null
}
return value
}
update() {
this.run()
}
run() {
this.getAndInvoke(this.callback)
}
getAndInvoke(cb) {
const obj = this.target
const newValue = this.get()
if (this.value !== newValue || typeof newValue === 'object') {
const oldValue = this.value
this.value = newValue
cb.call(obj, newValue, newValue, oldValue)
}
}
}
function parsePath(str) {
var segments = str.split('.');
return (obj) => {
for (let i = 0; i < segments.length; i++) {
if (!obj) return;
obj = obj[segments[i]]
}
return obj;
};
}
// task2.js
import observe from "../observe.js";
import Watcher from "../Watcher.js";
const task2 = function() {
const a = {
b: {
c: {
d: {
h: 1
}
}
},
e: {
f: 2
},
g: [ 1, 2, 3, { k: 1 }]
}
const ob_a = observe(a)
const w_a = new Watcher(a, 'b.c.d.h', (val) => {
console.log('1111111111')
})
a.b.c.d.h = 10
a.b.c.d.h = 10
console.log(a)
}
task2()
執行結果如下,可以看到成功響應了數據變化