手写 Vue 系列 之 Vue1.x
前言
前面我们用 12 篇文章详细讲解了 Vue2 的框架源码。接下来我们就开始手写 Vue 系列,写一个自己的 Vue 框架,用最简单的代码实现 Vue 的核心功能,进一步理解 Vue 核心原理。
为什么要手写框架
有人会有疑问:我已经详细阅读过框架源码了,甚至不止两三遍,这难道还不够吗?我自认为对框架的源码已经很熟悉了,我觉得没必要再手写。
有没有必要手写框架 这个事情,和 有没有必要阅读框架源码 的答案一样。看你的出发点是什么。
读源码
如果你是抱以学习的态度,那不用说,阅读框架源码肯定是有必要的。
大家都明白,平时的业务开发中,你身边人的水平可能都跟你差不多,所以你在业务中基本是看不到太多的优秀编码和思想。
而一个框架所包含的优秀设计和最佳实践就很多了,在阅读的时候有太多让你恍然大悟和惊艳的地方。即使你觉得自己现在段位不够,可能看不到那么多,但是源码对你的影响是潜移默化的。看多了优秀的代码,在你自己平时的编码中会不自觉的应用你学到的这些优秀编码方式。更何况 Vue 的大部分代码都是尤大自己写的,代码质量那是毋庸置疑的。
手写框架
至于 手写框架是否有必要 ?只要你读了框架源码,就必须自己手写一个。理由很简单,你阅读框架源码的目的是学习,你说你对源码已经非常熟了,你说你都学会了,那怎么检验?检验的方式也很简单,把你学到的东西向外输出,分三个阶段:
-
写技术博客、画思维导图(掌握 30%)
-
给他人分享,比如组内分享、录视频都行(掌握 60%)
-
手写框架,造轮子是检验你学习成果最好的方式(掌握 90%)
有没有发现前两阶段都是在讲他人的东西,你说你学到了,确实,你能向外输出,学你肯定是学到了,但是学到了多少呢?我觉得差不多是 60%,举个例子:
别人问你 Vue 的响应式原理是什么?经过前两个阶段的输出,你可能说的头头是道,比如 Object.defineProperty、getter、setter、依赖收集、依赖通知 watcher 更新等等。但是这整个过程你能否写出来呢?如果你第一次写,大概率是不行的,实现的时候会发现,根本不像你说的那么简单,要考虑东西远不止你说的那些。如果不信大家可以试试,检验一下。
要知道,造轮子的过程其实就是你应用的过程,只有你真的写出来了,你才算是真的学到了。如果只看不写,基本上可以算是进阶版的 只看不练。
所以,检验你是否真的学会并深入理解某个框架的实现原理,模仿 造轮子 是最好的检验方式。
手写 Vue1.x
在开始之前,我们先做好准备工作,在自己的工作目录下,新建我们的源码目录,比如:
mkdir lyn-vue && cd lyn-vue
这里我不想额外安装和配置打包工具,太麻烦了,采用现代浏览器原生支持的 ESM 的方式,所以大家需要在本地装一个 serve,起一个服务器。vite 就是这个原理,只不过它的服务端是自己实现的,因为它需要针对 import 的不同资源做相应的处理,比如解析 import 请求的是 node_modules 还是 用户自己的模块,亦或者是 TS 模块的转译等等。
npm i serve -g
安装好之后,在 lyn-vue
目录下执行 serve
命令,会在本地起一个服务器,接下来就进入编码阶段。
目标
下面的示例代码就是今天的目标,用我们自己手写的 Vue 框架把这个示例跑起来。
我们需要实现以下能力:
-
数据响应式拦截
-
原始值
-
普通对象
-
数组
-
-
数据响应式更新
-
依赖收集,Dep
-
依赖通知 Watcher 更新
-
编译器,compiler
-
-
methods + 事件 + 数据响应式更新
-
v-bind 指令
-
v-model 双向绑定
-
input 输入框
-
checkbox
-
select
-
/vue1.0.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Lyn Vue1.0</title>
</head>
<body>
<div id="app">
<h3>数据响应式更新 原理</h3>
<div>{{ t }}</div>
<div>{{ t1 }}</div>
<div>{{ arr }}</div>
<h3>methods + 事件 + 数据响应式更新 原理</h3>
<div>
<p>{{ counter }}</p>
<button v-on:click="handleAdd"> Add </button>
<button v-on:click="handleMinus"> Minus </button>
</div>
<h3>v-bind</h3>
<span v-bind:title="title">右键审查元素查看我的 title 属性</span>
<h3>v-model 原理</h3>
<div>
<input type="text" v-model="inputVal" />
<div>{{ inputVal }}</div>
</div>
<div>
<input type="checkbox" v-model="isChecked">
<div>{{ isChecked }}</div>
</div>
<div>
<select v-model="selectValue">
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>
<div>{{ selectValue }}</div>
</div>
</div>
<script type="module">
import Vue from './src/index.js'
const ins = new Vue({
el: '#app',
data() {
return {
// 原始值和对象的响应式原理
t: 't value',
t1: {
tt1: 'tt1 value'
},
// 数组的响应式原理
arr: [1, 2, 3],
// 响应式更新
counter: 0,
// v-bind
title: '看我',
// v-model
inputVal: 'test',
isChecked: true,
selectValue: 2
}
},
// methods + 事件 + 数据响应式更新 原理
methods: {
handleAdd() {
this.counter++
},
handleMinus() {
this.counter--
}
},
})
// 数据响应式拦截
setTimeout(() => {
console.log('********** 属性值为原始值时的 getter、setter ************')
console.log(ins.t)
ins.t = 'change t value'
console.log(ins.t)
}, 1000)
setTimeout(() => {
console.log('********** 属性的新值为对象的情况 ************')
ins.t = {
tt: 'tt value'
}
console.log(ins.t.tt)
}, 2000)
setTimeout(() => {
console.log('********** 验证对深层属性的 getter、setter 拦截 ************')
ins.t1.tt1 = 'change tt1 value'
console.log(ins.t1.tt1)
}, 3000)
setTimeout(() => {
console.log('********** 将值为对象的属性更新为原始值 ************')
console.log(ins.t1)
ins.t1 = 't1 value'
console.log(ins.