熟悉vue
的小夥伴應該都知道,談到vue
的原理,最重要的莫過於:響應式,虛擬dom
及diff
算法,模版編譯,今天,咱們一塊兒來深刻vue
的響應式,探討vue2.x
響應式的實現原理與不足,以及vue3.0
版本如何重寫響應式實現方案。javascript
vue
是一個MVVM
框架,所謂MVVM
,最核心的就是數據驅動視圖,通俗一點講就是,用戶不直接操做dom
,而是經過操做數據,當數據改變時,vue
內部監聽數據變化而後更新視圖。一樣,用戶在視圖上的操做(事件)也會反過來改變數據。而響應式,則是實現數據驅動視圖的第一步,即監聽數據的變化,使得用戶在設置數據時,能夠通知vue
內部進行視圖更新 好比vue
<template>
<div>
<div> {{ name }} </div>
<button @click="changeName">更名字</button>
</div>
</template>
<script>
export default {
data () {
return {
name: 'A'
}
},
methods: {
changeName () {
this.name = 'B'
}
}
}
</script>
複製代碼
上面代碼,點擊button
按鈕後,name
屬性會改變,同時頁面顯示的A
會變成B
java
vue2.x
實現響應式我想絕大多數人有了解過vue,都應該或多或少的知道一些,vue響應式的核心就是Object.defineProperty()
, 這裏簡單作一個回顧react
const data = {}
let name = 'A'
Object.defineProperty(data, 'name', {
get () {
return name
},
set (val) {
name = val
}
})
console.log(data.name) // get()
data.name = 'B' // set()
複製代碼
上面代碼中咱們能夠看到,Object.defineProperty()的用法就是給一個對象定義一個屬性(方法),並提供set和get兩個內部實現,讓咱們能夠獲取或者設置這個屬性(方法)算法
首先,咱們定義一個初始數據以下api
const data = {
name: 'A',
age: 18,
isStudent: true,
gender: 'male',
girlFriend: {
name: 'B',
age: '19',
isStudent: true,
gender: 'female',
parents: {
mother: {
name: 'C',
age: '44',
isStudent: false,
gender: 'female'
},
father: {
name: 'D',
age: '46',
isStudent: false,
gender: 'male'
}
}
},
hobbies: ['basketball', 'one-piece', 'football', 'hiking']
}
複製代碼
咱們一樣定義一個渲染視圖的方法數組
function renderView () {
// 數據變化時,渲染視圖
}
複製代碼
以及一個實現響應式的核心方法,這個方法接收三個參數,target
就是數據對象自己,key
和value
是對象的key
以及對應的value
瀏覽器
function bindReactive (target, key, value) {
}
複製代碼
最後咱們定義實現響應式的入口方法bash
function reactive () {
// ...
}
複製代碼
咱們最終調用就是框架
const reactiveData = reactive(data)
複製代碼
上面的數據,咱們模擬了一我的的簡單信息介紹,能夠看到對象的字斷值有字符串,數字,布爾值,對象,數組。對於字符串,數字,布爾值這樣的原始類型,咱們直接返回就行了
function reactive () {
// 首先,不是對象直接返回
if (typeof target !== 'object' || val === null) {
return target
}
}
const reactiveData = reactive(data)
複製代碼
若是字段值是對象這樣的引用類型,咱們就須要對對象進行遍歷,分別設置對對象的每個key值作Object.defineProperty()
,注意,這個過程是須要遞歸調用的,由於如咱們給出的數據所示,對象多是多層嵌套的。咱們定義一個函數bindReactive
來描述響應式監聽對象的過程
function bindReactive (target, key, value) {
Object.defineProperty(target, key, {
get () {
return value
},
set (val) {
value = val
// 觸發視圖更新
renderView()
}
})
}
// val是對象key對應的value
function reactive (val) {
// 首先,不是對象直接返回
if (typeof target !== 'object' || val === null) {
return target
}
// 遍歷對象,對每一個key進行響應式監聽
for (let key in target) {
bindReactive(target, key, target[key])
}
}
const reactiveData = reactive(data)
複製代碼
考慮到遞歸,咱們須要在執行核心方法bindReactive
開始時,遞歸的調用reactive
爲對象屬性進行響應式監聽,同時設置(更新)數據時候也要遞歸的調用reactive
更新,因而咱們的核心方法bindReactive
變爲
function bindReactive (target, key, value) {
reactive(value)
Object.defineProperty(target, key, {
get () {
return value
},
set (val) {
reactive(val)
value = val
// 觸發視圖更新
renderView()
}
})
}
// val是對象key對應的value
function reactive (val) {
// 首先,不是對象直接返回
if (typeof target !== 'object' || val === null) {
return target
}
// 遍歷對象,對每一個key進行響應式監聽
for (let key in target) {
bindReactive(target, key, target[key])
}
}
const reactiveData = reactive(data)
複製代碼
上面的代碼能夠作一步優化,就是set的時候,若是新設置的值和以前的值相同,不觸發視圖更新,因而咱們的方法變爲
function bindReactive (target, key, value) {
reactive(value)
Object.defineProperty(target, key, {
get () {
return value
},
set (newVal) {
if (newVal !== value) {
reactive(newVal)
value = newVal
// 觸發視圖更新
renderView()
}
}
})
}
// val是對象key對應的value
function reactive (val) {
// 首先,不是對象直接返回
if (typeof target !== 'object' || val === null) {
return target
}
// 遍歷對象,對每一個key進行響應式監聽
for (let key in target) {
bindReactive(target, key, target[key])
}
}
const reactiveData = reactive(data)
複製代碼
目前,咱們以及實現了對於原始類型和對象的響應式監聽,當數據變化時,會在數據更新後,調用renderView方法(這個方法能夠作任何事情)進行視圖更新。
很明顯,雖然Object.defineProperty()
很好的完成了對於原始類型和普通對象的響應式監聽,可是這個方法對數組是無能爲力的。那麼,vue是如何實現數組的響應式監聽的呢? 咱們首先再次回到vue的官方文檔
能夠看到,vue在執行數組的push, pop, shift, unshift
等方法的時候,是能夠響應式監聽到數組的變化,從而觸發更新視圖的。
可是咱們都知道,數組原生的這些方法,是不具備響應式更新視圖能力的,因此,咱們能夠知道,vue
必定是改寫了數組的這些方法,因而,如今問題就從數組如何實現響應式變成了,如何改寫數組的api。
這裏要用到的核心方法就是Object.create(prototype)
,這個方法就是建立一個對象,他的原型指向參數prototype
,因而,咱們也能夠實現對這些數組方法的改寫了:
// 數組的原型
const prototype = Array.prototype
// 建立一個新的原型對象,他的原型是數組的原型(因而newPrototype上具備全部數組的api)
const newPrototype = Object.create(prototype)
const methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
methods.forEach(method => {
newPrototype[method] = () => {
prototype[method].call(this, ...args)
// 視圖更新
renderView()
}
})
複製代碼
實現了數組的響應式,咱們完善入口方法reactive
function bindReactive (target, key, value) {
reactive(value)
Object.defineProperty(target, key, {
get () {
return value
},
set (newVal) {
if (newVal !== value) {
reactive(newVal)
value = newVal
// 觸發視圖更新
renderView()
}
}
})
}
// val是對象key對應的value
function reactive (val) {
// 首先,不是對象直接返回
if (typeof target !== 'object' || val === null) {
return target
}
// 對於數組,原型修改
if (Array.isArray(val)) {
value.__proto__ = newPrototype
}
// 遍歷對象,對每一個key進行響應式監聽
for (let key in target) {
bindReactive(target, key, target[key])
}
}
const reactiveData = reactive(data)
複製代碼
到目前爲止,咱們已經講述清楚了vue2.x版本的響應式原理
經過咱們的分析,也就看到了vue2.x版本響應式實現的弊端:
Object.defineProperty()
這個api沒法原生的對數組進行響應式監聽Object.defineProperty()
這種實現,以及數組的實現,都存在一個問題,那就是沒辦法監聽到後續的手動新增刪除屬性元素,好比數組,直接經過索引去設置和改變值是不會觸發視圖更新的,固然vue爲咱們提供了vue.set
和vue.delete
這樣的api
,但終究是不方便的vue3.0
實現響應式前不久vue3.0
也正式發佈了,雖然尚未正式的推廣,不過裏面的一些變化是值得咱們去關注和學習的
Proxy
和Reflect
由於vue2.x版本響應式的實現存在的那些問題,vue
官方在3.0版本中徹底重寫了響應式的實現,改用Proxy
和Reflect
代替Object.defineProperty()
。
Proxy
首先來看MDN對Proxy的定義:
The Proxy object is used to define custom behavior for fundamental operations(e.g. property lookup, assignment, enumeration, function invocation, etc).
複製代碼
翻譯爲中文大概就是:Proxy對象用來給一些基本操做定義自定義行爲(好比查找,賦值,枚舉,函數調用等等) 基本用法:
let proxy = new Proxy(target, handler)
複製代碼
上面的參數意義:(注意target
能夠是原生數組)
target
: 用Proxy
包裝的目標對象(能夠是任何類型的對象,包括原生數組
,函數,甚至另外一個代理)。handler
: 一個對象,其屬性是當執行一個操做時定義代理的行爲的函數。舉個栗子:
let handler = {
get: function(target, name){
return name in target ? target[name] : 'sorry, not found';
}
};
let p = new Proxy({}, handler);
p.a = 1;
p.b = undefined;
console.log(p.a, p.b); // 1, undefined
console.log('c' in p, p.c); // false, 'sorry, not found'
複製代碼
Reflect
首先來看MDN對Reflect的定義:
Reflect is a built-in object that provides methods for interceptable JavaScript operations. The methods are the same as those of proxy handlers. Reflect is not a function object, so it's not constructible. 複製代碼
大概意思就是說:Reflect 是一個內置的對象,提供攔截 JavaScript 操做的方法。這些方法與proxy的 handlers相同。Reflect不是一個函數對象,所以它是不可構造的。
Refelct對象提供不少方法,這裏只介紹實現響應式會用到的幾個經常使用方法:
Reflect.get()
: 獲取對象身上某個屬性的值,相似於 target[name]
。Reflect.set()
: 將值分配給屬性的函數。返回一個Boolean
,若是更新成功,則返回true
。Reflect.has()
: 判斷一個對象是否存在某個屬性,和 in
運算符 的功能徹底相同。Reflect.deleteProperty()
: 做爲函數的delete操做符,至關於執行 delete target[name]。因而,咱們能夠聯合Proxy
和Reflect
完成響應式監聽
Proxy
和Reflect
實現響應式下面直接貼出代碼,對以前咱們實現的方法進行改造:
function bindReactive (target) {
if (typeof target !== 'object' || target == null) {
// 不是對象或數組,則直接返回
return target
}
// 由於Proxy原生支持數組,因此這裏不須要本身實現
// if (Array.isArray(value)) {
// value.__proto__ = newPrototype
// }
// 傳給Proxy的handler
const handler = {
get(target, key) {
const reflect = Reflect.get(target, key)
// 當咱們獲取對象屬性時,Proxy只會遞歸到獲取的層級,不會繼續遞歸子層級
return bindReactive(reflect)
},
set(target, key, val) {
// 重複的數據,不處理
if (val === target[key]) {
return true
}
// 這裏能夠更具是不是已有的key,作不一樣的操做
if (Reflect.has(key)) {
} else {
}
const succuss = Reflect.set(target, key, val)
// 設置成功與否
return success
},
deleteProperty(target, key) {
const success = Reflect.deleteProperty(target, key)
// 刪除成功與否
return success
}
}
// 生成proxy對象
const proxy = new Proxy(target, handler)
return proxy
}
// 實現數據響應式監聽
const reactiveData = bindReactive(data)
複製代碼
上述代碼咱們能夠看到,對於vue2.x
響應式存在的問題,都獲得了很好的解決:
Proxy
支持監聽原生數組Proxy
的獲取數據,只會遞歸到須要獲取的層級,不會繼續遞歸Proxy
能夠監聽數據的手動新增和刪除那是否是vue3.0
的響應式方案就是完美的呢,答案是否認的,主要緣由在於Proxy
和Reflect
的瀏覽器兼容問題,且沒法被polyfill
。
本文詳細深刻的剖析了vue
響應式原理,對於2.x
和3.0
版本的實現差別,各有利弊,沒有什麼方案是完美的,相信將來,當瀏覽器兼容問題愈來愈少的時候,生活會更美好!