# mini-vue2
**Repository Path**: egu0/mini-vue2
## Basic Information
- **Project Name**: mini-vue2
- **Description**: 手写 Vue2
- **Primary Language**: Unknown
- **License**: MIT
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 0
- **Forks**: 0
- **Created**: 2024-04-29
- **Last Updated**: 2024-05-09
## Categories & Tags
**Categories**: Uncategorized
**Tags**: None
## README
# 手写 Vue2
node 版本: v16
## 1.项目搭建
```sh
npm init -y
npm install @babel/preset-env @babel/core rollup rollup-plugin-babel rollup-plugin-serve -D
```
`src/index.js`
```js
function Vue() { }
export default Vue;
```
`rollup.config.js`
```js
import babel from 'rollup-plugin-babel'
import serve from 'rollup-plugin-serve'
export default {
input: './src/index.js',
output: {
file: 'dist/vue.js',
format: 'umd',
name: 'Vue',
sourcemap: true
},
plugins: [
babel({
exclude: 'node_modules/**'
}),
serve({
port: 3000,
contentBase: '',
openPage: '/index.html'
})
]
}
```
`.babelrc`
```
{
"presets": [
"@babel/preset-env"
]
}
```
`index.html`
```html
Document
hello
```
在 `package.json` 中配置 rollup 命令
```json
"scripts": {
"dev": "rollup -c rollup.config.js -w"
},
```
执行命令
```
npm run dev
```
## 2.处理 data 属性
1. 模块化。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/5e7c82f8f4cf790c567518b75ed226531df57b8d)
2. 使用 `Object.defineProperty` 函数为对象中的第一层数据添加观测。比如 `{x: 1, y: 2}`。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/6ca5928ea989089de5f7ea9ab534bfa845063add)
3. 为嵌套对象中的数据添加观测。比如 `{a: 1, b: {c: 2, d: 3}}`。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/15847655fa667fc31535f1adcafa2f3f96bd48b5)
4. 为通过 `=` 添加的新对象设置观测。比如 `vm._data.b = {e: 4}`。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/957c8bd3de74180204d6d34120e0949ae370cc5c)
5. 通过代理模式(利用原型链)劫持数组的方法。比如为 `vm._data.scores` 数组的 push 方法添加自定义逻辑。效果:可在控制台看到 `vm._data.scores` 数组原型的方法为所定义的目标方法。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/35be0b1a8e4b8aa1ae77fb3f19f6d7d88be4d7ab)
6. 为数组中的对象中的元素添加观测。比如`{arr: [ { z: 6 } ]}`。效果:`z` 被添加了观测。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/7f4648f2edee78805379634e0f75d1aad444cd2d)
7. 为通过 `push,unshift,splice` 方法添加的元素添加观测。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/34cc985515cb4ad5a369742423e2ea03b628d24c)
8. 将 `_data` 中的属性代理到 vm 实例。[checkpoint](https://gitee.com/egu0/mini-vue2/commit/7cf7a457d8ddebe03878615cbceaa3b2ce7c732d)
## 3.将 thml 模板解析为 ast
### 1.获取 html
初次渲染逻辑:
1. 初始化数据
2. 编译模板
3. render 虚拟节点
4. 转为真实 DOM 并放入页面中
编译模板的流程:([source](https://v2.vuejs.org/v2/guide/instance.html#Lifecycle-Diagram))
简述:
1. 必须包含 `el` 属性(要么显式指定,要么从 `$mount` 方法参数处获取);
2. 如果指定了 `template` 属性,那么优先编译 `template` 属性指定的模板;如果未指定 `template` 属性,那么编译 `el` 元素
### 2.了解 AST
AST:抽象语法树,[Wikipedia](https://en.wikipedia.org/wiki/Abstract_syntax_tree)
示例:将一段 html 转为对应的 ast 对象
```html
```
结果对象
```json
{
tag: 'div',
attrs: {
id: 'app'
},
children: [
{
tag: null,
text: "hello"
},
{
tag: 'span',
text: 'jetson'
},
{
tag: 'br'
},
{
tag: 'p',
children: [
{
tag: null,
text: 'how are you'
}
]
}
]
}
```
### 3.解析 html
将 html 解析为 AST 对象
缺点:不能解析单标签元素,比如 img,link
## 4.将 ast 转换为 render 函数
### 1.render 函数字符串
将 ast 变为 render 函数字符串
示例
1、准备 html
```
hello, {{ msg}}, {{greeting }}
are you good?
```
2、将 html 转为 ast。省略
3、将 ast 转为 render 函数字符串
```js
_c("div",
{id: "app", class: "box", style: {"color":"red","margin-top":"30px"}},
_v("hello, " + _s(msg) + ", " + _s(greeting) + "\n "), _c("h1", null, _v("are you good? ")))
```
- `_c(tagName, attrs, ...args)` 支持多个参数
- `_v(text)` 只接收一个参数
- `_s(varName)` 只接收一个参数
注意:`4.2` 提交时 `_v` 逻辑有点问题(`src/compile/generate.js`),应该为
```js
if (lastIndex < text.length) {
tokens.push(JSON.stringify(text.slice(lastIndex)))
}
return `_v(${tokens.join(' + ')})`// + 而不是 , 因为 _v 只接受一个参数
}
}
```
修正:`charts` 函数无需对 tagText 进行处理
```js
function charts(tagText) {
// tagText = tagText.replace(/^\s+/, '')
if (tagText && stack.length > 0) {
stack[stack.length - 1].children.push({
text: tagText,
type: 3
})
}
}
```
### 2.render 函数
将 render 函数字符串转为 render 函数
```js
export function compileToFunction(el) {
console.log('html:', el)
// 1.将 html 解析为 ast 语法树
let ast = parseHTML(el)
console.log('ast:', ast)
// 2.将 ast 语法树解析为 render 函数字符串
let render_string = parseTagNode(ast)
console.log('render string:', render_string)
// 3.将 render 函数字符串转为 render 函数
let render = new Function(`with(this) { return ${render_string} }`)
console.log('render:', render)
}
```
## 5.转为真实 DOM
### 1.虚拟 DOM
将 render 函数转为虚拟 DOM
### 2.真实 DOM
将虚拟 DOM 转为真实 DOM,并替换原 DOM
效果:基本的模板渲染
## 总结渲染流程
1. **数据初始化**
2. **将 html 模板解析为 ast 对象**
3. **将 ast 对象转换为 render 函数**:先将 ast 对象转换为 render 函数字符串,再将 render 字符串转换为 render 函数)
4. **根据 render 函数获取虚拟 dom,将虚拟 dom 转换为真实 dom 并替换原 dom**
## 6.生命周期
### 1.合并选项到 `vm.$options`
具体的,合并的内容是:
1. 通过 `Vue.Mixin()` 添加的选项
2. 通过 `new Vue({..})` 创建实例时添加的选项
### 2.调用生命周期钩子
## 7.watch
### 1.Watcher类
[source](https://www.bilibili.com/video/BV1Jv4y1q7nE?p=27)
### 2.Dep 类
dep 实例与 data 中的属性一一对应
watcher 实例与属性的关系为 `N:1`,表示某个属性被用了几次就有几个对应的 watcher 实例
### 3.收集对象的依赖
收集对象的依赖、对象的自动更新
### 4.dep和watcher多对多
注意:额外做了 `dep.subs` 的去重
### 5.收集数组的依赖
收集数组的依赖、数组的自动更新
## 8.nextTick
问题:数据更新多次,同时也进行了多次重新渲染,造成性能消耗
希望的结果:只更新一次
### 1.用队列处理watcher
内容:进行属性的 set 操作,与属性关联的 dep 执行了 dep.notify 方法,进而调用了 watcher.update 方法;在 watcher.js 中,将 `watcher` 去重后放到一个队列中,同时进行防抖处理,利用 setTimeout 将 `vm._update(vm._render(el))` 推迟执行
`observe/watcher.js` 部分代码
```js
//...
let watcherQueue = []
let hittedWatchers = {} // 键为 watcher 的 id,值为布尔表示是否存在
let setUp = false
function watcherEnqueue(watcher) {
let id = watcher.id
//去重。通过 hittedWatchers 集合记录已经加入的 watcher
if (hittedWatchers[id] == null) {
watcherQueue.push(watcher)
hittedWatchers[id] = true
//防抖。setUp 表示是否已设置异步处理钩子
if (!setUp) {
setTimeout(() => {
watcherQueue.forEach(watcher => {
watcher.run()
})
// 状态重置
watcherQueue = []
hittedWatchers = {}
setUp = false
}, 0)
}
setUp = true
}
}
```
### 2.实现 nextTick
[nextTick](https://stackoverflow.com/a/47636157/23681037)
### 3.实现 updated 钩子
## 9.watch
watch 使用的四种方式
```html
{{name}}
```
### 9.1实现watch功能
初始化 watch:创建 watcher 实例,监听数据的变化
## 10.diff 算法
`patch(oldVNode, newVNode)` 方法
- 作用:将虚拟 dom 转为真实 dom,并用得到的真实 dom 替换原真实 dom
- 调用:第一次调用时,`oldVNode` 是真实 dom,`newVNode` 是虚拟 dom;之后再调用时,两个参数都是虚拟 dom
- 优化(`diff 算法`):因为虚拟 dom 和真实 dom 具有映射关系,所以当虚拟 dom 变化时,可以只比较它变化的内容从而只更新受影响的部分真实 dom。相比于每次虚拟 dom 变化都重新从头创建真实 dom,利用 diff 算法提高了资源利用率和程序性能
面试题:`key` 的选择,[参考](https://zhuanlan.zhihu.com/p/124019708)
## 11.computed
### 1.实现 computed 代理
用 defineProperty 为 comptuted 属性实现代理功能
### 2.实现缓存机制
为每个计算属性创建一个对应的 Watcher 实例,实现缓存机制
### 3.实现 computed 属性的同步更新
## 总结:Watcher 四用
- 用于初次渲染
- watch 功能
- nextTick 功能
- computed 功能
## 12.自定义组件
### 1.component 和 extend 两个静态方法
`Vue.component` 依赖于 `Vue.extend`
`Vue.extend` 方法:创建 Vue 子类
### 2.创建自定义组件的虚拟 dom