《Vue.js设计与实现》学习笔记 | Vue.js 3 的设计思路

本文同样发布在我的个人博客网站:【still-soda 的个人博客 | 《Vue.js设计与实现》学习笔记 | Vue.js 3 的设计思路】,欢迎来访!

声明式地描述UI

Vue.js 3 是一个声明式的 UI 框架,意思是说用户在使用 Vue.js 3 开发页面时是声明式地描述 UI 的。

我们知道,在编写前端页面的时候主要涉及 4 点内容,它们包含:DOM元素、元素属性、事件(如 clickkeydown)和元素的层级结构(DOM 树的结构)。

使用模板语法

为了实现声明式的特性, Vue.js 创造了一套名为 模板(template) 的语法,其长相酷似 HTML,但是又在 HTML 的基础上定义了一些特别的描述性语法。例如:

  • 使用 :v-bind 来描述 动态 绑定的属性;
  • 使用 @v-on 来描述事件;

使用 JS 对象

在 Vue.js 中,除了可以使用模板语法对 UI 进行描述,我们也可以使用 JavaScript 对象对 UI 进行描述:

const title = {
    // 标签名字
    tag: 'h1',	
    // 标签属性
    props: {	
        onClick: handler
    },
    // 标签的子元素
    children: [
        { tag: 'span' }
    ]
}

而这种对象,其实就是虚拟DOM。 它们等同于以下的模板语法:

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

这两者的优势也显而易见,前者更加灵活,后者更加直观。书中举了个很好的例子,假设我们要根据一个名为 level 数字变量动态渲染 h1 ~ h6 标签,如果我们使用模板语法进行编写,代码是长这样的:

<h1 v-if="level === 1"></h1>
<h2 v-else-if="level === 2"></h2>
<h3 v-else-if="level === 3"></h3>
<h4 v-else-if="level === 4"></h4>
<h5 v-else-if="level === 5"></h5>
<h6 v-else-if="level === 6"></h6>

这看起来非常繁琐!如果此时我们用 JavaScript 对象的形式进行编写,那么就会显得非常简单:

let level = 6; // 1 ~ 6

const title = {
    tag: `h${level}` // 将 'h' 和 level 拼接就是标签名啦
};

实际上,模板语法只是帮助开发者描述 UI 的工具而已,其类似于 JSX,并不等同于 HTML。所有模板在运行前都会被编译器编译成 JS 代码,并在运行时交由渲染器渲染为真实 DOM。

使用 h 函数

我们都知道 Vue.js 中有个名为 h 的函数,大多数人第一次见到这个函数的时候可能都会感到迷惑,不知道它是干啥的 —— 其实这个函数是为了帮助我们更加方便地创造虚拟 DOM 而存在的。

譬如对于这段代码:

const title = {
    tag: 'h1',	
    props: {	
        onClick: handler
    },
    children: '这是标题文本'
}

h 函数的返回值就是这样的对象。如果我们借助 h 函数进行改写,代码就是长这样的:

const title = h('h1', { onClick: handler }, '这是标题文本');

差别显而易见,这个函数帮助开发者省去了书写属性名的功夫,让手写虚拟 DOM 变得更加简单。

在 Vue.js 组件中,我们可以在渲染函数中编写虚拟 DOM:

import { h } from 'vue';

export default {
    render() {
        return h('h1', {}, 'Hello world!');
    }
}

那么这个组件的渲染内容就通过渲染函数描述出来了,而且并不需要借助模板语法。


初识渲染器

渲染器的作用就是把虚拟 DOM 渲染为真实 DOM

请添加图片描述
假设我们有如下虚拟 DOM:

const vnode = {
    tag: 'div',
    props: {
        onClick: handler
    },
    children: [
        { tag: 'span', children: 'hello world' }
    ]
}

我们应该如何把它渲染为真实 DOM 呢?要实现自动渲染,我们要借助一个名为 renderer 的渲染函数来实现。renderer 函数接收两个参数,第一个参数为一个虚拟 DOM 对象,第二个参数为渲染结果要挂载的目标 DOM。

这里假设我们要将上述虚拟 DOM 渲染并挂载在 document.body 节点下:

renderer(vnode, document.body);

实现简单的渲染器函数

⭐接下来实现一个最简单 renderer 函数:

/**
 * @param vnode 虚拟DOM
 * @param container 挂载点
 */
function renderer(vnode, container) {
    // 创建vnode的tag标签对应的元素,此处为div
    const el = document.createElement(vnode.tag);
    // 遍历vnode的props属性,将每个属性和事件都添加到元素中
    for (const attr in vnode.props) {
        // 以on开头的是事件
        // 可以把头两个字母去掉,转化为小写,并添加对应的监听器
        if (attr.startsWith('on')) {
            el.addEvenListener(
            	attr.subStr(2).toLowerCase(),
                vnode.props[attr]
            );
            continue;
        }
        // 其他的就是普通属性
        el[attr] = vnode.props[attr];
    }
    // 处理children
    if (typeof vnode.children === 'string') {
        // 如果children是字符串,那么创建文本节点
        el.appendChild(document.createTextNode(vnode.children));
    } else if (Array.isArray(document.children)) {
        // 如果children是数组,那么遍历每个孩子并递归渲染
        vnode.children.forEach(child => renderer(child, el));
    }
    // 将el挂载到container上
    container.appendChild(el);
}

上述关于渲染器的代码其实并不复杂。主要就是四个步骤:

  1. 创建 tag 属性对应的节点
  2. 循环创建事件监听,并对元素属性进行赋值
  3. 利用递归分类处理子元素
  4. 将创建的节点挂载到挂载点上

但是渲染器的精髓其实在于更新节点的阶段,如果我们对 vnode 做一些小小的修改,渲染器需要精确地找到变更处并更新变更的内容。这里涉及到 Diff 算法的应用,书中的后续章节将会重点讲解。


组件的本质

重要特性

如果你有学习过 React,那你一定对它的函数式组件印象深刻。在函数组件之前,React 使用曾经提倡使用类组件。为什么这两种方式都能实现组件呢?因为它们都具备闭包和可复用这两个重要特性。

Vue.js 中的组件本质上是对模板的复用,这就要求我们的组件必须是能够被重复创建且能够相互独立存在的。我们在使用虚拟 DOM 形式编写组件的时候,会将 UI 编写在 render 方法中,那么每当这个方法被调用的时候,它就会产生一组虚拟 DOM,想要多少组,你就调用多少次。这样就实现了组件的重复创建。并且由于函数的闭包特性,产生的每组虚拟 DOM 之间都相互独立、互不相干,这样就实现了组件间的独立性。

我们都知道,虚拟 DOM 属于树形数据结构,因此一组虚拟 DOM 是可以挂载在另外一组虚拟 DOM 上的,那么组件就可以通过这种方式发挥作用。

如何在虚拟DOM中表示组件

在上述 vnode 的示例中,tag 属性都表示了对应元素的标签名。但是对于组件而言,并没有一个明确的标签名,因此,我们可以选择使用该组件的 render 函数来代替元素的标签字符串。就像这样:

const vnode = {
    // tag不再是字符串,而是一个返回虚拟 DOM 的函数
    tag: () => {	
        return { tag: 'h1' }
    },
    props: { onClick: handler }
}

实现组件渲染函数

上面实现的 renderer 实际上是元素的渲染函数,为了实现组件的渲染函数,我们需要对其进行重构:

function renderer(vnode, container) {
    if (typeof vnode.tag === 'string') {
        // 字符串代表该虚拟DOM是一个元素
        mountElement(vnode, container);
    } else if (typeof vnode.tag === 'function') {
        // 函数代表该虚拟DOM是一个组件
        mountComponent(vnode, container);
    }
}

其中 mountElement 函数的实现与最开始实现的 renderer 函数是一样的,而接下来我们将给出 mountComponent 函数的实现:

function mountComponent(vnode, container) {
    // 因为vnode.tag是一个函数,所以直接调用它获得虚拟 DOM 即可
    const subtree = vnode.tag();
    // 递归调用renderer进行渲染
    renderer(subtree, container);
}

可以看到,非常简单。因为我们已经知道 vnode.tag 是一个返回虚拟 DOM 的函数,所以我们直接调用它,获取返回的对象,然后继续使用 renderer 函数进行递归渲染即可。

我们也可以使用对象来创建组件,Vue.js 的有状态组件就是使用对象进行实现的。

模板的工作原理

上面已经提到,模板在编译阶段会被编译器编译为 JS 代码,然后再在运行阶段交由渲染器渲染为真实 DOM。

对于下属 .vue 格式的代码:

<template>
	<div @click="handler">
        Click me
    </div>
</template>

<script>
export default {
    data() { /* ... */ },
    methods: {
        handler: () => { /* ... */ }
    }
}
</script>

在编译阶段交由编译器编译后会产生以下 JS 代码:

export default {
    data() { /* ... */ },
    methods: {
        handler: () => { /* ... */ }
    },
    // 多出来的内容
    render() {
        return h('div', { onClick: handler }, 'Click me');
    }
}

很容易可以发现前后的差异:<template> 不见了,多出来了一个 render 函数,而 render 中的内容正是原先 <template> 中的内容经过编译后产生的虚拟 DOM 形式的代码。

我们可以用一张图来直观地了解这个过程:

请添加图片描述


总结

本章主要讲解了 Vue.js 3 如何通过声明式描述 UI,使开发者可以使用模板语法和 JavaScript 对象来定义 DOM 元素、属性和事件。

然后,讲解了如何使用 JavaScript 对象描述虚拟 DOM,使代码更加灵活,以及如何使用 h 函数简化虚拟 DOM 的创建,使手写虚拟 DOM 更加简便。

最后讲解了渲染器的作用,并指出模板在编译阶段被编译为 JavaScript 代码,在运行时由渲染器渲染为真实 DOM。

  • 28
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值