初始化Vue實例時,Vue將遞歸遍歷data
對象,經過Object.defineProperty
將其中已有的屬性轉化爲響應式的屬性(getter/setter)。響應式屬性的變化纔可以被Vue觀察到。
這就是爲何,Vue文檔建議咱們在初始化Vue實例以前,提早初始化data
中全部可能用到的屬性。若是想要在Vue實例建立之後添加響應式屬性,須要使用Vue.set(object, key, value)
,而不能直接經過賦值來添加新屬性(這樣添加的新屬性不具備響應性)。html
在 運行時才能肯定數據屬性的鍵,這稱爲 動態屬性。相對地,若是在 編程時就能肯定屬性的鍵,這稱爲 靜態屬性。
注意,Vue.set的第一個參數不能是Vue實例或者Vue實例的數據對象,能夠是數據對象內嵌套的對象,或者props中的對象。也就是說,不能動態添加根級響應式屬性。vue
Vue文檔: Vue does not allow dynamically adding new
root-level reactive properties to an
already created instance. However, it’s possible to add reactive properties to a
nested object using the
Vue.set(object, key, value)
method.
let vm = new Vue({ data: { nestedObj: {} } }); // 建立Vue實例 Vue.set(vm, 'a', 2); // not works,不能爲Vue實例添加根級響應式屬性 Vue.set(vm.$data, 'b', 2); // not works,不能爲Vue數據對象添加根級響應式屬性 Vue.set(vm.nestedObj, 'c', 2); // works,vm.nestedObj是數據對象內的一個嵌套對象 Vue.set(vm.$data.nestedObj, 'd', 2); // works,vm.$data.nestedObj是數據對象內的一個嵌套對象
Vue.set會作適當的檢查並報錯:set源碼。react
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> <test-dynamic></test-dynamic> </div> </body> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script> <script> const testDynamicComponent = { template: ` <div> <button @click="onClick">test</button> <p v-if="show">{{ nestedObj.dynamic }}</p> </div> `, data() { return ({ show: false, nestedObj: {} }) }, methods: { onClick() { // Vue.set(this, 'dynamic', 'wait 2 seconds...'); // this will not works! // Vue.set(this.$data, 'dynamic', 'wait 2 seconds...'); // this will not works! Vue.set(this.$data.nestedObj, 'dynamic', 'wait 2 seconds...'); // this works // Vue.set(this.nestedObj, 'dynamic', 'wait 2 seconds...'); // this also works this.show = true; setTimeout(() => { this.nestedObj.dynamic = 'createReactiveProxy works!'; }, 2000); } } }; var app = new Vue({ el: '#app', components: { 'test-dynamic': testDynamicComponent } }) </script> </html>
實際使用場景中,有時碰到這種狀況:在建立Vue實例的時候,你還不肯定會用到哪些屬性(須要與用戶進行交互以後才知道),或者有大量的屬性都有可能被用到(而你不想爲數據對象初始化那麼多的屬性)。這時候,提早初始化全部數據對象的屬性就不太現實了。git
一個原始的解決方案:與用戶交互的過程當中,每當發現須要用到新的屬性,就經過Vue.set
添加響應式屬性。github
牢記上面講到的
Vue.set的限制。動態添加的屬性只能放在data內嵌套的對象中,或者props中的對象。實戰中能夠在data數據對象中專門用一個屬性來存放動態屬性,好比
data: { staticProp1: '', staticProp2: '', dynamicProps: {} }
。
在這個方法的基礎上,能夠擴展出一個一勞永逸的方案:使用ES6 Proxy,爲data
建立一個代理,攔截對data
的賦值操做,若是發現此次賦值是屬性添加,則使用Vue.set
來動態添加響應式屬性。npm
再進一步,咱們還能夠:編程
實現以下:api
import Vue from "vue"; // 已經擁有createReactiveProxy的對象擁有如下特殊屬性,方便咱們檢測、獲取reactiveProxy const REACTIVE_PROXY = Symbol("reactiveProxy擁有的特殊標記,方便識別"); /** * @description 攔截賦值操做, * 若是發現此次賦值是屬性添加,則使用Vue.set(object, key, value)來添加響應式屬性。 */ export function createReactiveProxy(obj) { if (typeof obj !== "object" || obj === null) { throw new Error( "createReactiveProxy的參數不是object: " + JSON.stringify(obj) ); } if (obj[REACTIVE_PROXY]) { // 若是傳入的對象已經擁有reactiveProxy,或者它就是reactiveProxy,則直接返回已有reactiveProxy return obj[REACTIVE_PROXY]; } // console.log("creating reactiveProxy", obj); const proxy = new Proxy(obj, { set(target, property, value, receiver) { // 若是receiver === target,代表proxy處於被賦值對象的原型鏈上 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set // 僅僅攔截直接對proxy的賦值操做(reactiveProxy.newProperty=newValue) if (!target.hasOwnProperty(property) && receiver === proxy) { if (typeof value === "object" && value !== null) { // 若是要賦的值也是對象,則也要攔截這個對象的賦值操做 value = createReactiveProxy(value); } // console.log("Vue.set ", target, property); Vue.set(target, property, value); return true; } else { // console.log("Reflect.set ", target, property); return Reflect.set(...arguments); } } }); // 方便之後檢測、找到對象的reactiveProxy Object.defineProperty(obj, REACTIVE_PROXY, { value: proxy }); Object.defineProperty(proxy, REACTIVE_PROXY, { value: proxy }); // 檢測這個對象已有的屬性,若是是對象,則也要被攔截 Object.keys(obj).forEach(key => { if (typeof obj[key] === "object" && obj[key] !== null) { obj[key] = createReactiveProxy(obj[key]); } }); return proxy; }
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta http-equiv="X-UA-Compatible" content="ie=edge"> <title>Document</title> </head> <body> <div id="app"> <test-dynamic></test-dynamic> </div> </body> <script src="https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"></script> <script> // 已經擁有createReactiveProxy的對象擁有如下特殊屬性,方便咱們檢測、獲取reactiveProxy const REACTIVE_PROXY = Symbol("reactiveProxy擁有的特殊標記,方便識別"); /** * @description 攔截賦值操做, * 若是發現此次賦值是屬性添加,則使用Vue.set(object, key, value)來添加響應式屬性。 */ function createReactiveProxy(obj) { if (typeof obj !== "object" || obj === null) { throw new Error( "createReactiveProxy的參數不是object: " + JSON.stringify(obj) ); } if (obj[REACTIVE_PROXY]) { // 若是傳入的對象已經擁有reactiveProxy,或者它就是reactiveProxy,則直接返回已有reactiveProxy return obj[REACTIVE_PROXY]; } console.log("creating reactiveProxy", obj); const proxy = new Proxy(obj, { set(target, property, value, receiver) { // 若是receiver === target,代表proxy處於被賦值對象的原型鏈上 // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set // 僅僅攔截直接對proxy的賦值操做(reactiveProxy.newProperty=newValue) if (!target.hasOwnProperty(property) && receiver === proxy) { if (typeof value === "object" && value !== null) { // 若是要賦的值也是對象,則也要攔截這個對象的賦值操做 value = createReactiveProxy(value); } console.log("Vue.set ", target, property, value); Vue.set(target, property, value); return true; } else { console.log("Reflect.set ", target, property, value); return Reflect.set(...arguments); } } }); // 方便之後檢測、找到對象的reactiveProxy Object.defineProperty(obj, REACTIVE_PROXY, { value: proxy }); Object.defineProperty(proxy, REACTIVE_PROXY, { value: proxy }); // 檢測這個對象已有的屬性,若是是對象,則也要被攔截 Object.keys(obj).forEach(key => { if (typeof obj[key] === "object" && obj[key] !== null) { obj[key] = createReactiveProxy(obj[key]); } }); return proxy; } </script> <script> const testDynamicComponent = { template: ` <div> <button @click="onClick">test</button> <p v-if="show">{{ dynamicProps.dynamic }}</p> </div> `, data() { return createReactiveProxy({ show: false, dynamicProps: {} }); }, methods: { onClick() { this.dynamicProps.dynamic = 'wait 2 seconds...'; this.show = true; setTimeout(() => { this.dynamicProps.dynamic = 'createReactiveProxy works!'; }, 2000); } } }; var app = new Vue({ el: '#app', components: { 'test-dynamic': testDynamicComponent } }) </script> </html>
data.prop=$event
來更新綁定,這時會觸發proxy的set handler。也就是說,v-model不單單是data.prop=$event
這樣的語法糖,它會自動添加尚不存在、但當即須要的屬性(利用Vue.set)。app