美文网首页
vue中响应式数据状态原理分析

vue中响应式数据状态原理分析

作者: 雪燃归来 | 来源:发表于2020-06-17 18:06 被阅读0次

       如果你熟悉vue、react,相信你对数据驱动这个概念不会太陌生,数据驱动的实现主要依托于响应式数据渲染。本篇文章,我们将讲解一下vue中实现响应式数据状态的原理。

       在vue2.X中,实现响应式的方式主要使用了Object.definePropertyAPI,而在vue3.X中采用了Proxy对象进行了重写,由于vue3.X还不是一个主流版本,而且浏览器对Proxy的支持程度良莠不齐,要使用这个更加高级的API,可能还需要再过一段时间。

一、Object.defineProperty的基本用法

       Object.defineProperty()方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。我们可以通过这个对象,改变要监测对象属性的getset方法,在获取和设置对象属性的值之前,做一个拦截层,以实现特定的逻辑。

const data = {}
let name = 'antiai'
 Object.defineProperty(data, 'name', {
    get(){
        console.log('get')
        return name
    },
    set(newVal){
        console.log('set')
        name = newVal
    }
})
    
data.name                  // 'get'
data.name = 'aiguo'        // 'set '

二、Object.defineProperty实现对对象的监听

1、准备要监听改变的数据源

const data = {
  name:'antiai',
  age: '20'
}

2、定义监听对象属性方法

function observer(target){
  if(typeof target !== 'object' || target === null){
    // 不是对象或者数组
    return target
  }

  //重新定义各个属性(for in也可以遍历数组)
  for(let key in target){
    defineReactive(target, key, target[key])
  }
} 

observer(data)

       我们只对对象和数据的改变做监听。

3、重新定义属性,监听起来

function defineReactive(target, key, value){
  // 核心API
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newValue){
      if(newValue !== value){
        // 设置新值
        // 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
        value = newValue
        
        //触发更新视图
        updateView()
      }
    }
  })
}

       此处是我们响应式设计的核心。使被监听的对象在使用get、set操作的时候能够执行特定的逻辑。要注意的是在set操作的时候,value一值存在闭包中,此处设置完后,在get也是可以获取到的,所以在value = newValue打上断点,结果如下图所示:

value的值一直存在闭包Closure中

4、触发更新试图

// 触发更新试图
function updateView(){
  console.log('视图更新')
}

5、测试代码

data.name = 'aiguo'          //视图更新
data.age = 21                //视图更新

       在对data数据源做更新的时候,都执行了监测机制中的相关逻辑。

6、实现对数据源的深度监听

       但是还有一个问题。如执行下面的代码

data.info.address = 'shenzhen'

       我们发现,并没有触发更新视图操作。这是什么原因呢?因为我们的observer函数只监听了单层数据,所以当我们碰到多层对象的时候,需要递归的调用defineReactive实现多层数据监听。

function observer(target){
 ...
  //重新定义各个属性(for in也可以遍历数组)
  for(let key in target){
    defineReactive(target, key, target[key])
  }
} 

完善后的代码

function defineReactive(target, key, value){
  // 深度监听
+  observer(value)

  // 核心API
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newValue){
      if(newValue !== value){
        // 深度监听
+        observer(newValue)

        // 设置新值
        // 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
        value = newValue
        
        //触发更新视图
        updateView()
      }
    }
  })
}

       请你注意带+的两行代码。此处主要用到了递归的操作,没有可以多做解释的。可以通过断点调试一下。现在执行刚刚那段代码,视图函数就被执行了。

7、拓展对数组属性的监听

       是不是在上面的操作完成后,我们的响应式操作就完成了呢?你可以下执行下面的的代码。

data.nums.push(4)

发现并没有触发我们的视图更新函数。下面我们就针对数组属性做特殊的处理。原理就是如下:
       1、将Array.prototype备份到oldArrayProperty中,创建新的数组对象arrProto,并将其原型设置为oldArrayProperty,保证arrProto可以调用到原生的数组的方法。

       2、然后对arrProto创建类方法,在这些方法中可以实现数据视图更新的操作。

       3、在调用类方法的时候,一并调用相应的示例方法,保证将数据保存到源对象data中。

       4、在observer函数中进行调用。

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshift','splice'].forEach(method => {
  arrProto[method] = function(){
    updateView() // 触发更新视图
    oldArrayProperty[method].call(this, ...arguments)
  }
})
//监听对象属性
function observer(target){
  
  if(typeof target !== 'object' || target === null){
    // 不是对象或者数组
    return target
  }

+  if(Array.isArray(target)){
+     target.__proto__ = arrProto
+  }

  //重新定义各个属性(for in也可以遍历数组)
  for(let key in target){
    defineReactive(target, key, target[key])
  }
} 

       再次运行上面的代码,就可以触发视图更新了。下面是完整代码:

// 触发更新试图
function updateView(){
  console.log('视图更新')
}

// 重新定义数组原型
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push','pop','shift','unshift','splice'].forEach(method => {
  arrProto[method] = function(){
    updateView() // 触发更新视图
    oldArrayProperty[method].call(this, ...arguments)
  }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value){
  // 深度监听
  observer(value)
  
  // 核心API
  Object.defineProperty(target, key, {
    get(){
      return value
    },
    set(newValue){
      if(newValue !== value){
        // 深度监听
        observer(newValue)

        // 设置新值
        // 注意,value一值存在闭包中,此处设置完后,在get也是可以获取到的
        value = newValue
        
        //触发更新视图
        updateView()
      }
    }
  })
}

//监听对象属性
function observer(target){
  
  if(typeof target !== 'object' || target === null){
    // 不是对象或者数组
    return target
  }

  if(Array.isArray(target)){
      target.__proto__ = arrProto
  }

  //重新定义各个属性(for in也可以遍历数组)
  for(let key in target){
    defineReactive(target, key, target[key])
  }
} 

//准备数据源
const data = {
  name:'antiai',
  age: '20',
  info: {
    address: '北京'
  },
  nums: [1,2,3]
}

observer(data)

//测试
data.name = 'aiguo'
data.age = 21
data.info.address = 'shenzhen'
data.nums.push(4)

       在vue3.x中使用了proxy,说明Object.defineProperty这个方法是有缺点,具体的缺点主要表现在以下几个方面:
       1、深度监听需要递归到底,一次性计算量大
       2、无法监听新增属性/删除属性,要使用Vue.set、Vue.delete
       例如下面的测试代码,将不能被监听到。

data.x = '100'      // 新增属性,监听不到 ——所以有Vue.set
delete data.name //删除属性,监听不到——所以有Vue.delete

       3、无法原生监听数组,需要特殊处理

相关文章

网友评论

      本文标题:vue中响应式数据状态原理分析

      本文链接:https://www.haomeiwen.com/subject/fkpsxktx.html