1. 数据观测[vue2原理总结]

159 阅读3分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

完整代码 随文章更新....

前言

前段时间跟着教学视频,过了一遍vue2的底层实现。主要聚焦:1. 响应式数组 2. 组件渲染 3. diff算法这三个方面。理解这三块有助于自己写出更健壮的代码,若来年要出去面试的话这一块也必不可少。距离上次学习这部分知识已经过去了很久了,感觉还在,细节已经模糊。所以想重新出发,就当是的代码和源码一起去对这部分知识来一个系统的总结。

概览

个人理解数据观测就是重写data对象的属性的getter,setter。之所以需要重写,是因为我们需要在getter的时候搜集依赖,setter的时候通知依赖更新(以后的章节)。

这一章聚焦在以下问题:

  1. 对对象观测
  2. Observe类的实现
  3. 为什么不观测数组的索引
  4. 劫持数组部分原型方法

目标

有一个data数据结构如下:

data: {
    name: 'xxx',
    schoole: {
        name: 'xxx',
        age: 19
    },
    arr: [
        { name: 'xxx' }, 2, 3,
    ]
}

经过一个方法洗礼,完成蜕变

observe(data)
data.app.push({name: 1})

image.png

对对象进行观测

总结之后,思路如下:

  1. 创建observe函数,接受一个data。遍历对象的属性,设置递归结束条件
  2. 创建Observe类,其功能就是对对象进行观测

observe函数实现

function observe(data) {
  // 递归结束条件
  if( !isObject(data)) return
  return new Observe(data)
}

Observe类

class Observe {
  constructor (data) {
    if(Array.isArray(data)) {
     // 数组劫持策略
    } else {
      this.wark(data)
    }
  }
  wark (data) {
    // 遍历对象属性
    Object.keys(data).map(key => {
      let value = data[key]
      observe(value)
      Object.defineProperty(data, key, {
        get() {
          console.log(`getter: ${key}: ${value}`)
          return value
        },
        set (newVal) {
          // 如果设置的值是一个对象,对设置的值观测  
          if(isObject(newVal)) observe(newVal)
          console.log(`setter: ${key}: ${JSON.stringify(newVal)}`)
          value = newVal
        }
      })
    })
  }
}
  1. 使用Object.defineProperty重写对象属性的getter,setter。
  2. 如果setter的值也是一个对象,则对设置的值进行观测

数组观测

对于数组有不同的策略,一是不能对其索引进行观测,后续会说明原因。再则就是对一些能够增长数组的原型方法如:push。要对其参数进行观测。

数组为什么不能观测其索引

数组也是对象,数组的key就是其下标

image.png

如果对数组的key进行观测,就会下图的效果

image.png 取值arr(vm.arr),效果如下。可以看到首先会输出数组的每一项,然后再输出整个数组。你所认为,简简单单的一个取值操作,其实底层有这么多的性能开销。

image.png

所以在写代码的时候要反复用到对象的属性,首先应该把它取出来。避免不必要的开销

// 坏习惯
let obj = {name: 'pual'}
console.log(obj.name)
console.log(obj.name)
console.log(obj.name)

// 好习惯
let {name} = obj
console.log(name)
console.log(name)
console.log(name)
console.log(name)

数组观测策略

  1. 只对数组的项进行观测,不对索引观测 image.png

  2. 劫持数组部分原型方法

思路:数组实例(arr)<----自创原型(重写方法)<-----数组原型(Array.prototype)。在数组实例与数组原型一个自己的原型对象,在这个原型对象上重写方法,达到劫持效果。思路如下:

let arr = [1,2]
let arrProtoNext = Object.create(Array.prototype)

arrProtoNext.push = function push(item) {
    // 这里可对item进行观测
    const res = Array.prototype.push.call(this, item)
    return res
}

Object.setPrototypeOf(arr, arrProtoNext)

总结

写到这里差不多完了,完整代码查看git仓库。写到这里突然发现了一个问题,代码如下:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta http-equiv="X-UA-Compatible" content="IE=edge">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app">
    <span v-for="(item, i) in arr">{{item}},</span>
  </div>
  <script src="https://cdn.jsdelivr.net/npm/vue@2.6.14/dist/vue.js"></script>
  <script>
    const vm = new Vue({
      data() {
        return {
          name: 'xxx',
          schoole: {
            name: 'xxx',
            age: 19
          },
          arr: [
            1, 2, 3,
          ],
        }
      },
      el: '#app'
    })
    vm.arr[3] = 4
  </script>
</body>
</html>

这里通过cdn引入Vue,然后vue里面有一个长度为3的数组,当我们通过索引,显示增加一行的时候。页面并没有更新。原因很简单:数组的索引没有做观测。我们只监听了数组变化的原型函数。所以操作数组的时候我们要通过原型上的方法操作数

vue3使用了proxy来代替Object.defineProperty,出于什么原因了?是不是能解决这个问题?