vue3.0API詳解


Vue 3.0 於 2020-09-18 發布了,使用了 Typescript 進行了大規模的重構,帶來了 Composition API RFC 版本,類似 React Hook 一樣的寫 Vue,可以自定義自己的 hook ,讓使用者更加的靈活。
為什么推出vue3.x?

一、setup

  • setup 是Vue3.x新增的一個選項, 他是組件內使用 Composition API的入口。
  • setup 函數會在 beforeCreate 之后、created 之前執行。
  • vue3 取消了beforeCreate,created這兩個鈎子,統一用 setup 代替
  • 該函數相當於一個生命周期函數,vue 中過去的 data,methods,watch 等全部都用對應的新增 api 寫在 setup()函數中
<script>
export default {
   components: {},
   setup(props,context){
        context.attrs
        context.slots
        context.parent
        context.root
        context.emit
        context.refs

        return {

        }
   }
}
</script>

參數說明

  1. props: 組件傳入的屬性
  2. context:attrs,emit,slots...

props

  • setup中接受的props是響應式的, 當傳入新的props 時,會及時被更新。
  • 由於是響應式的, 所以不可以使用ES6解構,解構會消除它的響應式。需要結構可以用toRefs()
<template>
<div>
{{data}}
</div>
</template>

<script>
export default {
    props:{
        data:Number
    },
   components: {},
   setup(props,con){
     console.log(props.data)
       return {}
   }
}
</script>

context

  • setup中不能訪問Vue2中最常用的this對象
  • 所以context中就提供了this中最常用的三個屬性:attrs、slot 和emit,
  • 分別對應Vue2.x中的:$attr屬性、slot插槽 和$emit發射事件。

二、reactive、ref

  • vue2.x中, 定義數據都是在data中,
  • Vue3.x中, 使用reactive/ref來進行數據定義。

1、reactive

  1. reactive() 函數接收一個普通對象,返回一個響應式的數據對象
  2. setup 中 return 出去,
  3. 直接在 template 中調用即可
<template>
  <div>
    <!-- 3、調用 -->
    <span>{{ user.name }}</span>
    <span>{{ user.label }}</span>
  </div>
</template>

<script>
import { reactive } from "vue";
export default {
  components: {},
  setup(props, con) {
      //1、定義
    const user = reactive({
      name: "夏利",
      label: "",
    }); 
    //2、導出
    return {
      user,
    };
  },
};
</script>

不能直接對user進行結構, 這樣會消除它的響應式, 這里就和上面我們說props不能使用ES6直接解構就呼應上了。
那我們就想使用解構后的數據怎么辦?
解決辦法就是使用toRefs。

return {
    // 使用reRefs
    ...toRefs(user)
}

2、ref() 函數

ref() 函數用來根據給定的值創建一個響應式的數據對象,ref() 函數調用的返回值是一個對象,這個對象上只包含一個 value 屬性, 只在 setup 函數內部訪問 ref 函數需要加.value

<template>
    <div class="mine">
        {{count}} // 10
    </div>
</template>

<script >
import { ref } from 'vue';
export default ({
  setup() {
    const count = ref(10)
    // 在js 中獲取ref 中定義的值, 需要通過value屬性
    console.log(count.value);
    /這里ref取值需要加value
    if (count.value > 5) {
      console.log('牛逼')
    } else {
     console.log( "一般")
    }
    return {
       count
    }
   }
});
</script>

reactive中訪問ref

<template>
  <div class="mine">{{ count }} -{{ t }}-{{ tD }} </div>
</template>

<script >
import { reactive, ref, toRefs } from "vue";
export default {
  setup() {
    const count = ref(10);

    const obj = reactive({
      t: 100,
      count,
    });
    const tD = ref(obj.t);
    return {
      tD,
      ...toRefs(obj),
    };
  },
};
</script>

reactive與ref區別

  1. reactive用於處理對象的雙向綁定,ref則處理js基礎類型的雙向綁定,
  2. reactive不能代理基本類型,例如字符串、數字、boolean等。

三、isRef() 、toRefs()、unref()

1、ref訪問dom

通過 ref 來獲取真實 dom 元素, 這個和 react 的用法一樣,為了獲得對模板內元素或組件實例的引用,我們可以像往常一樣在 setup()中聲明一個 ref 並返回它

  1. 還是跟往常一樣,在 html 中寫入 ref 的名稱
  2. 在steup 中定義一個 ref
  3. steup 中返回 ref的實例
  4. onMounted 中可以得到 ref的RefImpl的對象, 通過.value 獲取真實dom
<template>
  <!--第一步:還是跟往常一樣,在 html 中寫入 ref 的名稱-->
  <div class="mine" ref="elmRefs">
    <span>1111</span>
  </div>
</template>

<script >
import { set } from 'lodash';
import { onMounted, ref } from 'vue';
export default{
  setup(props, context) {
    // 獲取真實dom
    const elmRefs = ref(null);
    onMounted (() => {
      console.log(elmRefs.value); // 得到一個 RefImpl 的對象, 通過 .value 訪問到數據
    })

    return {
      elmRefs
    }
  }
}
</script>

2、isRef()

isRef() 用來判斷某個值是否為 ref() 創建出來的對象

<script>
import { , isRef, ref } from 'vue';
export default ({
  setup(props, context) {
    const name= 'vue'
    const age = ref(18)
    console.log(isRef(age)); // true
    console.log(isRef(name)); // false
    return {
      age,
      name
    }
  }
});
</script>

3、toRefs()

  • toRefs() 函數可以將 reactive() 創建出來的響應式對象,轉換為普通的對象
  • 不過,這個對象上的每個屬性節點,都是 ref() 類型的響應式數據
<template>
  <div class="mine">
    {{name}} // test
    {{age}} // 18
  </div>
</template>

<script >
import {  reactive, ref, toRefs } from 'vue';
export default ({
  setup(props, context) {
    let state = reactive({
      name: 'test'
    });

    const age = ref(18)

    return {
      ...toRefs(state),
      age
    }
  }
});
</script>

4、unref()

unRef() 用來判斷某個值是否為 ref() 創建出來的對象有拋出該對象

setup(props, context) {
    const name: string = 'vue'
    const age = ref(18)
    console.log(unRef(age)); // 18
    console.log(unRef(name)); // vue

    return {
      age,
      name
    }
}

四、computed()

  • 該函數用來創造計算屬性,和過去一樣,它返回的值是一個 ref 對象。
  • 里面可以傳方法,或者一個對象,對象中包含 set()、get()方法。

創建只讀的計算屬性

復制代碼
import { computed, ref } from 'vue';
export default ({
  setup(props, context) {
    const age = ref(18)

    // 根據 age 的值,創建一個響應式的計算屬性 readOnlyAge,它會根據依賴的 ref 自動計算並返回一個新的 ref
    const readOnlyAge = computed(() => age.value++) // 19

    return {
      age,
      readOnlyAge
    }
  }
});
</script>

通過 set()、get()方法創建一個可讀可寫的計算屬性

<template>
  <div>
    <p>refCount: {{refCount}}</p>
    <p>計算屬性的值computedCount : {{computedCount}}</p>
    <button @click="refCount++">refCount + 1</button>
  </div>
</template>

<script>
import { computed, ref } from '@vue/composition-api'
export default {
  setup() {
    const refCount = ref(1)
    // 可讀可寫
    let computedCount = computed({
      // 取值函數
      get: () => refCount.value + 1,
      // 賦值函數
      set: val => {
        refCount.value = refCount.value -5
      }
    })
  //觸發get函數
    console.log(computedCount.value)

    // 為計算屬性賦值的操作,會觸發 set 函數
    computedCount.value = 10
    console.log(computedCount.value)
    // 觸發 set 函數后,count 的值會被更新
    console.log(refCount.value)

    return {
      refCount,
      computedCount
    }
  }
};
</script>

五、watch()與 watchEffect

1、watch

  • watch 函數用來偵聽特定的數據源,並在回調函數中執行副作用。
  • 默認情況是惰性的,也就是說僅在偵聽的源數據變更時才執行回調。
    1. 需要一個明確的數據資源
    2. 需要有一個回調函數
    3. 等到改變才會執行函數

監聽用 ref 聲明的數據源

<script >
import {  ref, watch } from 'vue';
export default ({
  setup(props, context) {
    const age = ref(10);

    watch(age,
     (nowValue,oldValue) =>{ 
         console.log(age.value)
         }
         ); // 100
     // 4、ref簡寫 
    watch(age, () => {});
    // 修改age 時會觸發watch 的回調, 打印變更后的值
    age.value = 100
    return {
      age
    }
  }
});
</script>

監聽用 reactive 聲明的數據源

監聽對象獲數組
  • 第一個參數傳入的 state 對象,
  • 第二個參數是回調函數,
  • 只要 state 中任意的屬性發生改變都會執行回調函數,和 vue2 的區別是不要寫 deep 屬性,默認就是深度監聽了。
<script >
import {  reactive, toRefs, watch } from 'vue';

export default ({
  setup(props, context) {
    const state = reactive({ name: 'vue', age: 10 })

    watch(
     state,
      (nowAge, oldAge) => {
      }
    )
    // 修改age 時會觸發watch 的回調, 打印變更前后的值
    state.age = 100
    return {
      ...toRefs(state)
    }
  }
});
</script>
監聽對象或數組中某個值

現在是監聽整個對象,當然我們也可以監聽對象上的某個屬性,注意下面代碼的寫法:第一個是回調函數,第二個參數也是回調函數。

<script >
import {  reactive, toRefs, watch } from 'vue';

export default ({
  setup(props, context) {
    const state = reactive({ name: 'vue', age: 10 })

    watch(
      () => state.age,
      (age, preAge) => {
        console.log(age); // 100
        console.log(preAge); // 10
      }
    )
    // 修改age 時會觸發watch 的回調, 打印變更前后的值
    state.age = 100
    return {
      ...toRefs(state)
    }
  }
});
</script>

同時監聽多個值

setup(props, context) {
    const state = reactive({ name: "vue", age: 10 });
    const count = ref(87);
    watch(
      [() => state.name, () => state.age, count],
      ([newName, newAge, count], [oldName, oldAge, oldCount]) => {
        console.log("111111111111111111111111");
        console.log(newName, "names");
        console.log("111111111111111111111111", oldCount);
        console.log(newAge);

        console.log(oldName);
        console.log(oldAge);
      }
    );
    // 修改age 時會觸發watch 的回調, 打印變更前后的值, 此時需要注意, 更改其中一個值, 都會執行watch的回調
    state.age = 100;
    state.name = "vue3";
    count.value += 1;
    return {
      ...toRefs(state),
    };
  },
};

stop 停止監聽

  • 在 setup() 函數內創建的 watch 監視,會在當前組件被銷毀的時候自動停止。
  • 如果想要明確地停止某個監視,可以調用 watch() 函數的返回值即可(stop()),語法如下:
setup(props, context) {
    const state = reactive({ name: 'vue', age: 10 })

    const stop =  watch(
      [() => state.age, () => state.name],
      ([newName, newAge], [oldName, oldAge]) => {
        console.log(newName);
        console.log(newAge);

        console.log(oldName);
        console.log(oldAge);
      }
    )
    // 修改age 時會觸發watch 的回調, 打印變更前后的值, 此時需要注意, 更改其中一個值, 都會執行watch的回調
    state.age = 100
    state.name = 'vue3'

    setTimeout(()=> {
      stop()
      // 此時修改時, 不會觸發watch 回調
      state.age = 1000
      state.name = 'vue3-'
    }, 1000) // 1秒之后講取消watch的監聽

    return {
      ...toRefs(state)
    }
  }

2、watchEffect

該函數有點像update函數,但他執行在update與beforeuodate之前,監聽的是所有數據的變化。

  1. 首次加載會立即執行
  2. 響應的最終所有依賴監聽變化(數據改變)
  3. 在卸載onUnmounte時自動停止
  4. 執行stop就會停止監聽,否則一直監聽
  5. 異步函數先執行再去監聽改變
<script>
import { watchEffect, ref, onMounted } from "vue";
export default {
  components: {},
  setup(props, con) {
    const count = ref(0);

    setTimeout(() => {
      count.value = 1;
    }, 2000);

    const stop = watchEffect(
      () => {
        /*
         *    1、首次加載會立即執行
         *    2、響應的最終所有依賴監聽變化(數據改變)
         *    3、在卸載onUnmounte時自動停止
         *    4、執行stop就會停止監聽,否則一直監聽
         *    5、異步函數先執行再去監聽改變
         */
      },
      {
        // 6、在update之后執行
        flush: "post",
        // 同步執行
        flush: "async",
      }
    );
    setTimeout(() => {
      stop();
    }, 4000);

    // 7、組件掛在ref
    const myRef = ref(null);
    // 避免監聽時先見聽到null 在監聽到h1
    onMounted(() => {
      watchEffect(() => {
        console.log(myRef.value);
      });
    });
    // 8、debugging  開發模式使用
    watchEffect(
      () => {
        console.log(count.value);
      },
      {
        onTrack(e) {
          // 監聽到count和改變count
        },
        onTrigger(e) {
          // count改變了會觸發
        },
      }
    );
    return {
      myRef,
      count,
    };
  },
};
</script>

六、生命周期

  • 新版的生命周期函數,可以按需導入到組件中,且只能在 setup() 函數中使用, 但是也可以在 setup 外定義, 在 setup 中使用

  • 我們可以看到beforeCreate和created被setup替換了(但是Vue3中你仍然可以使用, 因為Vue3是向下兼容的, 也就是你實際使用的是vue2的)。

  • 鈎子命名都增加了on;

  • Vue3.x還新增用於調試的鈎子函數onRenderTriggered和onRenderTricked

<script>
import {
  defineComponent,
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onErrorCaptured,
  onRenderTracked,
  onRenderTriggered,
} from "vue";

export default defineComponent({
  // beforeCreate和created是vue2的
  beforeCreate() {
    console.log("------beforeCreate-----");
  },
  created() {
    console.log("------created-----");
  },
  setup() {
     onBeforeMount(()=> {
      console.log('beformounted!')
    })
    onMounted(() => {
      console.log('mounted!')
    })

    onBeforeUpdate(()=> {
      console.log('beforupdated!')
    })
    onUpdated(() => {
      console.log('updated!')
    })

    onBeforeUnmount(()=> {
      console.log('beforunmounted!')
    })
    onUnmounted(() => {
      console.log('unmounted!')
    })

    onErrorCaptured(()=> {
      console.log('errorCaptured!')
    })
  },
});
</script>

七、vue 的全局配置

vue的config配置

通過 vue 實例上 config 來配置,包含 Vue 應用程序全局配置的對象。
您可以在掛載應用程序之前修改下面列出的屬性:

const app = Vue.createApp({})
app.config = {...}

為組件渲染功能和觀察程序期間的未捕獲錯誤分配處理程序。
錯誤和應用程序實例將調用處理程序

app.config.errorHandler = (err, vm, info) => {}

全局定義屬性 globalProperties

在項目中往往會全局定義公共屬性或方法,方便我們組件間調用。

import { createApp } from 'vue'
import App from './App.vue'
import utils from './utils'

// vue2.0寫法
let vue=new Vue()
vue.prototype.utils=utils

// vue3.0寫法
const app=createApp(App)
app.config.globalProperties.utils=utils

app.mount('#app')

vue3.0中使用全局定義的屬性

  • 可以在組件用通過 getCurrentInstance() 來獲取全局 globalProperties 中配置的信息,getCurrentInstance 方法獲取當前組件的實例,
  • 然后通過 ctx 屬性獲得當前上下文,這樣我們就能在 setup 中使用 router 和 vuex, 通過這個屬性我們就可以操作變量、全局屬性、組件屬性等等
<script>
import { getCurrentInstance } from "vue";
export default {
  components: {},
  setup(props, con) {
    const { ctx } = getCurrentInstance();
    console.log(ctx.utils);
  },
};
</script>

八、自定義Hooks

一個實現加減的例子, 這里可以將其封裝成一個hook, 我們約定這些「自定義 Hook」以 use 作為前綴,和普通的函數加以區分。
useCount.js

import {ref,computed} from 'vue'
export default function useCount(initValue=1) {
    const count=ref(initValue)
    const inCrearse=(delta)=>{
        if(delta){
            count.value+=delta
        }else{
            count.value+=1
        }
    }
    const multiple=computed(()=>count.value*2)
    return{
        count,multiple,inCrearse
    }
 }

接下來看一下在組件中使用useCount這個 hook:

<template>
  <div>
    <p>count: {{ count }}</p>
    <p>倍數: {{ multiple }}</p>
    <div>
      <button @click="increase()">加1</button>
      <button @click="decrease()">減一</button>
    </div>
  </div>
</template>

<script>
import useCount from "../hooks/useCount";
export default {
  components: {},
  //con==context(attrs,emit,slots)
  setup(props, con) {
    const { count, multiple, increase, decrease } = useCount(10);
    return {
      count,
      multiple,
      increase,
      decrease,
    };
  },
};
</script>

<style lang='scss' scoped>
</style>

九、Teleport自定義傳送門

舉例:我們希望繼續在組件內部使用Dialog,又希望渲染的DOM結構不嵌套在組件的DOM中。
我們可以用 包裹Dialog, 此時就建立了一個傳送門,可以將Dialog渲染的內容傳送到任何指定的地方。

使用

  1. index.html
    Dialog渲染的dom和頂層組件是兄弟節點關系, 在index.html文件中定義一個供掛載的元素:
<div id="app"></div>
    <div id="dialog"></div>
</body>
  1. Dialog.vue,
    定義一個Dialog組件Dialog.vue, 留意 to 屬性, 與上面的id選擇器一致:
<template>
    <teleport to="#dialog">
        <div class="dialog">
            <div class="dialog_wrapper">
                <div class="dialog_header" v-if="title">
                    <slot name="header">
                        <span>{{title}}</span>
                    </slot>
                </div>
            </div>
            <div class="dialog_content">
                <slot></slot>
            </div>
            <div class="dialog_footer">
                <slot name="footer"></slot>
            </div>
        </div>
    </teleport>
</template
  1. Header.vue
    最后在一個子組件Header.vue中使用Dialog組件,這里主要演示 Teleport的使用,不相關的代碼就省略了。header組件
<div class="header">
    ...
    <navbar />
+    <Dialog v-if="dialogVisible"></Dialog>
</div>
...

可以看到,我們使用 teleport 組件,通過 to 屬性,指定該組件渲染的位置與

同級,也就是在 body 下,但是 Dialog 的狀態 dialogVisible 又是完全由內部 Vue 組件控制

十、Suspense 異步組件

在vue2.0前后端交互獲取數據時, 是一個異步過程,一般我們都會提供一個加載中的動畫,當數據返回時配合v-if來控制數據顯示。

<div>
    <div v-if="!loading">
        ...
    </div>
    <div v-if="loading">
        加載中...
    </div>
</div>

如果你使用過vue-async-manager這個插件來完成上面的需求, 你對Suspense可能不會陌生,Vue3.x感覺就是參考了vue-async-manager.

Suspense, 它提供兩個template slot, 剛開始會渲染一個fallback狀態下的內容, 直到到達某個條件后才會渲染default狀態的正式內容, 通過使用Suspense組件進行展示異步渲染就更加的簡單。如果使用 Suspense, 要返回一個promise 組件的使用:

<Suspense>
        <template #default>
            <async-component></async-component>
        </template>
        <template #fallback>
            <div>
                Loading...
            </div>
        </template>
    </Suspense>

其他

1、readonly 只讀屬性

在使用readonly后重新進行復制是不允許修改的,這個api不常用

setup(props,con){
    const reactiveObj=reactive({
        a:1,b:2,
        c:{
            d:3,e:4
        }
    })
    const newReactiveObj=readonly(reactiveObj)
    reactiveObj.a=10
    console.log(reactiveObj.a)//10
     newReactiveObj.a=30
    console.log(newReactiveObj.a)//10
}

2、片段(Fragment)

  • 在 Vue2.x 中, template中只允許有一個根節點:
  • 但是在 Vue3.x 中,你可以直接寫多個根節點, 是不是很爽:
<template>
    <span></span>
    <span></span>
</template>

3、vue3與vue2相比的一些變動

slot 具名插槽語法

在Vue3.0中將slot和slot-scope進行了合並。

// 子組件 
<slot name="content" :data="data"></slot>
export default {
    data(){
        return{
            data:["1234","2234","3234"]
        }
    }
}
<!-- 父組件中使用 -->
 <template v-slot:content="scoped">
   <div v-for="item in scoped.data">{{item}}</div>
</template>

<!-- 也可以簡寫成: -->
<template #content="{data}">
    <div v-for="item in data">{{item}}</div>
</template>

數據響應對比

響應方法

  1. vue2.x:object.defineProperty數據劫持
  2. vue3.x:使用ES6的proxy映射

vue2.x響應實現原理

  1. vue 雙向數據綁定是通過 數據劫持 結合 發布訂閱模式的方式來實現的,也就是說數據和視圖同步,數據發生變化,視圖跟着變化,視圖變化,數據也隨之發生改變;
  2. 關於VUE雙向數據綁定核心: Object.defineProperty()
    1. 三個參數:
      • obj:要定義其上屬性的對象
      • prop:要定義或修改的屬性
      • descriptor:具體的改變方法
    2. 簡單地說,就是用這個方法來定義一個值。當調用時我們使用了它里面的get方法,當我們給這個屬性賦值時,又用到了它里面的set方法;
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>雙向數據綁定原理</title>
</head>
<body>
<h1>極簡版的雙向數據綁定</h1>
<input type="text" id="txt_id">
<p id="p_id"></p>


<script>
    var obj = {}
    Object.defineProperty(obj, 'newKey', { //這里的newKey相當於data里的屬性
        get: function () {
            console.log('觸發get操作')
            return 'abd'
        },
        set: function (value) {
            console.log('觸發set操作')
            document.getElementById('p_id').innerText = value
        },
        // value: 'pcm',
        // writable: true
        // 注意:get、set不能和value、writable等屬性共同存在,原因如下:
        // 如果一個描述符不具有value,writable,get 和 set 任意一個關鍵字,那么它將被認為
        // 是一個數據描述符。如果一個描述符同時有(value或writable)和(get或set)關鍵字,將
        // 會產生一個異常。 ---------取自MDN原話
    })
    console.log(obj.newKey)

    document.addEventListener('keyup', function (e) {
        let ev = e || event
        obj.newKey = ev.target.value

    })
</script>
</body>
</html>

發布訂閱模式的方式來實現

class Subject {
    constructor () {
       this.state = 0
       this.observes = []
    }
    getState () {
        return this.state
    }
    setState (state) {
        this.state = state
        this.notifyAllObservers()
    }
    attach (observer) {
       this.observes.push(observer)
    }
    notifyAllObservers () {
        this.observes.forEach(observe => {
            observe.update()
        })
    }
}

class Observer {
    constructor (name, subject) {
        this.name = name
        this.subject = subject
        this.subject.attach(this)
    }
    update () {
        console.log(`${this.name} update, state: ${this.subject.getState()}`)
    }
}

let subject = new Subject()
let o1 = new Observer('o1', subject)
let o2 = new Observer('o2', subject)

subject.setState(2)

vue3.x響應實現原理

vue3.x響應原理詳解
proxy
Reflect
map
set
首先熟練一下ES6中的 Proxy、Reflect 及 ES6中為我們提供的 Map、Set兩種數據結構。

  • Proxy
    用於創建一個對象的代理,從而實現基本操作的攔截和自定義(如屬性查找、賦值、枚舉、函數調用等)

    語法:const p = new Proxy(target, handler)
    參數:

    • target:目標對象(可以是任何類型的對象,包括原生數組,函數,甚至另一個代理)。
    • handler:一個通常以函數作為屬性的對象,各屬性中的函數分別定義了在執行各種操作時代理 p 的行為。
    const handler = {
        get: function(obj, prop) {
            return prop in obj ? obj[prop] : 37;
        }
    };
    
    const 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, 37
    
  • Reflect
    是一個內置的對象,它提供攔截 JavaScript 操作的方法
    1. 作為函數的delete操作符,相當於執行 delete target[name]。

      Reflect.deleteProperty(target, propertyKey)
        

    2. 將值分配給屬性的函數。返回一個Boolean,如果更新成功,則返回true。

      Reflect.set(target, propertyKey, value[, receiver])
        

    3. 獲取對象身上某個屬性的值,類似於 target[name]。

      Reflect.get(target, propertyKey[, receiver])

    let p = Vue.reactive({name:'youxuan'});
    Vue.effect(()=>{ // effect方法會立即被觸發
        console.log(p.name);
    })
    p.name = 'webyouxuan';; // 修改屬性后會再次觸發effect方法
    
  • reactive方法實現
    通過proxy 自定義獲取、增加、刪除等行為
    // 1、聲明響應式對象
    function reactive(target) {
        return createReactiveObject(target);
    }
    // 是否是對象類型
    function isObject(target) {
        return typeof target === 'object' && target !== null;
    }
    // 2、創建
    function createReactiveObject(target) {
        // 判斷target是不是對象,不是對象不必繼續
        if (!isObject(target)) {
            return target;
        }
        const handlers = {
            get(target, key, receiver) { // 取值
                console.log('獲取')
                let res = Reflect.get(target, key, receiver);
                return res;
            },
            set(target, key, value, receiver) { // 更改 、 新增屬性
                console.log('設置')
                let result = Reflect.set(target, key, value, receiver);
                return result;
            },
            deleteProperty(target, key) { // 刪除屬性
                console.log('刪除')
                const result = Reflect.deleteProperty(target, key);
                return result;
            }
        }
        // 開始代理
        observed = new Proxy(target, handlers);
        return observed;
    }
    
    let p = reactive({ name: 'youxuan' });
    console.log(p.name); // 獲取
    p.name = 'webyouxuan'; // 設置
    delete p.name; // 刪除
    
    ``` 
    

對比

為何替換掉Object.defineProperty?

  1. vue2.x問題:
    1. 存在更新對象類型時無法即時渲染。
    2. 需要用到$set或$forceUpdata強制刷新。
    3. vm.items[indexOfItem] = newValue這種是無法檢測的
  2. 事實上,Object.defineProperty 本身是可以監控到數組下標的變化的,只是在 Vue 的實現中,從性能 / 體驗的性價比考慮,放棄了這個特性。

那么Object.defineProperty 和 Proxy 對比存在哪些優缺點呢?

  1. 只能劫持對象的屬性,而 Proxy 是直接代理對象。

    • Object.defineProperty 只能對屬性進行劫持,需要遍歷對象的每個屬性,如果屬性值也是對象,則需要深度遍歷。
    • Proxy 直接代理對象,不需要遍歷操作。
  2. 對新增屬性需要手動進行 Observe。

    • Object.defineProperty 劫持的是對象的屬性,所以新增屬性時,需要重新遍歷對象,對其新增屬性再使用 Object.defineProperty 進行劫持。
    • 也正是因為這個原因,使用 Vue 給 data 中的數組或對象新增屬性時,需要使用 vm.$set 才能保證新增的屬性也是響應式的。

總結

  1. Object.defineProperty 並非不能監控數組下標的變化,Vue2.x 中無法通過數組索引來實現響應式數據的自動更新是 Vue 本身的設計導致的,不是 defineProperty 的鍋。
  2. Object.defineProperty 和 Proxy 本質差別是,defineProperty 只能對屬性進行劫持,所以出現了需要遞歸遍歷,新增屬性需要手動 Observe 的問題。
  3. Proxy 作為新標准,瀏覽器廠商勢必會對其進行持續優化,但它的兼容性也是塊硬傷,並且目前還沒有完整的 polyfill 方案。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM