Skip to content

Vue响应式原理

1. 导出构造函数

js
// /src/core/instance/index.js
import { initMixin } from "./init";

function Vue(options) {
  this._init(options);
}

initMixin(Vue);

export default Vue;

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

js
// init.js

import { initState } from "./state";

export function initMixin(Vue) {

    // 初始化
  Vue.prototype._init = function(option) {
    const vm = this;
    // TODO: 选项合并
    vm.$options = option || {};

    // 数据观测(初始化状态)
    initState(vm);
  };
}

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

初始化顺序

Vue中初始化的顺序为 props methods data computed watch

js
export function initState(vm){
    const opts = vm.$options;
    if(opts.props){
        initProps(vm);
    }
    if(opts.methods){
        initMethod(vm);
    }
    if(opts.data){
        // 初始化data
        initData(vm);
    }
    if(opts.computed){
        initComputed(vm);
    }
    if(opts.watch){
        initWatch(vm);
    }
}
function initProps(){}
function initMethod(){}
function initData(){}
function initComputed(){}
function initWatch(){}

初始化数据(data)

js
function initData(vm) {
  // 取到用户传入的data数据
  let data = vm.$options.data;
  
  // 取到数据, 并将数据挂载到vm._data上, 以方便用户通过vm._data.xxxx访问到数据
  data = vm._data = typeof data === "function" ? data.call(vm) : data;
  // 对数据进行观察
  observer(data);
}

对象递归劫持

js
// 观察者类
class Observer {
	// value为被观察的对象
    constructor(value){
		
		// 对象的劫持
        this.walk(value);
    }
	
	// 对象劫持的方法
    walk(data){
        let keys = Object.keys(data);
		// 让对象的每一个key依次进行观测, 变成响应式
        keys.forEach(key=> defineReactive(data,key,data[key]);)
    }
}

// 定义响应式
function defineReactive(data,key,value){
    observe(value); // 对象的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. Object.keys对于数组, 取到的key是索引, 因此会对索引进行观测, 将索引变成响应式, 这会造成性能的浪费, 且开发中很少对数组的索引进行操作
    1. 操作数组的push, unshift, splice方法, 新增数组元素, 新增的元素不会被观测到

数组的观测

针对以上两个问题, 需要区分数组以及对象, 对象和数组分开观测

js
import { isObject } from "../../utils/index";
import { arrayMethods } from "./array";

class Observer {
  constructor(value) {
	
	// 给每一个被观察过的对象身上添加一个__ob__属性
	value.__ob__ = this;
	
	  // 区分是数组还是对象
    if (Array.isArray(value)) {
      // 重写数组的7个方法
      value.__proto__ = arrayMethods;

      // 对数组的每一项进行观测
      this.observerArray(value);
    } else {
		
	  // 对象的观测
      this.walk(value);
    }
  }

  // 观测对象
  walk(value) {
    const keys = Object.keys(value);
    keys.forEach((key) => defineReavtive(value, key, value[key]));
  }

  // 观测数组
  observerArray(ary) {
    ary.forEach((item) => observer(item));
  }
}

export function observer(value) {
  if (!isObject(value)) return;
  return new Observer(value);
}
// ......

重写数组原型上的7个方法

js
const arrayProto = Array.prototype; // 先将数组原型对象保存下来
export const arrayMethods = Object.create(arrayProto); // 创建一个空对象, 继承自数组原型对象

// 下面7个方法是会改变原数组的方法
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse",
];

// 对数组重写上述的7方法
methodsToPatch.forEach((method) => {
	// 注意, 这里是我们创建的对象添加数组原型上的7个方法, 等于是对数组原型上的7个方法做一个切片
	// 最终调用时候会根据原型链, 先找到我们自己创建的方法, 找不到再找原型上的方法
  arrayMethods[method] = function (...args) {
    // 先调用原始的方法, 此时的this指的是数组
    const res = arrayProto[method].apply(this, args);

    // this表示当前数组 value.push(), 这里的value会走到这里
	// 由于上一步我们在Observe中给每一个被观测对象上添加了__ob__属性, 因此这里可以通过this.__ob__取到
    const ob = this.__ob__;

    let inseted; // 取到新增的数组项
    switch (method) {
      case "push":
      case "unshift":
        inseted = args; // args是一个数组
        break;
      case "splice":
        inseted = args.slice(2); // splice的第3个开始的参数是要新增或编辑后的元素
        break;
      default:
    }

    // 对新增属性进行观测
    if (inseted) ob.observerArray(inseted);

    return res;
  };
});

上述步骤完成后会存在一个问题, 观测进入了死循环, 由于每一个被观察过的对象身上都会加一个__ob__属性, 在this.walk观测对象时候, 就会通过Object.keys获取到__ob__属性, 并且会对该属性进行观察, __ob__的值是this(observer),因此会进入递归观察,从而进入死循环

那么只需要让Object.keys拿不到__ob__就解决问题了, 因此在给value身上添加__ob__属性时, 应该让这个属性不可枚举def(data, '__ob__', this)

js
import { def } from "../../utils/index";
class Observer {
  constructor(data) {

    // 为每一个被监控过的对象添加一个__ob__属性
    def(data, '__ob__', this)

    if (Array.isArray(data)) {
      // 重写数组的7个方法
      // @ts-ignore
      data.__proto__ = arrayMethods;

      // 对数组的每一项进行观测
      this.observerArray(data);
    } else {
      this.walk(data);
    }
  }

  // 观测对象
  walk(value) {
    const keys = Object.keys(value);
    keys.forEach((key) => defineReavtive(value, key, value[key]));
  }

  // 观测数组
  observerArray(ary) {
    ary.forEach((item) => observer(item));
  }
}

定义不可枚举的属性

js
// src/utils/index.js
export function def(obj, key, val) {
  Object.defineProperty(obj, key, {
    enumerable: false, // 不可枚举
    configurable: false, // 不可配置
    value: val
  });
}

数据代理

为了方便, 可以将vm._data上的数据全部代理到vm

js
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);
}

小结

至此, 数据响应式的核心原理 手写完成, 可以通过vm.xxxx直接访问data里面的数据了

  1. 通过initMixin扩展Vue原型方法, 内部包含_init初始化逻辑
  2. _init内部先取到new Vue时候用户传入的options挂到vm.$options上并且开始进行initState
  3. initState内部会按props、methods、data、computed、watch的顺序进行初始化,这里先写initData
  4. initData内部会将data取到后挂在vm._data上, 在将_data上面的属性通过proxy代理了一下, 挂到vm上,最后调用observer对data上的属性进行观察, 让其变为响应式
  5. 数据响应式的重点就在observer里面, observer区分了对象和数组进行观察, 核心是用Object.defineProperty方法将递归的将对象的每一个key重新定义, 数组中对象也一样,数组还会用切片编程的方式重写7个原型方法