vue响应式原理 | vue2篇

449 阅读6分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第2天,点击查看活动详情

在学习过一段时间的vue2之后,也是浅浅的了解了一些偏原理性的东西,那我们今天就来聊聊如何在vue2中实现数据响应式原理。

响应式原理

响应式从字面上理解就是数据发生改变视图随着改变而改变 ,在VUE2.0响应式主要是利用了Object.defineProperty()数据劫持,以及观察者。

Object.defineProperty()

Object.defineProperty()  方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回此对象。其中有两个关键的可选键getset

  • get

  • 属性的 getter 函数,如果没有 getter,则为 undefined。当访问该属性时,会调用此函数。执行时不传入任何参数,但是会传入 this 对象(由于继承关系,这里的this并不一定是定义该属性的对象)。该函数的返回值会被用作属性的值。 默认为 undefined

  • set

  • 属性的 setter 函数,如果没有 setter,则为 undefined。当属性值被修改时,会调用此函数。该方法接受一个参数(也就是被赋予的新值),会传入赋值时的 this 对象。 默认为undefined

看个例子:

// 在对象中添加一个属性与存取描述符的示例
var obj = {};
var bValue;
Object.defineProperty(obj, "name", {
  get : function(){
    console.log('数据劫持正在监听name属性')
    return bValue
  },
  set : function(newValue){
    console.log('数据劫持正在修改name的属性值')
    bValue = newValue;
  },
  enumerable : true,
  configurable : true
});

obj.name = 18;
console.log(obj.name)

运行结果如下:

image.png

而在vue2.0中实现响应式的核心原理就是利用了javascript原生的Object.defineProperty()这个API,它只能劫持一个对象;可以重新定义属性,给属性安插 getter setter 方法

updateView

模拟diff算法去比较两个虚拟dom树的改变,来进行视图更新,并不是实现响应式原理的核心,所以就用一个简单的函数模拟一下;(在 Vue 中表现为 template 模板中引用了该变量值的 DOM 元素的变化

// 检验视图是否更新
function updateView() {
  console.log('更新视图');
}

defineReactive

对Object.defineProperty()进行二次封装接受三个参数,监听的目标对象(target)、属性名(key),以及属性值(value),一个target(对象)通过调用 defineReactive 就能够实现对 key(对应属性名)进行监听,类比到 Vue 中:

<script>
export default {
    data(){            // data ---> target
        name: '前端'  // name ---> key
    }                  // '前端'---> value
}
</script>

具体实现:

// 响应式
function defineReactive(target, key, value) {
  Object.defineProperty(target, key, {
      get() {
          return value
      },
      set(newVal) {
          if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
              value = newVal
              updateView()    // 数据更改,视图更新
          } 
      }
  })
}

observe

既然只能监听对象,那么我们就需要一个观察者函数,来用于对对象中的每个属性进行监听

function observer(target) {
  if (typeof(target) !== 'object' || target == null) { 
      return target
  }
  for (let key in target) {
      defineReactive(target, key, target[key])
  }
}

用typeof判断类型时null时特例,不太了解类型判断的可以看看这篇文章JS中令人疑惑的数据类型及其判断方法

如图最基本的响应式原理就实现了,我们修改了数据源的值,视图也相应的更新了

function updateView() {
  console.log('更新视图');
}

function defineReactive(target, key, value) {
  Object.defineProperty(target, key, {
      get() {
          return value
      },
      set(newVal) {
          if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
              value = newVal
              updateView()    // 数据更改,视图更新
          } 
      }
  })
}

function observer(target) {
  if (typeof(target) !== 'object' || target == null) { 
      return target
  }
  for (let key in target) {
      defineReactive(target, key, target[key])
  }
}

let data = {
  name: 'HTML'
}
observer(data)
console.log(data.name = 'CSS');
image.png

深度对象监听

我们再想象一个场景,我们在写vue的项目的时候,数据源里再套对象很常见的对吧,但是嵌套的对象,defineReactive就无法监听到,那我们应该用什么方法来解决呢? 其实只需要使用递归就可以了,在defineReactive内部再调用一次观察者函数就可以,这样就可以循环访问到嵌套的最底层,这样就可以解决嵌套对象的情况了。

// 1.

function updateView() {
  console.log('更新视图');
}

function defineReactive(target, key, value) {
  observer(value)
  Object.defineProperty(target, key, {
      get() {
          return value
      },
      set(newVal) {
          if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
              value = newVal
              updateView()    // 数据更改,视图更新
          } 
      }
  })
}

function observer(target) {
  if (typeof(target) !== 'object' || target == null) { 
      return target
  }
  for (let key in target) {
      defineReactive(target, key, target[key])
  }
}

let data = {
  name: 'HTML',
  information: {
    tel: '18888888888'
  }
}
observer(data)
// console.log(data.name = 'CSS');
console.log(data.information = {tel: '18888888889' });
image.png

但是有一个在vue2中我们以上这样封装无法实现给数据源添加没有的属性,因为在监听时,for in 显然遍历不到这个属性,那么就到不了 Object.defineProperty的里面去给它添加属性。

实现数组监听

前面我们已经实现了对象、嵌套对象的所有属性的的监听,但如果某个属性是数组,对数组进行push、pop等操作会触发更新吗?很显然是不会的,因为 Object.defineProperty 并不具备监听数组内部变化的能力,那么我们该如何解决呢————重写数组原型上的方法。

1.定义监听数组的原型 在 JS 中,通常对象都有原型(Object.create(null)创建的对象没有原型),而我们的目的是通过重写数组原型上方法(push、pop等)实现监听,而作为库或是框架,我们都不应该去改变全局原型上的任何原生方法或者属性,污染全局环境,所以,这里分3步:

首先:创建一个对象,将数组对象的原型赋给创建的对象

let oldArrayPrototype = Array.prototype

然后:创建新对象,原型指向该对象 (继承)

let proto = Object.create(oldArrayPrototype)

最后: 重写该对象上的方法

proto.push = function(){}...

proto.pop = function(){}...

代码实现:

let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)  // 继承

Array.from(['push','shift','pop','unshift']).forEach(method => {
  proto[method] = function() { // pushxxx 函数劫持, 把函数内部重写
    oldArrayPrototype[method].call(this, ...arguments)
    updateView()
  }
})

2、将需要监听的数组的原型指向自定义的特殊原型

对原来的 observe 进行修改,加入数组判断,如果是数组则修改该数组的原型

function observer(target) {
  if (typeof(target) !== 'object' || target == null) {
      return target
  }

  if(Array.isArray(target)) {
    // target.__proto__ = proto
    Object.setPrototypeOf(target,proto)
  }

  for (let key in target) {
      defineReactive(target, key, target[key])

  }
}

完整代码:

// 1.

function updateView() {
  console.log('更新视图');
}

function defineReactive(target, key, value) {
  observer(value)
  Object.defineProperty(target, key, {
      get() {
          return value
      },
      set(newVal) {
          if (newVal !== value) { // 如果两次的值相同就不更新视图,以达到节约性能
              value = newVal
              updateView()    // 数据更改,视图更新
          } 
      }
  })
}

let oldArrayPrototype = Array.prototype
let proto = Object.create(oldArrayPrototype)  // 继承

Array.from(['push','shift','pop','unshift']).forEach(method => {
  proto[method] = function() { // pushxxx 函数劫持, 把函数内部重写
    oldArrayPrototype[method].call(this, ...arguments)
    updateView()
  }
})

function observer(target) {
  if (typeof(target) !== 'object' || target == null) {
      return target
  }

  if(Array.isArray(target)) {
    // target.__proto__ = proto
    Object.setPrototypeOf(target,proto)
  }

  for (let key in target) {
      defineReactive(target, key, target[key])

  }
}

let data = {
  name: 'HTML',
  information: {
    tel: '18888888888'
  },
  num: [1,2,3]
}
observer(data)
// console.log(data.name = 'CSS');
// console.log(data.information = {tel: '18888888889' });
data.num.push(4)
image.png

缺点: 在性能开销方面,因为可能存在深层对象嵌套,所以需要对对象进行深度遍历,递归到底,需要开销不小的小的性能,如果有大量的层级非常高的对象进行响应式监听的绑定,会极大耗费初始化时的性能,导致拖慢 First Paint Time

点赞.jpg