前言
vue3源码系列 1 juejin.cn/post/722481…
上一篇我们简单聊了下 vue3 源码的下载,安装依赖打包时遇到的问题以及如何开启 sourcemap 等一系列准备工作。本篇文章我们来进行 vue2 与 vue3 响应式的了解与学习,以及根据vue3源码,创建我们自己的初始化配置文件,进行初始化打包流程测试
vue2 响应性
<!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>
<div id="#app"></div>
<button onclick="changePrice()">修改价格</button>
</head>
<body>
<script>
const params={
name:"甄姬",
price:18888
}
let price=params.price
Object.defineProperty(params,'price',{
get(){
// console.log('触发get')
return price
},
set(newVal){
// console.log('触发set')
price=newVal
effect()
}
})
const effect=()=>{
document.getElementById("#app").innerHTML=`出战英雄为${params.name},价格为${params.price}`
}
const changePrice=()=>{
params.price=('0000' + Math.floor(Math.random() * 9999)).slice(-4) //赋值随机数
}
changePrice()
</script>
</body>
</html>
我们通过 Object.defineProperty 方式实现了对 params 对象中 price 属性的监听,当取值,赋值时会相应的触发 get 和 set 。整个流程为 changePrice()修改价格,监听到 set 事件,触发 effect() 修改我们定义的 price 值。修改 html 内容显示触发 get 完成赋值,更新流程!
那为什么我们要在外面定义一个值来对数据进行赋值呢? 如果我们直接修改 params.price 就会陷入 set get 死循环之中,而我们修改外面定义的 price 字符串则不会触发监听事件。
vue2 设计缺陷
<!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>
<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>
</head>
<body>
<style>
li{
list-style: none;
}
</style>
<div id="app">
<ul>
<div>对象遍历</div>
<li v-for="(value,key,index) in params" :key="index">key={{key}},value={{value}}</li>
<button @click="addObject">对象增加属性</button>
</ul>
<div>
数组遍历
<ul>
<li v-for="(item,index) in list" :key="index">{{item}}</li>
</ul>
<button @click="addArray">数组增加属性</button>
</div>
</div>
<script>
const app= new Vue({
el:"#app",
data(){
return {
params:{
name:"甄姬",
price:18888
},
list:['妲己','甄姬','王昭君']
}
},
methods:{
addObject(){
//不可监测响应性
// this.params.desc="我是一个法师"
// console.log(this.params) //数据已经改变,但页面无变化
//可监测响应性
// this.params={...this.params,desc:"我是一个法师"} //1
// this.$set(this.params,'desc','我是一个法师') //2
// this.$forceUpdate() //3
},
addArray(){
//不可监测响应性
// this.list[3]='李白'
// console.log(this.list)
//可监测响应性
this.list.push('李白')
}
}
})
</script>
</body>
</html>
测试中我们可以看到当对象新增属性,或者数组通过下标进行插入数据时,并不能实时响应更新。从上面响应性的代码中我们可以看到,监听一个属性时必须先进行定义,不然是无法监测到改变的。在 vue2 中对对象的响应性处理可以通过 三点运算符,$set 以及$forceUpdate 等方法,但并不推荐使用 $forceUpdate 进行强制更新。对数组则是对数组原型进行了优化,从而实现响应性。
vue3 响应性
<!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>
<div id="#app"></div>
<button onclick="changePrice()">修改价格</button>
<script>
let params={
name:'测试1号',
price:999
}
let obj=new Proxy(params,{
set(target,key, newVal,receiver){
target[key]=newVal
effect()
return true
},
get(target, key, receiver){
// console.log(target,key,receiver)
return target[key]
}
})
const effect=()=>{
document.getElementById("#app").innerHTML=`出战英雄为${obj.name},价格为${obj.price}`
}
const changePrice=()=>{
obj.price=('0000' + Math.floor(Math.random() * 9999)).slice(-4) //随便赋值一个随机数
}
effect()
</script>
</body>
</html>
在 vue3 中 使用了 Proxy 对对象进行了拦截,这样也就省去一层对数据的遍历,也就避免了对象新增属性无法监测到的问题。
Reflect
我们先来大致了解一下 Reflect 具体详情的可以参考文档学习
developer.mozilla.org/zh-CN/docs/…
Reflect 是一个内置的对象,它提供拦截 JavaScript 操作的方法。
Reflect不是一个函数对象,因此它是不可构造的。
Reflect.get(target, propertyKey[, receiver]) 的用法,举个例子来看下
let obj={
name:'vue3学习'
}
obj.name
Reflect.get(obj,'name')
//访问对象的属性我们可以通过对象方式来获取,也可以通过 Reflect 拿到
//既然访问对象可以直接通过对象属性拿到,为何要多此一举呢我们继续来看
let p1={
message:'vue3源码学习',
get getMessage(){
//如果以 get 方式定义,我们取值就无需在执行该方法 直接 p1.getMessage 就可以
console.log(this)
return this.message
}
}
console.log(p1.getMessage) //vue3源码学习
console.log(Reflect.get(p1,'getMessage')) //vue3源码学习
console.log(Reflect.get(p1,'getMessage',window))
//undefined 此时改变了zhis的指向,使this指向了window,window 上无 getMessage
console.log(Reflect.get(p1,'getMessage',p1)) //vue3源码学习
我们来看下在 proxy 中的使用
let p1={
message:'vue3源码学习',
get getMessage(){
console.log(this)
return this.message
}
}
const proxy=new Proxy(p1,{
get(target,key,receiver){
console.log('执行getter')
return target[key]
}
})
console.log(proxy.getMessage)
//输出: 执行getter vue3源码学习 结果对吗?
// 其实是不对的 proxy.getMessage this 指向 proxy 正常触发
//this.message 也应该触发一次getter 但是无触发 getter
//我们知道只有代理对象 proxy 获取数据时才会触发getter
//我们在p1 getMessage 方法里面打印this this 指向了p1 并不是proxy 所以不会触发getter
//我们用Reflect 修改proxy 中的代码
const proxy2=new Proxy(p1,{
get(target,key,receiver){
console.log('执行getter')
return Reflect.get(target,key,receiver)
}
})
console.log(proxy2.getMessage) 此时就会执行两次 getter
从0到1创建我们自己的源码项目
- 创建文件夹 进入进入终端 npm init -y 初始化 package.json 文件
- 根据 vue3 源码创建项目目录
- 配置 ts 如果未安装ts 先安装全局安装一下
npm install -g typescript通过tsc -v查看版本 目前使用的是4.7.4。在每个模块中创建src文件夹,src中创建index.ts,如compiler-core/src/index.ts。通过tsc -init生成tsconfig.json文件 配置如下。
// https://www.typescriptlang.org/tsconfig,也可以使用 tsc -init 生成默认的 tsconfig.json 文件进行属性查找
{
// 编辑器配置
"compilerOptions": {
// 根目录
"rootDir": ".",
// 严格模式标志
"strict": true,
// 指定类型脚本如何从给定的模块说明符查找文件。
"moduleResolution": "node",
// https://www.typescriptlang.org/tsconfig#esModuleInterop
"esModuleInterop": true,
// JS 语言版本
"target": "es5",
// 允许未读取局部变量
"noUnusedLocals": false,
// 允许未读取的参数
"noUnusedParameters": false,
// 允许解析 json
"resolveJsonModule": true,
// 支持语法迭代:https://www.typescriptlang.org/tsconfig#downlevelIteration
"downlevelIteration": true,
// 允许使用隐式的 any 类型(这样有助于我们简化 ts 的复杂度,从而更加专注于逻辑本身)
"noImplicitAny": false,
// 模块化
"module": "esnext",
// 转换为 JavaScript 时从 TypeScript 文件中删除所有注释。
"removeComments": false,
// 禁用 sourceMap
"sourceMap": false,
// https://www.typescriptlang.org/tsconfig#lib
"lib": ["esnext", "dom"],
// 设置快捷导入
"baseUrl": ".",
"paths": {
"@vue/*": ["packages/*/src"]
}
},
// 入口
"include": [
"packages/*/src"
]
}
- 配置 rollup 模块打包器 (类似于webpack) 根目录创建
rollup.config.js以及相应的配置
// https://www.rollupjs.com/
import resolve from '@rollup/plugin-node-resolve'
import commonjs from '@rollup/plugin-commonjs'
import typescript from '@rollup/plugin-typescript'
export default[
{
input:"packages/vue/src/index.ts", //入口文件
//打包出口
output:[
{
sourcemap:true,//开启sourcemap 便于打debugger
file:"./packages/vue/dist/vue.js",//导出文件地址
format:"iife",//生成的包的格式
name:"Vue"//变量名 const { ref }=Vue
}
],
plugins:[
//ts
typescript({
sourceMap:true
}),
resolve(),//,模块导入的路径补全
commonjs()// 将 CommonJS 模块转换为 ES2015
]
}
]
几个依赖包需要安装一下
"devDependencies": {
"@rollup/plugin-commonjs": "^22.0.1",
"@rollup/plugin-node-resolve": "^13.3.0",
"@rollup/plugin-typescript": "^8.3.4",
"tslib": "^2.4.0",
"typescript": "^4.7.4"
}
- 配置打包。在
package.json中
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1",
"build": "rollup -c",
"dev":"rollup -c -w"
}
npm run build 直接打包
npm run dev 会进行文件监听,随时更新
打包测试
最后
到这里基本配置已经完成了,下一步就正式开始进行源码的学习与开发!