Vue2响应式

125 阅读3分钟

什么是响应式

所谓vue的响应式呢,就是在组件data的数据一旦变化,立刻触发视图的更新

我们先看一个案例

<template>
  <div id="app">
    <p @click="change">{{ name }}</p>
    <ul>
      <li v-for="(item, index) in list" :key="index">{{ item }}</li>
    </ul>
    <button @click="add">add</button>
  </div>
</template>

<script>
 export default {
  name: "app",
  data() {
    return {
      name: "vue",
      list: ["a", "b", "c"],
    };
  },
  methods: {
    change() {
      this.name = "zhangsan";
    },
    add() {
      this.list.push("d");
    },
  },
};
</script>

但是组件的data数据变化,立刻触发视图更新,这个事情是怎么实现的,如何知道data数据发生变化了呢?

带着这个疑问我们就探讨下data数据变化后,我们是怎么立刻第一时间知道的

核心API - Object.defineProperty

vue2实现响应式的最核心的API

我们现看下 Object.defineProperty 的基本用法

let data = {};
let name = 'zhangsan';
Object.defineProperty(data, 'name', {
	enumerable: true,
  configurable: true,
	get: function() {
		console.log('get');
		return name
	},
	set: function(newVal) {
		console.log('set', newVal);
		name = newVal;
	}
});
console.log(data.name);  // get zhangsan
data.name = 'lisi';			 // set lisi
console.log(data.name);	 // get lisi

可以看到在修改name值后,可以打印出 set lisi,这说明我们已经监听到了数据的变化了

对象的监听

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

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // 核心 API
  Object.defineProperty(target, key, {
		enumerable: true,
	  configurable: true,
    get() {
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue;
        // 触发更新视图
        updateView();
      }
    },
  });
}

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

  // 重新定义各个属性
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

const data = {
  name: 'zhangsan',
  age: 18
};

// 监听数据
observer(data);

console.log(data.name);
data.name = 'lisi';
console.log(data.name);
// console.log(data.age);
// data.age = 19;
// console.log(data.age);

复杂对象的深度监听

const data = {
  name: 'zhangsan',
  age: 18,
  info: {
    address: '武汉' // 需要深度监听
  }
};

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

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // ⭐️ 深度监听 value是个对象{address: '武汉'}
  observer(value)
  
  // 核心 API
  Object.defineProperty(target, key, {
		enumerable: true,
	  configurable: true,
    get() {
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // ⭐️ 深度监听 新设置的值如果是数组或对象,所以这里也需要调用observer
        observer(newValue);
        
        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue;
        // 触发更新视图
        updateView();
      }
    },
  });
}

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

  // 重新定义各个属性
  for (let key in target) {
    defineReactive(target, key, target[key]);
  }
}

observer(data);

data.info.address = '上海'
data.age = {num: 19};
data.age.num = 20;

data.x = '123'; // 新增属性,监听不到,不会触发视图更新 - 需要使用Vue.set
delete data.name; // 删除属性,监听不到,不会触发视图更新 - 需要使用Vue.delete

数组的监听

const data = {
  name: 'zhangsan',
  age: 18,
  info: {
    address: '武汉'
  },
  nums: [1, 2, 3]
};

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

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

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
  // 深度监听 value:nums
  observer(value)
  
  // 核心 API
  Object.defineProperty(target, key, {
		enumerable: true,
	  configurable: true,
    get() {
      return value;
    },
    set(newValue) {
      if (newValue !== value) {
        // 深度监听 新设置的值如果是数组或对象,所以这里也需要调用observer
        observer(newValue);
        
        // 设置新值
        // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
        value = newValue;
        // 触发更新视图
        updateView();
      }
    },
  });
}

// 监听对象属性
function observer(target) {
  if (typeof target !== "object" || target === null) {
    // 不是对象或数组
    return target;
  }
    
  // ⭐️ 如果是数组 nums 
  if (Array.isArray(target)) {
      target.__proto__ = arrProto;
  } else {
    // 重新定义各个属性
    for (let key in target) {
      defineReactive(target, key, target[key]);
    }
  }
}

observer(data);

data.nums.push(4); 

Object.defineProperty的缺点

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性/删除属性(Vue.set/Vue.delete)
  • 无法原生监听数组,需要特殊处理

总结

我们现在知道了vue2是怎么做到响应式监听的,当data的数据发生改变了,我们就可以知道现在需要触发视图更新了,那么视图又是怎么更新的呢?带着这个疑问,我们下次见。