在实现之前我们先了解下VUE的响应式是什么;
它是Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。
博主网站:www.dzyong.top
微信公众号:《前端筱园》
VUE中实现响应式运用到了JavaScript中object的一个很重要的属性Object.defineProperty
。Object.defineProperty
方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。
VUE会将一个普通的JavaScript对象传入VUE实例中作为data选项,data中就是我们运用到的所有变量,也就是下图所示的部分。
Vue会遍历data中的所有属性,并使用Object.defineProperty
把这些属性全部转为getter/setter。Object.defineProperty
是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。
每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
先定义一个普通对象,并输出在控制台中查看。
let info = {name: '张三'}
console.log(info);
可以看到它就是一个很普通的对象,那么如果我们使用Object.defineProperty来新增一属性再输出看看有什么不同呢。
let info = {name: '张三'}
Object.defineProperty(info, 'age' ,{
get(){
},
set(param){
}
})
console.log(info);
可以看到这里多了get age
和set age
。这些 getter/setter
对用户来说是不可见的,但是在内部它们让 Vue 能够追踪依赖,在属性被访问和修改时通知变更。这里需要注意的是不同浏览器在控制台打印数据对象时对 getter/setter
的格式化并不同,所以建议安装 vue-devtools 来获取对检查数据更加友好的用户界面。
接下来我们在get()
和set()
中分别进行读取数据与更新数据的操作。
let info = {name: '张三'}
let age = 18
Object.defineProperty(info, 'age' ,{
get(){
console.log('读取数据:' + age);
return age
},
set(param){
age = param
console.log('更新数据:' + age);
}
})
info.age = 20
console.log(info.age);
可以看到只要我们改变或更新数据时,就会触发set()和get()。我们可以利用这点来进行视图的更新。视图更新处理必然是在set()中。这里我们以使用两个按钮分别用来增加和减小年龄为例。效果如下:
传统方法的做法是:为这两个按钮绑定事件,封装一个改变函数,这个函数中获取年龄节点,然后再去改变这个节点的innerText。
而响应式的做法就简单了很多,我们把上面所封装的函数放到set()
中,这个年龄是用一个名为age
的变量存储的,只需要对这个变量的值进行改变那么就会触发setter
,从而自动的更新视图。我们来看一下完整效果与代码。
let info = {name: '张三'}
let age = 18
Object.defineProperty(info, 'age' ,{
get(){
console.log('读取数据:' + age);
return age
},
set(param){
age = param
watcher() //触发watcher
}
})
console.log('初始数据:' + info.age);
//绑定事件
let reduce = document.getElementById('reduce')
let add = document.getElementById('add')
reduce.onclick = function(){
info.age = --age
}
add.onclick = function(){
info.age = ++age
}
function watcher(){
updateView()
console.log('更新数据:' + age);
}
//更新视图
function updateView(){
let text = document.getElementById('age')
text.innerText = info.age
}
由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:
var vm = new Vue({
data: {
// 声明 message 为一个空值字符串
message: ''
},
template: '{{ message }}'
})
// 之后设置 `message`
vm.message = 'Hello!'
如果你未在 data
选项中声明 message
,Vue 将警告你渲染函数正在试图访问不存在的属性。
我们先看在Vue中的一个例子,在 updateMessage 中明明对message进行了改变,但是为什么后面输出的值还是“未更新”呢。
Vue.component('example', {
template: '{{ message }}',
data: function () {
return {
message: '未更新'
}
},
methods: {
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
}
}
})
这是因为Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的Promise.then
、MutationObserver
和 setImmediate,如果执行环境不支持,则会采用 setTimeout(fn, 0)
代替。
当你设置 vm.someData = 'new value'
,该组件不会立即重新渲染。当刷新队列时,组件会在下一个事件循环“tick”中更新。多数情况我们不需要关心这个过程,但是如果你想基于更新后的 DOM 状态来做点什么,这就可能会有些棘手。虽然 Vue.js 通常鼓励开发人员使用“数据驱动”的方式思考,避免直接接触 DOM,但是有时我们必须要这么做。为了在数据变化之后等待 Vue 完成更新 DOM,可以在数据变化之后立即使用 Vue.nextTick(callback)
。这样回调函数将在 DOM 更新完成后被调用。例如:
updateMessage: function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
this.$nextTick(function () {
console.log(this.$el.textContent) // => '已更新'
})
}
因为 $nextTick()
返回一个 Promise
对象,所以你可以使用新的 ES2017 async/await 语法完成相同的事情:
methods: {
updateMessage: async function () {
this.message = '已更新'
console.log(this.$el.textContent) // => '未更新'
await this.$nextTick()
console.log(this.$el.textContent) // => '已更新'
}
}