vue2-响应式数据原理

164 阅读3分钟

现象+核心

现象:data变化会立即触发视图更新,视图view点击后会更新data中的数据

核心:Object.defineProperty(target,key,desc),通过 Object.defineProperty 遍历对象的每一个属性,把每一个属性变成一个 getter 和 setter 函数,读取属性的时候调用 getter,给属性赋值调用 setter。监听基本数据类型没问题,若是对象需要深度监听迭代,若是数组需要改写原型上的方法来触发视图更新。改写原型方法里面会先触发视图更新再修改this指向执行真正的方法。

Object.defineProperty(target,key,desc)

作用:新增或修改对象上的属性

  • target属性所在的对象
  • key属性名
  • desc
    • 数据属性:value configurable writable enumerable
    • 访问器属性:get set,get返回一个内容,set赋值一个内容。属性的赋值和获取变为函数,从而可以监听。
var p1 = {
    name: "lisi",
}
// configurable能否delete删除属性从而重新定义属性,能否修改属性特性,能否把属性改为访问器属性,默认false。
Object.defineProperty(p1, "name", {
    configurable: false,
})
console.log(p1); //{ name: 'lisi' }
delete p1.name;
console.log(p1); //{ name: 'lisi' }

// writable:能否修改属性的值。默认值为false。
Object.defineProperty(p1, "age", {
    writable: false,
    value: 15,
})
console.log(p1.age); //15
p1.age = 20;
console.log(p1.age); //15

// enumerable:能否通过for in循环访问属性,默认false
Object.defineProperty(p1, "age", {
    enumerable: false
})
for (var i in p1) {
    console.log(p1[i]);
} // lisi

// get set
var book = {
    _year: 2004,
    edition: 1
}

Object.defineProperty(book, "year", {
    get: function () {
        return this._year
    },
    set: function (newYear) {
        if (newYear > 2004) {
            this._year = newYear;
            this.edition += newYear - 2004
        }
    }
})

book.year = 2005;
console.log(book.edition); // 2
console.log(book._year); //2005


// 定义多个属性
var student = {};
Object.defineProperties(student, {
    name: {
        writable: false,
        value: "lisi"
    },
    age: {
        writable: true,
        value: 16,
    },
    sex: {
        get() {
            return '男';
        },
        set(v) {
            p1.sex = v
        }
    }
})

p1.sex = "男";
console.log(student.name + ":" + student.age);
console.log(p1.sex); // 男
student.sex = "女";
console.log(student.sex); //男
console.log(p1.sex); // 女

双向数据绑定实现

缺点:

  • 不能监听到对象内部。

    • 解决:复杂对象要深度监听,递归到底,一次性计算量大,若value下面嵌套多层对象,会一直执行深度监听函数,若特别深初始化就会卡死。改进:啥时候用啥时候监听
  • 不能监听数组,要特殊处理

    • 解决:重新定义数组原型,创建新对象A继承Array.prototype并新增方法fn,fn内部先触发视图更新再通过call执行真正的数组方法。若target是数组将target.proto=A,后续target.push实际调用的是新对象A上边改写后的push,改写后的push里面会触发视图更新+执行真正的push。
  • 不能监听新增删除属性,不能走到Object.defineProperty,不会触发视图监听。

    • 解决:Vue.set Vue.delete

扩展:vue3使用proxy实现响应式,但是有兼容性问题。

// 数据
const data = {
  name: "aa",
  age: 20,
  info: {
    address: "beijing"//深度监听,否则key只能传到info,不能监听到下一层的address
  },
  nums: [1, 2]//修改原型上的方法
}
function updateView() {
  console.log("视图更新")
}
// 重新定义数组原型,创建新对象,原型指向oldArrayProperty,扩展新方法不会影响原型
const oldArrayProperty = Array.prototype;
const arrproto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach((methodName) => {
  arrproto[methodName] = function () {
    updateView()
    // 调用原型上的方法
    oldArrayProperty[methodName].call(this, ...arguments)
  }
})

function definReactive(target, key, value) {
  // 深度监听  observer里面会判断target是不是对象
  // value是对象时候,address可以作为key传入
  observer(value)
  Object.defineProperty(target, key, {
    get() {
      return value
    },
    set(newValue) {
      if (newValue !== value) {
        // 解决:data中key设置value新值可能从string类型变为对象,然后对这个对象动态修改,就不会触发视图更新,保证新值可以作为key被监听。
        observer(newValue)
        // 设置新值,value一直在闭包中,设置完之后,get可以获取到最新值
        value = newValue
        // 触发更新视图
        updateView()
      }
    }
  })
}
// 监听对象属性
function observer(target) {
  // 不是对象
  if (typeof target !== 'object' || target === null) {
    // 不是对象 数组
    return target
  }
  // 数组
  if (Array.isArray(target)) {
    // 改变target原型,调用改写后的方法,改写后的方法是先触发视图更新再调用Array原型上的方法
    target.__proto__ = arrproto
  }
  // 遍历data重定义各个属性,添加get set
  // target = {name: "aa",age: 20}
  for (let key in target) {
    definReactive(target, key, target[key])
  }
}
// 监听数据
observer(data)
// 测试
data.name = 'bb'//触发set
data.age = 21//触发set
data.info.address = 'shanghai'//要深度监听
data.nums.push(4)//要改原型方法
data.x = '1000'//新增不能触发视图更新
delete data.name//删除不能触发视图更新