Vue - 深入响应式原理 川长思鸟来 2022-12-20 06:07 202阅读 0赞 **推荐:**[Vue汇总][Vue] ## Vue - 深入响应式原理 ## 现在是时候深入一下了!Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。这使得状态管理非常简单直接,不过理解其工作原理同样重要,这样你可以避开一些常见的问题。在这个章节,我们将研究一下 Vue 响应式系统的底层的细节。 **如何追踪变化** 当你把一个普通的 JavaScript 对象传入 Vue 实例作为 `data` 选项,Vue 将遍历此对象所有的 `property`,并使用 `Object.defineProperty` 把这些 `property` 全部转为 `getter/setter`。`Object.defineProperty` 是 ES5 中一个无法 `shim` 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因(`shim`可以将新的API引入到旧的环境中,而且仅靠旧环境中已有的手段实现。意思就是,`Object.defineProperty`这个特性是无法使用低级浏览器中的方法来实现的,所以Vue不支持IE8以及更低版本的浏览器)。 这些 `getter/setter` 对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在 `property` 被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 `getter/setter` 的格式化并不同,所以建议安装 `vue-devtools` 来获取对检查数据更加友好的用户界面。 每个组件实例都对应一个 `watcher` 实例,它会在组件渲染的过程中把“接触”过的数据 `property` 记录为依赖。之后当依赖项的 `setter` 触发时,会通知 `watcher`,从而使它关联的组件重新渲染。 ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70] **检测变化的注意事项** 由于 JavaScript 的限制,Vue 不能检测数组和对象的变化。尽管如此我们还是有一些办法来回避这些限制并保证它们的响应性。 **对于对象** Vue 无法检测 `property` 的添加或移除。由于 Vue 会在初始化实例时对 `property` 执行 `getter/setter` 转化,所以 `property` 必须在 `data` 对象上存在才能让 Vue 将它转换为响应式的。例如: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="./js/vue.js"></script> </head> <body> <div id="div"> <p v-for="value in obj" :key="value">{ {value}}</p> </div> </body> </html> <script> var vue = new Vue({ el: '#div', data: { obj: { a: 1, b: 2 } } }) </script> 效果: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 1] 对于已经创建的实例,Vue 不允许动态添加根级别的响应式 `property`。但是,可以使用 `Vue.set(object, propertyName, value)` 方法向嵌套对象添加响应式 `property`。例如,对于: Vue.set(vue.obj , 'c' , 3); 如下图所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 2] 还可以使用 `vm.$set` 实例方法,这也是全局 `Vue.set` 方法的别名: vue.$set(vue.obj , 'd' , 4); 如下图所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 3] 有时你可能需要为已有对象赋值多个新 `property`,比如使用 `Object.assign()` 或 `_.extend()`。但是,这样添加到对象上的新 `property` 不会触发更新。在这种情况下,你应该用原对象与要混合进去的对象的 `property` 一起创建一个新的对象。 vue.obj = Object.assign({}, vue.obj, { c: 3, d: 4 }) 如下图所示: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 4] **对于数组** Vue 不能检测以下数组的变动: * 当你利用索引直接设置一个数组项时,例如:`vue.list[0] = 'b'`。 * 当你修改数组的长度时,例如:`vue.list.length = 10`。 代码: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="./js/vue.js"></script> </head> <body> <div id="div"> <p v-for="value in list" :key="value">{ {value}}</p> </div> </body> </html> <script> var vue = new Vue({ el: '#div', data: { list:['Kaven' , 'Java' , 'Jack' , 'Vue'] } }) </script> 效果: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 5] ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 6] 所以`vue.list[0] = 'Hello Kaven'`和`vue.list.length = 1`都不是响应式的。 为了解决第一类问题,以下两种方式都可以实现和 `vue.list[0] = 'b'`相同的效果,同时也将在响应式系统内触发状态更新: Vue.set(vue.list , 4 , 'nacos') ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 7] vue.list.splice(4 , 1 , 'dubbo') ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 8] 你也可以使用 `vue.$set` 实例方法,该方法是全局方法 `Vue.set` 的一个别名: vue.$set(vue.list , 4 , 'feign') ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 9] 为了解决第二类问题,你可以使用 `splice`: vue.list.splice(2) ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 10] **声明响应式 property** 由于 Vue 不允许动态添加根级响应式 `property`,所以你必须在初始化实例前声明所有根级响应式 `property`,哪怕只是一个空值: var vue = new Vue({ data: { // 声明 message 为一个空值字符串 message: '' }, template: '<div>{ { message }}</div>' }) // 之后设置 `message` vue.message = 'Hello!' 如果你未在 `data` 选项中声明 `message`,Vue 将警告你渲染函数正在试图访问不存在的 `property`。 这样的限制在背后是有其技术原因的,它消除了在依赖项跟踪系统中的一类边界情况,也使 Vue 实例能更好地配合类型检查系统工作。但与此同时在代码可维护性方面也有一点重要的考虑:`data` 对象就像组件状态的结构 (`schema`)。提前声明所有的响应式 `property`,可以让组件代码在未来修改或给其他开发人员阅读时更易于理解。 **异步更新队列** 可能你还没有注意到,Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 `watcher` 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环`“tick”`中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的 `Promise.then`、`MutationObserver` 和 `setImmediate`,如果执行环境不支持,则会采用 `setTimeout(fn, 0)` 代替。 例如,当你设置 `vue.message = 'new value'`,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环`“tick”`中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 `Vue.nextTick(callback)`。这样回调函数将在 DOM 更新完成后被调用。例如: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="./js/vue.js"></script> </head> <body> <div id="div"> { {message}} </div> </body> </html> <script> var vue = new Vue({ el: '#div', data: { message: 'Kaven' } }); vue.message = 'Java'; console.log(vue.$el.textContent); Vue.nextTick(function () { console.log('nextTick'); console.log(vue.$el.textContent); }) </script> 效果: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 11] 在组件内使用 `vue.$nextTick()` 实例方法特别方便,因为它不需要全局 Vue,并且回调函数中的 `this` 将自动绑定到当前的 Vue 实例上: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="./js/vue.js"></script> </head> <body> <div id="div"> <button @click="click">{ {count}}</button> </div> </body> </html> <script> var vue = new Vue({ el: '#div', data: { count: 0 }, methods:{ click(){ this.count++; console.log(vue.$el.textContent); this.$nextTick(function () { console.log('nextTick'); console.log(vue.$el.textContent); }) } } }); </script> 效果: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 12] 因为 `$nextTick()` 返回一个 Promise 对象,所以你可以使用新的 ES2017 `async/await` 语法完成相同的事情: <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>v-for</title> <script src="./js/vue.js"></script> </head> <body> <div id="div"> <button @click="click">{ {count}}</button> </div> </body> </html> <script> var vue = new Vue({ el: '#div', data: { count: 0 }, methods:{ async click() { this.count++; console.log(vue.$el.textContent); await this.$nextTick(); console.log('async/await'); console.log(this.$el.textContent); } } }); </script> 效果: ![在这里插入图片描述][watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 13] 写博客是博主记录自己的学习过程,如果有错误,请指正,谢谢! 参考:[深入响应式原理][Link 1] [Vue]: https://kaven.blog.csdn.net/article/details/109508022 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70]: /images/20221120/33ee9b01deb2463fb1e53dbe19199778.png [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 1]: https://img-blog.csdnimg.cn/20201111105833676.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 2]: https://img-blog.csdnimg.cn/20201111110141866.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 3]: https://img-blog.csdnimg.cn/2020111111055869.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 4]: https://img-blog.csdnimg.cn/20201111110845662.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 5]: https://img-blog.csdnimg.cn/20201111111228326.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 6]: https://img-blog.csdnimg.cn/20201111111633319.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 7]: https://img-blog.csdnimg.cn/20201111111900662.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 8]: https://img-blog.csdnimg.cn/20201111112228991.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 9]: https://img-blog.csdnimg.cn/20201111112437882.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 10]: https://img-blog.csdnimg.cn/20201111112649275.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 11]: https://img-blog.csdnimg.cn/20201111113918525.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 12]: https://img-blog.csdnimg.cn/20201111114620775.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [watermark_type_ZmFuZ3poZW5naGVpdGk_shadow_10_text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz_size_16_color_FFFFFF_t_70 13]: https://img-blog.csdnimg.cn/20201111114843851.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzM3OTYwNjAz,size_16,color_FFFFFF,t_70 [Link 1]: https://cn.vuejs.org/v2/guide/reactivity.html
还没有评论,来说两句吧...