likes
comments
collection
share

【Vue源码】Vue核心应用之数据劫持与属性代理

作者站长头像
站长
· 阅读数 16

Vue全家桶的系统学习,其中包括Vue源码分析Vue-Router的使用和原理Vuex的用法和原理Vue-ssr 和 一些常见的Vue面试题


数据(data)初始化

扩展 initData 方法之前,我们需要先知道一个概念。什么是数据劫持

数据劫持 就是通过 Object.defineProperty 来重写对象的 gettersetter 。数据更新时视图会发生改变,而视图改变时数据也会跟着更新,从而达到一个 视图和数据 互相影响的效果。

这种设计模式我们又称呼它为 观察者模式

import {observe} from './observer/index.js'
function initData(vm){
    let data = vm.$options.data;
    data = vm._data = typeof data === 'function' ? data.call(vm) : data; // 如果data是函数,改变其this指向
    observe(data);
}

vm._data 的作用其实就是为了让 vm实例上可以获取到 data 的值

下面我们就来利用观察者模式,实现Vue中的数据劫持

对象属性劫持

数据劫持主要包括 对象劫持数组劫持 两个方面。

先来看一下对象属性劫持的完整代码。

class Observer {
  // 观测值
  constructor(value) {
    this.walk(value);
  }
  walk(data) {
    let keys = Object.keys(data);
    keys.forEach((key) => {
      defineReactive(data, key, data[key]);
    });
  }
}
function defineReactive(data, key, value) {
  observe(value);
  Object.defineProperty(data, key, {
    get() {
      return value;
    },
    set(newValue) {
      if (newValue == value) return;
      observe(newValue);
      value = newValue;
    },
  });
}
export function observe(data) {
  if (typeof data !== "object" || data == null) {
    return;
  }
  return new Observer(data);
}

  1. 导出 observe 函数,进行类型判断。

    当前文件,我们只处理 Object 类型的值。不是 Object 类型的值不做处理。

    export function observe(data) {
      if (typeof data !== "object" || data == null) {
        return;
      }
      return new Observer(data);
    }
    
  2. 完善 Observer 类,让对象上的所有属性依次进行观测。

    class Observer {
      // 观测值
      constructor(value) {
        this.walk(value);
      }
      // 让对象上的所有属性依次进行观测
      walk(data) {
        let keys = Object.keys(data); // 获取对象上的key值
        keys.forEach((key) => {
          defineReactive(data, key, data[key]);
        });
      }
    }
    
  3. 定义 defineReactive 函数,用来对 Object.defineProperty 进行封装。

    循环调用 observe 函数,保证当前对象内的所有属性都被监听到。

    注:主要是为了处理 {a:{a:{a:1}}} 这种情况。但这样处理,层级越深性能越差,所以在 Vue3 中将其替换成了 proxy 进行处理。

    function defineReactive(data, key, value) {
      observe(value);
      Object.defineProperty(data, key, {
        get() {
          return value;
        },
        set(newValue) {
          if (newValue == value) return;
          observe(newValue);
          value = newValue;
        },
      });
    }
    

    setter 方法中我们会注意到,我们又重新监听了一次 newValue 值的变化。这个是因为用户在赋值时,可能会传入一个新的 Object 类型的对象,所以我们需要对新传入的值,也进行一次监听。保证所有值的 getset 都是被重写的。

如果我们使用这种方法去处理数组,也是可以行得通的。但是只能通过索引去进行监听,我们在处理数组时一般会使用专门的方法去进行处理(pushshiftpop等等)。为了性能考虑,我们还需要单独对数组方法进行处理。

数组方法劫持

其实数组的劫持,就是对原型上数组的方法进行重写

首先,需要先判断当前传入的值是否是数组。

import {arrayMethods} from './array';
class Observer { // 观测值
	constructor(value) {
    if (Array.isArray(value)) {
      value.__proto__ = arrayMethods; // 重写数组原型方法
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  // 循环并观测数组上的每一个值
  observeArray(value) {
    value.forEach((item) => {
      observe(item);
    });
  }
}

observeArray 可以保证数组上的每一个值都是被检测的,包括 Object 和 其他类型。

arrayMethods 就是重写了当前数组原型上的方法。

重写数组原型方法

下面我们来看一下 arrayMethods 到底是什么。

let oldArrayProtoMethods = Array.prototype;
export let arrayMethods = Object.create(oldArrayProtoMethods);
let methods = ["push", "pop", "shift", "unshift", "reverse", "sort", "splice"]; // 需要重写的方法
methods.forEach((method) => {
  arrayMethods[method] = function (...args) {
    const result = oldArrayProtoMethods[method].apply(this, args);
    // ...
    return result;
  };
});

上面的意思是将 原始数组原型上的方法继承一份,并导出。然后将我们需要重写的方法循环出来,依次进行处理

我们这里使用 Object.create 相当于 arrayMethods.__proto__ = oldArrayProtoMethods,这样会只处理重写后的方法,没重写的方法不会被处理。

现在,我们需要对一些特定的方法进行处理。

// ...
methods.forEach((method) => {
  arrayMethods[method] = function (...args) {
    // ...
    const ob = this.__ob__;
    let inserted; // 需要添加的项
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2); // 截取新添加的项
      default:
        break;
    }
    if (inserted) ob.observeArray(inserted); // 对新添加的每一项进行观测
    return result;
  };
});

pushunshift 都是添加项的意思,一个是在头部添加,一个是在尾部添加。splice 从第三个参数开始,就是添加的项。

因为新添加的项可能也是 Object 类型,所以我们需要再次 对新添加的项进行劫持

但是这里就出现了一个问题,我们并没有办法使用劫持方法。又已知当前方法中的this就是被调用数组的value上面的 apply改变了 this指向)。所以我们就可以在value上增加一个属性__ob__

增加 __ ob __ 属性

__ob__ 属性的作用是给所有响应式数据增加标识,用来判断当前对象是否被劫持过。并且可以在响应式上获取Observer实例上的方法

class Observer { 
  constructor(value){
		// 添加 __ob__
    Object.defineProperty(value, "__ob__", {
      enumerable: false,   // 不可枚举,也就是不能被循环出来
      configurable: false, // 不能删除
      value: this,
    });
    // ...
  }
}

这样我们就在每一项中都追加了一个 __ob__ 属性,现在我们还需要进行一步处理。

// ...
export function observe(data) {
  if (typeof data !== "object" || data == null) {
    return data;
  }
  if (data.__ob__) {
    return data;
  }
  return new Observer(data);
}

如果当前项已经被添加了 __ob__ 属性,则不进行处理。这样可以防止数据被重复劫持。

这样我们就完成了对数组的数据劫持。

注:在Vue中不能直接修改指定项,需要通过Array数组中的方法来进行修改。Vue.$set 可以修改特定值,后续我会详细介绍一下其使用和原理。

属性代理

我们已经对数据进行了相应的劫持,现在可以对 data 进行相应的操作了。

let vm = new Vue({
// ...
})
vm._data.arr.push({
   b: 2
});
vm._data.arr[1].b = 3;

setget 方法都可以被正常触发。

但是这种操作太麻烦了,所以我们现在需要对属性进行代理,让用户可以直接通过 vm.arr 这种方式直接修改值。

function proxy(vm, source, key) {
  Object.defineProperty(vm, key, {
    get() {
      return vm[source][key];
    },
    set(newValue) {
      vm[source][key] = newValue;
    },
  });
}
function initData(vm) {
  let data = vm.$options.data;
  data = vm._data = typeof data === "function" ? data.call(vm) : data;
  for (let key in data) {
    // 将_data上的属性全部代理给vm实例
    proxy(vm, "_data", key);
  }
  observe(data);
}

同样是使用 Object.defineProperty 对传入的属性进行劫持监听。

当用户到 vm 中获取属性时,属性的值被代理到 vm._data 上。