用原生JS搞懂VUE的响应式原理,这篇文章就够了

在实现之前我们先了解下VUE的响应式是什么;

它是Vue 最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的 JavaScript 对象。而当你修改它们时,视图会进行更新。

博主网站:www.dzyong.top

微信公众号:《前端筱园》

原理是什么

VUE中实现响应式运用到了JavaScript中object的一个很重要的属性Object.definePropertyObject.defineProperty 方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

VUE会将一个普通的JavaScript对象传入VUE实例中作为data选项,data中就是我们运用到的所有变量,也就是下图所示的部分。

Vue会遍历data中的所有属性,并使用Object.defineProperty把这些属性全部转为getter/setter。Object.defineProperty 是 ES5 中一个无法 shim 的特性,这也就是 Vue 不支持 IE8 以及更低版本浏览器的原因。

每个组件实例都对应一个 watcher 实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。

如何用JS来实现

先定义一个普通对象,并输出在控制台中查看。

let info = {name: '张三'}
console.log(info);

可以看到它就是一个很普通的对象,那么如果我们使用Object.defineProperty来新增一属性再输出看看有什么不同呢。

let info = {name: '张三'}
Object.defineProperty(info, 'age' ,{
    get(){
    },
    set(param){
    }
})
console.log(info);

可以看到这里多了get ageset 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中的注意事项

由于 Vue 不允许动态添加根级响应式属性,所以你必须在初始化实例前声明所有根级响应式属性,哪怕只是一个空值:

var vm = new Vue({
 data: {
 // 声明 message 为一个空值字符串
 message: ''
 },
 template: '<div>{{ message }}</div>'
})
// 之后设置 `message`
vm.message = 'Hello!'

如果你未在 data 选项中声明 message,Vue 将警告你渲染函数正在试图访问不存在的属性。

最为重要的一点

我们先看在Vue中的一个例子,在 updateMessage 中明明对message进行了改变,但是为什么后面输出的值还是“未更新”呢。

Vue.component('example', {
  template: '<span>{{ message }}</span>',
  data: function () {
    return {
      message: '未更新'
    }
  },
  methods: {
    updateMessage: function () {
      this.message = '已更新'
      console.log(this.$el.textContent) // => '未更新'
    }
  }
})

这是因为Vue 在更新 DOM 时是异步执行的。只要侦听到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据变更。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作是非常重要的。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。Vue 在内部对异步队列尝试使用原生的Promise.thenMutationObserver 和 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) // => '已更新'
 }
}

发布了72 篇原创文章 · 获赞 75 · 访问量 7万+

猜你喜欢

转载自blog.csdn.net/DengZY926/article/details/104824710
今日推荐