实现步骤(实现我们自己的Vue—MyVue)
- MyVue(主入口的构造函数)
- Observer(用于实现数据响应化)
- Compile(解析模板、创建 Watcher、保存更新函数)
- Watcher(执行更新函数、被 Dep 收集)
- Dep(管理 Watcher、一旦数据更新就通知相应的watcher更新视图)
一、MyVue的构造函数
new MyVue({
el: '#app',
data: {
name: 'hello world!',
}
});
(PS:MyVue构造函数接收上方这些参数并保存到实例中,并将实例往下传递)
- 把 data 使用 observe 进行递归数据劫持
- 使用代理方式处理传入的 data(方便在实例中可以直接 this.name 就获取或修改 data 中的值)
- 把 el 传到模板解析中
class MyVue {
constructor(options) {
this.$options = options;
this.$data = options.data;
// 数据劫持 (数据响应化处理)
observe(this.$data);
// 数据代理
proxy(this, "$data");
// 模板解析 (传递 this 实例)
new Compiler(options.el, this);
}
}
二、Dep 和 Watcher 说明
- 数据响应化的定义是页面绑定的某个数据,数据改变,同时页面也会自动更新
- 在上方,我们把模板解析和数据响应分成2个方法去执行。但是他们相互之间没有关联的话怎么去实现页面和数据同步呢?
- 我们就需要借助 Dep 和 Watcher 实现一个订阅、发布模式(观察者模式)来实现他们之间的关联
- Dep相当于一个容器,在对数据劫持时生成的,每一个Dep对应一个属性
- addDep方法是用来收集 Watcher
- notify方法是用来调用所有的 Watcher 中的更新方法来达到页面的更新
class Dep {
constructor() {
this.deps = [];
}
addDep(dep) {
this.deps.push(dep);
}
notify() {
this.deps.forEach((dep) => dep.update());
}
}
- Watcher 生成在编译解析中,一旦解析到页面有数据(如:{ { name }}) 就会生成一个Watcher
- 在生成的同时被Dep收集,这里由于Dep是在数据劫持中生成的,我们只需将当前的 Watcher 实例保存到一个变量中,再触发对应的dep劫持的属性的 get 回调 即可收集,收集完再将变量置为null
- 由于Vue源码也是用Dep.target的保存Watcher实例,我这边也一样参照源码,其实是可以用全局变量也是可以的
- (ps:这边需要理解一下Object.defineProperty()这个劫持的api才能更好理解)
- (ps:因为每一个属性劫持都会生成一个Dep,只要this.name访问一下就会触发劫持的 get 回调,然后再 get 回调中判断是否有这个变量,有即调用Dep里的addDep()方法即可收集到Dep中)
class Watcher {
constructor(vm, key, updateFn) {
this.vm = vm;
this.key = key;
this.updateFn = updateFn;
// 被Dep收集过程
Dep.target = this;
this.vm[this.key];
Dep.target = null;
}
update() {
//更新函数的调用, 由于更新函数内部可能需要this实例, 所以用call方法改变一下调用 this 指向
this.updateFn.call(this.vm, this.vm[this.key]);
}
}
三、数据响应式 (数据劫持)
- 紧接 MyVue 的构造函数调用 observe() 方法
- 类型判断处理,同时也是递归点
function observe(obj) {
if (typeof obj !== "object" || obj === null) return;
new Observer(obj);
}
- 新建 Observer 实例
- 因为上方 observe() 方法的类型判断唔法判断 array 和 object 在使用Array原型上的isArray()方法去判断
- 因为数组原型上有7个原型方法能修改数组的值,所以需要复写数组的7个方法,所以这里需要区分对象和数组2个不同类型的处理
- (ps:typeof === ‘object’ 判断 object 和 array 都是为 true 的)
- (ps:重写数组原型的方法目的是在执行完数组操作后要通知视图更新以达到响应式,所以要执行原原型链上的方法,并且还要通知 dep 更新视图)
class Observer {
constructor(obj) {
this.obj = obj;
if (typeof obj === "object") {
Array.isArray(obj) ? this.walkArr(obj) : this.walk(obj);
}
}
// 对象数据响应化
walk(obj) {
Object.keys(obj).forEach((key) => {
// 这里可以理解为取出对象的键值并对obj的key做劫持
// 下方会有defineReactive()方法
defineReactive(obj, key, obj[key]);
});
}
// 数组数据响应化
walkArr(arr) {
// 覆盖原型的7个方法
arr.__proto__ = this.arrayProto;
// 这里取出的键值其实是数组的下标
const keys = Object.keys(arr);
for (let i = 0; i < keys.length; i++) {
// 以arr的下标做数据劫持
defineReactive(arr, i, arr[i])
}
}
}
// Observer的类原型保存新的数组原型方法(如:push等)用于数组响应化时覆盖数组的原型方法
const defaultProto = Array.prototype;
// 深拷贝数组的原型链方法
const arrayProto = Object.create(defaultProto);
// 对以下的方法做重写。并生成新的数组原型方法
["push", "pop", "shift", "unshift"].forEach(
method => {
arrayProto[method] = function () {
// 默认原型链上的对应的方法还是需要执行,该 push 就还是要 push
defaultProto[method].apply(this, arguments);
// 这里的notify() 会在下方 defineReactive() 时保存到该数据的原型链上从而在此处可以调用
this.notify()
};
}
);
// 保存到 Observer 原型链上
Observer.prototype.arrayProto = arrayProto
defineReactive() 方法
- 开启递归,如果val是对象就需要继续往下递归,如果不是的话上面说的递归点就起作用,直接就返回到这里
- 因为当前的属性是唯一的,同时在这新建的dep也是和该属性一一对应,也是唯一的
- 在对数组做响应化时需要利用闭包保存当前的dep,并在该数据的原型上添加一个notify方法,因为push操作调用者肯定是属性本身(如:this.name.push(xxx)),所以上方在push等操作的时候也就可以使用当前添加的notify()方法从而实现通知视图更新
function defineReactive(obj, key, val) {
// 递归
observe(val);
// 创建一个Dep和当前key 一一对应
const dep = new Dep()
// 利用原型链把dep的notify方法保存起来,数组使用push的方法就可以直接通知修改数组的值
if(Array.isArray(obj[key]))