【Vue技巧】利用Proxy自动添加响应式属性

<p><code></p> <h1 id="articleHeader0">相关原理</h1> <p>初始化Vue实例时,Vue将递归遍历<code>data</code>对象,通过<code>Object.defineProperty</code>将其中<strong>已有的</strong>属性转化为响应式的属性(getter/setter)。响应式属性的变化才能够被Vue观察到。<br />这就是为什么,Vue文档建议我们在初始化Vue实例之前,提前初始化<code>data</code>中所有可能用到的属性。如果想要在Vue实例创建以后添加响应式属性,需要使用<code>Vue.set(object, key, value)</code>,而不能直接通过赋值来添加新属性(这样添加的新属性不具有响应性)。</p> <blockquote><p>在<strong>运行时</strong>才能确定数据属性的键,这称为<strong>动态</strong>属性。相对地,如果在<strong>编程时</strong>就能确定属性的键,这称为<strong>静态</strong>属性。</p></blockquote> <h2 id="articleHeader1">Vue.set的限制</h2> <p>注意,Vue.set的第一个参数不能是<strong>Vue实例</strong>或者<strong>Vue实例的数据对象</strong>,可以是<strong>数据对象内嵌套的对象</strong>,或者<strong>props中的对象</strong>。也就是说,不能动态添加<strong>根级</strong>响应式属性。</p> <blockquote><p> <a href="https://vuejs.org/v2/guide/reactivity.html#Change-Detection-Caveats" rel="nofollow noreferrer" target="_blank">Vue文档</a>: Vue does not allow dynamically adding new <strong>root-level reactive properties</strong> to an <strong>already created instance</strong>. However, it’s possible to add reactive properties to a <strong>nested object</strong> using the <code>Vue.set(object, key, value)</code> method.</p></blockquote> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="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是数据对象内的一个嵌套对象" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="js"><span class="hljs-keyword">let</span> vm = <span class="hljs-keyword">new</span> Vue({ <span class="hljs-attr">data</span>: { <span class="hljs-attr">nestedObj</span>: {} } }); <span class="hljs-comment">// 创建Vue实例</span> Vue.set(vm, <span class="hljs-string">'a'</span>, <span class="hljs-number">2</span>); <span class="hljs-comment">// not works,不能为Vue实例添加根级响应式属性</span> Vue.set(vm.$data, <span class="hljs-string">'b'</span>, <span class="hljs-number">2</span>); <span class="hljs-comment">// not works,不能为Vue数据对象添加根级响应式属性</span> Vue.set(vm.nestedObj, <span class="hljs-string">'c'</span>, <span class="hljs-number">2</span>); <span class="hljs-comment">// works,vm.nestedObj是数据对象内的一个嵌套对象</span> Vue.set(vm.$data.nestedObj, <span class="hljs-string">'d'</span>, <span class="hljs-number">2</span>); <span class="hljs-comment">// works,vm.$data.nestedObj是数据对象内的一个嵌套对象</span></code></pre> <p>Vue.set会做适当的检查并报错:<a href="https://github.com/vuejs/vue/blob/7a145d86430bad65271f4d6ab1344b215fefe52a/src/core/observer/index.js#L198" rel="nofollow noreferrer" target="_blank">set源码</a>。</p> <h2 id="articleHeader2">Vue.set例子</h2> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="<!DOCTYPE html> <html lang=&quot;en&quot;> <head> <meta charset=&quot;UTF-8&quot;> <meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;> <meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;> <title>Document</title> </head> <body> </p> <div id=&quot;app&quot;> <test-dynamic></test-dynamic> </div> <p> </body> <script src=&quot;https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js&quot;></script> <script> const testDynamicComponent = { template: ` </p> <div> <button @click=&quot;onClick&quot;>test</button> </p> <p v-if=&quot;show&quot;>{{ nestedObj.dynamic }}</p> </p></div> <p> `, 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>" title="" data-original-title="复制"></span> </div> </p></div> <pre class="xml hljs"><code class="html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"ie=edge"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Document<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">test-dynamic</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">test-dynamic</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript"> <span class="hljs-keyword">const</span> testDynamicComponent = { template: ` &lt;div&gt; &lt;button @click=<span class="hljs-string">"onClick"</span>&gt;test&lt;/button&gt; &lt;p v-<span class="hljs-keyword">if</span>=<span class="hljs-string">"show"</span>&gt;{{ nestedObj.dynamic }}&lt;/p&gt; &lt;/div&gt; `, data() { <span class="hljs-keyword">return</span> ({ show: <span class="hljs-literal">false</span>, nestedObj: {} }) }, methods: { onClick() { <span class="hljs-comment">// Vue.set(this, 'dynamic', 'wait 2 seconds...'); // this will not works!</span> <span class="hljs-comment">// Vue.set(this.$data, 'dynamic', 'wait 2 seconds...'); // this will not works!</span> Vue.set(<span class="hljs-keyword">this</span>.$data.nestedObj, <span class="hljs-string">'dynamic'</span>, <span class="hljs-string">'wait 2 seconds...'</span>); <span class="hljs-comment">// this works</span> <span class="hljs-comment">// Vue.set(this.nestedObj, 'dynamic', 'wait 2 seconds...'); // this also works</span> <span class="hljs-keyword">this</span>.show = <span class="hljs-literal">true</span>; setTimeout(() =&gt; { <span class="hljs-keyword">this</span>.nestedObj.dynamic = <span class="hljs-string">'createReactiveProxy works!'</span>; }, <span class="hljs-number">2000</span>); } } }; <span class="hljs-keyword">var</span> app = <span class="hljs-keyword">new</span> Vue({ el: <span class="hljs-string">'#app'</span>, components: { <span class="hljs-string">'test-dynamic'</span>: testDynamicComponent } }) </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></code></pre> <h1 id="articleHeader3">问题背景</h1> <p>实际使用场景中,有时碰到这种情况:在创建Vue实例的时候,你还不确定会用到哪些属性(需要与用户进行交互之后才知道),或者有大量的属性都有可能被用到(而你不想为数据对象初始化那么多的属性)。这时候,提前初始化所有数据对象的属性就不太现实了。</p> <h1 id="articleHeader4">解决方案</h1> <p>一个原始的解决方案:与用户交互的过程中,每当发现需要用到新的属性,就通过<code>Vue.set</code>添加响应式属性。</p> <blockquote><p>牢记上面讲到的<strong>Vue.set的限制</strong>。动态添加的属性只能放在data内嵌套的对象中,或者props中的对象。实战中可以在data数据对象中专门用一个属性来存放动态属性,比如<code>data: { staticProp1: '', staticProp2: '', dynamicProps: {} }</code>。</p></blockquote> <p>在这个方法的基础上,可以扩展出一个一劳永逸的方案:使用<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="nofollow noreferrer" target="_blank">ES6 Proxy</a>,为<code>data</code>创建一个代理,拦截对<code>data</code>的赋值操作,如果发现这次赋值是属性添加,则使用<code>Vue.set</code>来动态添加响应式属性。</p> <p>再进一步,我们还可以:</p> <ol> <li>递归为已存在的子属性创建代理。</li> <li>动态添加属性时,如果赋值的属性值是对象,那么也为这个对象创建代理。</li> </ol> <p>实现如下:</p> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="import Vue from &quot;vue&quot;; // 已经拥有createReactiveProxy的对象拥有以下特殊属性,方便我们检测、获取reactiveProxy const REACTIVE_PROXY = Symbol(&quot;reactiveProxy拥有的特殊标记,方便识别&quot;); /** * @description 拦截赋值操作, * 如果发现这次赋值是属性添加,则使用Vue.set(object, key, value)来添加响应式属性。 */ export function createReactiveProxy(obj) { if (typeof obj !== &quot;object&quot; || obj === null) { throw new Error( &quot;createReactiveProxy的参数不是object: &quot; + JSON.stringify(obj) ); } if (obj[REACTIVE_PROXY]) { // 如果传入的对象已经拥有reactiveProxy,或者它就是reactiveProxy,则直接返回已有reactiveProxy return obj[REACTIVE_PROXY]; } // console.log(&quot;creating reactiveProxy&quot;, 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) &amp;&amp; receiver === proxy) { if (typeof value === &quot;object&quot; &amp;&amp; value !== null) { // 如果要赋的值也是对象,则也要拦截这个对象的赋值操作 value = createReactiveProxy(value); } // console.log(&quot;Vue.set &quot;, target, property); Vue.set(target, property, value); return true; } else { // console.log(&quot;Reflect.set &quot;, 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] === &quot;object&quot; &amp;&amp; obj[key] !== null) { obj[key] = createReactiveProxy(obj[key]); } }); return proxy; }" title="" data-original-title="复制"></span> </div> </p></div> <pre class="javascript hljs"><code class="js"><span class="hljs-keyword">import</span> Vue <span class="hljs-keyword">from</span> <span class="hljs-string">"vue"</span>; <span class="hljs-comment">// 已经拥有createReactiveProxy的对象拥有以下特殊属性,方便我们检测、获取reactiveProxy</span> <span class="hljs-keyword">const</span> REACTIVE_PROXY = <span class="hljs-built_in">Symbol</span>(<span class="hljs-string">"reactiveProxy拥有的特殊标记,方便识别"</span>); <span class="hljs-comment">/** * @description 拦截赋值操作, * 如果发现这次赋值是属性添加,则使用Vue.set(object, key, value)来添加响应式属性。 */</span> <span class="hljs-keyword">export</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createReactiveProxy</span>(<span class="hljs-params">obj</span>) </span>{ <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> obj !== <span class="hljs-string">"object"</span> || obj === <span class="hljs-literal">null</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>( <span class="hljs-string">"createReactiveProxy的参数不是object: "</span> + <span class="hljs-built_in">JSON</span>.stringify(obj) ); } <span class="hljs-keyword">if</span> (obj[REACTIVE_PROXY]) { <span class="hljs-comment">// 如果传入的对象已经拥有reactiveProxy,或者它就是reactiveProxy,则直接返回已有reactiveProxy</span> <span class="hljs-keyword">return</span> obj[REACTIVE_PROXY]; } <span class="hljs-comment">// console.log("creating reactiveProxy", obj);</span> <span class="hljs-keyword">const</span> proxy = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Proxy</span>(obj, { set(target, property, value, receiver) { <span class="hljs-comment">// 如果receiver === target,表明proxy处于被赋值对象的原型链上</span> <span class="hljs-comment">// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set</span> <span class="hljs-comment">// 仅仅拦截直接对proxy的赋值操作(reactiveProxy.newProperty=newValue)</span> <span class="hljs-keyword">if</span> (!target.hasOwnProperty(property) &amp;&amp; receiver === proxy) { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> value === <span class="hljs-string">"object"</span> &amp;&amp; value !== <span class="hljs-literal">null</span>) { <span class="hljs-comment">// 如果要赋的值也是对象,则也要拦截这个对象的赋值操作</span> value = createReactiveProxy(value); } <span class="hljs-comment">// console.log("Vue.set ", target, property);</span> Vue.set(target, property, value); <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>; } <span class="hljs-keyword">else</span> { <span class="hljs-comment">// console.log("Reflect.set ", target, property);</span> <span class="hljs-keyword">return</span> <span class="hljs-built_in">Reflect</span>.set(...arguments); } } }); <span class="hljs-comment">// 方便以后检测、找到对象的reactiveProxy</span> <span class="hljs-built_in">Object</span>.defineProperty(obj, REACTIVE_PROXY, { <span class="hljs-attr">value</span>: proxy }); <span class="hljs-built_in">Object</span>.defineProperty(proxy, REACTIVE_PROXY, { <span class="hljs-attr">value</span>: proxy }); <span class="hljs-comment">// 检测这个对象已有的属性,如果是对象,则也要被拦截</span> <span class="hljs-built_in">Object</span>.keys(obj).forEach(<span class="hljs-function"><span class="hljs-params">key</span> =&gt;</span> { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> obj[key] === <span class="hljs-string">"object"</span> &amp;&amp; obj[key] !== <span class="hljs-literal">null</span>) { obj[key] = createReactiveProxy(obj[key]); } }); <span class="hljs-keyword">return</span> proxy; }</code></pre> <h2 id="articleHeader5">createReactiveProxy例子</h2> <div class="widget-codetool" style="display:none;"> <div class="widget-codetool--inner"> <span class="selectCode code-tool" data-toggle="tooltip" data-placement="top" title="" data-original-title="全选"></span> <span type="button" class="copyCode code-tool" data-toggle="tooltip" data-placement="top" data-clipboard-text="<!DOCTYPE html> <html lang=&quot;en&quot;> <head> <meta charset=&quot;UTF-8&quot;> <meta name=&quot;viewport&quot; content=&quot;width=device-width, initial-scale=1.0&quot;> <meta http-equiv=&quot;X-UA-Compatible&quot; content=&quot;ie=edge&quot;> <title>Document</title> </head> <body> </p> <div id=&quot;app&quot;> <test-dynamic></test-dynamic> </div> <p> </body> <script src=&quot;https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js&quot;></script> <script> // 已经拥有createReactiveProxy的对象拥有以下特殊属性,方便我们检测、获取reactiveProxy const REACTIVE_PROXY = Symbol(&quot;reactiveProxy拥有的特殊标记,方便识别&quot;); /** * @description 拦截赋值操作, * 如果发现这次赋值是属性添加,则使用Vue.set(object, key, value)来添加响应式属性。 */ function createReactiveProxy(obj) { if (typeof obj !== &quot;object&quot; || obj === null) { throw new Error( &quot;createReactiveProxy的参数不是object: &quot; + JSON.stringify(obj) ); } if (obj[REACTIVE_PROXY]) { // 如果传入的对象已经拥有reactiveProxy,或者它就是reactiveProxy,则直接返回已有reactiveProxy return obj[REACTIVE_PROXY]; } console.log(&quot;creating reactiveProxy&quot;, 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) &amp;&amp; receiver === proxy) { if (typeof value === &quot;object&quot; &amp;&amp; value !== null) { // 如果要赋的值也是对象,则也要拦截这个对象的赋值操作 value = createReactiveProxy(value); } console.log(&quot;Vue.set &quot;, target, property, value); Vue.set(target, property, value); return true; } else { console.log(&quot;Reflect.set &quot;, 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] === &quot;object&quot; &amp;&amp; obj[key] !== null) { obj[key] = createReactiveProxy(obj[key]); } }); return proxy; } </script> <script> const testDynamicComponent = { template: ` </p> <div> <button @click=&quot;onClick&quot;>test</button> </p> <p v-if=&quot;show&quot;>{{ dynamicProps.dynamic }}</p> </p></div> <p> `, 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>" title="" data-original-title="复制"></span> </div> </p></div> <pre class="xml hljs"><code class="html"><span class="hljs-meta">&lt;!DOCTYPE html&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">html</span> <span class="hljs-attr">lang</span>=<span class="hljs-string">"en"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">head</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">charset</span>=<span class="hljs-string">"UTF-8"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">name</span>=<span class="hljs-string">"viewport"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"width=device-width, initial-scale=1.0"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">meta</span> <span class="hljs-attr">http-equiv</span>=<span class="hljs-string">"X-UA-Compatible"</span> <span class="hljs-attr">content</span>=<span class="hljs-string">"ie=edge"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">title</span>&gt;</span>Document<span class="hljs-tag">&lt;/<span class="hljs-name">title</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">head</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">body</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"app"</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">test-dynamic</span>&gt;</span><span class="hljs-tag">&lt;/<span class="hljs-name">test-dynamic</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">body</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span> <span class="hljs-attr">src</span>=<span class="hljs-string">"https://cdn.jsdelivr.net/npm/vue@2.5.17/dist/vue.js"</span>&gt;</span><span class="undefined"></span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="javascript"> <span class="hljs-comment">// 已经拥有createReactiveProxy的对象拥有以下特殊属性,方便我们检测、获取reactiveProxy</span> <span class="hljs-keyword">const</span> REACTIVE_PROXY = <span class="hljs-built_in">Symbol</span>(<span class="hljs-string">"reactiveProxy拥有的特殊标记,方便识别"</span>); <span class="hljs-comment">/** * @description 拦截赋值操作, * 如果发现这次赋值是属性添加,则使用Vue.set(object, key, value)来添加响应式属性。 */</span> <span class="hljs-function"><span class="hljs-keyword">function</span> <span class="hljs-title">createReactiveProxy</span>(<span class="hljs-params">obj</span>) </span>{ <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> obj !== <span class="hljs-string">"object"</span> || obj === <span class="hljs-literal">null</span>) { <span class="hljs-keyword">throw</span> <span class="hljs-keyword">new</span> <span class="hljs-built_in">Error</span>( <span class="hljs-string">"createReactiveProxy的参数不是object: "</span> + <span class="hljs-built_in">JSON</span>.stringify(obj) ); } <span class="hljs-keyword">if</span> (obj[REACTIVE_PROXY]) { <span class="hljs-comment">// 如果传入的对象已经拥有reactiveProxy,或者它就是reactiveProxy,则直接返回已有reactiveProxy</span> <span class="hljs-keyword">return</span> obj[REACTIVE_PROXY]; } <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"creating reactiveProxy"</span>, obj); <span class="hljs-keyword">const</span> proxy = <span class="hljs-keyword">new</span> <span class="hljs-built_in">Proxy</span>(obj, { set(target, property, value, receiver) { <span class="hljs-comment">// 如果receiver === target,表明proxy处于被赋值对象的原型链上</span> <span class="hljs-comment">// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set</span> <span class="hljs-comment">// 仅仅拦截直接对proxy的赋值操作(reactiveProxy.newProperty=newValue)</span> <span class="hljs-keyword">if</span> (!target.hasOwnProperty(property) &amp;&amp; receiver === proxy) { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> value === <span class="hljs-string">"object"</span> &amp;&amp; value !== <span class="hljs-literal">null</span>) { <span class="hljs-comment">// 如果要赋的值也是对象,则也要拦截这个对象的赋值操作</span> value = createReactiveProxy(value); } <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Vue.set "</span>, target, property, value); Vue.set(target, property, value); <span class="hljs-keyword">return</span> <span class="hljs-literal">true</span>; } <span class="hljs-keyword">else</span> { <span class="hljs-built_in">console</span>.log(<span class="hljs-string">"Reflect.set "</span>, target, property, value); <span class="hljs-keyword">return</span> <span class="hljs-built_in">Reflect</span>.set(...arguments); } } }); <span class="hljs-comment">// 方便以后检测、找到对象的reactiveProxy</span> <span class="hljs-built_in">Object</span>.defineProperty(obj, REACTIVE_PROXY, { <span class="hljs-attr">value</span>: proxy }); <span class="hljs-built_in">Object</span>.defineProperty(proxy, REACTIVE_PROXY, { <span class="hljs-attr">value</span>: proxy }); <span class="hljs-comment">// 检测这个对象已有的属性,如果是对象,则也要被拦截</span> <span class="hljs-built_in">Object</span>.keys(obj).forEach(<span class="hljs-function"><span class="hljs-params">key</span> =&gt;</span> { <span class="hljs-keyword">if</span> (<span class="hljs-keyword">typeof</span> obj[key] === <span class="hljs-string">"object"</span> &amp;&amp; obj[key] !== <span class="hljs-literal">null</span>) { obj[key] = createReactiveProxy(obj[key]); } }); <span class="hljs-keyword">return</span> proxy; } </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;<span class="hljs-name">script</span>&gt;</span><span class="actionscript"> <span class="hljs-keyword">const</span> testDynamicComponent = { template: ` &lt;div&gt; &lt;button @click=<span class="hljs-string">"onClick"</span>&gt;test&lt;/button&gt; &lt;p v-<span class="hljs-keyword">if</span>=<span class="hljs-string">"show"</span>&gt;{{ dynamicProps.dynamic }}&lt;/p&gt; &lt;/div&gt; `, data() { <span class="hljs-keyword">return</span> createReactiveProxy({ show: <span class="hljs-literal">false</span>, dynamicProps: {} }); }, methods: { onClick() { <span class="hljs-keyword">this</span>.dynamicProps.dynamic = <span class="hljs-string">'wait 2 seconds...'</span>; <span class="hljs-keyword">this</span>.show = <span class="hljs-literal">true</span>; setTimeout(() =&gt; { <span class="hljs-keyword">this</span>.dynamicProps.dynamic = <span class="hljs-string">'createReactiveProxy works!'</span>; }, <span class="hljs-number">2000</span>); } } }; <span class="hljs-keyword">var</span> app = <span class="hljs-keyword">new</span> Vue({ el: <span class="hljs-string">'#app'</span>, components: { <span class="hljs-string">'test-dynamic'</span>: testDynamicComponent } }) </span><span class="hljs-tag">&lt;/<span class="hljs-name">script</span>&gt;</span> <span class="hljs-tag">&lt;/<span class="hljs-name">html</span>&gt;</span></code></pre> <h1 id="articleHeader6">关于v-model的补充</h1> <ol> <li>Vue.set添加属性时,是通过defineProperty来添加getter和setter,并不会触发<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/set" rel="nofollow noreferrer" target="_blank">set handler</a>,而是触发<a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy/handler/defineProperty" rel="nofollow noreferrer" target="_blank">defineProperty handler</a>。</li> <li>如果<strong>v-model</strong>绑定的属性不存在对象上,那么v-model会在第一次@input事件发生时,通过Vue.set添加绑定属性,让绑定的属性拥有响应性。如上一条所说,这个过程不会触发proxy的set handler。</li> <li>在后续的@input事件,v-model才会通过<code>data.prop=$event</code>来更新绑定,这时会触发proxy的set handler。</li> </ol> <p>也就是说,v-model不仅仅是<code>data.prop=$event</code>这样的语法糖,它会自动添加<strong>尚不存在、但立即需要的属性</strong>(利用Vue.set)。</p> <h1 id="articleHeader7">参考资料</h1> <ol> <li><a href="https://cn.vuejs.org/v2/guide/reactivity.html" rel="nofollow noreferrer" target="_blank">深入响应式原理 - Vue文档</a></li> <li><a href="https://vuejs.org/v2/api/#Vue-set" rel="nofollow noreferrer" target="_blank">Vue.set文档</a></li> <li><a href="https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Proxy" rel="nofollow noreferrer" target="_blank">Proxy</a></li> </ol> <p></code></p>
脚本宝典为你提供优质服务
脚本宝典 » 【Vue技巧】利用Proxy自动添加响应式属性

发表评论

提供最优质的资源集合

立即查看 了解详情