vue2中通过数组索引修改数据,会引起视图的变化吗?

2,063 阅读6分钟

vue中通过数组索引修改数据,会引起视图的变化吗?

带着这个问题,跟我一起手写vue响应式数据原理,一探究竟吧。

1. 导出vue构造函数

import {initMixin} from './init';

function Vue(options) {
    this._init(options);
}
initMixin(Vue); // 给原型上新增_init方法
export default Vue;

2. init方法中初始化vue状态

首先在 Vue 原型上挂载 init 方法, 用户传入的 options 挂载到 vm 的$options 上
注:vm 指代 new 出来的 Vue 实例

import { initState } from "./state";
export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options;
    initState(vm);
  };
}

3. 根据不同属性进行初始化操作

初始化数据

再通过initSate 对vm的数据进行初始化(包括 props,data,computed,watch) 今天我们就只讲data初始化,其他的我放下篇文章再讲。

export function initState(vm) { // 状态的初始化
    const opts = vm.$options;
    if (opts.data) {
        initData(vm);
    }
    // if(opts.computed){
    //     initComputed();
    // }
    // if(opts.watch){
    //     initWatch();
    // }
}
function initData(vm) {
  let data = vm.$options.data;
  //data有两种写法,有属性和函数两种写法
  //所以我们在传入观测数据前要对data进行类型判断
  data = vm._data = isFunction(data) ? data.call(vm) : data;
  //data是函数,则使用call改变将this指向vm,并立即执行data函数,把函数返回的 data 对象赋值给 vm.\_data,否则直接返回 data
  // 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
  observer(data);
}
export function observe(data) {
  //如果是对象才观测
  if (!isObject(data)) {
    return;
  }
//是对象则返回一个Observer类的实例来观测数据
  return new Observer(data);
}

4. 数据劫持

step1.劫持data.key的每一个key

先使用walk方法对data的进行遍历,将data每个属性用defineProperty重新定义,加上get、set属性访问器。

需要注意的一点:

直接给 v._data.b=1这样给v._data添加一个新的属性b是没办法 observe 的。因为进行数据遍历的时候用的是初始data上的已经存在的key。

step2.劫持data[key]

再对data[key]中的数据进行劫持,数组和对象的劫持方法不同。

第一种情况:data[key]为对象时,observe对象的每一项

有两个需要注意的点:

  1. 如果是对象里面包对象的数据格式,那么还要对对象进行 observe,通过递归来实现。
  2. 如果给一个旧值赋值成一个对象的话,也要对这个新值进行 observe。
import { isObject } from "../utils";
import { arrayMethods } from "./array";

// 1.如果数据是对象 会将对象不停的递归 进行劫持
// 2.如果是数组,会劫持数组的方法,并对数组中不是基本数据类型的进行检测

class Observer {
    constructor(data) { // 对对象中的所有属性 进行劫持
        Object.defineProperty(data,'__ob__',{
            value:this,
            enumerable:false //设置为不可枚举,就不能遍历,可以解决多次遍历__ob__导致栈溢出的问题。
        })
        //因为向数组新增的时候还要observe新增的数据,所以需要调用Observer类上的observeArray方法,所以vue通过__ob__==this,把实例传过来,这样我们就在数组方法重写那个地方调用observeArray方法了。
        if(Array.isArray(data)){
            // 数组劫持的逻辑
            // 对数组原来的方法进行改写, 切片编程  高阶函数
            data.__proto__ = arrayMethods;
            // 如果数组中的数据是对象类型,需要监控对象的变化
            this.observeArray(data);
        }else{
            this.walk(data); //对象劫持的逻辑 
        }
    }
    observeArray(data){ // 对我们数组的数组 和 数组中的对象再次劫持 递归了
        // [{a:1},{b:2}]
        data.forEach(item=>observe(item))
    }
    walk(data) { // 对象
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key]);
        })
    }
}
// vue2 会对对象进行遍历 将每个属性 用defineProperty 重新定义 性能差
function defineReactive(data,key,value){ // value有可能是对象
    observe(value); // 本身用户默认值是对象套对象 需要递归处理 (性能差)
    Object.defineProperty(data,key,{
        get(){
            return value
        },
        set(newV){ 
            // todo... 更新视图
            observe(newV); // 如果用户赋值一个新对象 ,需要将这个对象进行劫持
            value = newV;
        }
    })
}

export function observe(data) {
    // 如果是对象才观测
    if (!isObject(data)) {
        return;
    }
    if(data.__ob__){
        return;
    }
    // 默认最外层的data必须是一个对象
    return new Observer(data)
}

第二种情况:data[key]为数组时:重写数组原型方法

用户很少通过索引去操作数组,arr[3]=100,vue 直接监听数组索引,性能消耗严重.所以内部数组监听不采用 objectProperty,通过重写数组方法来监听数组

  1. push shift unshift pop reverse sort splice 如果用户调用的是以上七个方法,会调用 vue 重写的,否则用原来的数组方法。
    为什么只有这七个呢?
    因为这七个方法会改变数组方法,而其他像concat这种就不会改变原数组。
  2. 数组没有监控索引的变化,但是如果数组中的数据是对象类型,会对对象则需要observe对象变化.所以如果arr:[{name:"chibaozi"}],那么像这样vm.arr[0].name="早上吃包子"通过下标修改 是可以被监听到的。
  3. 数组新增的数据是对象类型时,也需要observe新增的对象。
let oldArrayPrototype = Array.prototype
export let arrayMethods = Object.create(oldArrayPrototype);
// arrayMethods.__proto__ = Array.prototype 继承
let methods = [
    'push',
    'shift',
    'unshift',
    'pop',
    'reverse',
    'sort',
    'splice'
]
methods.forEach(method =>{
    // 用户调用的如果是以上七个方法 会用我自己重写的,否则用原来的数组方法
    arrayMethods[method] = function (...args) { //  args 是参数列表 arr.push(1,2,3)
        oldArrayPrototype[method].call(this,...args); // arr.push(1,2,3);
        let inserted;
        let ob = this.__ob__; // 根据当前数组获取到observer实例
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args ; // 就是新增的内容
                break;
            case 'splice':
                inserted = args.slice(2)
            default:
                break;
        }
        // 如果有新增的内容要进行继续劫持, 我需要观测的数组里的每一项,而不是数组
        // 更新操作.... todo...
        if(inserted) ob.observeArray(inserted)
    }
})

5. 数据代理

我们还需要将 vm._data.key 代理到 vm.key上,也是通过defineProperty的方法。

function vmProxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key];
    },
    set(newV) {
      data[source][key] = newV;
    },
  });
}
function initData(vm) { //
    let data = vm.$options.data; // vm.$el  vue 内部会对属性检测如果是以$开头 不会进行代理
    // vue2中会将data中的所有数据 进行数据劫持 Object.defineProperty
    // 这个时候 vm 和 data没有任何关系, 通过_data 进行关联
    data = vm._data = isFunction(data) ? data.call(vm) : data;
    // 用户去vm.xxx => vm._data.xxx
    for(let key in data){ // vm.name = 'xxx'  vm._data.name = 'xxx'
        proxy(vm,'_data',key); 
    }
    observe(data);
}

小结

看到这里,相信你也有答案了。 在vue2中修改数组的索引和长度是无法监控到的。

Vue2中data对象内部通过defineReactive方法,对对象的每个属性进行遍历,使用Object.defineProperty将属性进行劫持。(只会劫持已经存在的属性)。数组因为考虑性能原因,没有使用Object.defineProperty对数组的每一项进行拦截。数组是通过重写数组方法来实现。需要通过pop splice shift unshift reverse sort这七个变异方法修改数组,才会触发数组对应的watcher更新,数组中如果是对象数据类型也会递归劫持。 vue3改为使用proxy来实现响应式数据。 内部依赖收集,是通过每个属性都拥有自己dep属性,存放他所依赖的watcher,当属性变化时,通知自己对象的watcher去更新。

在更加深入理解响应式原理后,我们也可以总结出一些vue代码优化的方法:

  1. 对象层级不要嵌套太多层。因为vue2中多层对象是通过递归来实现劫持,所以对象层级过深,性能就会差。
  2. 不需要响应式的内容不要放到data中。
  3. 可以使用object.freeze()冻结数据 下一篇文章再手写模版编译,嘿嘿。