初始化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