Skip to content

Vue.js 设计与实现阅读笔记(三)第3章-Vue.js 3 的设计思路

3.1 声明式的描述UI

Vue3是一个声明式的框架,对于一个DOM树来说,它用声明式,可以这么写:

html
<div @click="handler"><span></span></div>
<div @click="handler"><span></span></div>

不管是dom元素的创建,还是dom上的事件绑定,都不需要我们手动用命令式来写。

除了上面这种模板写法,Vue3还支持用Javascript对象来描述:

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

那对象的写法有什么好处呢?可以想象如下场景:

html
<h1 v-if="tag === 'h1'"></h1>
<h2 v-else-if="tag === 'h2'"></h2>
<h3 v-else-if="tag === 'h3'"></h3>
<h1 v-if="tag === 'h1'"></h1>
<h2 v-else-if="tag === 'h2'"></h2>
<h3 v-else-if="tag === 'h3'"></h3>

如果用对象的写法就非常简单了:

js
const div = {
  tag: tag,
}
const div = {
  tag: tag,
}

3.2 初识渲染器

渲染器的作用就是把虚拟 DOM 渲染为真实 DOM, 如下图

image.png

js
renderer(vnode, document.body)
renderer(vnode, document.body)

渲染器 renderer 的实现思路,总体来说 分为三步。

  • 创建元素:把 vnode.tag 作为标签名称来创建 DOM 元素。

  • 为元素添加属性和事件:遍历 vnode.props 对象,如果 key 以 on 字符开头,说明它是一个事件,把字符 on 截取掉后再调用 toLowerCase 函数将事件名称小写化,最终得到合法的事件名 称,例如 onClick 会变成 click,最后调用 addEventListener 绑定事件处理函数。

  • 处理 children:如果 children 是一个数组,就递归地调用 renderer 继续渲染,注意,此时我们要把刚刚创建的元素作为 挂载点(父节点);如果 children 是字符串,则使用 createTextNode 函数创建一个文本节点,并将其添加到新创建 的元素内。

简单来讲:在上面的代码中,renderer 会递归遍历Vnode, 根据vnode创建对应的dom节点,最后append到document.body上。对于渲染器来说,它需要精确地找到 vnode 对象的变更点并且只 更新变更的内容。就上例来说,渲染器应该只更新元素的文本内容, 而不需要再走一遍完整的创建元素的流程。

3.3 组件的本质

js
const MyComponent = {
   render() {
      return {
        tag: 'div'
      }
   }
}
const MyComponent = {
   render() {
      return {
        tag: 'div'
      }
   }
}

可以看到,组件的返回值也是虚拟 DOM,它代表组件要渲染的内 容。搞清楚了组件的本质,我们就可以定义用虚拟 DOM 来描述组件。很简单,我们可以让虚拟 DOM 对象中的 tag 属性来存储组件:

js
const vnode = {
   tag: MyComponent
}
const vnode = {
   tag: MyComponent
}

渲染器在递归渲染的过程中,如果遇到组件vnode, 就需要调用它内部的render, 获取subtree,并进一步递归其中的子vnode。

3.4 模板的工作原理

简单来讲,就是需要编译器编译成渲染函数,后续章节会讲~

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

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

html
<div id="foo" :class="cls"></div>
<div id="foo" :class="cls"></div>

根据上文的介绍,我们知道编译器会把这段代码编译成渲染函数:

js
render() {
  return {
    tag: 'div',
    props: {
      id: 'foo',
      class: cls
    },
    patchFlags: 1 // 假设数字 1 代表 class 是动态的
  }
}
render() {
  return {
    tag: 'div',
    props: {
      id: 'foo',
      class: cls
    },
    patchFlags: 1 // 假设数字 1 代表 class 是动态的
  }
}

在生成的虚拟 DOM 对象中多出了一个 patchFlags 属性,我们假设数字 1 代表“ class 是动态的”,这样渲 染器看到这个标志时就知道:“哦,原来只有 class 属性会发生改 变。”对于渲染器来说,就相当于省去了寻找变更点的工作量,性能自 然就提升了。 通过这个例子,我们了解到编译器和渲染器之间是存在信息交流 的,它们互相配合使得性能进一步提升,而它们之间交流的媒介就是 虚拟 DOM 对象。