菜鸡手写vue(二)-响应式数据原理

139 阅读3分钟

响应式数据原理

首先会使用实例化过程中的参数对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;