vue.js设计与实现笔记1 框架设计基础

框架设计基础

1.框架设计基础

2.虚拟Dom本身的性能

虚拟dom与innerHTML

​ 这里需要纠正一个常见的误区:虚拟Dom的性能比原生Dom更好。但其实本质上虚拟Dom只是实际Dom的一个映射形式,理论上虚拟Dom的性能就不可能比原生操作Dom更高了。其实通常说的虚拟Dom性能更好是指基于虚拟Dom以及diff算法的渲染模式比直接重新渲染整个区域的性能更好。对于声明式代码,更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗,那么虚拟Dom的存在,是服务于降低性能差异而存在的。

​ 虚拟Dom和直接用innerHTML的差异如下:
在这里插入图片描述

​ 可以看出,虚拟Dom最大的优势在于让更新过程从“销毁所有旧Dom,再新建所有新Dom”变成“进行必要的Dom更新”,这是一个质的飞跃,因为浏览器渲染Dom元素本身的开销是巨大的。

一点题外话:

​ 其实自己手动渲染也是可以的,像笔者曾经在一个练手项目中,如果某个频繁切换的视图使用直接修改innerHTML这个方法的话,每次切换这个视图都会陷入白屏一段时间,这种其实就是暴力地插入innerHTML后,由于视图需要整个重新渲染带来的白屏。那么当时每一次视图切换的时候,innerHTML改变的只有部分的Text和部分元素的属性,那么我其实可以用

document.createRange().createContextualFragment(innerHtmlToChange)

来生成一个新的视图Dom,然后进行新老的比对

const newElement = Array.from(newDom.querySelectorAll('*'));
const curElement = Array.from(this._parentElement.querySelectorAll('*'));

newElement.forEach((newEl,index) => {
const curEl = curElement[index];

// 当不同的节点为文本节点
if(!newEl.isEqualNode(curEl) &&
newEl.firstChild?.nodeValue.trim()!='') {
curEl.textContent = newEl.textContent;
}

// 当不同节点的差异发生在属性上
if(!newEl.isEqualNode(curEl)) {
Array.from(newEl.attributes).forEach(attr => {
curEl.setAttribute(attr.name,attr.value);
})
}
})

3.运行时与编译时框架

当我在纯运行时框架中,我将虚拟Dom写成以下形式,并假定渲染函数只支持这一种形式:

01  const obj = {
02   tag: 'div',
03   children: [
04     { tag: 'span', children: 'hello world' }
05   ]
06 }

那么这种树形结构其实很多时候并不利于实际开发,那么如果用户可以书写类似HTML的标签形语言,再转成虚拟Dom,框架本身会友好很多,这其实就是编译时框架:

在这里插入图片描述

vue3本身是运行时+编译时的框架,它既可以直接使用模板语言去渲染组件,也可以直接写虚拟Dom,直接交由Render函数去渲染。

2.框架设计的核心要素

对于一个框架来说,功能的实现并不是终点,实际上,框架设计的时候需要考虑很多问题,诸如:我们的框架应该给用户提供哪些构建产物?产物的模块格式如何?当用户没有以预期的方式使用框架时,是否应该打印合适的警告信息从而提供更好的开发体验,让用户快速定位问题?开发版本的构建和生产版本的构建有何区别?热更新(hot module replacement,HMR)需要框架层面的支持,我们是否也应该考虑?另外,当你的框架提供了多个功能,而用户只需要其中几个功能时,用户能否选择关闭其他功能从而减少最终资源的打包体积?

1.正确的打印信息

(1)假如我挂在一个不存在的节点:

 createApp(App).mount('#not-exist')

若是原生的JavaScript提示信息,大概率会得到一个不清晰的提示信息,例如 Uncaught TypeError: Cannot read property ‘xxx’ of null,而根据此信息我们很难知道问题出在哪里。

而通过vue.js内部的处理,输出信息会比较明确:

在这里插入图片描述

友好的警告信息对于开发过程中是至关重要的,警告信息应该尽可能地帮助开发者定位到发生错误的地方,在vue.js内部会做很多复杂的处理,但本质还是一系列console.warn()的输出。

(2)当我在控制台打印一个ref输出的时候:

01 const count = ref(0)
02 console.log(count)

可能得到的结果并不直观:

在这里插入图片描述

在 Vue.js 3 的源码中,你可以搜索到名为 initCustomFormatter 的函数,该函数就是用来在开发环境下初始化自定义 formatter 的。以 Chrome 为例,我们可以打开 DevTools 的设置,然后勾选“Console”→“Enable custom formatters”选项

在这里插入图片描述

那么现在输出会变得非常直观:

外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传

2.尽可能地减少体积

在vue中,每一个warn信息都会配合__DEV__常量 :

01 if (__DEV__ && !res) {
02   warn(
03     `Failed to mount app: mount target selector "${container}" returned null.`
04   )
05 }

Vue.js 使用 rollup.js 对项目进行构建,这里的 __DEV__常量实际上是通过rollup.js 的插件配置来预定义的,其功能类似于 webpack 中的 DefinePlugin 插件。那么vue输出资源时,会输出两个版本,一个用于开发环境,如vue.global.js,另一个用于生产环境,如 vue.global.prod.js。

那么上边的代码在构建成生产环境的时候必然永远都不会执行,这个部分被称为dead code,在生产环境的包中,这个代码是永远不会出现的。

3.tree shaking

所谓tree shaking,是指消除掉那些永远不会删除的代码,也就是上边的dead code。想要实现tree-shaking,模块必须是ESM,因为tree-shaking依赖于ESM的静态结构。

tree-shaking的第二个关键点就是,若一个函数会产生“副作用”,那么不能将这个函数移除。副作用函数指的是调用函数会对外部产生影响的函数,例如Array.prototype.sort就是一个典型的副作用函数。有时候副作用是难以静态判断的:例如当我们读一个对象的值,看起来不会产生什么影响,但是当我对对象用Proxy进行拦截get夹子,那么get中可能存在副作用。

那么如何删除dead code呢? rollup.js会提供一个机制:特定的魔法注释——当在函数前面打上/*#__PURE__*/,指明这一个函数是没有副作用的函数。一般来讲,副作用函数都是在模块内函数的顶级调用。

顶级调用的形式如下:

01 foo() // 顶级调用
02
03 function bar() {
04   foo() // 函数内调用
05 }

当然,这个注释不仅仅适用于函数,也适用于任何语句。

4.框架应该怎么构建输出的产物?

vue本身会为生产环境/开发环境输出不一样的包,当然在不同的使用场景下,也会输出不一样的产物。

以在HTML中引入vue为例:

01 <body>
02   <script src="/path/to/vue.js"></script>
03   <script>
04   const { createApp } = Vue
05   // ...
06   </script>
07 </body>

要实现这种场景,vue会构建成IIFE输出,即可以暴露出全局的vue对象:

01 var Vue = (function(exports){
02   // ...
03   exports.createApp = createApp;
04   // ...
05   return exports
06 }({}))

在 rollup.js 中,我们可以通过配置 format: ‘iife’ 来输出这种形式的资源:

01 // rollup.config.js
02 const config = {
03   input: 'input.js',
04   output: {
05     file: 'output.js',
06     format: 'iife' // 指定模块形式
07   }
08 }
09
10 export default config

5.特性开关

假设我们给用户A、B、C三个特性,并给他们A、B、C三个变量以通过真值来控制特性是否使用,那么这三个变量我们称为特性开关。

特性开关和__DEV__一样,本质上是利用 rollup.js 的预定义常量插件来实现。

3.vue的一些设计思路

1.初识渲染器

渲染器就是将我们写的虚拟dom给渲染成真实dom的一个中间层:

在这里插入图片描述

假设有如下的虚拟dom:

01 const vnode = {
02   tag: 'div',
03   props: {
04     onClick: () => alert('hello')
05   },
06   children: 'click me'
07 }

那么渲染器可以为:

01 function renderer(vnode, container) {
02   // 使用 vnode.tag 作为标签名称创建 DOM 元素
03   const el = document.createElement(vnode.tag)
04   // 遍历 vnode.props,将属性、事件添加到 DOM 元素
05   for (const key in vnode.props) {
06     if (/^on/.test(key)) {
07       // 如果 key 以 on 开头,说明它是事件
08       el.addEventListener(
09         key.substr(2).toLowerCase(), // 事件名称 onClick ---> click
10         vnode.props[key] // 事件处理函数
11       )
12     }
13   }
14
15   // 处理 children
16   if (typeof vnode.children === 'string') {
17     // 如果 children 是字符串,说明它是元素的文本子节点
18     el.appendChild(document.createTextNode(vnode.children))
19   } else if (Array.isArray(vnode.children)) {
20     // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点
21     vnode.children.forEach(child => renderer(child, el))
22   }
23
24   // 将元素添加到挂载点下
25   container.appendChild(el)
26 }

其中vnode是输入的虚拟dom对象,而container为挂在虚拟dom的父元素。

2.组件的本质

本质上:组件就是一组DOM元素的封装。

这里是一个组件的例子:

01 const MyComponent = function () {
02   return {
03     tag: 'div',
04     props: {
05       onClick: () => alert('hello')
06     },
07     children: 'click me'
08   }
09 }

可以看到,组件返回的也是一组虚拟dom,那么虚拟dom在描述组件时,可以让虚拟dom的tag来存储组件函数:

01 const vnode = {
02   tag: MyComponent
03 }

那么这个时候我们需要修改一下上边的Renderer函数,我们需要根据tag的类型去分别做处理:

01 function renderer(vnode, container) {
02   if (typeof vnode.tag === 'string') {
03     // 说明 vnode 描述的是标签元素
04     mountElement(vnode, container)
05   } else if (typeof vnode.tag === 'function') {
06     // 说明 vnode 描述的是组件
07     mountComponent(vnode, container)
08   }
09 }

那么mountComponent也是需要借助renderer函数来进行实现:

01 function mountComponent(vnode, container) {
02   // 调用组件函数,获取组件要渲染的内容(虚拟 DOM)
03   const subtree = vnode.tag()
04   // 递归地调用 renderer 渲染 subtree
05   renderer(subtree, container)

3.模板的工作原理

上面提到的renderer函数本身是面向虚拟dom这一js对象的,vue的声明式UI中还有一个更常用的:模板,模板本身更接近与前端开发常写的html。那么想要让模板编程真实dom,必然中间需要将模板转换为虚拟dom,才能进行渲染。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值