手撸Vue(1)——响应式对象实现(对象属性、数组劫持实现)
迪丽瓦拉
2024-03-27 08:26:01
0

搭建Rollup环境

Rollup是啥

Rollup 是一个 JavaScript 模块打包工具,可以将多个小的代码片段编译为完整的库和应用。与传统的 CommonJS 和 AMD 这一类非标准化的解决方案不同,Rollup 使用的是 ES6 版本 Javascript 中的模块标准。新的 ES 模块可以让你自由、无缝地按需使用你最喜爱的库中那些有用的单个函数。这一特性在未来将随处可用,但 Rollup 让你现在就可以,想用就用。

npm命令打包安装

项目初始化
npm init -y
安装rollup
npm install rollup rollup-plugin-Babel @babel/core @babel/preset-env --save-dev
rollup配置

1、修改packet.json脚本,使用rollup命令来当dev环境

{"name": "rollup","version": "1.0.0","description": "","main": "index.js","scripts": {"dev": "rollup -cw" },"keywords": [],"author": "","license": "ISC","devDependencies": {"@babel/core": "^7.20.5","@babel/preset-env": "^7.20.2","rollup": "^2.79.1","rollup-plugin-babel": "^4.4.0"}
}

2、编写rollup配置文件rollup.config.js

import babel from 'rollup-plugin-babel'
// rollup默认可以导出一个对象作为配置文件
export default {input: './src/index.js', //入口output:{file:'./dist/vue.js', //出口name:"Vue", // global.Vueformat:'umd', // esm es6模块  commonjs模块 iife 自执行函数 umd 统一模块规范 (commonjs amd)sourcemap:true, // 希望可以调试代码},pulgins:[babel({exclude:'node_modules/**' // 排除mode_modules下的所有文件})]
}

初始化数据

为了代码的规范,防止函数越写越乱,我们采用prototype的形式来编写Vue函数:

import { initMixin } from "./init";
function Vue(options) { //options就是用户的选项// debuggerthis._init(options)
}
initMixin(Vue); //扩展了init
export default Vue;

然后,我们在initMixin函数中,通过Prototype来定义我们的_init方法,实现初始化:

import { initState } from "./state";
export function initMixin(Vue){Vue.prototype._init = function(options){ // 用于初始化// Vue vm.$options 获取用户的配置// 我们使用的Vue时,有很多$开头的api,表示是Vue自己的const vm = this;this.$options = options; //挂载用户选项在实例上// 初始化状态initState(vm);}
}

我们再在initState中实现对数据进行劫持:

// 对数据进行劫持
export function initState(vm) {const opts = vm.$options; // 获取所有的选项if (opts.data) {initData(vm);}
}
function initData(vm) {let data = vm.$options.data; // 可能是函数或者对象data = typeof data === 'function' ? data.call(vm) : data;vm._data = data// 对数据进行劫持 Vue2采用了一个api definePropertyobserve(data);
}

对象、数组劫持

接下来来写劫持函数,首先,我们编写一个劫持类:

class Observe{constructor(data){Object.defineProperty(data, '__ob__',{ // 变成不可枚举,防止循环时被获取,导致递归失败value:this,enumerable:false})//object.defineProperty只能劫持已经存在的属性,新增的或者删除的都不知道。(Vue里因此会写一些api:$set $delete)// data.__ob__ = this; // 给数据加了一个标识,如果数据上有__ob__则说明这个属性被观测过if (Array.isArray(data)){// 重写数组方法,7个变异法。还需要对数组的引用类型进行劫持data.__proto__= newArrayProto; // 保持原有的特征,并且可以重写部分方法this.observeArray(data); // 若数组中放置的是对象,则会被监控到}else{this.walk(data);}}walk(data){ // 遍历data,对属性依次劫持// 重新定义属性Object.keys(data).forEach(key=>defineReactive(data,key,data[key]))}observeArray(data){data.forEach(item=>observe(item));}
}

这里用到了defineReactive函数,这个函数也是我们自己实现的。所以我们接下来来实现一下这个函数:

export function defineReactive(target, key, value){ // 闭包 属性劫持observe(value); // 这里触发了递归,对所有对象进行劫持Object.defineProperty(target, key, {get(){ // 取值时会执行getreturn value;},set(newValue){ // 修改时会执行setif(newValue === value) returnobserve(newValue);value = newValue;}})
}

最后编写我们的劫持函数:

export function observe(data){// 对这个对象进行劫持if (typeof data!= 'object' || data == null){return; // 只劫持对象}if (data.__ob__ instanceof Observe){return data.__ob__;}// 若一个对象被劫持过了,那就不需要再被劫持了(要判断对象是否被劫持过,可以增添一个实例,用实例来判断是否被劫持)return new Observe(data);
}

这里劫持实现的可能有些绕。大致理一下,
首先定义一个class对象为被劫持的对象。
外部调用的是observe函数来劫持dataobserve函数通过new Observe的方式返回被劫持的对象。
由于data不只一个参数,所以在Observe的构造函数中需要遍历每一个元素(对应walkobserveArray方法),然后调用defineReactive实现劫持。
而在函数defineReactive中,有可能遇到数组的情况,所以需要对每个元素进行调用一遍observe函数来确保是元素还是对象,若是元素则通过Object.defineProperty给所有元素添加getset方法。若不是则触发递归。

完成

至此,Data中的所有参数在被getset的时候,就能够被监听到了,我们成功实现了响应式对象。

相关内容