Vue数据响应式、模板解析的实现原理(实现一个简易的Vue)

本文档详细介绍了如何实现一个简易版的Vue,包括MyVue构造函数、数据响应式(Observer和Watcher)、数据代理、模板解析等关键步骤。通过数据劫持、订阅发布模式(观察者模式)实现页面与数据的同步,以及如何处理数组的响应式。最后,文章提供了整个简易Vue项目的代码结构。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

实现步骤(实现我们自己的Vue—MyVue)

github地址:https://github.com/Rawsdom/my-vue-demo.git

  • MyVue(主入口的构造函数)
  • Observer(用于实现数据响应化)
  • Compile(解析模板、创建 Watcher、保存更新函数)
  • Watcher(执行更新函数、被 Dep 收集)
  • Dep(管理 Watcher、一旦数据更新就通知相应的watcher更新视图)
一、MyVue的构造函数
 new MyVue({
   
   
        el: '#app',
        data: {
   
   
          name: 'hello world!',       
        }
      });

(PS:MyVue构造函数接收上方这些参数并保存到实例中,并将实例往下传递)

  1. 把 data 使用 observe 进行递归数据劫持
  2. 使用代理方式处理传入的 data(方便在实例中可以直接 this.name 就获取或修改 data 中的值)
  3. 把 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]))
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值