首页 最新 热门 推荐

  • 首页
  • 最新
  • 热门
  • 推荐

阅读《Vue.js设计与实现》 -- 03

  • 25-04-23 06:21
  • 3690
  • 6524
juejin.cn

接上一篇:阅读《Vue.js设计与实现》 -- 02

第三章

本章菜鸟读完,感觉就是讲了两个事:

  • h函数就是一个辅助创建虚拟 DOM 的工具函数,让我们创建虚拟DOM更加简单,拨开了第一章中提到的虚拟dom的神秘面纱,其实虚拟dom就是一个使用 JavaScript 对象来描述 UI 的方式

  • 渲染器(render函数)是怎么从能解析对象到能解析组件的过程

声明式地描述 UI

编写前端界面会涉及的内容

image.png

vue3是这样完成声明式的

image.png

上述对应的 vue.js 类似:

html
代码解读
复制代码
<div @click="handler"><span>span>div>

除了上述的方法,还能用 JavaScript 对象来描述 UI!

js
代码解读
复制代码
const title = { // 标签名称 tag: 'div', // 属性 props: { onClick: handler }, // 子节点 children: [ { tag: 'span' } ] }

使用模板和 JavaScript 对象描述 UI 有何不同呢?

答案是:使用 JavaScript 对象描述 UI 更加灵活。即:可以通过循环、变量、判断等描述UI,不会像模板那样全部枚举出来!

例如:我们要表示一个标题,根据标题级别的不同,会分别采用 h1~h6 这几个标签。

用 JavaScript 对象描述可以写为:

js
代码解读
复制代码
// h 标签的级别 let level = 3; const title = { tag: `h${level}` // h3 标签 };

如果是模板就需要:

html
代码解读
复制代码
<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 对象来描述 UI,就是虚拟DOM!

在 vue.js 组件中,手写的渲染函数就是使用虚拟 DOM 来描述 UI 的:

js
代码解读
复制代码
import { h } from 'vue' export default { render() { return h('h1', { onClick: handler }) // 虚拟 DOM } }

这里h函数内部做了处理,传进去的东西,会被处理成js描述对象(虚拟dom),然后交给render函数去渲染UI!

如果上面的代码不用h函数,而是用js对象的话,那么其复杂度比较高(有子节点就更复杂):

js
代码解读
复制代码
export default { render() { return { tag: "h1", props: { onClick: handler } }; } };

h函数的目的就是让我们编写虚拟dom更加容易!h函数就是一个辅助创建虚拟 DOM 的工具函数,仅此而已。

初识渲染器

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

image.png

那怎么把js对象渲染成真实DOM?编写如下代码:

js
代码解读
复制代码
const vnode = { // 标签名称 tag: "h1", // 属性 props: { onClick: () => { alert("你好"); } }, // 子节点 children: "Hello World" }; function renderer(vnode, container) { // 使用 vnode.tag 作为标签名称创建 DOM 元素 const el = document.createElement(vnode.tag); // 遍历 vnode.props,将属性、事件添加到 DOM 元素 for (const key in vnode.props) { if (/^on/.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)) { // 递归地调用 renderer 函数渲染子节点,使用当前元素 el 作为挂载点 vnode.children.forEach((child) => renderer(child, el)); } // 将元素添加到挂载点下 container.appendChild(el); }

这里的 renderer 函数接收如下两个参数:

  • vnode:虚拟 DOM 对象
  • container:一个真实 DOM 元素,作为挂载点,渲染器会把虚拟 DOM 渲染到该挂载点下

运行函数

js
代码解读
复制代码
let home = document.querySelector("#content"); renderer(vnode, home)

结果

image.png

但是渲染器的作用并不只是渲染而已,更重要的是发现元素的变化,并将对应的地方重新渲染,而不需要再走一遍完整的创建元素的流程! --> 后续会讲,暂时没深入

组件的本质

虚拟 DOM 除了能够描述真实 DOM 之外,还能够描述组件。因为组件其实就是一组真实 DOM 的集合体,这组 DOM 元素就是组件要渲染的内容,因此我们可以定义一个函数来代表组件,而函数的返回值就代表组件要渲染的内容!

js
代码解读
复制代码
const MyComponent = function () { return { tag: "div", props: { onClick: () => alert("hello") }, children: "click me" }; };

注意

返回的是虚拟DOM对象 --> 下一节才会讲怎么渲染模板!

然后可以将 tag 设置为 MyComponent,只不过此时的 tag 属性不是标签名称,而是组件函数。为了能够渲染组件,需要渲染器的支持,修改 renderer 函数:

js
代码解读
复制代码
function renderer(vnode, container) {   if (typeof vnode.tag === "string") {     // 说明 vnode 描述的是标签元素     mountElement(vnode, container); // --> 之前写的 renderer 函数   } else if (typeof vnode.tag === "function") {     // 说明 vnode 描述的是组件     mountComponent(vnode, container);   } }

现在要做的就是写一个mountComponent方法了:

js
代码解读
复制代码
function mountComponent(vnode, container) { // 调用组件函数,获取组件要渲染的内容(虚拟 DOM) const subtree = vnode.tag(); // 递归地调用 renderer 渲染 subtree renderer(subtree, container); }

一定要函数吗?

组件一定得是函数吗?我们完全可以使用一个 JavaScript 对象来表达组件:

js
代码解读
复制代码
const MyComponent = { render() { return { tag: "div", props: { onClick: () => alert("hello") }, children: "click me" }; } };

这里使用一个对象来代表组件,该对象有一个函数,叫作render,其返回值代表组件要渲染的内容。为了完成适配返回对象的组件的渲染,需要修改 renderer 渲染器以及 mountComponent 函数。

js
代码解读
复制代码
function renderer(vnode, container) { if (typeof vnode.tag === "string") { // 没变 mountElement(vnode, container); } else if (typeof vnode.tag === "object") { // 使用对象而不是函数来表达组件 mountComponent(vnode, container); } } function mountComponent(vnode, container) { // vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM) const subtree = vnode.tag.render(); // 递归地调用 renderer 渲染 subtree renderer(subtree, container); }

模板的工作原理

无论是手写虚拟 DOM(渲染函数)还是使用模板,都属于声明式地描述 UI,并且 Vue.js 同时支持这两种描述 UI 的方式。

我们讲解了虚拟 DOM 是如何渲染成真实 DOM 的,那么模板是如何工作的呢?这就要提到 Vue.js 框架中的另外一个重要组成部分:编译器。

编译器和渲染器一样,只是一段程序而已,不过它们的工作内容不同。编译器的作用其实就是将模板编译为渲染函数,例如给出如下模板:

html
代码解读
复制代码
<div @click="handler">click mediv>

对于编译器来说,模板就是一个普通的字符串,它会分析该字符串并生成一个功能与之相同的渲染函数:

js
代码解读
复制代码
render(){ return h('div', { onClick: handler }, 'click me') }

我们熟悉的.vue文件:

js
代码解读
复制代码
<script> export default { data() { /* ... */ }, methods: { handler: () => { /* ... */ } } }; script>

其中 template 标签里的内容就是模板内容,编译器会把模板内容编译成渲染函数并添加到 script 标签块的组件对象上,所以最终在浏览器里运行的代码就是:

js
代码解读
复制代码
export default { data() { /* ... */ }, methods: { handler: () => { /* ... */ } }, render() { return h("div", { onClick: handler }, "click me"); } };

至于是咋编辑成这样的,这里暂时没说!

无论是使用模板还是直接手写渲染函数,对于一个组件来说,它要渲染的内容最终都是通过渲染函数产生的,然后渲染器再把渲染函数返回的虚拟 DOM 渲染为真实 DOM,这就是模板的工作原理,也是 Vue.js 渲染页面的流程。

Vue.js 是各个模块组成的有机整体

如前所述,组件的实现依赖于渲染器,模板的编译依赖于编译器,并且编译后生成的代码是根据渲染器和虚拟 DOM 的设计决定的,因此 Vue.js 的各个模块之间是互相关联、互相制约的,共同构成一个整体。在学习 Vue.js 原理的时候,应该把各个模块结合到一起去学习。

这里我们以编译器和渲染器这两个非常关键的模块为例,看看它们是如何配合工作,并实现性能提升的。

假设有如下模板:

html
代码解读
复制代码
<div id="foo" :class="cls">div>

编译器会把这段代码编译成渲染函数:

js
代码解读
复制代码
render() { // 为了效果更加直观,这里没有使用 h 函数,而是直接采用了虚拟 DOM 对象 // 下面的代码等价于:return h('div', { id: 'foo', class: cls }) return { tag: 'div', props: { id: 'foo', class: cls } } }

patchFlag 静态标记

可以发现,在这段代码中,cls 是一个变量,它可能会发生变化。我们知道渲染器的作用之一就是寻找并且只更新变化的内容,所以当变量 cls 的值发生变化时,渲染器会自行寻找变更点。对于渲染器来说,这个“寻找”的过程需要花费一些力气。那么从编译器的视角来看,它能否知道哪些内容会发生变化呢?如果编译器有能力分析动态内容,并在编译阶段把这些信息提取出来,然后直接交给渲染器,这样渲染器不就不需要花费大力气去寻找变更点了吗?这是个好想法并且能够实现。Vue.js 的模板是有特点的,拿上面的模板来说,我们一眼就能看出其中 id="foo" 是永远不会变化的,而 :class="cls"是一个 v-bind 绑定,它是可能发生变化的。所以编译器能识别出哪些是静态属性,哪些是动态属性,在生成代码的时候完全可以附带这些信息:

js
代码解读
复制代码
render() { return { tag: "div", props: { id: "foo", class: cls }, patchFlags: 1 // 假设数字 1 代表 class 是动态的 }; }

如上面的代码所示,在生成的虚拟 DOM 对象中多出了一个 patchFlags 属性,我们假设数字 1 代表“ class 是动态的”,这样渲染器看到这个标志时就知道:“只有 class 属性会发生改变。”对于渲染器来说,就相当于省去了寻找变更点的工作量,性能自然就提升了。

vue底层:通过位运算符,做枚举,每一个枚举就是一个状态,通过状态去更新!

注:本文转载自juejin.cn的PBitW的文章"https://juejin.cn/post/7482422034506514470"。版权归原作者所有,此博客不拥有其著作权,亦不承担相应法律责任。如有侵权,请联系我们删除。
复制链接
复制链接
相关推荐
发表评论
登录后才能发表评论和回复 注册

/ 登录

评论记录:

未查询到任何数据!
回复评论:

分类栏目

后端 (14832) 前端 (14280) 移动开发 (3760) 编程语言 (3851) Java (3904) Python (3298) 人工智能 (10119) AIGC (2810) 大数据 (3499) 数据库 (3945) 数据结构与算法 (3757) 音视频 (2669) 云原生 (3145) 云平台 (2965) 前沿技术 (2993) 开源 (2160) 小程序 (2860) 运维 (2533) 服务器 (2698) 操作系统 (2325) 硬件开发 (2492) 嵌入式 (2955) 微软技术 (2769) 软件工程 (2056) 测试 (2865) 网络空间安全 (2948) 网络与通信 (2797) 用户体验设计 (2592) 学习和成长 (2593) 搜索 (2744) 开发工具 (7108) 游戏 (2829) HarmonyOS (2935) 区块链 (2782) 数学 (3112) 3C硬件 (2759) 资讯 (2909) Android (4709) iOS (1850) 代码人生 (3043) 阅读 (2841)

热门文章

143
阅读
关于我们 隐私政策 免责声明 联系我们
Copyright © 2020-2024 蚁人论坛 (iYenn.com) All Rights Reserved.
Scroll to Top