簡單手寫實現Vue2.x

github: github.com/OUDUIDUI/vu…javascript

Vue的設計思想

Vue設計思想參考了MVVM模型,即將視圖View和行爲Model抽象化,即將視圖UI和業務邏輯分開來,而後經過ViewModel層來實現雙向數據綁定。html

MVVMMVC 最大的不一樣就是MVVM實現了 ViewModel 的自動同步,也就是當Model 的屬性改變時,咱們不用再本身手動操做 Dom 元素,來改變 View 的顯示,而是改變屬性後該屬性對應 View 層顯示會自動改變。vue

MVVM框架的三個要素:數據響應式、模板引擎及其渲染java

  • 數據響應式
    • 監聽數據變化並在視圖中更新
    • Vue2.x中,是根據Object.defineProperty()來實現數據響應式的
  • 模板引擎
    • 提供描述視圖的模板語法
    • Vue的插槽{{}}和指令v-bindv-onv-model
  • 渲染
    • 將模板渲染成HTML進行顯示

數據響應式原理

JavaScript的對象Object中有一個屬性叫訪問器屬性,其中有[[Get]][[Set]]特性,它們分別是獲取函數或設置函數,即在獲取對象特定屬性的時候回調用到。node

而訪問器屬性是不能直接定義的,必須使用Object.defineProperty()進行定義。react

const obj = {
  	_name: 'Matt'
};
Object.defineProperty(obj, 'name', {
  	get() {
      	return this._name;
    },
  	set(newVal) {
      	console.log('set name')
       	this._name = newVal;
    }
})

console.log(obj.name);   // 'Matt'
obj.name = 'OUDUIDUI';   // 'set name'
console.log(obj.name);   // 'Henry'
複製代碼

Vue2.x就是在set函數中進行監聽,當數據發生變化了,就會進行響應操做。git

所以,咱們能夠簡單實現一個Vue中的defineReactive函數。github

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>reactive app</title>
</head>
<body>
<div id="app"></div>
<script> /** * defineReactive : 將對象中某一個屬性設置爲響應式數據 * @param obj<Object>: 對象 * @param key<any>: key名 * @param val<any>: 初始值 */ function defineReactive(obj, key, val) { Object.defineProperty(obj, key, { get() { console.log(`get ${key}`) return val; // 此時val存在obj的閉包裏面 }, set(newVal) { console.log(`set ${key}`) if (newVal !== val) { val = newVal; update(); // 更新函數 } } }) } /** * update : 更新函數,從新渲染app DOM */ function update() { const app = document.getElementById('app'); app.innerHTML = `obj.time = ${obj.time}` } const obj = {}; defineReactive(obj, 'time', new Date().toLacaleTimeString()); // 將obj進行響應式處理 setInterval(() => obj.time = new Date().toLacaleTimeString(), 1000); // 定時更新obj.time的值 </script>
複製代碼

在代碼中,咱們在set中,調用了update更新函數,所以咱們定時器每更新obj.time一次,update函數就會被調用一次,所以頁面數據也會更新一次。這時候,咱們就簡單的實現了數據響應式。web

defineReactive函數有個問題,就是一次只能對一個屬性值進行響應式處理,並且若是這個屬性是個對象的話,咱們更改對象裏面的值的時候,是實現不了響應式的。數組

const obj = {};
defineReactive(obj, 'info', {name: 'OUDUIDUI', age: 18});  // 將obj進行響應式處理
setTimeout(() => obj.info.age++, 1000);  // 這時候不會觸發set函數
複製代碼

demo1.gif

所以,咱們須要一個新的方法去實現對整個對象進行響應式處理,在Vue中這個方法叫observe

在這個函數中,咱們先須要對傳入的obj進行類型判斷,而後對對象進行遍歷,對每個屬性進行響應式處理。這個地方須要對數組作處理,這個放到後面再說。

/** * observe: 將整個對象設置爲響應式數據 * @param obj<Object>: 對象 */
function observe(obj) {
    // 若是obj不是對象的話,跳出函數
    if (typeof obj !== "object" || obj === null) {
        return;
    }

    // 判斷傳入obj的類型
    if(Array.isArray(obj)){
        // TODO
    }else {
        // 遍歷obj全部全部key,作響應式處理
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key]);
        })
    }
}
複製代碼

同時,咱們須要實現對這個對象一個遞歸處理,所以咱們須要修改一下defineReactive函數。咱們只須要在最開始的地方,調用一次observe函數,若是傳入的val是對象,就會進行遞歸響應式處理,若是不是就返回。

function defineReactive(obj, key, val) {
    observe(val);  // 遞歸處理:若是val是對象,繼續作響應式處理

    Object.defineProperty(obj, key, {
        ...
    })
}
複製代碼

咱們來測試一下:

const obj = {
    time: new Date().toLocaleTimeString(),
    info: {
        name: 'OUDUIDUI',
        age: 18
    }
};
observe(obj);

setInterval(() => {
    obj.time = new Date().toLocaleTimeString();
}, 1000)

setTimeout(() => {
    obj.info.age++;
}, 2000)
複製代碼

demo2.gif

這裏還有一個小問題,就是若是obj本來有一個屬性是常規類型,即字符串、數值等等,而後再將其改成引用類型時,如對象、數值等,該引用類型內部的屬性,是沒有響應式的。好比下來這種狀況:

const obj = {
    text: 'Hello World',
};
observe(obj);  // 響應式處理

obj.text = { en: 'Hello World' };    // 將obj.text由字符串改爲一個對象

setTimeout(() => {
    obj.text.en = 'Hi World';   // 此時修改text對象屬性頁面是不會更新的,由於obj.text.en不是響應式數據
}, 2000)
複製代碼

對於這種狀況,咱們只須要在defineReactive函數中,set的時候調用一下observe函數,將newVal傳入,若是是對象就進行響應式處理,不然就直接返回。

function defineReactive(obj, key, val) {
    observe(val); 

    Object.defineProperty(obj, key, {
        get() {
            console.log(`get ${key}`)
            return val;
        },
        set(newVal) {
            console.log(`set ${key}`)
            if (newVal !== val) {
                observe(newVal);  // 若是newVal是對象,再次作響應式處理
                val = newVal;
                update();
            }
        }
    })
}
複製代碼

咱們測試一下。

function update() {
    const app = document.getElementById('app');
    app.innerHTML = `obj.text = ${JSON.stringify(obj.text)}`
}

const obj = {
    text: 'Hello World'
};

// 響應式處理
observe(obj);

setTimeout(() => {
    obj.text = {     // 將obj.text由字符串改爲一個對象
        en: 'Hello World'
    }
}, 2000)

setTimeout(() => {
    obj.text.en = 'Hi World';
}, 4000)
複製代碼

demo3.gif

最後咱們來完成前面樓下的一個問題,就是數組的響應式處理。

之因此數組須要特殊處理,由於數組有七個自帶方法能夠去處理數組的內容,分別是pushpopshiftunshiftreversesortsplice,它們都是能夠修改數組自己的。

因此,咱們須要對七個方法進行監聽。咱們能夠先克隆一個新的數組原型,而後在新的原型中,新建這七個方法,先執行對應的方法操做後,進行數據響應式更新處理。

// 數組響應式
const originalProto = Array.prototype;
const arrayProto = Object.create(originalProto);  // 以Array.prototype爲原型創新一個新對象
['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => {
    arrayProto[method] = function () {
        // 原始操做
        originalProto[method].apply(this, arguments);
        // 覆蓋操做:通知更新
        update();
    }
})
複製代碼

而後繼續完成observe函數操做。

若是類型是數組的話,將其的原型進行覆蓋,而後再數組每個元素進行響應式處理。

function observe(obj) {
    if (typeof obj !== "object" || obj === null) {
        return;
    }

    // 判斷傳入obj的類型
    if (Array.isArray(obj)) {
        // 覆蓋原型
        obj.__proto__ = arrayProto;
        // 對數組內部原型執行響應式
        for (let i = 0; i < obj.length; i++) {
            observe(obj[i]); 
        }
    } else {
        Object.keys(obj).forEach(key => {
            defineReactive(obj, key, obj[key]);
        })
    }
}
複製代碼

測試一下:

function update() {
    const app = document.getElementById('app');
    app.innerHTML = `obj.nums = ${JSON.stringify(obj.nums)}`
}

const obj = {
    nums: [4, 2, 3]
};

// 響應式處理
observe(obj);

setTimeout(() => {
    obj.nums.push(1);
}, 2000)

setTimeout(() => {
    obj.nums.sort((a,b) => a - b);
}, 4000)
複製代碼

demo4.gif

簡單手寫Vue

原理分析

當咱們使用vue的時候,首先都會建立一個Vue實例,而後在裏面初始化elementdatamethods等等。

const app = new Vue({
    el: '#app',
    data: {
      	count: 1
    },
    methods:{}
});
複製代碼

而後咱們能夠在data裏面設置一些變量,而這些變量會被處理爲響應式數據,而後咱們就可使用模板語句去渲染data數據。

<div id="app">
    <p>{{counter}}</p>
</div>
複製代碼

因此咱們須要實現的功能就是data進行響應式處理編譯和渲染模板、以及數據變化時更新模板

所以咱們建立Vue實例須要實現如下內容:

  • data執行響應式處理,這個過程發生在Observer中;
  • 對模板執行編譯,找到其中動態綁定的數據,從data中獲取並初始化視圖,這個過程發生在Compile中;
  • 每建立一個響應式數據,同時定義一個更新函數和Watcher,未來對應數據變化時Watcher會調用更新函數;
  • 因爲data的某個key在一個視圖中可能出現屢次,因此每一個key都須要一個依賴Dependence來管理多個Watcher;未來data中數據一旦發生變化,會首先找到對應的Dependence,而後Dependence通知對應全部的Watcher執行更新函數。

Vue1.jpg

實現

數據響應式

首先咱們新建一個vue.js,建立一個Vue的類,在constructor對參數數據進行保存。

/** * Vue: * 1. 對data選項作響應式處理 * 2. 編譯模板 * @param options<Object>: 包含el、data、methods等等 */
class Vue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;    // data選項
      
      	// 對data進行響應式處理
        observe(this.$data);
    }
}
複製代碼

observe()方法跟前面所說的相似,只不過咱們把大部份內容放入Observer類中,由於咱們須要對每個響應式數據進行監聽並通知Dep

/** * observe: 將整個對象設置爲響應式數據 * @param obj<Object>: 對象 */
function observe(obj) {
    // 若是obj不是對象的話,跳出函數
    if (typeof obj !== "object" || obj === null) {
        return;
    }

    // 響應式處理
    new Observer(obj);
}
複製代碼

Observerconstructor構造函數的內容,基本就是以前observe方法中的內容,以及類中的defineReactive方法也跟前面講的一致,這裏就不說了。

惟一不一樣的是,這裏再也不是調用update函數,而在後面咱們須要建立一個依賴Dependence實例並調用,如今咱們先留空着。

/** * Observer: * 1. 根據傳入value的類型作響應的響應式處理 * @param value<Object || Array> */
class Observer {
    constructor(value) {
        this.value = value;

        // 數據類型判斷
        if(Array.isArray(value)){
            // 覆蓋原型
            value.__proto__ = this.getArrayProto();
            // 對數組內部原型執行響應式
            for (let i = 0; i < value.length; i++) {
                observe(value[i]);
            }
        }else {
            // 遍歷obj全部全部key,作響應式處理
            Object.keys(value).forEach(key => {
                this.defineReactive(value, key, value[key]);
            })
        }
    }

    getArrayProto() {
      	const self = this;
      	
        const originalProto = Array.prototype;
        const arrayProto = Object.create(originalProto); 
        ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => {
            arrayProto[method] = function () {
                originalProto[method].apply(self, arguments);
              
                // TODO 通知變化
            }
        })
        return arrayProto;
    }

    /** * defineReactive : 將對象中某一個屬性設置爲響應式數據 * @param obj<Object>: 對象 * @param key<any>: key名 * @param val<any>: 初始值 */
    defineReactive(obj, key, val) {
        observe(val); 

        Object.defineProperty(obj, key, {
            get() {
                Dependence.target && dep.addDep(Dependence.target);
                return val;
            },
            set(newVal) {
                if (newVal !== val) {
                    observe(newVal);
                    val = newVal;
										
                  	// TODO 通知變化
                }
            }
        })
    }
}
複製代碼

如今咱們基本實現了對data數據進行響應式處理。

但如今咱們在JavaScript中建立了Vue實例後,咱們沒法直接在實例中獲取到data數據,而是須要經過實例中的$data中獲取到data的內容。

const app = new Vue({
    el: '#app',
    data: {
        desc: 'HelloWorld',
    }
});

console.log(app.desc);   	// undefined
console.log(app.$data.desc);   // 'HelloWorld'
複製代碼

由於咱們得對data中的數據實現一下代理,代理的實現也是經過對象的訪問器屬性實現,這裏也很少說。

class Vue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;
        observe(this.$data);

        // 代理
        proxy(this);
    }
}

/** * proxy: 數據代理 * @param vm<Object> */
function proxy(vm) {
    Object.keys(vm.$data).forEach(key => {
        Object.defineProperty(vm, key, {
            get() {
                return vm.$data[key]
            },
            set(v) {
                vm.$data[key] = v;
            }
        })
    })
}
複製代碼

這時候咱們就能夠用app.desc訪問到data.desc屬性了。

模板編譯和渲染

在咱們實現數據響應式後,咱們就能夠對模板進行編譯和渲染,這時候就須要來實現Compile類。

class Vue {
    constructor(options) {
        this.$options = options;
        this.$data = options.data;    // data選項

        observe(this.$data);
        proxy(this);

        // 模板編譯和渲染
        new Compile(options.el, this);
    }
}
複製代碼

Compile類的構造函數接收兩個參數,一個是element,一個是Vue實例中的this,這個實際上就是View Model的數據,也是咱們在Vue中常見的vm

在構造函數中,先對傳入數據進行保存,而後獲取節點,若是節點存在的話,就開始進行編譯處理。

/** * Compile: * 1. 解析模板 * a. 處理插值 * b. 處理指令和事件 * c. 以上二者初始化和更新 * @param el * @param vm */
class Compile {
    constructor(el, vm) {
        this.$vm = vm;
        this.$el = document.querySelector(el);

        if(this.$el){
          	// 編譯節點
            this.compile(this.$el);
        }
    }

  	/** * compile: 遞歸節點,對節點進行編譯 * @param el */
    compile(el){ }
}
複製代碼

首先,咱們須要對節點進行遞歸遍歷,而後經過nodeType識別出當前節點的信息,若是是元素節點的話,咱們須要對其進行指令和事件處理,若是是文本節點的話,同時含有{{}}的話,咱們須要對齊進行文本替換處理。

class Compile {
    constructor(el, vm) { ... }

    /** * compile: 遞歸節點,對節點進行編譯 * @param el */
    compile(el){
        // 遍歷el子節點,判斷他們類型作相應的處理
        const childNodes = el.childNodes;

        childNodes.forEach(node => {
            if(node.nodeType === 1){
                // 元素
                console.log('元素', node.nodeName);
              	// TODO 指令和事件處理
            }else if(this.isInter(node)){
                // 文本
                console.log('文本', node.textContent);
              	// TODO 文本替換處理
            }
          
            // 遞歸
            if(node.childNodes){
                this.compile(node);
            }
        })
    }

    // 判斷是否爲插值表達式
    isInter(node) {
        return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
    }
}
複製代碼

首先咱們來實現一下文本編譯。

由於咱們前面判斷的時候,使用過正則去判斷node.textContent,所以若是符合標準的話,咱們就能夠經過RegExp.$1獲取到屬性名,所以咱們就能夠那屬性名去data中進行匹配。

class Compile {
    constructor(el, vm) { ... }

    compile(el){
        const childNodes = el.childNodes;

        childNodes.forEach(node => {
            if(node.nodeType === 1){
              	// TODO 指令和事件處理
            }else if(this.isInter(node)){
              	// 文本初始化
                this.compileText(node);
            }
          
            if(node.childNodes){
                this.compile(node);
            }
        })
    }

    // 編譯文本
    compileText(node) {
        node.textContent = this.$vm[RegExp.$1];
    }
}
複製代碼

這時候,咱們能夠測試一下。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue app</title>
</head>
<body>
<div id="app">
    <p>{{desc}}</p>
</div>

<script src="./src/vue.js"></script>
<script> const app = new Vue({ el: '#app', data: { desc: 'HelloWorld', }, }); </script>
</body>
</html>
複製代碼

demo5.png

接下來,咱們簡單實現一下指令和實現,這個demo就實現一下v-textv-html以及事件綁定@click

首先,當咱們遞歸節點的時候,當nodeType === 1的時候,咱們得知該節點是一個元素,就能夠經過node.attributes去獲取該標籤中的全部指令。而後經過遍歷和識別attrName是否以v-或者@開頭的。

if(node.nodeType === 1) {
    // 元素
    console.log('元素', node.nodeName);
    // 處理指令和事件
    const attrs = node.attributes;
    Array.from(attrs).forEach(attr => {
        const attrName = attr.name;
        const exp = attr.value;
        if (attrName.startsWith('v-')) {
            // 處理指令
        }
        if (attrName.indexOf('@') === 0) {
            // 處理事件
        }
    })
}
複製代碼

由於事件處理比較簡單,因此咱們先來處理事件。

咱們只須要提取出事件的類型,而後將節點node、方法名exp和事件類型dir進行事件監聽。

這裏須要主要的是,addEventListener事件監聽第二個參數的方法,須要綁定this.$vm,由於在方法中有可能會用到data數據。

class Compile {
    constructor(el, vm) { ... }

    compile(el){
        const childNodes = el.childNodes;

        childNodes.forEach(node => {
            if(node.nodeType === 1){
                const attrs = node.attributes;
                Array.from(attrs).forEach(attr => {
                    const attrName = attr.name;
                    const exp = attr.value;
                    if(attrName.startsWith('v-')){
                        // 處理指令
                    }
                    // 處理事件
                    if(attrName.indexOf('@') === 0){
                        const dir = attrName.substring(1);
                        // 事件監聽
                        this.eventHandler(node, exp, dir);
                    }
                })
            }else if(node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)){
                console.log('文本', node.textContent);
                this.compileText(node);
            }

            if(node.childNodes){
                this.compile(node);
            }
        })
    }

    /** * eventHandler: 節點事件處理 * @param node: 節點 * @param exp: 函數名 * @param dir: 事件類型 */
    eventHandler(node, exp, dir) {
        const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
        node.addEventListener(dir, fn.bind(this.$vm));
    }
}
複製代碼

如今來測試一下。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue app</title>
</head>
<body>
<div id="app">
    <button @click="add">測試</button>
</div>

<script src="./src/vue.js"></script>
<script> const app = new Vue({ el: '#app', data: { desc: 'HelloWorld' }, methods:{ test() { console.log(this.desc); } } }); </script>
</body>
</html>
複製代碼

demo6.gif

接下來來處理指令。

對不一樣指令的處理是不同,所以得對每一種指令都須要新建一個更新函數。這裏只實現如下v-textv-htmlv-model

每一個方法名是與指令名一致,這有利於後面直接用指令名去查找。而後每一個方法都接受兩個參數——node節點和exp變量名。

class Compile {
    constructor(el, vm) { ... }

    compile(el){ ... }

    // v-text
    text(node, exp) {
        node.textContent = this.$vm[exp];
    }

    // v-html
    html(node, exp) {
        node.innerHTML = this.$vm[exp];
    }

    // v-model
    model(node, exp){
        // 表單原生賦值
        node.value = value;
        // 事件監聽
        node.addEventListener('input', e => {
            // 賦值實現雙向綁定
            this.$vm[exp] = e.target.value;
        })
    }
}
複製代碼

而後處理指令只須要直接查找一下this有沒有這個指令方法,有的話調用。

// 處理指令
if(attrName.startsWith('v-')){
    const dir = attrName.substring(2);
    this[dir] && this[dir](node, exp);
}
複製代碼

最後試驗一下。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue app</title>
</head>
<body>
<div id="app">
    <p v-text="desc"></p>
    <p v-html="desc2"></p>
    <input type="text" v-model="desc" />
</div>

<script src="./src/vue.js"></script>
<script> const app = new Vue({ el: '#app', data: { counter: 1, desc: 'HelloWorld', desc2: `<span style="font-weight: bolder">Hello World</span>` } }); </script>
</body>
</html>
複製代碼

demo7.png

數據更新

數據的更新就會用到Watcher監聽器和Dependence觀察者。

當咱們視圖中用到了data中某個屬性key,這稱爲依賴,好比<div>{{desc}}</div>desc就是一個依賴。而同一個key出現屢次的時候,每一次都會建立一個Watcher來維護它們,而這個過程稱爲依賴收集。然而但某個key發生變化的時候,咱們須要經過該依賴下的全部Watcher去更新,這時候就須要一個Dependence來管理,須要更新的時候就由它來統一通知。

Vue2.jpg

在實現這個功能以前,咱們須要先來重構一個地方的代碼。

就是咱們只需在模板中用到data屬性的地方須要建立一個Watcher監聽器,所以咱們須要在Compile中建立。可是在其中咱們插值表達式用到了一個更新方法,每一個指令各用到了一個更新方法。

所以咱們須要一個高級函數,將其都封裝起來。也就是當用到每一種指令或插值表達式,咱們都會經歷調用這個高級函數,所以咱們也能夠在這個高級函數中建立Watcher

class Compile {
    constructor(el, vm) { ... }

    compile(el){ ... }

    /** * update: 高階函數 —— 操做節點 * @param node: 節點 * @param exp: 綁定數據變量名 * @param dir: 指令名 */
    update(node, exp, dir) {
        // 初始化
        const fn = this[dir + 'Updater'];
        fn && fn(node, this.$vm[exp]);

        // TODO 建立監聽器
    }

    // 編譯文本
    compileText(node) {
        this.update(node, RegExp.$1, 'text');
    }

    // v-text
    text(node, exp) {
        this.update(node, exp, 'text');
    }
    textUpdater(node, value) {
        node.textContent = value;
    }

    // v-html
    html(node, exp) {
        this.update(node, exp, 'html');
    }
    htmlUpdater(node, value) {
        node.innerHTML = value;
    }

    // v-model
    model(node, exp){
        this.update(node,exp, 'model');
        node.addEventListener('input', e => {
            this.$vm[exp] = e.target.value;
        })
    }
    modelUpdater(node, value){
        node.value = value;
    }

    eventHandler(node, exp, dir) { ... }
}
複製代碼

緊接着,咱們就能夠來建立Watcher類。

這個類的功能其實很簡單,就是保存這個更新函數,而後當數據更新的時候,咱們調用一下更新函數就能夠了。

/** * Watcher: * 1. 監聽器 —— 負責依賴更新 * @param vm * @param key: 綁定數據變量名 * @param updateFn: 更新函數 */
class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFn = updateFn;
    }

    update() {
        // 執行實際更新操做
        this.updateFn.call(this.vm, this.vm[this.key]);
    }
}
複製代碼

而後在高階函數中調用。

update(node, exp, dir) {
    const fn = this[dir + 'Updater'];
    fn && fn(node, this.$vm[exp]);

    // 建立Watcher監聽器
    new Watcher(this.$vm, exp, function (val){
        fn && fn(node, val);
    })
}
複製代碼

Dependence這個類,主要就三個功能:

  • 一個是在每一次將data響應式處理的時候,都要建立一個相應的空數組deps,用於收集相應的監聽器;
  • 第二個是再每一次建立新的Watcher,都要將其放置對應的deps數組中;
  • 第三個是每次數據更新的時候,咱們就要遍歷對應的deps,通知對應的全部監聽器更新視圖。

所以,咱們就能夠來實現Dependence類。

/** * Dependence: * 觀察者 —— 負責通知監聽器更新 */
class Dependence {
    constructor() {
        this.deps = [];
    }

    /** * addDep: 添加新的監聽器 * @param dep */
    addDep(dep) {
        this.deps.push(dep);
    }

    /** * notify: 通知更新 */
    notify() {
        this.deps.forEach(dep => dep.update());
    }
}
複製代碼

而後咱們在Observer類中,實現數據響應式的時候,須要建立一個Dependence實例,而且更新的時候通知更新。

class Observer {
    constructor(value) {
        this.value = value;
        // 建立Dependence實例
        this.dep = new Dependence();

        ...
    }

    getArrayProto() {
      	const self = this;
      
        const originalProto = Array.prototype;
        const arrayProto = Object.create(originalProto);
        ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => {
            arrayProto[method] = function () {
                originalProto[method].apply(self, arguments);
                // 覆蓋操做:通知更新
                self.dep.notify();
            }
        })
        return arrayProto;
    }

    defineReactive(obj, key, val) {
        observe(val); 

        const self = this;

        Object.defineProperty(obj, key, {
            get() {
                return val;
            },
            set(newVal) {
                if (newVal !== val) { 
                    observe(newVal); 
                    val = newVal;
                    // 通知更新
                    self.dep.notify();
                }
            }
        })
    }
}
複製代碼

最後一步,就是收集監聽器。這一步的一個難點就在於咱們在建立Watcher以後,須要將其放置對應keydeps中,而對應的deps,只能在對應的Observer類中才能訪問到。

所以,咱們能夠調用一次get,來完成收集工做。

因此咱們能夠直接在建立完Watcher後,而後將這個this賦值給Dependence類的一個新建屬性中,而後訪問一下對應key,所以觸發get方法,就執行收集工做。

固然對於數組也是同樣獲得了,咱們能夠調用一下push方法且不傳參,就能夠將Watcher實例添加到數組對應的deps中。

class Watcher {
    constructor(vm, key, updateFn) {
        this.vm = vm;
        this.key = key;
        this.updateFn = updateFn;

        // 觸發依賴收集 
        Dependence.target = this;   // 將this賦值給Dependence的target屬性
        Array.isArray(this.vm[this.key]) ? this.vm[this.key].push() : '';  // 觸發收集
        Dependence.target = null;   // 收集完成後,將target設置回null
    }

    update() { ... }
}
複製代碼
get() {
    // 依賴收集
    Dependence.target && self.dep.addDep(Dependence.target);
    return val;
}
複製代碼
getArrayProto() {
    const self = this;

    const originalProto = Array.prototype;
    const arrayProto = Object.create(originalProto); 
    ['push', 'pop', 'shift', 'unshift','reverse', 'sort', 'splice'].forEach(method => {
        arrayProto[method] = function () {
            originalProto[method].apply(self, arguments);
            // 收集監聽器
            Dependence.target && self.dep.addDep(Dependence.target);

            self.dep.notify();
        }
    })
    return arrayProto;
}
複製代碼

最後測試一下。

<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, user-scalable=no, initial-scale=1.0, maximum-scale=1.0, minimum-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>vue app</title>
</head>
<body>
<div id="app">
    <p @click="add" style="cursor: pointer">{{counter}}</p>
    <p v-text="desc"></p>
    <p v-html="desc2"></p>
    <input type="text" v-model="desc" />
    <div @click="pushArr">{{arr}}</div>
</div>

<script src="./src/vue.js"></script>
<script> const app = new Vue({ el: '#app', data: { counter: 1, desc: 'HelloWorld', desc2: `<span style="font-weight: bolder">Hello World</span>`, arr: [0], }, methods:{ add() { this.counter++; }, pushArr() { this.arr.push(this.arr.length); } } }); </script>
</body>
</html>
複製代碼

demo8.gif

相關文章
相關標籤/搜索