Hello,大家好,我是 Sunday。
最近一位同学在学习 vue3 源码的时候,把 vue 3 的大部分核心逻辑都整理到了脑图之中:
整理的内容非常详细。应该会对所有还在学习 vue3 源码的同学都有所帮助。所以分享给大家!
那么今天,咱们就借助这位同学的脑图作为契机,来为大家捋一捋 【Vue3 框架设计原理】(看完设计原理之后,再看脑图收获会更大哦~)
在了解 Vue3 框架设计之前,我们需要做两件事情,而这两件事情也是今天的主要内容。
那么准备好了?
我们开始吧!
针对于目前的前端开发而言,主要存在两种 编程范式:
这两种 范式 一般是相对来去说的。
那么首先我们先来说什么叫做 命令式。
具体例子:
张三的妈妈让张三去买酱油。
那么张三怎么做的呢?
- 张三拿起钱
- 打开门
- 下了楼
- 到商店
- 拿钱买酱油
- 回到家
以上的流程详细的描述了,张三在买酱油的过程中,每一步都做了什么。那么这样一种:详细描述做事过程 的方式就可以被叫做 命令式。
那么如果把这样的方式放到具体的代码实现之中,又应该怎么做呢?
我们来看以下这样的一个事情:
在指定的 div 中展示 “hello world”
那么如果想要完成这样的事情,通过命令式的方式我们如何实现呢?
我们知道命令式的核心在于:关注过程。
所以,以上事情通过命令式实现则可得出以下逻辑与代码:
// 1. 获取到指定的 divconst divEle = document.querySelector('#app')// 2. 为该 div 设置 innerHTML 为 hello worlddivEle.innerHTML = 'hello world'
该代码虽然只有两步,但是它清楚的描述了:完成这件事情,所需要经历的过程
那么假如我们所做的事情,变得更加复杂了,则整个过程也会变得更加复杂。
比如:
为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg
那么通过命令式完成以上功能,则会得出如下逻辑与代码:
// 1. 获取到第一层的 divconst divEle = document.querySelector('#app')// 2. 获取到它的子 divconst subDivEle = divEle.querySelector('div')// 3. 获取第三层的 pconst subPEle = subDivEle.querySelector('p')// 4. 定义变量 msgconst msg = 'hello world'// 5. 为该 p 元素设置 innerHTML 为 hello worldsubPEle.innerHTML = msg
那么通过以上例子,相信大家可以对命令式的概念有了一个基础的认识。
最后做一个总结,什么叫做命令式呢?
命令式是:关注过程 的一种编程范式,他描述了完成一个功能的 详细逻辑与步骤。
当了解完命令式之后,那么接下来我们就来看 声明式 编程。
针对于声明式而言,大家其实都是非常熟悉的了。
比如以下代码,就是一个典型的 声明式 :
<div>{{ msg }}</div>
对于这个代码,大家是不是感觉有些熟悉?
没错,这就是 Vue 中非常常见的双大括号语法。所以当我们在写 Vue 模板语法 的时候,其实一直写的就是 声明式 编程。
那么声明式编程具体指的是什么意思呢?
还是以刚才的例子为例:
张三的妈妈让张三去买酱油。
那么张三怎么做的呢?
在这个例子中,我们说:张三所做的事情就是命令式。那么张三妈妈所做的事情就是 声明式。
在这样一个事情中,张三妈妈只是发布了一个声明,她并不关心张三如何去买的酱油,只关心最后的结果。
所以说,所谓声明式指的是:不关注过程,只关注结果 的范式。
同样,如果我们通过代码来进行表示的话,以下例子:
为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg
将会得出如下代码:
<div id="app"> <div> <p>{{ msg }}</p> </div></div>
在这样的代码中,我们完全不关心 msg 是怎么被渲染到 p 标签中的,我们所关心的只是:在 p 标签中,渲染指定文本而已。
最后做一个总结,什么叫做声明式呢?
声明式是:关注结果 的一种编程范式,他 并不关心 完成一个功能的 详细逻辑与步骤。(注意:这并不意味着声明式不需要过程!声明式只是把过程进行了隐藏而已!)
那么在我们讲解完成 命令式 和 声明式 之后,很多同学肯定会对这两种编程范式进行一个对比。
是命令式好呢?还是声明式好呢?
那么想要弄清楚这个问题,那么我们首先就需要先搞清楚,评价一种编程范式好还是不好的标准是什么?
通常情况下,我们评价一个编程范式通常会从两个方面入手:
那么接下来我们就通过这两个方面,来分析一下命令式和声明式。
性能一直是我们在进行项目开发时特别关注的方向,那么我们通常如何来表述一个功能的性能好坏呢?
我们来看一个例子:
为指定 div 设置文本为 “hello world”
那么针对于这个需求而言,最简单的代码就是:
div.innerText = "hello world" // 耗时为:1
你应该找不到比这个更简单的代码实现了。
那么此时我们把这个操作的 耗时 比作 :1 。(PS:耗时越少,性能越强)
然后我们来看声明式,声明式的代码为:
<div>{{ msg }}</div> <!-- 耗时为:1 + n --><!-- 将 msg 修改为 hello world -->
那么:已知修改text最简单的方式是innerText ,所以说无论声明式的代码是如何实现的文本切换,那么它的耗时一定是 > 1 的,我们把它比作 1 + n(对比的性能消耗)。
所以,由以上举例可知:命令式的性能 > 声明式的性能
可维护性代表的维度非常多,但是通常情况下,所谓的可维护性指的是:对代码可以方便的 阅读、修改、删除、增加 。
那么想要达到这个目的,说白了就是:代码的逻辑要足够简单,让人一看就懂。
那么明确了这个概念,我们来看下命令式和声明式在同一段业务下的代码逻辑:
// 命令式// 1. 获取到第一层的 divconst divEle = document.querySelector('#app')// 2. 获取到它的子 divconst subDivEle = divEle.querySelector('div')// 3. 获取第三层的 pconst subPEle = subDivEle.querySelector('p')// 4. 定义变量 msgconst msg = 'hello world'// 5. 为该 p 元素设置 innerHTML 为 hello worldsubPEle.innerHTML = msg
// 声明式<div id="app"> <div> <p>{{ msg }}</p> </div></div>
对于以上代码而言,声明式 的代码明显更加利于阅读,所以也更加利于维护。
所以,由以上举例可知:**命令式的可维护性 < 声明式的可维护性 **
由以上分析可知两点内容:
那么双方各有优劣,我们在日常开发中应该使用哪种范式呢?
想要搞明白这点,那么我们还需要搞明白更多的知识。
企业应用的设计原则,想要描述起来比较复杂,为什么呢?
因为对于 不同的企业类型(大厂、中小厂、人员外包、项目外包),不同的项目类型(前台、中台、后台)来说,对应的企业应用设计原则上可能会存在一些差异。
所以我们这里所做的描述,会抛弃一些细微的差异,仅抓住核心的重点来进行阐述。
无论什么类型的企业,也无论它们在开发什么类型的项目,那么最关注的点无非就是两个:
项目成本非常好理解,它决定了一个公司完成“这件事”所付出的代价,从而直接决定了这个项目是否是可以盈利的(大厂的烧钱项目例外)。
那么既然项目成本如此重要,大家可以思考一下,决定项目成本的又是什么?
没错!就是你的 开发周期。
开发周期越长,所付出的人员成本就会越高,从而导致项目成本变得越高。
通过我们前面的分析可知,声明式的开发范式在 可维护性 上,是 大于 命令式的。
而可维护性从一定程度上就决定了,它会使项目的:开发周期变短、升级变得更容易 从而大量节约开发成本。
所以这也是为什么 Vue 会变得越来越受欢迎的原因。
决定开发者开发体验的核心要素,主要是在开发时和阅读时的难度,这个被叫做:心智负担。
心智负担可以作为衡量开发难易度的一个标准,心智负担高则证明开发的难度较高,心智负担低则表示开发的难度较低,开发更加舒服。
那么根据我们之前所说,声明式的开发难度明显低于命令式的开发难度。
所以对于开发体验而言,声明式的开发体验更好,也就是 心智负担更低。
Vue 作者尤雨溪在一次演讲中说道:框架的设计过程其实是一个不断取舍的过程 。
这代表的是什么意思呢?
想要搞明白这个,那么再来明确一下之前说过的概念:
当我们明确好了这样的一个问题之后,那么我们接下来来思考一个问题:框架的开发与设计原则是什么呢?
我们知道对于 Vue 而言,当我们使用它的是通过 声明式 的方式进行使用,但是对于 Vue 内部而言,是通过 命令式 来进行的实现。
所以我们可以理解为:Vue 封装了命令式的逻辑,而对外暴露出了声明式的接口
那么既然如此,我们明知 命令式的性能 > 声明式的性能 。那么 Vue 为什么还要选择声明式的方案呢?
其实原因非常的简单,那就是因为:命令式的可维护性 < 声明式的可维护性 。
为指定的 div 的子元素 div 的子元素 p 标签,展示变量 msg
以这个例子为例。
对于开发者而言,不需要关注实现过程,只需要关注最终的结果即可。
而对于 Vue 而言,他所需要做的就是:封装命令式逻辑,同时 尽可能的减少性能的损耗!它需要在 性能 与 可维护性 之间,找到一个平衡。从而找到一个 可维护性更好,性能相对更优 的一个点。
所以对于 Vue 而言,它的设计原则就是:在保证可维护性的基础上,尽可能的减少性能的损耗。
那么回到我们的标题:为什么说框架的设计过程其实是一个不断取舍的过程?
答案也就呼之欲出了,因为:
我们需要在可维护性和性能之间,找到一个平衡点。在保证可维护性的基础上,尽可能的减少性能的损耗。
所以框架的设计过程其实是一个不断在 可维护性和性能 之间进行取舍的过程
在 Vue 3 的 源代码 中存在一个 runtime-core 的文件夹,该文件夹内存放的就是 运行时 的核心代码逻辑。
runtime-core 中对外暴露了一个函数,叫做 渲染函数render
我们可以通过 render 代替 template 来完成 DOM 的渲染:
有些同学可能看不懂当前代码是什么意思,没有关系,这不重要,后面我们会详细去讲。
<head> <meta charset="UTF-8"> <title>Document</title> <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script></head><body> <div id="app"></div></body><script> const { render, h } = Vue // 生成 VNode const vnode = h('div', { class: 'test' }, 'hello render') // 承载的容器 const container = document.querySelector('#app') // 渲染函数 render(vnode, container)</script>
我们知道,在 Vue 的项目中,我们可以通过 tempalte 渲染 DOM 节点,如下:
<template> <div class="test">hello render</div></template>
但是对于 render 的例子而言,我们并没有使用 tempalte,而是通过了一个名字叫做 render 的函数,返回了一个不知道是什么的东西,为什么也可以渲染出 DOM 呢?
带着这样的问题,我们来看:
我们知道在上面的代码中,存在一个核心函数:渲染函数 render,那么这个 render 在这里到底做了什么事情呢?
我们通过一段代码实例来去看下:
假设有一天你们领导跟你说:
我希望根据如下数据:
渲染出这样一个 div:
{ type: 'div', props: { class: test }, children: 'hello render'}
<div class="test">hello render</div>
那么针对这样的一个需求你会如何进行实现呢?大家可以在这里先思考一下,尝试进行一下实现,然后我们再继续往下看..........
那么接下来我们根据这个需求来实现以下代码:
<script> const VNode = { type: 'div', props: { class: 'test' }, children: 'hello render' } // 创建 render 渲染函数 function render(vnode) { // 根据 type 生成 element const ele = document.createElement(vnode.type) // 把 props 中的 class 赋值给 ele 的 className ele.className = vnode.props.class // 把 children 赋值给 ele 的 innerText ele.innerText = vnode.children // 把 ele 作为子节点插入 body 中 document.body.appendChild(ele) } render(VNode)</script>
在这样的一个代码中,我们成功的通过一个 render 函数渲染出了对应的 DOM,和前面的 render 示例 类似,它们都是渲染了一个 vnode,你觉得这样的代码真是 妙极了!
但是你的领导用了一段时间你的 render 之后,却说:天天这样写也太麻烦了,每次都得写一个复杂的 vnode,能不能让我直接写 HTML 标签结构的方式 你来进行渲染呢?
你想了想之后,说:如果是这样的话,那就不是以上 运行时 的代码可以解决的了!
没错!我们刚刚所编写的这样的一个“框架”,就是 运行时 的代码框架。
那么最后,我们做一个总结:运行时可以利用render 把 vnode 渲染成真实 dom 节点。
在刚才,我们明确了,如果只靠 运行时,那么是没有办法通过 HTML 标签结构的方式 的方式来进行渲染解析的。
那么想要实现这一点,我们就需要借助另外一个东西,也就是 编译时。
Vue 中的编译时,更准确的说法应该是 编译器 的意思。它的代码主要存在于 compiler-core 模块下。
我们来看如下代码:
<!DOCTYPE html><html lang="en"><head> <meta charset="UTF-8"> <title>Document</title> <script src="https://unpkg.com/vue@3.2.36/dist/vue.global.js"></script></head><body> <div id="app"></div></body><script> const { compile, createApp } = Vue // 创建一个 html 结构 const html = ` <div class="test">hello compiler</div> ` // 利用 compile 函数,生成 render 函数 const renderFn = compile(html) // 创建实例 const app = createApp({ // 利用 render 函数进行渲染 render: renderFn }) // 挂载 app.mount('#app')</script></html>
对于编译器而言,它的主要作用就是:把 template 中的 html 编译成 render 函数。然后再利用 运行时 通过 render 挂载对应的 DOM。
那么最后,我们做一个总结:编译时可以把html 的节点,编译成 render函数
前面两小节我们已经分别了解了 运行时 和 编译时,同时我们也知道了:vue 是一个 运行时+编译时 的框架!
vue 通过 compiler 解析 html 模板,生成 render 函数,然后通过 runtime 解析 render,从而挂载真实 dom。
那么看到这里可能有些同学就会有疑惑了,既然 compiler 可以直接解析 html 模板,那么为什么还要生成 render 函数,然后再去进行渲染呢?为什么不直接利用 compiler 进行渲染呢?
即:为什么 vue 要设计成一个 运行时+编译时的框架呢?
那么想要理清楚这个问题,我们就需要知道 dom 渲染是如何进行的。
对于 dom 渲染而言,可以被分为两部分:
那么什么是初次渲染呢?
当初始 div 的 innerHTML 为空时,
<div id="app"></div>
我们在该 div 中渲染如下节点:
<ul> <li>1</li> <li>2</li> <li>3</li></ul>
那么这样的一次渲染,就是 初始渲染。在这样的一次渲染中,我们会生成一个 ul 标签,同时生成三个 li 标签,并且把他们挂载到 div 中。
那么此时如果 ul 标签的内容发生了变化:
<ul> <li>3</li> <li>1</li> <li>2</li></ul>
li - 3 上升到了第一位,那么此时大家可以想一下:我们期望浏览器如何来更新这次渲染呢?
浏览器更新这次渲染无非有两种方式:
那么大家觉得这两种方式哪一种方式更好呢?那么我们来分析一下:
那么根据以上分析,我们知道了:
那么这两种方式,哪一种更快呢?我们来实验一下:
const length = 10000 // 增加一万个dom节点,耗时 3.992919921875 ms console.time('element') for (let i = 0; i < length; i++) { const newEle = document.createElement('div') document.body.appendChild(newEle) } console.timeEnd('element') // 增加一万个 js 对象,耗时 0.402099609375 ms console.time('js') const divList = [] for (let i = 0; i < length; i++) { const newEle = { type: 'div' } divList.push(newEle) } console.timeEnd('js')
从结果可以看出,dom 的操作要比 js 的操作耗时多得多,即:dom** 操作比 js 更加耗费性能**。
那么根据这样的一个结论,回到我们刚才所说的场景中:
对比 旧节点 和 新节点 之间的差异
根据差异,删除一个 旧节点,增加一个 新节点
根据结论可知:方式一会比方式二更加消耗性能(即:性能更差)。
那么得出这样的结论之后,我们回过头去再来看最初的问题:为什么 vue 要设计成一个 运行时+编译时的框架呢?
答:
在 vue 的源码中,会大量的涉及到一个概念,那就 副作用。
所以我们需要先了解一下副作用代表的是什么意思。
副作用指的是:当我们 对数据进行 setter 或 getter 操作时,所产生的一系列后果。
那么具体是什么意思呢?我们分别来说一下:
setter 所表示的是 赋值 操作,比如说,当我们执行如下代码时 :
msg = '你好,世界'
这时 msg 就触发了一次 setter 的行为。
那么假如说,msg 是一个响应性数据,那么这样的一次数据改变,就会影响到对应的视图改变。
那么我们就可以说:msg 的 setter 行为,触发了一次副作用,导致视图跟随发生了变化。
getter 所表示的是 取值 操作,比如说,当我们执行如下代码时:
element.innerText = msg
此时对于变量 msg 而言,就触发了一次 getter 操作,那么这样的一次取值操作,同样会导致 element 的 innerText 发生改变。
所以我们可以说:msg 的 getter 行为触发了一次副作用,导致 element 的 innterText 发生了变化。
那么明确好了副作用的基本概念之后,那么大家想一想:副作用可能会有多个吗?
答案是:可以的。
举个简单的例子:
<template> <div> <p>姓名:{{ obj.name }}</p> <p>年龄:{{ obj.age }}</p> </div></template><script> const obj = ref({ name: '张三', age: 30 }) obj.value = { name: '李四', age: 18 }</script>
在这样的一个代码中 obj.value 触发了一次 setter 行为,但是会导致两个 p 标签的内容发生改变,也就是产生了两次副作用。
根据本小节我们知道了:
根据前面的学习我们已经知道了:
那么了解了这些内容之后,下来 vue3 的一个基本框架设计:
对于 vue3 而言,核心大致可以分为三大模块:
我们以以下基本结构来描述一下三者之间的基本关系:
<template> <div>{{ proxyTarget.name }}</div></template><script>import { reactive } from 'vue'export default { setup() { const target = { name: '张三' } const proxyTarget = reactive(target) return { proxyTarget } }}</script>
在以上代码中:
该方法是 reactivity 模块对外暴露的一个方法
可以接收一个复杂数据类型,作为 Proxy (现在很多同学可能还不了解什么是 proxy ,没有关系后面我们会详细介绍它,现在只需要有个印象即可)的 被代理对象(target)
返回一个 Proxy 类型的 代理对象(proxyTarget)
当 proxyTarget 触发 setter 或 getter 行为时,会产生对应的副作用
以上就是 reactivity、runtime、compiler 三者之间的运行关系。
当然除了这三者之外, vue 还提供了很多其他的模块,比如:SSR ,我们这里只是 概述了基本的运行逻辑。
本文链接:http://www.28at.com/showinfo-26-97893-0.html这张图,把 vue3 的源码讲清楚了!!!
声明:本网页内容旨在传播知识,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。邮件:2376512515@qq.com
上一篇: 使用 Python 进行财务数据分析实战
下一篇: 都用10年了,gRPC有什么不好的?