說明
vue實現雙向綁定原理,主要是利用Object.defineProperty 來給實例data的屬性添加 setter和getter.
並通過發佈訂閱模式(一對多的依賴關係,當狀態發生改變,它的所有依賴都將被通知)來實現響應。
這個環節中包含了三個部分
-
Observer 用來監聽攔截data的屬性爲監察者。
-
Dep用來添加訂閱者,爲訂閱器
-
Watcher 就是訂閱者
監察者通過 Dep 向 Watcher發佈更新消息
簡單實現
那麼首先
- 通過對set和get的攔截,在get階段進行依賴收集,在set階段對通知該屬性上所啊綁定的依賴。
如下我們就已經實現了一個簡單的雙向綁定了。
我們將data的value屬性綁定上set和get,通過 _value 來進行操作。
<!-- HTML部分 -->
<input type="text" id="inp" oninput="inputFn(this.value)">
<div id='div'></div>
<!-- JS部分 -->
var inp = document.getElementById('inp');
var div = document.getElementById('div');
var data = {
value:''
}
Object.defineProperty(data, 'value', {
enumerable: true,
configurable: true,
set: function (newValue) {
this._value = newValue;
div.innerText = data._value = value; //watcher
},
get: function () {
return this._value;
}
})
function inputFn(value) {
data._value = value;
}
如果只是實現一個簡單的雙向綁定那麼上面的代碼就已經實現了。
進一步完善模擬vue實現
首先我們將watcher抽出來 備用
function watcher(params) {
div.innerText = inp.value = params; // 派發watcher
}
聲明一個vm來模擬vue的實例,並初始化。
var vm = {
//類似vue實例上的data
data: {
value: ''
},
// vue私有, _data的所有屬性爲data中的所有屬性被改造爲 getter/setter 之後的。
_data: {
value: ''
},
// 代理到vm對象上,可以實現vm.value
value: '',
//value的訂閱器用來收集訂閱者
valueWatchers:[]
}
遍歷其data上的屬性 進行改造 這裏我們還是隻舉一個例子
// 利用 Object.defineProperty 定義一個屬性 (eg:value) 描述符爲存取描述符的屬性
Object.defineProperty(vm._data, 'value', {
enumerable: true, //是否可枚舉
configurable: true, //是否可配置
set: function (newValue) { //set 派發watchers
vm.data.value = newValue;
vm.valueWatchers.map(fn => fn(newValue));
},
get: function () {
// 收集wachter vue中會在compile解析器中通過 顯示調用 (this.xxx) 來觸發get進行收集
vm.valueWatchers.length = 0;
vm.valueWatchers.push(watcher);
return vm.data.value;
}
})
<!--直接通過顯示調用來觸發get進行綁定 vue中是在compile解析器中來進行這一步-->
vm._data.value
進行到這兒也已經實現了綁定,但是我們平時使用vue ,都是可以直接通過 this.xxx來獲取和定義數據
那麼我們還需要進行一步Proxy 代理
Object.defineProperty(vm, 'value', {
enumerable: true,
configurable: true,
set: function (newValue) {
this._data.value = newValue; //藉助
},
get: function () {
return this._data.value;
}
})
這樣我們就把vm._data.value 代理到vm.value上了,可以通過其直接操作了。
那麼按照官方的寫法
function proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return this[sourceKey][key];
},
set(val) {
this[sourceKey][key] = val;
}
});
}
proxy(vm, '_data', 'value');
完善後的完整代碼
以下爲整個頁面,可以直接運行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>雙向綁定簡單實現</title>
</head>
<body>
<input type="text" id="inp" oninput="inputFn(this.value)">
<br>
<input type="text" id="inp2" oninput="inputFn(this.value)">
<div id='div'></div>
<script>
var inp = document.getElementById('inp');
var inp2 = document.getElementById('inp2');
var div = document.getElementById('div');
function inputFn(value) {
div.innerText = vm.value = value;
}
function watcher(params) {
console.log(1)
div.innerText = inp.value = params; // 派發watcher
}
function watcher2(params) {
console.log(2)
div.innerText = inp2.value = params; // 派發watcher
}
function proxy (target, sourceKey, key) {
Object.defineProperty(target, key, {
enumerable: true,
configurable: true,
get() {
return this[sourceKey][key];
},
set(val) {
this[sourceKey][key] = val;
}
});
}
let handler = {
enumerable: true,
configurable: true,
set: function (newValue) {
vm.data.value = newValue;
vm.valueWatchers.map(fn => fn(newValue));
},
get: function () {
vm.valueWatchers = []; //防止重複添加,
vm.valueWatchers.push(watcher);
vm.valueWatchers.push(watcher2);
return vm.data.value;
}
}
var vm = {
data: {},
_data: {},
value: '',
valueWatchers: []
}
Object.defineProperty(vm._data, 'value', handler)
proxy(vm, '_data', 'value');
vm.value; //顯示調用綁定
</script>
</body>
</html>
解釋
再多講一點。實際上vue在初始化的時候是用解析器解析過程中將wathcer進行綁定的。
它會利用一個全局的Dep.target = watcher
然後在get收集中,只收集全局上Dep.target, 添加完畢後會重新初始化全局Dep.target = null;
類似如下操作
Dep.target = watcher;
vm.value; // 觸發get => Dep.target && valueWatchers.push(Dep.target);
Dep.target = null;
這樣也會防止我們在調用時觸發get重複去添加watcher。
而我們的例子中只是每次都初始化爲[]. 實際訂閱器也不只是一個watcher數組。
此例跟官方實現還是有很多差距,只是簡單模擬。
vue3.0 使用 Proxy
在vue3.0中,使用proxy這個功能更加強大的函數,它可以定義對象的基本操作的自定義行爲。對比defineProperty只能攔截對象的某一屬性,proxy的功能更方便。所提供的可自定義的操作也更多。
上面,我用defineProperty實現了vue的雙向綁定,接下來我們用proxy來實現。
首先我們可以先了解一下proxy的作用和用法
首先 defineProperty
的用法是Object.defineProperty(obj, prop, descriptor)
proxy的用法如下:
const p = new Proxy(target, handler)
我們用proxy來實現一下雙向綁定:
核心代碼就像這樣,在我們這個需求下分析
set
函數中target
爲所攔截的對象key
爲屬性名newValue
爲所賦予的值- set中需要
return true
代表設置成功,返回flase在嚴格模式下報TypeError (代表該值與期望值類型不同)
get
函數中target
爲所攔截的對象key
爲屬性名- get可返回任意值
let data = {value: 0}
const vm = new Proxy({value: 0 }, {
set: function(target, key, newValue){
console.log(key + '被賦值爲' + newValue)
target[key] = newValue
return true
},
get: function(target, key) {
console.log(target[key])
return target[key]
}
})
vm.value = 1 // 0; value被賦值爲1
proxy雙向綁定具體實現
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<h1 id="content"></h1>
<p><input type="text" id="enter" value=""></p>
</body>
<script>
let content = document.getElementById("content")
let enter_input = document.getElementById('enter')
let data = {
enter_input: '',
enter_input_watchers: []
}
let watcher = function watcherFn(value) {
content.innerText = value
}
let watcher2 = function watcher2Fn(value) {
enter_input.value = value
}
let handler = {
set: function(target, key, value) {
if (key === 'enter_input') {
target[key] = value;
target[key + "_watchers"].map(function (watcher) {
watcher(value)
})
}
},
get: function(target, key) {
target[key + "_watchers"] = [watcher, watcher2];
return target[key]
}
}
let db = new Proxy(data, handler);
db.enter_input; //收集監聽
enter_input.addEventListener('input', function(e){
db.enter_input = e.target.value;
})
</script>
</html>
雙向綁定總結
- vue2.x版本使用了
Object.defineProperty
來實現雙向綁定,由於其功能的限制,只能綁定對象的某個屬性,vue需要遞歸遍歷對象的所有屬性
挨個進行綁定,功能上不是很完美。 - vue3.0版本使用
proxy
進行雙向綁定,proxy提供了可以定義對象基本操作的自定義行爲的功能(如屬性查找、賦值
、枚舉、函數調用),可以直接攔截整個對象
,不需要再進行遞歸。本例中我們只使用到了proxy提供自定義set
和get
的能力。
錯誤類型擴展
平時我們常見的錯誤類型分爲ReferenceError
,TypeError
,SyntaxError
這三種。
一、 ReferenceError 代表我們的作用域查找錯誤。
let b = 1;
console.log(b)
console.log(a) //ReferenceError
-
我們在全局定義了
b
,所以console.log(b)爲1
,但是我們沒有定義a
,所以我們在全局作用域下找不到a,就會報ReferenceError
-
如果是在函數中定義,則在函數中查找不到時,會去父作用域查找,一直到全局,都找不到,纔會報
ReferenceError
二、 TypeError代表數據類型與預期不符。
let b = 1;
b() //TypeError
- 我們在全局定義了
b
,其類型爲Number,但是我們用()來執行它,把它當作了函數用,所以就會報TypeError
三、 SyntaxError代表語法錯誤。
let b > 1;//SyntaxError
//or
let let b//SyntaxError
- 很明顯,我們不可以這麼使用let,語法就錯誤了,所以就會報
SyntaxError
錯誤類型總結
ReferenceError
,TypeError
,SyntaxError
分別代表作用域,預期類型,語法
錯誤。
我們其實經常看到這幾種錯誤,但是平時不會太注意這個錯誤類型,只關注在哪錯誤了。倘若我們知道了這幾種錯誤類型代表的涵義,對我們排除錯誤也是非常有幫助的。