vue.js 设计与实现-vue3源码学习(2)

416 阅读5分钟

前言

vue3源码系列 1 juejin.cn/post/722481…

上一篇我们简单聊了下 vue3 源码的下载,安装依赖打包时遇到的问题以及如何开启 sourcemap 等一系列准备工作。本篇文章我们来进行 vue2vue3 响应式的了解与学习,以及根据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 属性的监听,当取值,赋值时会相应的触发 getset 。整个流程为 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创建我们自己的源码项目

  1. 创建文件夹 进入进入终端 npm init -y 初始化 package.json 文件
  2. 根据 vue3 源码创建项目目录
    image.png
  3. 配置 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"
	]
}

  1. 配置 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"
  }
  1. 配置打包。在 package.json
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "build": "rollup -c",
    "dev":"rollup -c -w"
  }

npm run build 直接打包
npm run dev 会进行文件监听,随时更新

打包测试

image.png

最后

到这里基本配置已经完成了,下一步就正式开始进行源码的学习与开发!