Vue.js设计与实现阅读笔记--第一篇 框架设计概念

前言

这本书是个人认为对vue.js的原理解释比较好的一本书。就像书中所说

它从高层的设计角度探讨框架需要关注的问题,从而帮助读者更好地理解一些具体的实现为何要做出这样的选择。

前端是一个变化很快的领域,新的技术不断出现。这本书也可以作为现代前端框架设计的一个非常有价值的参考。

在前言中说了很多vue3的优点。在 2020 年 9 月 18 日,正式迎来了它的 3.0 版本。得益于 Vue.js 2 的设计经验,Vue.js 3.0 不仅带来了诸多新特性,还在框架设计与实现上做了很多创新。在一定程度上,我们可以认为 Vue.js 3.0“还清”了在 Vue.js 2 中欠下的技术债务。

  • Vue.js 3.0 在模块的拆分和设计上做得非常合理
  • Vue.js 3.0 在设计内建组件和模块时也花费了很多精力,配合构建工具以及 Tree-Shaking 机制,实现了内建能力的按需引入,从而实现了用户 bundle 的体积最小化
  • Vue.js 3.0 的扩展能力非常强

第一篇 框架设计概览

第 1 章 权衡的艺术

“框架设计里到处都体现了权衡的艺术。”

作为框架设计者,一定要对框架的定位和方向拥有全局的把控,这样才能做好后续的模块设计和拆分。

同样,作为学习者,我们在学习框架的时候,也应该从全局的角度对框架的设计拥有清晰的认知,否则很容易被细节困住,看不清全貌。

1.1 命令式和声明式

从范式上来看,视图层框架通常分为命令式和声明式,他们各自有各自的优点和缺点。

我们先来看看命令式框架和声明式框架的概念。早年间流行的 jQuery 就是典型的命令式框架。命令式框架的一大特点就是关注过程。例如,我们把下面这段话翻译成对应的代码:

01 - 获取 id 为 app 的 div 标签
02 - 它的文本内容为 hello world
03 - 为其绑定点击事件
04 - 当点击时弹出提示:ok

对应的代码为:

/**
 * 01 - 获取 id 为 app 的 div 标签
 * 02 - 它的文本内容为 hello world
 * 03 - 为其绑定点击事件
 * 04 - 当点击时弹出提示:ok
 */
$('#app')//获得div
.text('hello world')//设置文本内容
.on('click',()=> {
    alert('ok')
})//绑定事件

以上就是 jQuery 的代码示例,考虑到有些读者可能没有用过jQuery,因此我们再用原生 JavaScript 来实现同样的功能:

/**
 * 01 - 获取 id 为 app 的 div 标签
 * 02 - 它的文本内容为 hello world
 * 03 - 为其绑定点击事件
 * 04 - 当点击时弹出提示:ok
 * js版本
 */
const div=document.querySelector('#app')//获得div
div.innerText='hello world!'//设置文本内容
div.addEventListener('click',()=>{
    alert('ok')
})//绑定点击事件

自然语言描述能够与代码产生一一对应的关系,代码本身描述的是“做事的过程”,这符合我们的逻辑直觉。

那么,什么是声明式框架呢?声明式框架更加关注结果

 <div @click="() => alert('ok')">hello world</div>

这段类 HTML 的模板就是 Vue.js 实现如上功能的方式。可以看到,我们提供的是一个“结果”,至于如何实现这个“结果”,我们并不关心,这就像我们在告诉 Vue.js:“嘿,Vue.js,看到没,我要的就是一个div,文本内容是 hello world,它有个事件绑定,你帮我搞定
吧。”至于实现该“结果”的过程,则是由 Vue.js 帮我们完成的。换句话说,Vue.js 帮我们封装了过程。因此,我们能够猜到 Vue.js 的内部实现一定是命令式的,而暴露给用户的却更加声明式。

1.2 性能与可维护性的权衡

命令式和声明式各有优缺点,这里书中给了一个结论:

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

假设现在我们要将 div 标签的文本内容修改为 hello vue3,那么如何用命令式代码实现呢?

div.textContent = 'hello vue3' // 直接修改

这个修改就是一个最佳性能的做法了。

但是声明式代码不一定能做到这一点,因为它描述的是结果:

 <!-- 之前: -->
 <div @click="() => alert('ok')">hello world</div>
 <!-- 之后: -->
 <div @click="() => alert('ok')">hello vue3</div>

对于框架来说,为了实现最优的更新性能,它需要找到前后的差异并只更新变化的地方,但是最终完成这次更新的代码仍然是

div.textContent = 'hello vue3' // 直接修改

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

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

Vue.js选择声明式的原因也很简单,为了更好的维护。声明式代码展示的就是我们要的结

1.3 虚拟 DOM 的性能到底如何

再创建页面时

image-20230815095332745

可以看到,无论是纯 JavaScript 层面的计算,还是 DOM 层面的计算,其实两者差距不大

在更新页面时

image-20230815095449935

在更新页面时加上性能因素

image-20230815095521349

因此得到的结论:

image-20230815095540904

1.4 总结
  • 讨论了命令式和声明式这两种范式的差异,其中命令式更加关注过程,而声明式更加关注结果
  • 讨论了虚拟 DOM 的性能,并给出了一个公式:声明式的更新性能消耗 = 找出差异的性能消耗 + 直接修改的性能消耗。

第 2 章 框架设计的核心要素

2.1 提升用户的开发体验

例如当我们去挂载一个不存在的Dom节点的时候。

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

image-20230816083453235

就会出现上面的警告。这样就可以让我们更加清晰的定位到问题所在

在 Vue.js 的源码中,我们经常能够看到 warn 函数的调用

例如刚才的例子,就是由下面的函数进行打印的。

warn$1(`Failed to mount app: mount target selector "${container}" returned null.`);

为什么这里是warn$1呢,因为在vue源码中

exports.warn = warn$1;

有着这样一行代码

除了提供必要的警告信息外,还有很多其他方面可以作为切入口,进一步提升用户的开发体验。

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

image-20230816084001429

可以发现不是很直观,这个时候需要我们进行一些设置。

image-20230816084040480

勾选这个就可以了。

image-20230816084053142

2.2 控制框架代码的体积

如果我们去看 Vue.js 3 的源码,就会发现每一个 warn 函数的调用都会配合 DEV 常量的检查,例如:

if (__DEV__ && !res) ?
 warn(
 `Failed to mount app: mount target selector "$?container?"
returned null.`
 )
}

这样写的好处很简单,可以通过dev为true或者false,来缩短体积。需要注意的是:

它不会出现在最终产物中,这样就做到了在开发环境中为用户提供友好的警告信息的
同时,不会增加生产环境代码的体积。

2.3 框架要做到良好的 Tree-Shaking

Tree-Shaking 指的就是消除那些永远不会被执行的代码,也就是排除 dead code

2.4错误处理

假设我们开发了一个工具模块,代码如下:

// utils.js
export default {
    foo(fn) {
        fn && fn()
    }
}

最好的优化方案就是我们能为用户提供统一的错误处理接口

//utils.js


let handleError = null
export default {
    foo(fn) {
        callWithErrorHandling(fn)
    },
    //用户可以调用该函数注册同意的错误处理函数
    registerProtocolHandler(fn) {
        handleError = fn
    }
}

function callWithErrorHandling(fn) {
    try {
        fn && fn()
    } catch (e) {
        handleError(e)
    }
}

我们提供了 registerErrorHandler 函数,用户可以使用它注册错误处理程序,然后在 callWithErrorHandling 函数内部捕获错误后,把错误传递给用户注册的错误处理程序。

import utils from "./test"
utils.registerProtocolHandler((e)=>{
    console.log(e)
})

utils.foo(()=>{/*...*/})
utils.bar(()=>{/*...*/})

实际上,这就是 Vue.js 错误处理的原理,你可以在源码中搜索到callWithErrorHandling 函数,另外我们也可以注册统一的错误处理函数

import App from "App.vue"

const app = createApp(App);
app.config.errorHandler=()=>{
    //错误处理程序
}
2.5良好的 TypeScript 类型支持

在编写大型框架时,想要做到完善的 TS 类型支持很不容易,大家可以查看 Vue.js 源码中的 runtime-core/src/apiDefineComponent.ts 文件,整个文件里真正会在浏览器中运行的代码其实只有 3 行,但是全部的代码接近 200 行,其实这些代码都是在为类型支持服务。由此可见,框架想要做到完善的类型支持,需要付出相当大的努力。

第 3 章 Vue.js 3 的设计思路

3.1 声明式地描述 UI

我们要是编写一个前端页面就要涉及到下面的内容

  • DOM元素
  • 属性
  • 事件
  • 元素的层级结构

下面是我们用JavaScript对象来描述的

const title={
    //标签名称
    tag : "h1",
    //标签属性
    props:{
        onClick:handler
    },
    //子节点
    children : [
        {tag : "span"}
    ]
}

对应到vue.js里面就是

<h1 ?click="handler"><span></span></h1>

使用 JavaScript 对象描述 UI 更加灵活

使用 JavaScript 对象来描述 UI的方式,其实就是所谓的虚拟 DOM

3.2 初识渲染器

虚拟 DOM 是如何变成真实DOM 并渲染到浏览器页面中的呢?

这个时候就用到了渲染器,渲染器的作用就是,把虚拟DOM渲染成为真实的DOM

如下图所示

image-20230818100132802

这本书给的例子非常的不错,例如我们有如下虚拟DOM

const vnode={
    tag : "div",
    props : {
        onClick : () => {
            alert("hello")
        },
    },
    children : 'click me'
}

之后写一个渲染器,把上面这段虚拟DOM渲染为真实的DOM

function renderer(vnode,container){
    //使用vnode.tag作为标签名称来创建DOM元素
    const el=document.createElement(vnode.tag);
    //遍历vnode.props,将属性和事件都添加到DOM元素
    for (const key in vnode.props){
        if (/^no/.test(key)){
            //如果key以on开头,说明他是事件
            el.addEventListener(
                key.substr(2).toLowerCase(),//事件名称 例如onclick--->click
                vnode.props[key]//事件处理函数
            )
        }
    }
    //处理children
    if (typeof vnode.children === "string"){
        //如果children是字符串,说明他是元素的文本子节点
        el.appendChild(document.createTextNode(vnode.children))
    }else if(Array.isArray(vnode.children)){
        //递归调用render函数渲染子节点,使用当前元素el作为挂载点
        vnode.children.forEach(child=>renderer(child,el))
    }
    //将元素挂在到挂载点上
    container.appendChild(el)
}

这里的vnode就是虚拟DOM对象

container就是一个真实DOM元素,作为挂载点。

总的来说,renderer的实现思路,总体来说分为三大步

  1. 创建元素
  2. 为元素添加属性和事件
  3. 处理children
3.3 组件的本质

其实组件的返回值也是虚拟DOM,它代表组件要渲染的内容。

这个时候,我们就需要修改之前的渲染器

function renderer(vnode,container){
    

    if (typeof vnode.tag==="string"){
        mountElement(vnode,container)
    } else if(typeof vnode.tag==="function"){
        //说明是组件
        mountCompoent(vnode,container)
    }  
}

mountElement就是我们之前所写的内容。而mountCompoent就是我们需要对组件进行处理的函数

function mountCompoent(vnode, container) {
    const subtree = vnode.tag();//返回值是虚拟DOM
    renderer(subtree,container)
}

因此我们可以举一反三,如果说,使用一个对象来表示组件呢?

const MYComponent={
    render(){
        return{
            tag:'div',
            props:{
                onClick : () => {
                    alert('hello')
                }
            },
            children : 'click me'
        }
    }
}
else if (typeof vnode.tag === 'object')

这个时候只需要我们简单的修改mountCompoent这个函数

function mountCompoent(vnode, container) {
    // vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM
    const subtree = vnode.tag.render();//返回值是虚拟DOM
    renderer(subtree,container)
}
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值