精读 Vue 官方文档系列 🎉
介绍
Vue 的非侵入性响应式系统依赖于其特殊的“数据模型”。更具体点指的就是具有响应式(反应式)特性的普通 JavaScript 对象,当它被修改时,视图也会随着更新。
如何追踪变化
Vue 会使用 Object.defineProperty 将组件 data 选项上的所有 property 转换为带有 getter/setter 的 property。然后通过这些 getter/setter 来追踪依赖。
Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
关于
shim与polyfill的定义目前没有一个严格的标准。它们共同的目标都是希望抹平环境和 API 之间的差异。Shim的含义是 “垫片”。更多被认为是指提供一套代码库,在旧的环境(不一定是浏览器环境)之上覆盖一套新的环境来整体性的抹平所有差异,而落实到具体功能或 API 上则由polyfill去实现。polyfill的含义是“填充物;腻子”。它更多的被认为是提供一个插件来解决浏览器中某个 API 之间的差异,例如,通过引入 Array 的 polyfill 插件来使用 ES5 标准中定义的数组遍历方法。 总结一下,shim更多是用来抹平环境之间的差异,是一个整体全面的概念,而polyfill更多指的是具体 API 或功能上的实现,因此shim包含着polyfill。关于这一点,可以在es5-shim库中更清晰的看出,es5-shim是一个整体,整体中的具体部分都是由像Array.prototype.every/blob/main/polyfill.js这样是具体的polyfill实现。
当带有 getter/setter 的 property 被访问或修改时就会通知变更,而通知的对象就是 watcher 实例。
每个 watcher 实例都对应着一个组件,它会在组件渲染的过程中把接触过的数据 property 记录为依赖,之后依赖项的 setter 触发时就会通知 watcher,从而使它关联的组件重新渲染。
由于不同浏览器的控制台在输出数据的格式上存在差异,所以为了能够获得统一友好的调试效果,建议使用
vue-devtools。
检测变化的注意事项
由于 Vue 会使用 Object.defineProperty 方法在组件实例初始化时(beforeCreated 钩子之后)对 property 执行 getter/setter 转化,所以必须要保证 property 在组件实例初始化之前就已经存在在 data 选项上。
本质区别就是
对象.属性和Object.defineProperty方法在功能效果上的差别。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式 property。
var vm = new Vue({
data:{
a:1
}
})
// `vm.a` 是响应式的
vm.b = 2
// `vm.b` 是非响应式的
// 所谓“根级”指的就是组件实例本身这一层的 property。
但支持向已经是响应式的引用类型值中动态添加响应式的 property。
{
data() {
return {
student: {
name: "xiaoming",
},
};
},
mounted() {
setTimeout(() => {
Vue.set(this.student, "id", "0001");
this.$set(this.student, "age", 18);
this.student = Object.assign({}, this.student, { sex: "male" });
this.student = { ...this.student, ...{ height: "tall" } };
}, 1000);
},
}
主要的方式有:
- 使用
Vue构造函数上的set方法。 - 使用当前组件实例上的
$set方法。 - 使用 ES6 中的
Object.assign方法和扩展运算符。 - 借助其它工具库的
_.extend()方法。
像 3,4 方法的基本原理就是为了产生一个新的对象来覆盖
student的值,因为student本身也是一个setter/getter的 property 更改它就可以正常触发Vue的检测。
student: Object
get student: ƒ proxyGetter()
set student: ƒ proxySetter(val)
对于引用类型常见的另一种类型 —— “数组”,Vue 不能检测以下方式数组的变动:
- 利用索引直接设置一个数组元素时,例如
arr[arr.length] = newValue。 - 修改数组的长度时,例如
arr.length = arr.length - 1。
对于数组,既可以使用数组自带的操作方法,例如 splice、push 来触发 Vue 检测更新,也可以继续使用 set 方法,只是在用法上稍微有点不同,其第二个参数是数组元素的下标索引。
this.$set(this.arr, 0, 1);
声明根级响应式 property
Vue 不允许动态添加根级响应式 property,所以你必须在初始化实例前声明所有根级响应式 property,哪怕只是一个空值。
异步更新队列
Vue 是采用 异步 的方式来更新 DOM。
异步的方式,大大提高了性能,如果采用同步,必然会堵塞 UI 的渲染。
只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
“事件循环” 是基于浏览器的 Event Loop。从同步代码执行 -> 微任务执行完成 -> 宏任务执行完成是为一个事件循环。 拥有相同 id 的 Watcher 不会被重复加入到队列中去,所以不会执行 1000 次 Watcher 的 run。最终的结果是直接把数据变化的值从 1 变成 1000,大大提升了性能。 tick 可以理解成步骤(数据图表的刻度概念🤔),Vue 的每个事件循环期间都会做很多工作,每个工作可以看成是一个 tick。例如,查找异步队列,推入异步队列是一个 tick,执行 DOM 更新是一个 tick,更新完成,执行回调是一个 tick。
Vue 在内部对异步队列尝试使用原生的 Promise.then、MutationObserver 和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0) 代替。
这是一种逐渐降级的方案,其中
Promise与MutationObserver属于微任务(Micro-task),而setImmediate(IE 专有) 与setTimeout属于宏任务(Macro-task)。前者的执行优先级要大于后者。
例如,当你设置 vm.someData = 'new value' 该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环的“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)。这样回调函数将在 DOM 更新完成后被调用。例如:
vm.someData = 'new value'的更改实际上是在同步环境下完成的。Vue.nextTick(callback)可以看成是watcher执行完成后的回调方法。
var vm = new Vue({
el: '#example',
data: {
message: '123'
}
})
vm.message = 'new message' // 更改数据
vm.$el.textContent === 'new message' // false
Vue.nextTick(function () {
vm.$el.textContent === 'new message' // true
})
可以直接使用组件实例上的
$nextTick方法更方便。
因为 $nextTick() 返回一个 Promise 对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}