熟悉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 () { // ... } 複製代碼
咱們最終調用就是markdown
const reactiveData = reactive(data) 複製代碼
上面的數據,咱們模擬了一我的的簡單信息介紹,能夠看到對象的字斷值有字符串,數字,布爾值,對象,數組。對於字符串,數字,布爾值這樣的原始類型,咱們直接返回就行了
function reactive (target) { // 首先,不是對象直接返回 if (typeof target !== 'object' || target === 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() } }) } function reactive (target) { // 首先,不是對象直接返回 if (typeof target !== 'object' || target === 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() } }) } function reactive (target) { // 首先,不是對象直接返回 if (typeof target !== 'object' || target === 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() } } }) } function reactive (target) { // 首先,不是對象直接返回 if (typeof target !== 'object' || target === 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() } } }) } function reactive (target) { // 首先,不是對象直接返回 if (typeof target !== 'object' || target === null) { return target } // 對於數組,原型修改 if (Array.isArray(target)) { target.__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(target)) { // target.__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 success = 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
版本的實現差別,各有利弊,沒有什麼方案是完美的,相信將來,當瀏覽器兼容問題愈來愈少的時候,生活會更美好!