手写Vue2.0源码(一)响应式原理

441 阅读3分钟

前言

本文仅以记录自己的学习过程,有其他理解的同学可留言。注意 我学习原理一直保持 28 策略 所谓百分之 20 的代码实现了百分之 80 的功能,所以此系列咱们只关心核心逻辑以及功能的实现


正文

大家都知道 Vue 的一个核心特点是数据驱动 如果按照以往 Jquery 的思想 咱们数据变化了想要同步到视图就必须要手动操作 dom 更新 但是 Vue 帮我们做到了数据变动自动更新视图的功能 那在 Vue 内部就一定有一个机制能监听到数据变化然后触发更新 本篇主要介绍响应式数据的原理

Vue构造函数

class Vue {
	constructor(options) {
		if (!(this instanceof Vue)) {
			return new Error('需使用new的方式')
		}
		this._init(options);
	}
}

_init(options)初始化大约做了哪些事?

// 一:合并options 这里涉及到mixin合并策略
// 二:初始化event、生命周期函数
// 三:callhock(vm, 'beforeCreate')
  // vuex的实例store就是在这个钩子中注入的
// 四:初始化参数选项 initState(this);
  // initProps(vm)、initMethod(vm)、initData(vm)、初始化computed、watch
// 五:callhock(vm, 'created')
// 六:this.$mount(el)

着重分析initData(this)

function initData(vm) {
  let data = vm.$options.data;
  // vue组件data推荐使用函数 防止数据在组件之间共享
  data = vm._data = typeof data === "function" ? data.call(vm) : data || {};
  // 举例:{data:{name: '张不皱'}},我们正常都是这么写的,
  // 但是模板中是this.name这么用的,而不是this.$options.data.name,
  // 所以下面的for循环就是实现这一个效果
  for (let key in data) {
    proxy(vm, `_data`, key);
  }
  // 对数据进行观测 --响应式数据核心
  observe(data);
}
// 数据代理
function proxy(object, sourceKey, key) {
  Object.defineProperty(object, key, {
    get() {
      return object[sourceKey][key];
    },
    set(newValue) {
      object[sourceKey][key] = newValue;
    },
  });
}

对象的数据劫持

// src/obserber/index.js
class Observer {
  // 观测值
  constructor(value) {
    // 防止反复观测
    this._ob_ = this;
    // 对象的响应式方法
    this.walk(value);
  }
  walk(data) {
    // 对象上的所有属性依次进行观测
    let keys = Object.keys(data);
    for (let i = 0; i < keys.length; i++) {
      let key = keys[i];
      let value = data[key];
      defineReactive(data, key, value);
    }
  }
}
// Object.defineProperty数据劫持核心 兼容性在ie9以及以上
function defineReactive(data, key, value) {
  // Dep收集依赖
  let deps = new Dep();
  observe(value); // 递归关键
  // --如果value还是一个对象会继续走一遍odefineReactive 层层遍历一直到value不是对象才停止
  //   思考?如果Vue数据嵌套层级过深 >>性能会受影响
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");
      if(Dep.target) {
        // 添加依赖
        deps.depend();
      }
      return value;
    },
    set(newValue) {
      if (newValue === value) return;
      console.log("设置值");
      value = newValue;
      // 可能设置的值为一个对象,所以继续观测
      observe(newValue);
      // 依赖管理通知订阅者做出响应(触发回调)
      deps.notify();
    },
  });
}
export function observe(value) {
  // 判断是否观测过
	if (value._ob_) {
		return value._ob_;
	}
	return new Observer(value);
}

思考 1.Object.defineProperty可以劫持数组吗?2.这样的数据劫持方式对数组有什么影响?

1有一大票的文章说上述接口无法劫持数组,全是瞎扯、互相抄的,是完全可以的! 2主要是因为数组内的数据量常常会比较大,这样对于性能来说是承担不起的,所以使用其他方式来实现

数组的数据劫持

// src/obserber/index.js
import { arrayMethods } from "./array";
class Observer {
  constructor(value) {
    // 举例data: {list: [1,2]},此时执行到 observe([1,2]),劫持的方法中需要拿到list属性的订阅者,
    // 这样例如在this.list.push(3),的时候才可以通知到list属性的订阅者
    + this.deps = new Dep();
    + if (Array.isArray(value)) {
      // 这里对数组做了额外判断
      // 通过重写数组原型方法来对数组的七种方法进行拦截
        value.__proto__ = arrayMethods;
      // 如果数组里面可能还有对象类型数据,所以需要递归判断
        this.observeArray(value);
    } else {
      this.walk(value);
    }
  }
  observeArray(items) {
    for (let i = 0; i < items.length; i++) {
      observe(items[i]);
    }
  }
}
function defineReactive(data, key, value) {
  // Dep收集依赖
  let deps = new Dep();
  let children = observe(value); 
  Object.defineProperty(data, key, {
    get() {
      console.log("获取值");
      if(Dep.target) {
        // 添加依赖,
          // 后续会增加去重操作,但是这一步是在watcher中执行的,达到deps中有watcher,watcher中有具体被哪些deps收集了
        deps.depend();
        + chidren.deps.depend();
      }
      return value;
    }
  });
}

对数组原型重写,改写了push、pop、unshift、shift、splice、reverse、sort这几个方法,通过这几个方法修改数组例如上例的this.list.push(3),都可以通知到this.list的订阅者,但也要做兼容操作:如果_proto_隐式原型不支持,那直接将push等方法直接加到数组本身的方法上list.push = xxx;

export const arrayMethods = Object.create(arrayProto);
let methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "reverse",
  "sort",
];
methodsToPatch.forEach((method) => {
  arrayMethods[method] = function (...args) {
    const result = arrayProto[method].apply(this, args);
    // this._ob_是new Observer()时添加在对象上的Observer实例
    const ob = this.__ob__;

    // 这里的标志就是代表数组有新增操作
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
      default:
        break;
    }
    // 如果有新增的元素 inserted是一个数组 调用Observer实例的observeArray对数组每一项进行观测
    if (inserted) ob.observeArray(inserted);
    // 之后咱们还可以在这里检测到数组改变了之后从而触发视图更新的操作--后续源码会揭晓
    return result;
  };
});

思维导图

初始化参数选项.png

如果觉得本文对你有帮助,记得点赞、收藏、评论,十分感谢!