创建一个项目文件
npm init -y
安装初始依赖
npm install rollup rollup-plugin-babel @babel/core @babel/preset-env --save-dev
创建rollup.config.js文件 配置打包
创建打包入口src文件夹 并新建index.js打包文件
编写调试脚本
{
"name": "my-vue2",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"dev": "rollup -cw"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"@babel/core": "^7.21.3",
"@babel/preset-env": "^7.20.2",
"rollup": "^2.79.1",
"rollup-plugin-babel": "^4.4.0"
}
}
编写rollup.config.js逻辑
//rollup 默认可以导出一个对象 作为打包的配置文件
import babel from 'rollup-plugin-babel';
export default {
input: "./src/index.js", //入口
output: {
file: "./dist/vue.js", //出口
name: "Vue", //希望new Vue可以生成一个vue实例 表示可以打包后会在全局增加一个Vue 即在global.Vue
format: "umd", //打包的格式 esm es6模块(相当于没有打包) commonjs模块(主要在node中使用) iife(子执行函数) umd(统一模块规范 兼容commonjs与amd)
sourcemap: true,//希望可以调试源代码
},
plugins:[
babel({
exclude: 'node_modules/**' //排除node_modules所有文件
})
]
};
由于引入了babel
所以要创建.babelrc文件设置babel的预设
{
//添加插件预设值
"presets": [
//许多插件的集合
"@babel/preset-env"
]
}
入口文件index.js
export const a = 100
export default {
a: 1,
};
终端运行npm run dev 打包后 多出dist下的vue.js与vue.js.map文件
新建index.html 引入vue.js 并输出Vue
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script src="vue.js"></script>
<script>
console.log(Vue);
</script>
</body>
</html>
我们来写index.js中的逻辑
首先需要获取到用户选项 即new Vue时传入的对象
我们可以使用options来表示
对于数据的初始化 我们新建一个_init的函数 挂载到Vue原型对象上 用于初始化操作
为了使用方便我们把this实例使用vm变量保存起来 并再vm上挂载一个$options属性(即把用户的选项挂载到实例上)
内部的具体初始化描述 我们新建一个initState函数来实现 参数是vm实例
//将所有的方法都耦合在一起
function Vue(options) {
//options就是用户的选项
this._init(options);
}
//就是给Vue增加init方法的
Vue.prototype._init = function (options) {
//用于初始化操作
// vue vm.$options 就是获取用户的配置
//我们使用vue的时候 $nextTick $data $attr 表示是Vue自己的属性
const vm = this;
vm.$options = options; //将用户的选项挂载到实例上 (原型中的this都是实例)
//初始化状态
initState(vm);
};
为了方便扩展init方法
我们创建一个initMixin函数 将Vue作为参数传递进去 在initMixin函数内实现_init逻辑
此时的index.js文件
import { initMixin } from "./init";
//将所有的方法都耦合在一起
function Vue(options) {
//options就是用户的选项
this._init(options);
}
initMixin(Vue); //扩展了init方法
export default Vue;
新建的init.js文件
import { initState } from "./state";
export function initMixin(Vue) {
//就是给Vue增加init方法的
Vue.prototype._init = function (options) {
//用于初始化操作
// vue vm.$options 就是获取用户的配置
//我们使用vue的时候 $nextTick $data $attr 表示是Vue自己的属性
const vm = this;
vm.$options = options; //将用户的选项挂载到实例上 (原型中的this都是实例)
//初始化状态
initState(vm);
};
}
这样就实现了方法的抽取
之后就是实现initState初始化状态逻辑 我们把它写在state.js文件里
因为传入了vm(即Vue实例)我们在上面绑定了$options(即用户所有选项)我们把它取出 并先使用其中的data
如果有data 执行initData() 具体初始化逻辑写在initData()中
export function initState(vm) {
const opts = vm.$options; //获取所有的选项
if (opts.data) {
initData(vm);
}
}
在initData中 我们可以从实例vm上获取data 判断data是函数还是对象 如果是函数则执行 对象则直接返回
并将返回的data挂载到vm._data上 方便获取
最后最重要的逻辑放在函数observe()中 用来做响应式
function initData(vm) {
let data = vm.$options.data; //data可能是函数和对象
data = typeof data === "function" ? data.call(vm) : data;
vm._data = data;//将返回的对象放到了_data上
//对数据进行劫持 vue2 里采用了一个api defineProperty
observe(data);
}
我们将observe函数写在observe文件夹下的index.js内
先判断data是否是对象 只有是对象才劫持
之后要判断次对象是否被劫持过 如果劫持过就不劫持 逻辑实现在Observer类中
export function observe(data) {
//对这个对象进行劫持
if(typeof data !== 'object' || data == null){
return //只对对象进行劫持
}
//如果一个对象被劫持过了 那就不需要再被劫持了(要判断一个对象是否被劫持过 可以增加一个实例 用实例来判断是否被劫持过)
return new Observer(data)
}
Observer类中实现了 对其进行监听劫持的方法 循环对象 对属性进行劫持
劫持逻辑写在defineReactive中
class Observer{
constructor(data){
//Object.defineProperty只能劫持已经存在的属性 后增的或删除的 不知道(vue2里会为此单独写一些api $set $delete)
this.walk(data)
}
walk(data){//循环对象 对属性一次劫持
//重新定义属性 -- 此时性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
}
实现劫持逻辑 应用Object.defineProperty() 中的get与set方法对属性重定义(性能差)
注意:如果data返回的对象中有参数也是一个对象 那么其深度的属性就不会被劫持 此时取药使用一次递归操作
export function defineReactive(target, key, value){//把当前的属性重新定义(属性劫持) 是个闭包
observe(value) //(递归)对所有的对象都进行属性劫持 因为 defineProperty只劫持一层 如果属性里存在对象 是不会劫持的
Object.defineProperty(target, key, {
get(){ //取值的时候会执行get
return value
},
set(newValue){//修改的时候会执行set
if(newValue === value) return
value = newValue
}
})
}
这样我们就实现了对象属性劫持 深度属性劫持
但是我们获取vm上的属性是 需要通过vm._data.xxx来获取的 比较麻烦
所以我们在进行一次代理操作 使vm._data在内部执行 使我们最终可以使用vm.xxx获取属性
function proxy(vm, target, key) {
Object.defineProperty(vm, key, { //vm.name
get() {
return vm[target][key]; //vm._data.name
},
set(newValue) {
vm[target][key] = newValue
}
});
}
function initData(vm) {
let data = vm.$options.data; //data可能是函数和对象
data = typeof data === "function" ? data.call(vm) : data;
vm._data = data;//将返回的对象放到了_data上
//对数据进行劫持 vue2 里采用了一个api defineProperty
observe(data);
//将vm._data用vm来代理就可以了
for (let key in data) {
proxy(vm, "_data", key);
}
}
index.html
DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta http-equiv="X-UA-Compatible" content="IE=edge" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Documenttitle>
head>
<body>
<script src="vue.js">script>
<script>
//响应式的数据变化 数据变化了我们可以监控到数据的变化
//数据的取值和更改我们要监控到
const vm = new Vue({
data() {
return {
name: "gu",
age: 24,
address: {
num: 30,
name: "哈哈哈",
},
};
},
});
console.log(vm);
script>
body>
html>
observe下index.js
class Observer{
constructor(data){
//Object.defineProperty只能劫持已经存在的属性 后增的或删除的 不知道(vue2里会为此单独写一些api $set $delete)
this.walk(data)
}
walk(data){//循环对象 对属性一次劫持
//重新定义属性 -- 此时性能差
Object.keys(data).forEach(key => defineReactive(data, key, data[key]))
}
}
export function defineReactive(target, key, value){//把当前的属性重新定义(属性劫持) 是个闭包
observe(value) //(递归)对所有的对象都进行属性劫持 因为 defineProperty只劫持一层 如果属性里存在对象 是不会劫持的
Object.defineProperty(target, key, {
get(){ //取值的时候会执行get
return value
},
set(newValue){//修改的时候会执行set
if(newValue === value) return
value = newValue
}
})
}
export function observe(data) {
//对这个对象进行劫持
if(typeof data !== 'object' || data == null){
return //只对对象进行劫持
}
//如果一个对象被劫持过了 那就不需要再被劫持了(要判断一个对象是否被劫持过 可以增加一个实例 用实例来判断是否被劫持过)
return new Observer(data)
}
index.js
import { initMixin } from "./init";
//将所有的方法都耦合在一起
function Vue(options) {
//options就是用户的选项
this._init(options);
}
initMixin(Vue); //扩展了init方法
export default Vue;
init.js
import { initState } from "./state";
export function initMixin(Vue) {
//就是给Vue增加init方法的
Vue.prototype._init = function (options) {
//用于初始化操作
// vue vm.$options 就是获取用户的配置
//我们使用vue的时候 $nextTick $data $attr 表示是Vue自己的属性
const vm = this;
vm.$options = options; //将用户的选项挂载到实例上 (原型中的this都是实例)
//初始化状态
initState(vm);
};
}
state.js
import { observe } from "./observe/index";
export function initState(vm) {
const opts = vm.$options; //获取所有的选项
if (opts.data) {
initData(vm);
}
}
function proxy(vm, target, key) {
Object.defineProperty(vm, key, { //vm.name
get() {
return vm[target][key]; //vm._data.name
},
set(newValue) {
vm[target][key] = newValue
}
});
}
function initData(vm) {
let data = vm.$options.data; //data可能是函数和对象
data = typeof data === "function" ? data.call(vm) : data;
vm._data = data;//将返回的对象放到了_data上
//对数据进行劫持 vue2 里采用了一个api defineProperty
observe(data);
//将vm._data用vm来代理就可以了
for (let key in data) {
proxy(vm, "_data", key);
}
}