在vue2中,什么是雙向綁定,為什么vue3要進行優化?

一、什么是雙向綁定
我們先從單向綁定切入單向綁定非常簡單,就是把Model綁定到View,當我們用JavaScript代碼更新Model時,View就會自動更新雙向綁定就很容易聯想到了,在單向綁定的基礎上,用戶更新了View,Model的數據也自動被更新了,這種情況就是雙向綁定舉個栗子

當用戶填寫表單時,View的狀態就被更新了,如果此時可以自動更新Model的狀態,那就相當于我們把Model和View做了雙向綁定關系圖如下

二、雙向綁定的原理是什么
我們都知道 Vue 是數據雙向綁定的框架,雙向綁定由三個重要部分構成
- 數據層(Model):應用的數據及業務邏輯
- 視圖層(View):應用的展示效果,各類UI組件
- 業務邏輯層(ViewModel):框架封裝的核心,它負責將數據與視圖關聯起來
而上面的這個分層的架構方案,可以用一個專業術語進行稱呼:MVVM這里的控制層的核心功能便是 “數據雙向綁定” 。自然,我們只需弄懂它是什么,便可以進一步了解數據綁定的原理
理解ViewModel
它的主要職責就是:
- 數據變化后更新視圖
- 視圖變化后更新數據
當然,它還有兩個主要部分組成
- 監聽器(Observer):對所有數據的屬性進行監聽
- 解析器(Compiler):對每個元素節點的指令進行掃描跟解析,根據指令模板替換數據,以及綁定相應的更新函數
三、實現雙向綁定
我們還是以Vue為例,先來看看Vue中的雙向綁定流程是什么的
new Vue()首先執行初始化,對data執行響應化處理,這個過程發生Observe中- 同時對模板執行編譯,找到其中動態綁定的數據,從
data中獲取并初始化視圖,這個過程發生在Compile中 - 同時定義?個更新函數和
Watcher,將來對應數據變化時Watcher會調用更新函數 - 由于
data的某個key在?個視圖中可能出現多次,所以每個key都需要?個管家Dep來管理多個Watcher - 將來data中數據?旦發生變化,會首先找到對應的
Dep,通知所有Watcher執行更新函數
流程圖如下:

實現
先來一個構造函數:執行初始化,對data執行響應化處理
class Vue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 對data選項做響應式處理
observe(this.$data);
// 代理data到vm上
proxy(this);
// 執行編譯
new Compile(options.el, this);
}
}
對data選項執行響應化具體操作
function observe(obj) {
if (typeof obj !== "object" || obj == null) {
return;
}
new Observer(obj);
}
class Observer {
constructor(value) {
this.value = value;
this.walk(value);
}
walk(obj) {
Object.keys(obj).forEach((key) => {
defineReactive(obj, key, obj[key]);
});
}
}
編譯Compile
對每個元素節點的指令進行掃描跟解析,根據指令模板替換數據,以及綁定相應的更新函數

class Compile {
constructor(el, vm) {
this.$vm = vm;
this.$el = document.querySelector(el); // 獲取dom
if (this.$el) {
this.compile(this.$el);
}
}
compile(el) {
const childNodes = el.childNodes;
Array.from(childNodes).forEach((node) => { // 遍歷子元素
if (this.isElement(node)) { // 判斷是否為節點
console.log("編譯元素" + node.nodeName);
} else if (this.isInterpolation(node)) {
console.log("編譯插值?本" + node.textContent); // 判斷是否為插值文本 {{}}
}
if (node.childNodes && node.childNodes.length > 0) { // 判斷是否有子元素
this.compile(node); // 對子元素進行遞歸遍歷
}
});
}
isElement(node) {
return node.nodeType == 1;
}
isInterpolation(node) {
return node.nodeType == 3 && /\{\{(.*)\}\}/.test(node.textContent);
}
}
依賴收集
視圖中會用到data中某key,這稱為依賴。同?個key可能出現多次,每次都需要收集出來用?個Watcher來維護它們,此過程稱為依賴收集多個Watcher需要?個Dep來管理,需要更新時由Dep統?通知

實現思路
defineReactive時為每?個key創建?個Dep實例- 初始化視圖時讀取某個
key,例如name1,創建?個watcher1 - 由于觸發
name1的getter方法,便將watcher1添加到name1對應的Dep中 - 當
name1更新,setter觸發時,便可通過對應Dep通知其管理所有Watcher更新
// 負責更新視圖
class Watcher {
constructor(vm, key, updater) {
this.vm = vm
this.key = key
this.updaterFn = updater
// 創建實例時,把當前實例指定到Dep.target靜態屬性上
Dep.target = this
// 讀一下key,觸發get
vm[key]
// 置空
Dep.target = null
}
// 未來執行dom更新函數,由dep調用的
update() {
this.updaterFn.call(this.vm, this.vm[this.key])
}
}
聲明Dep
class Dep {
constructor() {
this.deps = []; // 依賴管理
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
創建watcher時觸發getter
class Watcher {
constructor(vm, key, updateFn) {
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
}
依賴收集,創建Dep實例
function defineReactive(obj, key, val) {
this.observe(val);
const dep = new Dep();
Object.defineProperty(obj, key, {
get() {
Dep.target && dep.addDep(Dep.target);// Dep.target也就是Watcher實例
return val;
},
set(newVal) {
if (newVal === val) return;
dep.notify(); // 通知dep執行更新方法
},
});
}
四.vue3.0里為什么要用 Proxy API 替代 defineProperty API ?
1、Object.defineProperty
定義:Object.defineProperty() 方法會直接在一個對象上定義一個新屬性,或者修改一個對象的現有屬性,并返回此對象
為什么能實現響應式
通過defineProperty 兩個屬性,get及set
- get
屬性的 getter 函數,當訪問該屬性時,會調用此函數。執行時不傳入任何參數,但是會傳入 this 對象(由于繼承關系,這里的this并不一定是定義該屬性的對象)。該函數的返回值會被用作屬性的值
- set
屬性的 setter 函數,當屬性值被修改時,會調用此函數。該方法接受一個參數(也就是被賦予的新值),會傳入賦值時的 this 對象。默認為 undefined
下面通過代碼展示:
定義一個響應式函數defineReactive
function update() {
app.innerText = obj.foo
}
function defineReactive(obj, key, val) {
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
調用defineReactive,數據發生變化觸發update方法,實現數據響應式
const obj = {}
defineReactive(obj, 'foo', '')
setTimeout(()=>{
obj.foo = new Date().toLocaleTimeString()
},1000)
在對象存在多個key情況下,需要進行遍歷
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
如果存在嵌套對象的情況,還需要在defineReactive中進行遞歸
function defineReactive(obj, key, val) {
observe(val)
Object.defineProperty(obj, key, {
get() {
console.log(`get ${key}:${val}`);
return val
},
set(newVal) {
if (newVal !== val) {
val = newVal
update()
}
}
})
}
當給key賦值為對象的時候,還需要在set屬性中進行遞歸
set(newVal) {
if (newVal !== val) {
observe(newVal) // 新值是對象的情況
notifyUpdate()
}
}
上述例子能夠實現對一個對象的基本響應式,但仍然存在諸多問題
現在對一個對象進行刪除與添加屬性操作,無法劫持到
const obj = {
foo: "foo",
bar: "bar"
}
observe(obj)
delete obj.foo // no ok
obj.jar = 'xxx' // no ok
當我們對一個數組進行監聽的時候,并不那么好使了
const arrData = [1,2,3,4,5];
arrData.forEach((val,index)=>{
defineProperty(arrData,index,val)
})
arrData.push() // no ok
arrData.pop() // no ok
arrDate[0] = 99 // ok
可以看到數據的api無法劫持到,從而無法實現數據響應式,
所以在Vue2中,增加了set、delete API,并且對數組api方法進行一個重寫
還有一個問題則是,如果存在深層的嵌套對象關系,需要深層的進行監聽,造成了性能的極大問題
小結
- 檢測不到對象屬性的添加和刪除
- 數組
API方法無法監聽到 - 需要對每個屬性進行遍歷監聽,如果嵌套對象,需要深層監聽,造成性能問題
2、proxy
Proxy的監聽是針對一個對象的,那么對這個對象的所有操作會進入監聽操作,這就完全可以代理所有屬性了
在ES6系列中,我們詳細講解過Proxy的使用,就不再述說了
下面通過代碼進行展示:
定義一個響應式方法reactive
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相當于在對象外層加攔截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`獲取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`設置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`刪除${key}:${res}`)
return res
}
})
return observed
}
測試一下簡單數據的操作,發現都能劫持
const state = reactive({
foo: 'foo'
})
// 1.獲取
state.foo // ok
// 2.設置已存在屬性
state.foo = 'fooooooo' // ok
// 3.設置不存在屬性
state.dong = 'dong' // ok
// 4.刪除屬性
delete state.dong // ok
再測試嵌套對象情況,這時候發現就不那么 OK 了
const state = reactive({
bar: { a: 1 }
})
// 設置嵌套對象屬性
state.bar.a = 10 // no ok
如果要解決,需要在get之上再進行一層代理
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相當于在對象外層加攔截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`獲取${key}:${res}`)
return isObject(res) ? reactive(res) : res
},
return observed
}
3、總結
Object.defineProperty只能遍歷對象屬性進行劫持
function observe(obj) {
if (typeof obj !== 'object' || obj == null) {
return
}
Object.keys(obj).forEach(key => {
defineReactive(obj, key, obj[key])
})
}
Proxy直接可以劫持整個對象,并返回一個新對象,我們可以只操作新的對象達到響應式目的
function reactive(obj) {
if (typeof obj !== 'object' && obj != null) {
return obj
}
// Proxy相當于在對象外層加攔截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`獲取${key}:${res}`)
return res
},
set(target, key, value, receiver) {
const res = Reflect.set(target, key, value, receiver)
console.log(`設置${key}:${value}`)
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`刪除${key}:${res}`)
return res
}
})
return observed
}
Proxy可以直接監聽數組的變化(push、shift、splice)
const obj = [1,2,3] const proxtObj = reactive(obj) obj.psuh(4) // ok
Proxy有多達13種攔截方法,不限于apply、ownKeys、deleteProperty、has等等,這是Object.defineProperty不具備的
正因為defineProperty自身的缺陷,導致Vue2在實現響應式過程需要實現其他的方法輔助(如重寫數組方法、增加額外set、delete方法)
// 數組重寫
const originalProto = Array.prototype
const arrayProto = Object.create(originalProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'reverse', 'sort'].forEach(method => {
arrayProto[method] = function () {
originalProto[method].apply(this.arguments)
dep.notice()
}
});
// set、delete
Vue.set(obj,'bar','newbar')
Vue.delete(obj),'bar')
Proxy 不兼容IE,也沒有 polyfill, defineProperty 能支持到IE9


浙公網安備 33010602011771號