學習VUE時遇到的一些問題

記錄一些糾結了好久好久的問題html

爲何data中定義的屬性會直接出如今vm實例對象中 而不是被放到vm的data屬性上?

按照VUE傳遞數據的方式 若是讓我來暴露變量 我會經過這樣的方式:vue

function myVue(config){
    this.data = config.data
}
const vm = new myVue({
    data: {
        msg: "xxx"
    }
})
//獲取到實例裏面的變量
console.log(vm.data.msg)
複製代碼

那麼在vue中是如何處理的呢?數組

const vm = new myVue({
    data: {
        msg: "xxx"
    }
})
console.log(vm.data.msg)//undefined
複製代碼

可見在vm實例對象上並無vm.data.msg這樣的屬性。直接打印vm以後發現 其實msg直接被就放在vm的實例對象上,使用vm.msg就能夠直接獲取到變量值。也就是說 建立vue實例的時候,vue會將data當中的成員帶到vm實例上。瀏覽器

可是爲何要這麼作呢?我能想到的幾個緣由以下app

  1. 方便監控
    既然要實現響應式,那麼咱們得知道數據變化了才能去渲染頁面。這時候就要去監控對象了。並且vue的配置對象中除了data,還有不少其餘的對象須要監聽(例如computed)。咱們去監測一個對象(如監聽vm)是否發生改變,比監聽一個對象裏面的不少個對象(如監聽vm.data和vm.watch和vm.computed)是否發生改變動容易一些。
  2. 方便引用。
    咱們要常用data中的數據,若是到時候滿屏幕的data.xxx,那就很難看了。

總結: 目的是爲了更方便監控數據變化,而後執行某個監聽函數,實現響應式。
目前只想到這麼多 之後有想到什麼再補充一下dom

VUE是怎麼作到響應式的?

爲何咱們一改變數據中的內容VUE就會知道 並且從新渲染頁面嘞?異步

首先,VUE是怎麼監控數據變化的? 在vue2.0中,經過Object.defineProperty來監控數據的變化。在vue3.0中,經過proxy來監控數據的變化。(proxy下次抽時間搞明白了再模擬)函數

用Object.defineProperty實現監控數據

先給出一個基本結構:一個監聽數據的變化性能

const data = {
    name: "小餅"
}
var value = data.name
//給data.name賦值的時候實際上是在給data._name賦值
//若是在裏面使用了data.name會形成死循環
Object.defineProperty(data, "name", {
    //讀取name屬性的時候執行的方法
    get() {
        console.log("讀")
        //return的值就是讀取到的值
        return value
    },
    //設置name屬性的時候執行的方法
    //參數是被從新賦予的值
    set(newVal) {
        console.log("寫")
        value = newVal
    }
})
複製代碼

這樣子咱們就實現了監聽數據"小餅"的變化 接下來須要進行三點改進:測試

  1. 如今只能檢測到data中某一個屬性的改變 咱們但願對全部屬性進行監聽 這時候就要對data進行遍歷 獲得全部的data屬性而且進行監聽
  2. 遍歷的時候總不可能每次都把修改的變量放在value裏面吧 因此咱們要封裝成一個函數 每次執行函數都是一個全新的做用域 把value放在函數裏面就能夠避免賦值的衝突
  3. 排除一些沒必要要的操做 若是一個屬性改以前和改以後是徹底同樣的值 那還改個毛線

綜上 咱們能夠獲得改進以後的代碼:

// 傳入鍵和對應的變量 用於監聽
function defineReactive(obj, key) {
    var value = obj[key]
    Object.defineProperty(obj, key, {
        get() {
            return value
        },
        set(newVal) {
            //若是修改以前和修改以後是同樣的值 就不渲染頁面
            if(value === newVal){
                return;
            }
            //模擬頁面的渲染
            render()
            value = newVal
        }
    })
}
//簡陋的模擬頁面的渲染
function render(){
    console.log("頁面渲染啦")
}
for (key in data) {
    defineReactive(data, key)
}
複製代碼

接下來又有一個問題 當data中的某個屬性的值是一個對象的時候 咱們沒法監聽到他的改變 沒法觸發寫操做 問題以下:

const data = {
    name: "小餅",
    blog: {
        name: "快點吃餅"
    }
}
data.blog.name = "仙女"
console.log(data.blog.name) 
// 會觸發data.blog的讀操做 輸出"仙女"
// 這時候能從新賦值 可是咱們是直接拿出對象裏面的屬性來賦值的 
// 而不是經過defineProperty來賦值的 因此沒法觸發寫操做
複製代碼

這時候咱們就要進行遞歸了 讓他可以觀察對象中的對象

function defineReactive(obj, key) {
    var value = obj[key]
    //若是是對象就先去監控對象裏面的每個屬性
    recursive(value)
    Object.defineProperty(obj, key, {
        get() {
            return value
        },
        set(newVal) {
            value = newVal
            render()
        }
    })
}

//遍歷監控對象中的每個屬性
function recursive(obj) {
    //簡單判斷一下 沒有區分null和Array
    if (typeof obj === "object") {
        for (key in obj) {
            defineReactive(obj, key)
        }
    }
}
複製代碼

這樣就基本實現了利用defineProperty進行對數據的監聽

利用defineProperty的特性 咱們能夠解答一個問題:

爲何vue不能監聽到某些數組操做和對象操做?

在VUE中 不能響應式更新數據的操做有下面幾種:

  1. 數組不存在的索引的改變
  2. 數組長度的改變
  3. 對象的增刪

咱們能夠先用上面寫好的代碼測試一下可否進行這些操做:
首先測試他是否可以監聽到對象的增刪

//添加一個對象中沒有的數據
data.blog.articleNum = 3  //這時沒有執行寫操做 也就說明沒有執行頁面渲染
//根本緣由是使用for(let key in data)的時候並不會遍歷到這個屬性 天然也就沒法監聽
//刪除一個對象中存在的數據
delete data.shan.age
//刪除的時候不可能執行寫操做 因此依然監聽不到 也沒法渲染頁面
複製代碼

再測試他是否能監聽到數組的操做

data.arr[0] = 1  //觸發了寫的操做 頁面從新渲染了
data.arr[100] = 100 //沒有觸發寫的操做 頁面不會從新渲染 
//其實原理和對象是同樣的 他能夠監聽已有的屬性的變化 
//可是並無辦法遍歷到新增的屬性而且進行監聽 
複製代碼

因此咱們能夠得出利用Object.defineProperty實現響應式的劣勢

  1. 天生就須要進行遞歸 (在3.0版本的時候取消了遞歸)
  2. 監聽不到數組不存在的索引的改變
  3. 監聽不到數組長度的改變
  4. 監聽不到對象的增刪

雖然這時候可以用下標去修改一個數組 也能被監聽到 可是當咱們在Vue中使用下標去修改數組中的某個數據的時候是沒辦法修改爲功的 緣由是數組的數據太多了 若是要遍歷去監聽數組中每個數據的變化 是很浪費性能的 因此咱們直接不監聽數組的變化
因此咱們再改進一下代碼

function observer(data){
    //看到數組就返回
    if(Array.isArray(data)){
        return;
    }
    if(typeof data === "object"){
        for(let key in data){
            defineReactive(data, key, data[key])
        }
    }
}
複製代碼

可是你數組變了 不渲染頁面也不行吧~因此VUE直接給咱們提供了一套改造過的API 這些API都會在改變數據以後當即從新渲染頁面

模擬數組變異方法

以push操做爲例:

//保存原來的數組方法
const oldPush = Array.prototype.push
Array.prototype.push = function(){
    //首先要執行本來的數組方法 而後再執行咱們加入的渲染操做
    //傳入this和參數值
    oldPush.call(this, ...arguments);
    //從新渲染頁面
    render()
}
//執行
data.arr.push(100)
複製代碼

可是要修改的原型方法不僅這一個 咱們要修改全部須要重寫的原型方法 這時候要對數組的原型進行修改

//克隆一套原型鏈上的方法 在克隆的方法上去重寫
const arrayProto = Array.prototype
//由於咱們重寫的方法只是針對data中的數組 
//對於普通的數組使用原來提供的數組方法就能夠了
//不要污染原來的原型方法
const arrayMethods = Object.create(arrayProto)
//要修改以前observer中的代碼
function observer(data){
    if(Array.isArray(data)){
        //改變數組的原型 這樣就能使用咱們重寫的原型方法了
        data.__proto__ = arrayMethods;
        return;
    }
    if(typeof data === "object"){...}
}
//指定要修改的方法名 經過遍從來修改裏面的方法
["push", "pop", "shift", "unshift", "sort", "splice", "reverse"].forEach(method = >{
    arrayMethods[method] = function(){
        //先執行原來的方法
        arrayProto[method].call(this, ...arguments);
        //渲染頁面
        render()
    }
})

複製代碼

模擬$set方法和$delete方法

既然都模擬變異方法了 $set$delete方法就一塊兒模擬一下吧(抱着順便寫寫的寫法 不知不覺寫這麼長了...並且好像有點偏離主題了..)
$set修改數據的時候讓他直接渲染頁面 而後給他搞一個defineReactive監聽一下 讓他可以一修改就自動渲染頁面 不然就要老是手動渲染

function $set(data, key, value){
    data[key] = value
    //監聽設置的值 若是設置的是一個對象 還要遞歸監聽對象中的值
    defineReactive(data, key)
    //渲染頁面
    render()
    return value
}

//使用
const value = $set(data.blog, "otherBlog", "仙女")
複製代碼

可是若是是數組在$set 那就不須要使用defineReactive 直接使用splice就能夠了 因此針對數組的狀況還要單獨判斷一下

function $set(data, key, value){
    if(Array.isArray(Data)){
        //簡單寫寫 這裏只是修改值 若是要指定下標添加值就不能這麼作了
        //這裏就不用執行render了 由於splice方法裏面會幫咱們執行render
        data.splice(key, 1, value)
        return value
    }
    data[key] = value
    defineReactive(data, key, value)
    render()
    return value
}
//使用
$set(data.arr, 0, 100)
複製代碼

模擬$delete 同樣的原理

function $delete(data, key){
    if(Array.isArray(Data)){
        data.splice(key, 1)
        return
    }
    delete data[key]
    render()
    return
}
//數組使用
$delete(data.arr, 0)
console.log(data.arr)
//對象使用
$delete(data.shan, "name")
console.log(data.shan)
複製代碼

更改數據後 頁面會馬上從新渲染嗎

測試:

for(let i = 0; i < 10000; i++){
 vm.msg = i
}
//若是每次改變數據都會從新渲染 那麼頁面至關於渲染了10000次 會卡死
//因此他其實是等循環執行結束以後 等到i變成9999了 再去渲染頁面
複製代碼

由此可得 vue會記錄改變的數據 把原來的數據和最後一次改變進行對比 不同就從新渲染 同樣就不渲染了

事實上 若是vue是同步更新dom的 那麼每一次更改數據就要渲染頁面 而操做dom實際上很是浪費性能 可是若是把渲染頁面做爲異步操做 他就能先執行完同步任務 等到同步任務改完數據了 而後再去任務隊列裏面把渲染頁面的任務拿出來執行 減小操做DOM的次數

所以vue更新DOM的操做是異步執行的

只要偵聽到數據變化 VUE將開啓一個異步隊列 即便一個數據被屢次改變 最終也只會把這個渲染任務推入到隊列中一次 這樣能夠避免沒必要要的計算和DOM操做

任務執行的流程: 同步執行棧執行完畢後 會執行異步隊列的任務 在異步隊列中先執行微任務再執行宏任務 若是用戶的瀏覽器支持微任務的話 VUE就把渲染頁面的函數放到微任務中去 若是不支持就只能放到宏任務裏面了

另外一種方式證實頁面是異步渲染的:

<div id="app">{{ msg }}</div>
複製代碼
const vm = new Vue({
  el: '#app',
  data: {
    msg: '小餅'
  }
})
vm.msg = '不知道說什麼好';
console.log(vm.msg); // 輸出不知道說什麼好 說明此時數據已更改
console.log(vm.$el.innerHTML); // 輸出小餅 說明此時頁面還未從新渲染
//在宏任務中去執行 這時候微任務確定執行完了
setTimeout(()=>{
    console.log(vm.$el.innerHTML); // 輸出不知道說什麼好 說明此時頁面已從新渲染
})
複製代碼

vm.$nextTick & Vue.nextTick

先研究一下 明天再寫

相關文章
相關標籤/搜索