uTools-Manuals/docs/vue/guide/reactivity.html
2019-04-21 11:50:48 +08:00

104 lines
12 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<div class="content guide with-sidebar reactivity-guide">
<h1>深入响应式原理</h1>
<p>现在是时候深入一下了Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应式系统的底层的细节。</p>
<h2 id="如何追踪变化"><a class="headerlink" href="#如何追踪变化" title="如何追踪变化"></a>如何追踪变化</h2><p>当你把一个普通的 JavaScript 对象传入 Vue 实例作为 <code>data</code> 选项Vue 将遍历此对象所有的属性,并使用 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Object/defineProperty" rel="noopener" target="_blank"><code>Object.defineProperty</code></a> 把这些属性全部转为 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Working_with_Objects#定义_getters_与_setters" rel="noopener" target="_blank">getter/setter</a><code>Object.defineProperty</code> 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。</p>
<p>这些 getter/setter 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter 的格式化并不同,所以建议安装 <a href="https://github.com/vuejs/vue-devtools" rel="noopener" target="_blank">vue-devtools</a> 来获取对检查数据更加友好的用户界面。</p>
<p>每个组件实例都对应一个 <strong>watcher</strong> 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher从而使它关联的组件重新渲染。</p>
<p><img src="https://cn.vuejs.org/images/data.png"/></p>
<h2 id="检测变化的注意事项"><a class="headerlink" href="#检测变化的注意事项" title="检测变化的注意事项"></a>检测变化的注意事项</h2><p>受现代 JavaScript 的限制 (而且 <code>Object.observe</code> 也已经被废弃)Vue <strong>无法检测到对象属性的添加或删除</strong>。由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 <code>data</code> 对象上存在才能让 Vue 将它转换为响应式的。例如:</p>
<pre><code class="hljs js"><span class="hljs-keyword">var</span> vm = <span class="hljs-keyword">new</span> Vue({
<span class="hljs-attr">data</span>:{
<span class="hljs-attr">a</span>:<span class="hljs-number">1</span>
}
})
<span class="hljs-comment">// `vm.a` 是响应式的</span>
vm.b = <span class="hljs-number">2</span>
<span class="hljs-comment">// `vm.b` 是非响应式的</span></code></pre>
<p>对于已经创建的实例Vue 不允许动态添加根级别的响应式属性。但是,可以使用 <code>Vue.set(object, key, value)</code> 方法向嵌套对象添加响应式属性。例如,对于:</p>
<pre><code class="hljs js">Vue.set(vm.someObject, <span class="hljs-string">'b'</span>, <span class="hljs-number">2</span>)</code></pre>
<p>您还可以使用 <code>vm.$set</code> 实例方法,这也是全局 <code>Vue.set</code> 方法的别名:</p>
<pre><code class="hljs js"><span class="hljs-keyword">this</span>.$set(<span class="hljs-keyword">this</span>.someObject,<span class="hljs-string">'b'</span>,<span class="hljs-number">2</span>)</code></pre>
<p>有时你可能需要为已有对象赋值多个新属性,比如使用 <code>Object.assign()</code><code>_.extend()</code>。但是,这样添加到对象上的新属性不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的属性一起创建一个新的对象。</p>
<pre><code class="hljs js"><span class="hljs-comment">// 代替 `Object.assign(this.someObject, { a: 1, b: 2 })`</span>
<span class="hljs-keyword">this</span>.someObject = <span class="hljs-built_in">Object</span>.assign({}, <span class="hljs-keyword">this</span>.someObject, { <span class="hljs-attr">a</span>: <span class="hljs-number">1</span>, <span class="hljs-attr">b</span>: <span class="hljs-number">2</span> })</code></pre>
<p>也有一些数组相关的注意事项,之前已经在<a href="list.html#注意事项">列表渲染</a>中讲过。</p>
<h2 id="声明响应式属性"><a class="headerlink" href="#声明响应式属性" title="声明响应式属性"></a>声明响应式属性</h2><p>由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:</p>
<pre><code class="hljs js"><span class="hljs-keyword">var</span> vm = <span class="hljs-keyword">new</span> Vue({
<span class="hljs-attr">data</span>: {
<span class="hljs-comment">// 声明 message 为一个空值字符串</span>
message: <span class="hljs-string">''</span>
},
<span class="hljs-attr">template</span>: <span class="hljs-string">'&lt;div&gt;{{ message }}&lt;/div&gt;'</span>
})
<span class="hljs-comment">// 之后设置 `message`</span>
vm.message = <span class="hljs-string">'Hello!'</span></code></pre>
<p>如果你未在 <code>data</code> 选项中声明 <code>message</code>Vue 将警告你渲染函数正在试图访问不存在的属性。</p>
<p>这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:<code>data</code> 对象就像组件状态的结构 (schema)。提前声明所有的响应式属性,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。</p>
<h2 id="异步更新队列"><a class="headerlink" href="#异步更新队列" title="异步更新队列"></a>异步更新队列</h2><p>可能你还没有注意到Vue 在更新 DOM 时是<strong>异步</strong>执行的。只要侦听到数据变化Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后在下一个的事件循环“tick”中Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 <code>Promise.then</code><code>MessageChannel</code>,如果执行环境不支持,则会采用 <code>setTimeout(fn, 0)</code> 代替。</p>
<p>例如,当你设置 <code>vm.someData = 'new value'</code>该组件不会立即重新渲染。当刷新队列时组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM可以在数据变化之后立即使用 <code>Vue.nextTick(callback)</code>。这样回调函数将在 DOM 更新完成后被调用。例如:</p>
<pre><code class="hljs html"><span class="hljs-tag">&lt;<span class="hljs-name">div</span> <span class="hljs-attr">id</span>=<span class="hljs-string">"example"</span>&gt;</span>{{message}}<span class="hljs-tag">&lt;/<span class="hljs-name">div</span>&gt;</span></code></pre>
<pre><code class="hljs js"><span class="hljs-keyword">var</span> vm = <span class="hljs-keyword">new</span> Vue({
<span class="hljs-attr">el</span>: <span class="hljs-string">'#example'</span>,
<span class="hljs-attr">data</span>: {
<span class="hljs-attr">message</span>: <span class="hljs-string">'123'</span>
}
})
vm.message = <span class="hljs-string">'new message'</span> <span class="hljs-comment">// 更改数据</span>
vm.$el.textContent === <span class="hljs-string">'new message'</span> <span class="hljs-comment">// false</span>
Vue.nextTick(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
vm.$el.textContent === <span class="hljs-string">'new message'</span> <span class="hljs-comment">// true</span>
})</code></pre>
<p>在组件内使用 <code>vm.$nextTick()</code> 实例方法特别方便,因为它不需要全局 <code>Vue</code>,并且回调函数中的 <code>this</code> 将自动绑定到当前的 Vue 实例上:</p>
<pre><code class="hljs js">Vue.component(<span class="hljs-string">'example'</span>, {
<span class="hljs-attr">template</span>: <span class="hljs-string">'&lt;span&gt;{{ message }}&lt;/span&gt;'</span>,
<span class="hljs-attr">data</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">return</span> {
<span class="hljs-attr">message</span>: <span class="hljs-string">'未更新'</span>
}
},
<span class="hljs-attr">methods</span>: {
<span class="hljs-attr">updateMessage</span>: <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">this</span>.message = <span class="hljs-string">'已更新'</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">this</span>.$el.textContent) <span class="hljs-comment">// =&gt; '未更新'</span>
<span class="hljs-keyword">this</span>.$nextTick(<span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">this</span>.$el.textContent) <span class="hljs-comment">// =&gt; '已更新'</span>
})
}
}
})</code></pre>
<p>因为 <code>$nextTick()</code> 返回一个 <code>Promise</code> 对象,所以你可以使用新的 <a href="https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Statements/async_function" rel="noopener" target="_blank">ES2016 async/await</a> 语法完成相同的事情:</p>
<pre><code class="hljs js">methods: {
<span class="hljs-attr">updateMessage</span>: <span class="hljs-keyword">async</span> <span class="hljs-function"><span class="hljs-keyword">function</span> (<span class="hljs-params"></span>) </span>{
<span class="hljs-keyword">this</span>.message = <span class="hljs-string">'已更新'</span>
<span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">this</span>.$el.textContent) <span class="hljs-comment">// =&gt; '未更新'</span>
<span class="hljs-keyword">await</span> <span class="hljs-keyword">this</span>.$nextTick()
<span class="hljs-built_in">console</span>.log(<span class="hljs-keyword">this</span>.$el.textContent) <span class="hljs-comment">// =&gt; '已更新'</span>
}
}</code></pre>
<div class="guide-links">
<span><a href="ssr.html">服务端渲染</a></span>
<span style="float: right;"><a href="migration.html">从 Vue 1.x 迁移</a></span>
</div>
<div class="footer">
<script src="//m.servedby-buysellads.com/monetization.js" type="text/javascript"></script>
<div class="bsa-cpc"></div>
<script>
(function(){
if(typeof _bsa !== 'undefined' && _bsa) {
_bsa.init('default', 'CKYD62QM', 'placement:vuejsorg', {
target: '.bsa-cpc',
align: 'horizontal',
disable_css: 'true'
});
}
})();
</script>
发现错误?想参与编辑?
<a href="https://github.com/vuejs/cn.vuejs.org/blob/master/srcreactivity.md" target="_blank">
在 GitHub 上编辑此页!
</a>
</div>
</div>