須要的環境以下:vue
須要的知識儲備:typescript
該實踐的整體結構是以一個Watcher
實現類爲載體,模擬Vue
的方式,將須要進行響應的數據(data
)、渲染函數(render
)、掛載的dom
節點輸入進來。而後對傳參送進來的data
的屬性進行改變的時候,會觸發render
函數的調用(前提是這個修改的數據有在渲染函數中被使用到)。數組
Watcher
類的結構bash
class Watcher {
// 渲染函數數組,一個數據可能不止存在於一個渲染函數當中,可能會有多個渲染函數調用
renderList: Array<Function>;
// 數據
data: any;
// 掛載的el元素
el: String | HTMLElement;
}
複製代碼
上面是Watcher
類的結構,將數據、渲染函數、dom
元素傳進來後,就會進行自動進行監測。babel
代理工具實現dom
notifySet
,是一個Set
。這個集合存放着哪些屬性被觀察到,若是被觀察到的,對其進行setter
調用的時候,會觸發渲染函數進行渲染。flag
來實現是否深度觀察。代理思路函數
getter
和setter
進行改寫,在getter
的時候進行依賴的肯定(由於在render
函數使用到了,因此這個依賴應當被監控),在setter
的時候對渲染函數進行調用(當值改變的時候,須要對相應的渲染內容進行更新,這也就是本文章的目的)notifySet
進行添加屬性的時候,只須要將被觀察到的屬性放進這個set
中,無關的屬性則不放進去。本項目的實現思路是以下:
項目結構工具
-DataBind
--core
|- Proxy.ts // 代理工具
--utils
|- Utils.ts // 通用工具
Watcher.ts
複製代碼
Watcher
類的具體實現ui
構造器this
interface WatcherOption {
el: String | HTMLElement; // 綁定現有的dom對象
data: any; // 數據對象
render: Function; // 渲染函數
}
constructor(options: WatcherOptions) {
if (typeof options.el === 'string') {
this.el = document.getElementById(options.el);
} else {
// @ts-ignore
this.el = options.el;
}
this.data = makeProxy.call(this, options.data); // 先將整一個數據對象深度遍歷構建代理層
this.addRender(options.render); // 將渲染函數添加到渲染函數數組中
}
複製代碼
構造器傳進來配置(options
),配置有三個重要的屬性:掛載對象、數據對象、渲染函數。具體流程以下:
data
屬性渲染函數管理
/** * @description 爲渲染函數所調用到的對象查詢須要代理的對象 * @param fn */
public addRender(fn: Function): void {
Watcher.target = this; // 開啓代理模式,這個target對象是Watcher類的靜態變量,在proxy函數裏面會使用到
this.renderList.push(fn);
this.notify();
Watcher.target = null; // 關閉代理模式
}
複製代碼
Watcher.target
是Watcher
的靜態屬性,這個屬性的做用是記錄當前進行觀測的對象。這個對象會在代理的時候用到。使用這個緣由是:在添加依賴的時候,先當前的Watcher
設置爲Watcher.target
,而後調用渲染函數,渲染函數會調用響應的屬性的getter
,從而觸發代理層進行添加依賴(後面從新渲染的時候,是不會進行依賴的重複添加,由於Watcher.target
爲空。這個在makeProxy函數裏面能夠查看到。
因此這個函數是先將記錄當前的Watcher
實例,而後將渲染函數推動數組中,再進行調用渲染函數。此時會進行依賴的添加,而後將target
設爲空。
代理層的實現
/** * @description 這個是咱們本文章的核心代碼,由於我沒有設置watch、computed屬性,因此也就不須要筐來存放watcher。也就不會有Dep這個類 * @param object * @param this Wacther對象 */
export function makeProxy(this: Watcher, object: any): any {
object.__proxy__ = {};
// @ts-ignore
object.__proxy__.notifySet = new Set<string | number | symbol>();
object.__watcher__ = this;
// @ts-ignore
let proxy = new Proxy(object, {
get(target: any, p: string | number | symbol, receiver: any): any {
if (Watcher.target != null) {
Watcher.addDep(object, p); // 添加依賴
}
return target[p];
},
set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
if (target[p] !== value) {
// 兩個值不一樣的時候才須要去渲染視圖層
target[p] = value;
if (target.__proxy__.notifySet.has(p)) {
target.__watcher__.notify();
}
}
return true;
}
});
// 獲取對象的全部子屬性,而且對子屬性進行遞歸代理以便實現深度觀察
let propertyNames = Object.getOwnPropertyNames(object);
for (let i = 0; i < propertyNames.length; i++) {
// @ts-ignore
if (isPlainObject(object[propertyNames[i]]) && (!propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]);
}
}
return proxy;
}
複製代碼
此功能有兩個特別注意的點,第一個是對object屬性的添加、第二個是代理對象的細節。
object
屬性的添加:
__proxy__.notifySet
:這是存放set
實例的屬性,這個set
實例是進行記錄哪一個屬性被監聽到,若是該屬性被監聽,那麼會放到這個集合中,方便得知監聽哪一個屬性__watcher__
:這個是指向當前的wacher
實例對象。代理對象的生成:
new Proxy(object, {
get(target: any, p: string | number | symbol, receiver: any): any {
if (Watcher.target != null) {
Watcher.addDep(object, p); // 添加依賴
}
return target[p];
},
set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
if (target[p] !== value) {
// 兩個值不一樣的時候才須要去渲染視圖層
target[p] = value;
if (target.__proxy__.notifySet.has(p)) {
// 僅僅當notifySet擁有這個屬性的時候,才進行渲染函數的執行
target.__watcher__.notify();
}
}
return true;
}
});
複製代碼
getter
:要特別主要到一個判斷語句:if (Watcher.target != null) {
Watcher.addDep(object, p); // 添加依賴
}
複製代碼
還記得在添加渲染函數的時候,修改Watcher.target
嗎?這個條件不爲空的時候就是在添加渲染函數的時候,將對象的屬性添加進notifySet
中,方便調用該屬性的時候執行回調函數
setter
:這個已經代碼已經解釋得很清楚了,就是判斷這個屬性有沒有被渲染函數被添加至集合中,若是有的話,就進行調用渲染函數。Watcher
// @ts-ignore
import {makeProxy} from "./core/Proxy";
interface WatcherOption {
el: String | HTMLElement; // 綁定現有的dom對象
data: any; // 數據對象
render: Function; // 渲染函數
}
/** * @description 觀察者對象,因爲咱們目的是用代理模式來進行模擬vue數據響應系統,那麼就從簡設計這個類 */
export class Watcher {
// 全局使用到watcher實例,指向當前的watcher對象,方便proxy使用
public static target: any;
data: any = {};
el: HTMLElement;
renderList: Array<Function> = new Array<Function>();
constructor(options: WatcherOption) {
if (typeof options.el === 'string') {
this.el = document.getElementById(options.el);
} else {
// @ts-ignore
this.el = options.el;
}
this.data = makeProxy.call(this, options.data); // 先將整一個數據對象深度遍歷構建代理層
this.addRender(options.render); // 將渲染函數添加到渲染函數數組中
}
// 響應而且調用觀察者對象
notify(): void {
for (let item of this.renderList) {
item.call(this.data, this.createElement);
}
}
/** * @description 爲渲染函數所調用到的對象查詢須要代理的對象 * @param fn */
public addRender(fn: Function): void {
Watcher.target = this; // 進行添加依賴的時候,要肯定給哪一個
this.renderList.push(fn);
this.notify();
Watcher.target = null;
}
/** * @description 爲每一個數據對象添加代理層的須要觀察的觀察者列表 * @param object * @param property */
static addDep(object, property): void {
object.__proxy__.notifySet.add(property);
}
static removeDep(object, property): void {
object.__proxy___.notifySet.remove(property);
}
private createElement(innerHTML: string) {
_createElement(this.el, innerHTML);
}
}
const _createElement = (dom: HTMLElement, innerHtml: string) => {
dom.innerHTML = innerHtml;
};
複製代碼
Proxy
/** * 在對象上添加一個屬性 __proxy__ * 這個屬性表明着這個對象的代理層所存放的東西 */
import {isPlainObject} from "../utils/Utils";
import {Watcher} from "../Watcher";
/** * @description 這個是咱們本文章的核心代碼,由於我沒有設置watch、computed屬性,因此也就不須要筐來存放watcher。也就不會有Dep這個類 * @param object * @param this Wacther對象 */
export function makeProxy(this: Watcher, object: any): any {
object.__proxy__ = {};
// @ts-ignore
object.__proxy__.notifySet = new Set<string | number | symbol>();
object.__watcher__ = this;
// @ts-ignore
let proxy = new Proxy(object, {
get(target: any, p: string | number | symbol, receiver: any): any {
if (Watcher.target != null) {
Watcher.addDep(object, p); // 添加依賴
}
return target[p];
},
set(target: any, p: string | number | symbol, value: any, receiver: any): boolean {
if (target[p] !== value) {
// 兩個值不一樣的時候才須要去渲染視圖層
target[p] = value;
if (target.__proxy__.notifySet.has(p)) {
// 僅僅當notifySet擁有這個屬性的時候,才進行渲染函數的執行
target.__watcher__.notify();
}
}
return true;
}
});
// 獲取對象的全部子屬性,而且對子屬性進行遞歸代理以便實現深度觀察
let propertyNames = Object.getOwnPropertyNames(object);
for (let i = 0; i < propertyNames.length; i++) {
// @ts-ignore
if (isPlainObject(object[propertyNames[i]]) && (!propertyNames[i].startsWith('__') && !propertyNames[i].endsWith('__'))) {
object[propertyNames[i]] = makeProxy.call(this, object[propertyNames[i]]);
}
}
return proxy;
}
複製代碼
utils
const _toString = Object.prototype.toString
/** * @description 用於普通的函數,提取函數的內部代碼塊 * @param func */
export function getFunctionValue(func: Function): string {
let funcString: string = func.toLocaleString();
let start: number = 0;
for (let i = 0; i < funcString.length; i++) {
if (funcString[i] == '{') {
start = i + 1;
break;
}
}
return funcString.slice(start, funcString.length - 1);
}
export function isPlainObject (obj: any): boolean {
return _toString.call(obj) === '[object Object]'
}
複製代碼