实现Vue2(一):创建环境 对象属性劫持 深度属性劫持

vue2源码实现

创建开发环境

创建一个项目文件
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>

在这里插入图片描述

Vue响应式原理 对象属性劫持 深度属性劫持

我们来写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>Document</title>
	</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);
	}
}

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

_聪明勇敢有力气

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值