【Vue.js设计与实现解读-1】

Vue设计与实现阅读-1

前言

最近工作清闲了些,想着很久没有看书,Vue.js设计与实现这本书看了好几次都没有读完,趁着这个机会边读边记录一下吧。如果有理解的不正确的地方,欢迎指出。

1、命令式和声明式

命令式: 关注过程

例如实现这样一个代码

 -获取 id 为 app 的 div 标签
 -设置他的文本内容为hello world
 -为他绑定点击事件
 -点击时弹出提示:ok
javascript 复制代码
const div = document.querySelector('#app');
div.innerText = 'hello world';
div.addEventListener('click', () => {
	alert('ok');
});

声明式: 关注结果

同样用声明式的写法实验上面的功能

javascript 复制代码
<div @click="() => alert('ok')">hello world</div>

上面这个html模板采用的是Vue实现的方式,可以看出,我们提供的是一个结果,vue内部去帮我封装了这个过程,就像我们告诉vue:我需要一个div,他的文本内容是hello world,有一个绑定事件。可以猜测,Vue.js的内部实现是命令式的,暴露给用户的是声明式的。

2、性能

声明式代码的性能不优于命令式代码的性能。

例如,还是上面的例子,我们需要修改 div 中文本的内容为 hello Vue。

命令式的写法,直接调用相关命令

javascript 复制代码
div.textContent = 'hello Vue' // 直接修改

声明式写法

javascript 复制代码
// 修改前
<div @click="() => alert('ok')">hello world</div>
// 修改后
<div @click="() => alert('ok')">hello Vue</div>

从上面可以看出,命令式代码明确知道哪里发生了变更,只需要做必要的修改就行,但声明式还不一定能做到这一点。对于一个框架来说,为了实现最优的更新性能,他需要找到前后的差异且只更新变化的地方。

如果我们把直接修改的性能消耗定义为A,找出差异的消耗定义为B,则

	命令式代码的更新性能消耗 = A
	声明式代码的更新新能消耗 = A + B

在性能层面命令式是更好的选择,为什么Vue.js要选择声明式设计方案。原因在于声明式的可维护性更强。在上面的例子中可以看到,如果采用声明式开发,我们需要维护实现目标的整个过程(DOM创建、更新、删除等),而命令式我们只需要关注结果就行。

3、虚拟DOM性能

声明式代码的更新新能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。如果能够最小化找出差异的性能消耗,就可以让声明式代码的性能无限接近于命令式代码的性能。所谓的虚拟DOM,就是为了最小化找出差异这一步的性能消耗而出现的。

innerHTML创建页面的过程: 构造 HTML 字符串,将该字符串赋值给 DOM 元素的 innerHTML 属性。

javascript 复制代码
const html = `
	<div><span></span></div>
`
div.innerHTML = html
 innerHTML创建页面的性能:HTML 字符串拼接的计算量 + innerHTML 的 DOM 计算量

虚拟DOM创建页面过程:第一步,创建js对象(真实 dom),第二步递归遍历虚拟 dom 树并创建真实 dom

 虚拟dom创建页面的性能:创建JS对象的计算量 + 创建真实 DOM 的计算量

从创建页面来看,两者差距不大,都是需要新建所有 DOM 元素。但是继续看下更新页面时的性能

innerHTML更新页面的过程:相当于重新构建HTML字符串,再设置 DOM 元素的 innerHTML 属性。相当于更改了一个文字,也需要重新设置 innerHTML 属性,等价于销毁所有旧的 DOM 元素,再全量创建新的 DOM 元素。

虚拟DOM更新页面过程:创建 js 对象(虚拟DOM树),比较新旧虚拟 DOM, 找到变化的元素更新他。

对比:在更新页面的时候,虚拟 DOM 在 js 层面比创建页面时多了个找 diff 的性能消耗,但是比较是 js 层面的运算,不会产生数量级的差异,再观察 DOM 层面的运算,虚拟 DOM 只需要更新边户的COM, 而 innerHTML 需要全量更新。

4、运行时和编译时

设计框架,我们有三种选择,纯运行时、运行时+编译时、纯编译时。

假设设计一个框架,提供一个 Render 函数,用户可以为该函数提供一个属性结构的数据对象,Render 函数会根据对象递归将数据渲染成 DOM 元素。
规定树形结构如下:
const obj = {
	tag: 'div',
	children: [
		{
			tag: 'span',
			children: 'hello world'
		}
	]
}

每个对象有两个属性,tag 标签名称, children 可以是数组,表示子节点,可以是文本表示文本子节点。

Render 函数实现

javascript 复制代码
function Render(obj, root) {
	// 创建标签
	const el = document.createElement(obj.tag);
	// 如果是文本子节点
	if (typeof obj.children === 'string') {
		// 创建文本元素
		const text = document.createTextNode(obj.children);
		// 添加进标签元素
		el.appendChild(text);
	} else if (obj.children) {
		// 子节点,递归调用render 构造子元素
		obj.children.forEach((child) => Render(child, el))
	}
	// 将元素添加到根节点
	root.appendChild(el);
}

元素使用

javascript 复制代码
Render(obj, document.boby);

可以发现,用户在使用Render函数渲染内容的时候,可以直接提供一个树形数据对象给 Render 对象,不涉及其他额外步骤。

有一天用户抱怨,手写树形数据对象太麻烦,不直观,想支持用类型HMTL标签的方式描述树形结构对象。发现刚写的Render函数并不能满足。刚实现的Render函数,是一个纯运行时的框架。

为满足用户需求,使用编译的手段 把HTML 标签变异成树形结构的数据对象。

javascript 复制代码
<div>
	<span>hello world</span>
</div>

 ------ 经过编译 ------
 
const obj = {
		tag: 'div',
		children: [
			{
				tag: 'span',
				children: 'hello world'
			}
		]
	}

假设 你编写了一个 Compiler 的程序,他的作用是将 HTML 字符串编译成树形结构的对象,用户需要分别调用 Compiler 和 Render 函数就能实现。

javascript 复制代码
const html = `
	<div>
		<span>hello world</span>
	</div>
`
const obj = Compiler(html);
Render(obj, document.boby);

此时 框架变成了运行时 + 编译时 。既支持运行时,用户可以直接提供数据对象无需编译;又支持编译时,用户可以提供 HTML字符串,我们将它编译成数据对象交给运行时处理。

那编译器可以把 HTML 变成成数据对象,应该也可以编译成命令式代码。

javascript 复制代码
<div>
	<span>hello world</span>
</div>

 ------ 经过编译 ------
 
const div = document.createElement('div');
const span = document.createElement('span');
span.innerText = 'hello world';
div.appendChild(span);
document.boby.appendChild(div);

这样我们只需要一个 Compiler 函数就行。这就成了一个纯编译时的框架。我们不支持任何运行时内容,用户的代码通过编译器编译后才能运行。

一个框架可以是上面说的这三种的任意一种

纯运行时,无编译过程,因此没办法分析用户提供的内容。如果加入编译,我们可以分析用户提供内容,看到哪些发生改变,哪些没有,
那我们可以在编译时提取这些信息,传递给 Render 函数,Render函数根据这些信息进一步优化。但是如果是纯编译时,她可以分析用户提供的内容,
由于不需要任何运行时,而是直接编译,那性能会更好,但是因为用户提供的内容必须通过编译才能使用,所以有损灵活性

Vue3保持运行时+编译时的架构,在保持灵活性的基础上能够尽可能地去优化。

5、总结

  • 命令式和声明式,命令式关注过程,声明式关注结果
  • 声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗
  • 虚拟dom存在的意义在于使找出差异的性能消耗最小化
  • 运行时和编译时的差异,vue3是编译时+进行时的框架。
相关推荐
理想不理想v3 分钟前
vue种ref跟reactive的区别?
前端·javascript·vue.js·webpack·前端框架·node.js·ecmascript
知孤云出岫3 分钟前
web 渗透学习指南——初学者防入狱篇
前端·网络安全·渗透·web
贩卖纯净水.9 分钟前
Chrome调试工具(查看CSS属性)
前端·chrome
栈老师不回家1 小时前
Vue 计算属性和监听器
前端·javascript·vue.js
前端啊龙1 小时前
用vue3封装丶高仿element-plus里面的日期联级选择器,日期选择器
前端·javascript·vue.js
一颗松鼠1 小时前
JavaScript 闭包是什么?简单到看完就理解!
开发语言·前端·javascript·ecmascript
小远yyds2 小时前
前端Web用户 token 持久化
开发语言·前端·javascript·vue.js
程序媛小果2 小时前
基于java+SpringBoot+Vue的宠物咖啡馆平台设计与实现
java·vue.js·spring boot
小光学长2 小时前
基于vue框架的的流浪宠物救助系统25128(程序+源码+数据库+调试部署+开发环境)系统界面在最后面。
数据库·vue.js·宠物
吕彬-前端2 小时前
使用vite+react+ts+Ant Design开发后台管理项目(五)
前端·javascript·react.js