Vue源码学习
前言
基于大崔哥Github开源mini-vue以及霍春阳大佬的《Vue.js设计和实现》学习Vue3源码,最终实现一个具有happy-path的mini-vue
Vue3框架的设计概览
1、权衡的艺术?
一个框架的实现过程中,各个功能模块之间并不是相互独立的,而是相互关联、相互制约的。因此框架的设计需要全局的把控,以便于后续模块的设计和拆分,而我们作为学习者,在学习框架的时候,也应该从全局的角度对框架的进行学习和认知。
1.1 命令式和声明式的权衡
视图层框架通常分为命令时和声明式,那么什么式命令式、声明式?
命令式
命令式最主要的特点:关注过程。例如,我们把下面的这段话翻译成原生代码:
获取 id 为 app 的 div 标签 -------> const eDiv = document.getELementById("#app")
它的文本内容设置为 hello world -------> eDiv.innerText = "hello world"
为其绑定点击事件,当点击时弹出提示:ok -------> eDiv.addEventListen("click",()=>{alert("ok")})
命令式的代码本身就是描述“做事情的过程”,更加符合我们的思维逻辑。
声明式
声明式最主要的特点:关注结果
上述自然语言在Vue中的实现
<div id="app" @click="()=>{alert("ok")}>
我们只看到了实现了这个“结果”,但是“结果”的过程,则是由Vuejs帮我们完成。Vuejs帮我们完成了“命令式”这个过程的封装,暴露给我们用户的是声明式的指令。
1.2 性能与可维护性的权衡
声明式代码的性能不低于命令式代码
如果我们把直接修改的性能消耗定义为A,找出差异的性能消耗定义为B,那么有:
- 命令式代码的更新性能消耗 = A
- 声明式代码的更新性能小号 = A + B
只有在找出差异的性能消耗为0的时候,声明式和命令式代码的性能消耗相同
声明式代码的可维护性高
采用命令式代码开发的时候,我们需要维护实现目标的整个过程,包括要手动的完成DOM元素的创建、更新、删除等。而声明式展示的仅仅是我们要的结果,看上去更加直观
框架设计者要做的就是:在保持可维护性的同时,让性能损失最小
1.3 虚拟DOM
声明式代码的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,我们要做的就是尽可能减少找出差异的性能消耗。所谓的虚拟DOM就是为了最小化找出差异这一步。
innerHTML和虚拟DOM创建页面是的性能
虚拟DOM | innerHtml | |
---|---|---|
纯JavaScript运算 | 创建JavaScript对象(vnode) | 渲染HTML字符串 |
DOM运算 | 新建所有的DOM元素 | 新建所有的DOM元素 |
innerHTML和虚拟DOM更新页面是的性能
虚拟DOM | innerHtml | |
---|---|---|
纯JavaScript运算 | 创建JavaScript对象(vnode) + Diff | 渲染HTML字符串 |
DOM运算 | 必要的DOM更新 | 删除所有旧DOM,新建所有新DOM元素 |
性能因素 | 与数据变化量相关(JS层面与运算) | 与模板大小相关)(DOM层面运算) |
粗略总结:
性能低 ------------------->性能高
innerHTML < 虚拟DOM < 原生JavaScript方法
同时还与页面大小、变更部分大小都有关,除此之外,与创建页面和更新页面也有关系,权衡之后,选择虚拟DOM
1.4 编译时和运行时
纯运行时
假设我们设计的框架,它提供了一个render函数,用户可以为该函数提供一个树型的数据对象,然后Render函数会根据对象递归的处理数据渲染成DOM元素
function render(obj,root){
const { tag ,children } = obj
const el = document.createElement(tag)
if(typeof children === "string"){
const text = document.createTextNode(children)
el.appendChild(text)
}else if (Array.isArray(children)){
//递归调用
children.forEach(child=>{
render(children,el)
})
}
root.appendChildren(el)
}
const obj ={
tag:"div",
children:[
{tag:"span",children:"helloworld"}
]
}
render(obj,document.body)
用户在使用render函数渲染内容的时候,必须为render函数提供一个属性的数据结构,这就是一个纯运行时的框架
运行时 + 编译时
如果我们想使用HTML模板,就需要将HTML模板编译成render函数需要的属性结构,就可以继续使用render函数了
为此,编写一个compiler函数
const html = `
<div>hello world</div>`
const obj = function compiler(html)
render(obj,document.body)
这是框架就变成了运行时 + 编译时的框架
编译时
直接将html模板编译成命令式代码,不需要render函数,仅仅使用一个compiler函数,这是框架就是编译时框架
Vue.js3 是一个编译时 + 运行时的框架。
2、框架设计的核心要素
-
提升用户开发体验
-
控制框架的代码体积
-
框架良好的Tree-shaking
-
框架应该输出怎么的构建产物
-
特性开关
-
错误处理
-
良好的TypeScript类型支持
总结
通过《Vue.js设计和实现》来了解框架的设计的理念,具体内容请大家阅读书籍吧
初始化项目
-
在项目文件夹下执行
yarn init -y
命令生成package.json
文件 -
执行
yarn add typescript jest @types/jest -D
命令安装 typescript、jest 和 @types/jest -
执行
tsc --init
命令生成tsconfig.json
文件,并修改以下几项:"lib": ["DOM", "ES6"], // 引入 DOM 和 ES6 "noImplicitAny": false, // 允许在表达式和声明中不指定类型,使用隐含的 any "types": ["jest"], // 将 jest 的类型声明文件添加到编译中 "downlevelIteration": true // 开启 for-of 对可迭代对象的支持 复制代码
-
在
package.json
中配置scripts
"scripts": { "test": "jest" } 复制代码
-
根据 Jest 官网进行配置,以在 Jest 中使用 Babel 和 TypeScript:执行
yarn add -D babel-jest @babel/core @babel/preset-env @babel/preset-typescript
安装 babel-jest、@babel/core、@babel/preset-env 和 @babel/preset-typescript;创建babel.config.js
文件,并写入以下内容:module.exports = { presets: [ ['@babel/preset-env', { targets: { node: 'current' } }], '@babel/preset-typescript' ] } 复制代码
-
执行
yarn add ts-jest -D
命令安装 ts-jest,之后执行ts-jest
命令生成jest.config.js
文件 -
创建
src
文件夹用于保存项目的代码