侧边栏壁纸
博主头像
云之彼端博主等级

行动起来,活在当下

  • 累计撰写 27 篇文章
  • 累计创建 6 个标签
  • 累计收到 2 条评论

目 录CONTENT

文章目录

vue从0实现(1) —— vue响应式原理

Administrator
2022-12-14 / 0 评论 / 0 点赞 / 92 阅读 / 6018 字 / 正在检测是否收录...
温馨提示:
本文最后更新于 2023-12-26,若内容或图片失效,请留言反馈。部分素材来自网络,若不小心影响到您的利益,请联系我们删除。

前言

此系列文章用于记录vue2的简单实现过程,用于检验自己对库创作及vue原理的理解

本系列文章的目的是模仿vue实现一个支持双向绑定的js库,本篇分析vue响应式原理实现用户data对象属性劫持及深度劫持

响应式基础

vue2实现响应式的基础是 Object.defineProperty() api

Object.defineProperty() 是 JavaScript 中的一个函数,它用于定义对象的属性。它接受三个参数:要定义属性的对象、属性的名称和一个描述符对象,其中包含属性的相关信息,如属性的值、是否可读写等。例如:

const obj = {};

Object.defineProperty(obj, 'prop', {
  value: 'Hello',
  writable: false,
  enumerable: true,
  configurable: true
});

这段代码定义了一个新的对象 obj,并为它添加了一个名为 prop 的属性,该属性的值为 Hello,不可更改,可枚举,且可配置。
Object.defineProperty() 和 Object.defineProperties() 不同,它只能定义一个属性,而 Object.defineProperties() 则可以定义多个属性。

代码实现

入口

index.js 入口文件

import { initMixin } from "./_init/init";

function Vue(options) {
  this._init(options);
}
initMixin(Vue);
export default Vue;

initMixin 方法在vue的原型上添加了_init()方法用来进行vue的初始化操作

初始化封装

init.js 实现了initMixin和_init 将用户的options挂载到vue实例上

import { initData } from "./initData";

/**
 * 初始化配置项
 * @param {*} Vue vue对象
 * @param {*} options 用户配置
 */
export function initMixin(Vue) {
  Vue.prototype._init = function (options) {
    const vm = this;
    vm.$options = options; //挂载配置项到vm上
    initState(vm);
  };
}

/**
 * 初始化状态
 * @param {*} vm
 */
function initState(vm) {
  const opts = vm.$options;
  if (opts.data) {
    initData(vm); //data响应式
  }
}

代理data到vm示例上

initData.js 实现对象的劫持和代理 主要是用 Object.defineProperty实现get 和 set方法,通过递归调用observer实现深度劫持

import { observer } from "./observer/index";

/**
 * data初始化
 * @param {Object} vm
 * @param {Function} data
 */
export function initData(vm) {
  let data = vm.$options.data;
  data = typeof data === "function" ? data() : data;
  //挂载data到vm实例上并观测对象
  vm._data = data;
  observer(data);
  proxy(vm, data, "_data");
}

/**
 * 将_data上的属性代理到vm根节点上,方便用户访问
 * @param {*} vm
 * @param {*} data
 * @param {*} target
 */
export function proxy(vm, data, target) {
  Object.keys(data).forEach((key) => {
    if (key.indexOf("$") === 0) return; //不代理首字母是'$'的属性
    Object.defineProperty(vm, key, {
      get() {
        return vm[target][key];
      },
      set(newValue) {
        vm[target][key] = newValue;
      },
    });
  });
}

实现observer类(对属性进行劫持)

import newArrayProto from "./array";
/**
 * 构造observer类用于注册、管理劫持对象
 */
class Observer {
  constructor(data) {
    //保存observer实例到data上,设置不可枚举,防止死循环
    Object.defineProperty(data, "__ob__", {
      value: this,
      enumerable: false,
    });
    if (Array.isArray(data)) {
      //替换数组原型链为劫持原型链
      data.__proto__ = newArrayProto;
      this.observerArray(data);
    } else {
      this.walk(data);
    }
  }
  /**
   * 遍历data,劫持所有属性
   * @param {*} data
   */
  walk(data) {
    Object.keys(data).forEach((key) => {
      defineReactive(data, key, data[key]);
    });
  }

  /**
   * 劫持数组中的引用类型属性 优化性能
   * @param {*} data
   */
  observerArray(data) {
    Object.keys(data).forEach((key) => {
      observer(data[key]);
    });
  }
}

/**
 * 使用 Object.defineProperty实现响应式对象
 * @param {*} target
 * @param {*} key
 * @param {*} value
 */
export function defineReactive(target, key, value) {
  observer(value); //劫持所有对象的属性
  Object.defineProperty(target, key, {
    get() {
      console.log("getObserver", key);
      return value;
    },
    set(newValue) {
      console.log("setObserver", key);
      observer(newValue);
      if (value === newValue) return;
      value = newValue;
    },
  });
}

/**
 * 观测对象
 * @param {} data
 */
export function observer(data) {
  //不劫持非对象类型
  if (typeof data !== "object" || data == null) return;

  if (data.__ob__ instanceof Observer) return;
  return new Observer(data);
}

其中对数组的响应式实现需要单独处理,需要重写数组对象中可以改变原数组的方法,如’push’,'unshift’等

const oldArrayProto = Array.prototype;
const newArrayProto = Object.create(oldArrayProto); //不直接修改数组的原型链
let methods = ["push", "shift", "unshift", "pop", "reverse", "sort", "splice"]; //加强这些方法,可以监控到数组的修改
methods.forEach((funcName) => {
  newArrayProto[funcName] = function (...args) {
    const result = oldArrayProto[funcName].call(this, args);
    console.log(`劫持到:${funcName}`);
    //对新增的对象属性进行劫持
    let adds = args;
    const ob = this.__ob__;
    switch (funcName) {
      case "push":
      case "unshift":
        adds = args;
        break;
      case "splice":
        adds = args.slice(2);
        break;
    }

    if (adds) {
      ob.observerArray(adds);
    }

    return result;
  };
});

export default newArrayProto;

上面的代码重写了数组的原型链,实现数组中引用类型属性的劫持,以及修改数组方法的监听

结语

相关代码已上传gitee 点击跳转

0

评论区