Vue 3.0 於 2020-09-18 發布了,使用了 Typescript 進行了大規模的重構,帶來了 Composition API RFC 版本,類似 React Hook 一樣的寫 Vue,可以自定義自己的 hook ,讓使用者更加的靈活。
為什么推出vue3.x?
-
2.x對ts支持不夠友好(所有的屬性都放在了this對象上,很難推斷組件的數據類型)
-
2.x大量的API掛在到了Vue對象原型上,難以實現TreeShaking
-
2.x架構層面對跨平台 dom 渲染開發支持不友好
-
3.x采用CompositionAPI,受React Hook啟發
-
3.x支持的template多個跟標簽
-
3.x對虛擬DOM進行了從寫,對模板編譯進行了優化
一、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>
參數說明
- props: 組件傳入的屬性
- 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
- reactive() 函數接收一個普通對象,返回一個響應式的數據對象
- setup 中 return 出去,
- 直接在 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區別
- reactive用於處理對象的雙向綁定,ref則處理js基礎類型的雙向綁定,
- reactive不能代理基本類型,例如字符串、數字、boolean等。
三、isRef() 、toRefs()、unref()
1、ref訪問dom
通過 ref 來獲取真實 dom 元素, 這個和 react 的用法一樣,為了獲得對模板內元素或組件實例的引用,我們可以像往常一樣在 setup()中聲明一個 ref 並返回它
- 還是跟往常一樣,在 html 中寫入 ref 的名稱
- 在steup 中定義一個 ref
- steup 中返回 ref的實例
- 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 函數用來偵聽特定的數據源,並在回調函數中執行副作用。
- 默認情況是惰性的,也就是說僅在偵聽的源數據變更時才執行回調。
- 需要一個明確的數據資源
- 需要有一個回調函數
- 等到改變才會執行函數
監聽用 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之前,監聽的是所有數據的變化。
- 首次加載會立即執行
- 響應的最終所有依賴監聽變化(數據改變)
- 在卸載onUnmounte時自動停止
- 執行stop就會停止監聽,否則一直監聽
- 異步函數先執行再去監聽改變
<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中。
我們可以用
使用
- index.html
Dialog渲染的dom和頂層組件是兄弟節點關系, 在index.html文件中定義一個供掛載的元素:
<div id="app"></div>
<div id="dialog"></div>
</body>
- 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
- 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>
數據響應對比
響應方法
- vue2.x:object.defineProperty數據劫持
- vue3.x:使用ES6的proxy映射
vue2.x響應實現原理
- vue 雙向數據綁定是通過 數據劫持 結合 發布訂閱模式的方式來實現的,也就是說數據和視圖同步,數據發生變化,視圖跟着變化,視圖變化,數據也隨之發生改變;
- 關於VUE雙向數據綁定核心: Object.defineProperty()
- 三個參數:
- obj:要定義其上屬性的對象
- prop:要定義或修改的屬性
- descriptor:具體的改變方法
- 簡單地說,就是用這個方法來定義一個值。當調用時我們使用了它里面的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 操作的方法- 作為函數的delete操作符,相當於執行 delete target[name]。
Reflect.deleteProperty(target, propertyKey)
- 將值分配給屬性的函數。返回一個Boolean,如果更新成功,則返回true。
Reflect.set(target, propertyKey, value[, receiver])
- 獲取對象身上某個屬性的值,類似於 target[name]。
Reflect.get(target, propertyKey[, receiver])
let p = Vue.reactive({name:'youxuan'}); Vue.effect(()=>{ // effect方法會立即被觸發 console.log(p.name); }) p.name = 'webyouxuan';; // 修改屬性后會再次觸發effect方法
- 作為函數的delete操作符,相當於執行 delete target[name]。
- 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?
- vue2.x問題:
- 存在更新對象類型時無法即時渲染。
- 需要用到$set或$forceUpdata強制刷新。
- vm.items[indexOfItem] = newValue這種是無法檢測的
- 事實上,Object.defineProperty 本身是可以監控到數組下標的變化的,只是在 Vue 的實現中,從性能 / 體驗的性價比考慮,放棄了這個特性。
那么Object.defineProperty 和 Proxy 對比存在哪些優缺點呢?
-
只能劫持對象的屬性,而 Proxy 是直接代理對象。
- Object.defineProperty 只能對屬性進行劫持,需要遍歷對象的每個屬性,如果屬性值也是對象,則需要深度遍歷。
- Proxy 直接代理對象,不需要遍歷操作。
-
對新增屬性需要手動進行 Observe。
- Object.defineProperty 劫持的是對象的屬性,所以新增屬性時,需要重新遍歷對象,對其新增屬性再使用 Object.defineProperty 進行劫持。
- 也正是因為這個原因,使用 Vue 給 data 中的數組或對象新增屬性時,需要使用 vm.$set 才能保證新增的屬性也是響應式的。
總結
- Object.defineProperty 並非不能監控數組下標的變化,Vue2.x 中無法通過數組索引來實現響應式數據的自動更新是 Vue 本身的設計導致的,不是 defineProperty 的鍋。
- Object.defineProperty 和 Proxy 本質差別是,defineProperty 只能對屬性進行劫持,所以出現了需要遞歸遍歷,新增屬性需要手動 Observe 的問題。
- Proxy 作為新標准,瀏覽器廠商勢必會對其進行持續優化,但它的兼容性也是塊硬傷,並且目前還沒有完整的 polyfill 方案。