响应式数据原理
首先会使用实例化过程中的参数对vue进行初始化,其中涉及到的功能通过原型的方式添加
import { initMixin } from "./init";
function Vue(option){
this._init(option); // 实现vue的初始化
}
initMixin(Vue);
export default Vue;
会将参数放到实例对象的&options上,方便后续访问。后续组件开发的时候 Vue.extend可以创造一个子组件,子组件可以继承Vue,子组件也可以调用_init方法。
export function initMixin(Vue){
Vue.prototype._init = function(options){
const vm = this; // 当前的vue实例对象
vm.$options = options; // 方便后续其他方法访问options
initState(vm);
}
}
对data进行数据劫持,vue里面的data可能是函数也可能是对象,这是vue的一个考点。1、根实例的data可以是一个对象也可是函数 ;2、vue组件里面的data是一个函数,并且返回一个对象,这样复用组件时,每一个组件都有一个独立的data数据源,不共享数据。
function initData(vm){
let data = vm.$options.data;
data = vm._data = utils.isFunction(data) ? data.call(vm) : data;
observe(data);
}
在vue2里面主要使用里object.defineProperty实现劫持,主要分为两种情况:一种是对对象进行数据监听,这里就是使用object.defineProperty;另一种就是对数组进行监听,需要对数组的一些原生方法进行重写。
对象劫持
给监听的对象添加_ob_属性,一方面是为了标志已监听,另一方面是为了方便监听数组插入的内容,同时设置不可枚举,防止遍历时递归死循环。 vue2慢的一个原因就是这里,数据劫持,首先是用了Object.defineProperty,对每一个属性都进行了监听,并且又对每一层进行了递归监听,而在监听的过程中又用了闭包(可能会产生过多的闭包);只是对已有属性进行监听。所以只能监听已有属性,不能监听数组通过下标修改、添加的值,不能监听对象新增的属性,可以通过Vue.$set()进行设置监听。这是vue2的一个考点。
优化:
- 1、减少data中的属性,
- 2、不要在data中写层级过深的对象
- 3、不需要监听的数据可以使用冻结Object.freez()
constructor(data){
Object.defineProperty(data, '_ob_', {
value: this,
enumerable: false,
})
// 数组和对象的监听方式不一样
if(Array.isArray(data)){
data.__proto__ = newArray; // 改变数组的原型对象,从而重写数组方法
this.observeArray(data); // 如果数组里面是对象,就会递归进行监听
}else{
this.walk(data);
}
}
observeArray(arr){
arr.forEach(item => observe(item));
}
walk(data){
// 循环遍历data的key值进行观测
Object.keys(data).forEach(key => {
defineReactive(data, key, data[key]);
})
}
}
function defineReactive(obj, key, value){
observe(value); // 递归每一层,监听每一层对象
Object.defineProperty(obj, key, {
get(){
return value; // 这里实际上就是闭包的作用了,也就是说每次监听都会产生一个闭包
},
set(newValue){
if(value === newValue) return;
observe(newValue); // 如果新修改的值为对象,则对该对象继续监听
value = newValue;
}
})
}
export function observe(data){
if(!utils.isObject(data)){
return ;
}
if(data._ob_){
return ;
}
return new Observe(data);
}
数组劫持
只需重写vue data里面的数组的原型方法,不需对其他数组造成影响。在重写原数组的时候,先保留原方法,而后再加上自己的逻辑,这叫切片思想。
// 保存原数组上的方法
const newArray = Object.create(Array.prototype);
// 以下方法会改变原数组,只需对以下方法进行重写
const methodList = [
'shift',
'unshift',
'splice',
'pop',
'push',
'reverse',
'sort',
]
methodList.forEach(method => {
newArray[method] = function(...args){
// 执行原数组的函数逻辑
Array.prototype[method].apply(this, args);
// 接下来再执行新的逻辑,这种方式也叫做切片
let inserted = null; // 保存插入数组中的内容
const ob = this._ob_;
switch(method){
case 'splice':
inserted = args.slice(2);
break;
case 'unshift':
case 'push':
inserted = args;
break
default:
break;
}
if(inserted){
ob.observeArray(inserted);
}
console.log('重写后的数组');
}
})
export default newArray;