深刻vue響應式原理(包含vue3.0)

熟悉vue的小夥伴應該都知道,談到vue的原理,最重要的莫過於:響應式,虛擬domdiff算法,模版編譯,今天,咱們一塊兒來深刻vue的響應式,探討vue2.x響應式的實現原理與不足,以及vue3.0版本如何重寫響應式實現方案。javascript

1. 什麼是響應式

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會變成Bjava

2. vue2.x實現響應式

2.1 核心API --- Object.defineProperty()

我想絕大多數人有了解過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兩個內部實現,讓咱們能夠獲取或者設置這個屬性(方法)算法

2.2 如何實現響應式

首先,咱們定義一個初始數據以下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就是數據對象自己,keyvalue是對象的key以及對應的value瀏覽器

function bindReactive (target, key, value) {
    
}
複製代碼

最後咱們定義實現響應式的入口方法bash

function reactive () {
    // ...
}
複製代碼

咱們最終調用就是markdown

const reactiveData = reactive(data)
複製代碼

2.2.1 對於原始類型和對象

上面的數據,咱們模擬了一我的的簡單信息介紹,能夠看到對象的字斷值有字符串,數字,布爾值,對象,數組。對於字符串,數字,布爾值這樣的原始類型,咱們直接返回就行了

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方法(這個方法能夠作任何事情)進行視圖更新。

2.2.2 對於數組

很明顯,雖然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版本的響應式原理

2.3 vue2.x版本響應式實現方案的弊端

經過咱們的分析,也就看到了vue2.x版本響應式實現的弊端:

  1. Object.defineProperty()這個api沒法原生的對數組進行響應式監聽
  2. 實現過程當中對於深度嵌套的數據,遞歸消耗大量性能
  3. 咱們注意到,Object.defineProperty()這種實現,以及數組的實現,都存在一個問題,那就是沒辦法監聽到後續的手動新增刪除屬性元素,好比數組,直接經過索引去設置和改變值是不會觸發視圖更新的,固然vue爲咱們提供了vue.setvue.delete這樣的api,但終究是不方便的

3. vue3.0實現響應式

前不久vue3.0也正式發佈了,雖然尚未正式的推廣,不過裏面的一些變化是值得咱們去關注和學習的

3.1 ProxyReflect

由於vue2.x版本響應式的實現存在的那些問題,vue官方在3.0版本中徹底重寫了響應式的實現,改用ProxyReflect代替Object.defineProperty()

3.1.1 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能夠是原生數組)

  1. target: 用Proxy包裝的目標對象(能夠是任何類型的對象,包括原生數組,函數,甚至另外一個代理)。
  2. 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'
複製代碼

3.1.2 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對象提供不少方法,這裏只介紹實現響應式會用到的幾個經常使用方法:

  1. Reflect.get(): 獲取對象身上某個屬性的值,相似於 target[name]
  2. Reflect.set(): 將值分配給屬性的函數。返回一個Boolean,若是更新成功,則返回true
  3. Reflect.has(): 判斷一個對象是否存在某個屬性,和 in 運算符 的功能徹底相同。
  4. Reflect.deleteProperty(): 做爲函數的delete操做符,至關於執行 delete target[name]。

因而,咱們能夠聯合ProxyReflect完成響應式監聽

3.2 ProxyReflect實現響應式

下面直接貼出代碼,對以前咱們實現的方法進行改造:

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響應式存在的問題,都獲得了很好的解決:

  1. Proxy支持監聽原生數組
  2. Proxy的獲取數據,只會遞歸到須要獲取的層級,不會繼續遞歸
  3. Proxy能夠監聽數據的手動新增和刪除

那是否是vue3.0的響應式方案就是完美的呢,答案是否認的,主要緣由在於ProxyReflect的瀏覽器兼容問題,且沒法被polyfill

4. 總結

本文詳細深刻的剖析了vue響應式原理,對於2.x3.0版本的實現差別,各有利弊,沒有什麼方案是完美的,相信將來,當瀏覽器兼容問題愈來愈少的時候,生活會更美好!

相關文章
相關標籤/搜索