阅读 1934

VUE响应式原理

使用vue开发过程中,一直关注点是业务功能开发和实现,很少去关注底层的东西,如果面试是一个vue开发的岗位时,百分百会问到的一个问题是:请讲讲vue响应式实现的原理;
为此网上看过相关视频和搜索相关资料,得到的简单的一句总结是:
通过Object.defineProperty去劫持data里的属性,将data全部属性替换成gettersetter,配合发布者和订阅者模式,每一个组件都有一个watcher实例,当我们对data属性赋值和改变,就会触发settersetter会通知watcher,从而使它关联的组件进行重新渲染。

Object.defineProerty详解

Object.defineProerty的基础用法

首先这个使基于对象的方法

let obj ={text:''};
Object.defineProperty(obj, 'name', {
  //value: 14,
  //writable: true,
  configurable: true,
  enumerable: true,
  set:function(val){
  	this.text=val;
  },
  get: function(){
  	return this.name;
  }
});
复制代码
  • value 该属性的值,可以是任何有效的 JavaScript 值(数值,对象,函数等)
  • configurable当它值为true时,才能添加对象属性描述和删除,该值如果改成false将不可逆;
  • writable 当它值为true时,才能对该属性value进行赋值改变
  • enumerable 当它值为true时,能对该属性进行枚举
  • get 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值
  • set属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。

这里vue主要用到的是属性的gettersetter方法

注意 getset不能和writablevalue共存,否则浏览器会报错
Uncaught TypeError: Invalid property descriptor. Cannot both specify accessors and a value or writable attribute, # at Function.defineProperty (<anonymous>)

Object.defineProerty VS proxy

vue2.0主要是通过Object.defineProerty来劫持对象属性,更改gettersetter方法, vue3.0用proxy来替代2.0的核心功能,那么他们之间究竟有什么不同呢?

** Object.defineProperty**

  • 不能监听到数组length属性的变化;
  • 不能监听对象的添加;
  • 只能劫持对象的属性,因此我们需要对每个对象的每个属性进行遍历。

Proxy

  • 可以监听数组length属性的变化;
  • 可以监听对象的添加;
  • 可代理整个对象,不需要对对象进行遍历,极大提高性能;
  • 多达13种的拦截远超Object.defineProperty只有get和set两种拦截。

发布者订阅者模式实现

观察者模式(Observer)

通常又被称为发布-订阅者模式消息机制,它定义了对象间的一种一对多的依赖关系,只要当一个对象的状态发生改变时,所有依赖于它的对象都得到通知并被自动更新,解决了主体对象与观察者之间功能的耦合,即一个对象状态改变给其他对象通知的问题。

创建一个观察者

 const Observe =(function(){
    let message = {};
    return{
        // 注册消息
        on: function (type, fn){
            // 如果此消息不存在 创建一个该消息的类型 将执行方法推入该消息执行队列中
            if(typeof message[type] === 'undefined'){
                message[type] = [fn];
            }else{
                // 如果消息存在时,直接将执行方法推入该消息对应的执行队列中
                message[type].push(fn)
            }

        },
        // 发布消息?
        emit: function(type, args){
            // 如果没有注册该消息直接返回
            if(!message[type]){
                return;
            }
            // 定义消息信息
            let events={
                type: type,
                args: args || {}
            }
            let len = message[type].length;
            for(let i=0;i<len;i++){
                // 依次执行消息对应的方法
                 message[type][i].call(this, events)
            }
        },
        // 移除消息接口
        remove:function(type){
            //如果消息执行队列存在
            if(message[type] instanceof Array){
                for(let i=message[type].length-1;i>=0;i--){
                    message[type].splice(i,1);
                }
            }
        }
    }
}())
复制代码

订阅一个消息

Observe.on('add',function(data){
   console.log(data.args.text);
})
Observe.on('newadd', function (data) {
   console.log(data.args.text+'~~~~~');
})
复制代码

发布消息

Observe.emit('add',{text:'我发布了消息'})
Observe.emit('add', { text: '我又发布了消息' })
Observe.emit('newadd', { text: '我刚刚又发布了消息' })
复制代码

移除消息事件

Observe.remove('newadd')
Observe.emit('newadd', { text: '我刚刚又又发布了消息' })
复制代码

关于观察者模式就讲到这了,至于怎么去实现vue的双向绑定的代码可以查看这一篇
参考:从vue源码看观察者模式

VUE响应式注意事项

响应式的使用存在一些限制。理解Vue的响应式原理可以帮助你弄明白这些限制,你也可以只是简单地直接记住这些限制。因为只存在少数几条限制,所以记住它们并不困难

为对象添加新的属

因为getter/setter方法是在Vue实例初始化的时候添加的,只有已经存在的属性是响应式的;当为对象添加一个新的属性时,直接添加并不会使这个属性成为响应式的;

  let app = new Vue({
  	data(){
  		return {
  			mydata:{
  				firstName:'li'
  			}
  		}
 	}
  })
  app.mydata.lastName = 'jie'
复制代码

这种情况下lastName是不会被响应的

解决办法
    1. 初始化时在对象上定义这个属性,并把它的值设置为undefined
mydata:{
    firstName:'li'
    lastNmae: undefined
}
复制代码
    1. 使用Object.assign()来创建一个新的对象然后覆盖原有对象,这种方式优点是可以一次更改多个属性
app.mydata = Object.assign({},app.mydata,{
	lastName: 'jie'
})
复制代码
    1. 使用vue官方提供的Vue.set(),它可以将属性设置为响应式的
 Vue.set(mydata,'lastName','jie')
复制代码

在组件里可以使用this.$set()来调用这个方法

设置数组元素

Vue 不能检测以下数组的变动:

  • 当你利用索引直接设置一个数组项时,例如:vm.items[indexOfItem] = newValue
  • 当你修改数组的长度时,例如:vm.items.length = newLength

例子:

  var vm = new Vue({
  data: {
    items: ['a', 'b', 'c']
  }
})
vm.items[1] = 'x' // 不是响应性的
vm.items.length = 2 // 不是响应性的
复制代码
解决办法
  • Vue.set
 Vue.set(vm.items, indexOfItem, newValue)
复制代码
  • Array.prototype.splice
vm.items.splice(indexOfItem, 1, newValue)
复制代码

Vue.nextTick(callback)

当我们更改数组后,dom是异步更新的,如果这个时候我们需要去调用dom更新后的事件或内容时,需要调用到Vue.nextTick()方法
简单来说,Vue 在修改数据后,视图不会立刻更新,而是等同一事件循环中的所有数据变化完成之后,再统一进行视图更新

官网说法如下:

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 更新完成后被调用。

    <div id="example">{{message}}</div>
复制代码
  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
  })
复制代码

在组件中可以这样写this.$nextTick(callback})

结语

好了,关于vue的响应式原理及需要注意点先介绍到这里了,这里面主要是涉及到属性劫持和观察者模式在文中也详细说明清楚了。

文章分类
前端
文章标签