github: github.com/OUDUIDUI/vu…javascript
Vue
設計思想參考了MVVM
模型,即將視圖View
和行爲Model
抽象化,即將視圖UI和業務邏輯分開來,而後經過ViewModel
層來實現雙向數據綁定。html
MVVM
與 MVC
最大的不一樣就是MVVM
實現了 View
和 Model
的自動同步,也就是當Model
的屬性改變時,咱們不用再本身手動操做 Dom
元素,來改變 View
的顯示,而是改變屬性後該屬性對應 View 層顯示會自動改變。vue
MVVM
框架的三個要素:數據響應式、模板引擎及其渲染。java
Vue2.x
中,是根據Object.defineProperty()
來實現數據響應式的Vue
的插槽{{}}
和指令v-bind
、v-on
、v-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函數
複製代碼
所以,咱們須要一個新的方法去實現對整個對象進行響應式處理,在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)
複製代碼
這裏還有一個小問題,就是若是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)
複製代碼
最後咱們來完成前面樓下的一個問題,就是數組的響應式處理。
之因此數組須要特殊處理,由於數組有七個自帶方法能夠去處理數組的內容,分別是push
、pop
、shift
、unshift
、reverse
、sort
、splice
,它們都是能夠修改數組自己的。
因此,咱們須要對七個方法進行監聽。咱們能夠先克隆一個新的數組原型,而後在新的原型中,新建這七個方法,先執行對應的方法操做後,進行數據響應式更新處理。
// 數組響應式
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)
複製代碼
當咱們使用vue
的時候,首先都會建立一個Vue
實例,而後在裏面初始化element
、data
、methods
等等。
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
執行更新函數。首先咱們新建一個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);
}
複製代碼
Observer
中constructor
構造函數的內容,基本就是以前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>
複製代碼
接下來,咱們簡單實現一下指令和實現,這個demo
就實現一下v-text
、v-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>
複製代碼
接下來來處理指令。
對不一樣指令的處理是不同,所以得對每一種指令都須要新建一個更新函數。這裏只實現如下v-text
、v-html
、v-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>
複製代碼
數據的更新就會用到Watcher
監聽器和Dependence
觀察者。
當咱們視圖中用到了data
中某個屬性key
,這稱爲依賴,好比<div>{{desc}}</div>
,desc
就是一個依賴。而同一個key
出現屢次的時候,每一次都會建立一個Watcher
來維護它們,而這個過程稱爲依賴收集。然而但某個key
發生變化的時候,咱們須要經過該依賴下的全部Watcher
去更新,這時候就須要一個Dependence
來管理,須要更新的時候就由它來統一通知。
在實現這個功能以前,咱們須要先來重構一個地方的代碼。
就是咱們只需在模板中用到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
以後,須要將其放置對應key
的deps
中,而對應的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>
複製代碼