Vue.js 3 的设计思路

1,192 阅读5分钟

一、前言

我们从全局视角了解Vue.js 3 的设计思路、工作机制及其重要的组成部分。我们可以把这些组成部分当作独立的功能模块,看看它们之间是如何相互配合的。

  • UI的两种形式:模板字符串和虚拟DOM
  • Vue.js框架的两个重要组成部分:编译器和渲染器

二、本章内容

2.1 声明式地描述UI

Vue.js 3 是一个声明式的UI框架。设计一个这样的框架,我们需要了解编写前端页面都涉及哪些内容?具体如下:

  • DOM元素:是div标签还是a标签。
  • 属性:如a标签的href属性,再如idclass等通用属性
  • 事件:如clickkeydown等。
  • 元素的层级结构:DOM树的层级结构,既有子节点,又有父节点。

那么,如何声明式地描述上述内容呢? 在Vue.js 3 中的解决方案是:

  • 使用与HTML标签一致的方式来描述DOM元素、属性与层级结构等,例如 <div id="app"><span>趋动科技</span></div>;
  • 使用:或v-bind来描述动态绑定的属性,使用@或v-on来描述事件例如 <div :name="name" @click="handleClick"></div>; 这样,用户不需要手写任何命令式代码,就可以实现声明式地描述UI。当然除了使用这种模板形式描述UI外,还可以用JS对象来描述:
const title = {
    tag: 'div',
    props: {
       onClick: handleClick
    },
    children: [
        {tag: 'span'}
    ]
}

对应到Vue.js模板是:

<div @click="handleClick"><span></span></div>

使用JS对象来描述比模板来说的优势是更加灵活。假如我们根据变量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>

//JS对象描述,只需要一个变量来代表h标签即可
let level = 1; //h标签的级别
const title = {
  tag:`h${level}`  //h1标签
}

我们在Vue.js组件中通过手写渲染函数就是使用虚拟DOM来描述UI, h函数的返回值就是一个对象,让我们编写虚拟DOM变动更加轻松:

import { h } from 'vue'
export default {
  render(){
    return h('h1',{ onClick:handler }) //虚拟DOM
  }
}

组件的渲染函数:一个组件要渲染的内容是通过渲染函数来描述的,也就是代码中的render函数,Vue.js会根据组件的render函数的返回值拿到虚拟DOM,然后将组件的内容渲染出来。

2.2 初识渲染器

我们可以使用JS对象来描述真实的DOM结构。那么虚拟DOM又是如何通过渲染函数转为真实DOM后,渲染到页面中的呢? 渲染器的作用就是把虚拟DOM渲染为真实DOM

未命名绘图.png

假如我们有如下虚拟DOM:

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

接下来,我们需要编写一个渲染器,把上面这段虚拟DOM渲染为真实DOM,renderer函数接收两个参数:vnode虚拟DOM对象与container真实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(/^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的实现思路:

  • 创建元素
  • 为元素添加属性和事件
  • 递归遍历children创建节点

2.3 组件的本质

组件就是一组DOM元素的封装,这组DOM元素就是组件要渲染的内容,因此可以定义函数来描述组件本身的内容。

const MyComponent = function(){
    return {
      tag:'div',
      props:{
        onClick:() => alert('hello')
      },
      children:'click me'
    }  
}

这时候的虚拟DOM就是这样的:

const vnode = {
    tag: MyComponent
}

对于处理函数和字符串的时候,render 函数也会有所不同:

// 字符串 渲染函数
function mountElement(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)
}

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

//主 render 函数
function renderer(vnode, container) {
    if (typeof vnode.tag === 'string') {
        // 说明 vnode 描述的是标签元素
        mountElement(vnode, container)
    } else if (typeof vnode.tag === 'function') {
        // 说明 vnode 描述的是组件
        mountComponent(vnode, container)
    }
}

2.4 模板的工作原理

无论是手写虚拟 DOM还是使用模板,都是属于声明式 UI,上文中讲过,需要将虚拟 DOM 转换为真实 DOM,这一过程需要的就是编译器。 编译器和渲染器一样,就是一个程序,编译器的作用就是将模板编译成渲染函数,如下模板:

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

最终通过编译器编译,会将其转化为渲染函数:

export default {
  render() {
    return h('div', { onClick: handler }, 'click me')
  }
}

对于一个组件来说,最终都是通过渲染函数产生的,然后渲染器把渲染函数返回的虚拟 DOM,渲染为真实 DOM,这就是模板的工作原理,也是Vue.js 渲染页面的流程。

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

在Vue.js框架设计中,组件的实现依赖于渲染器和编译器,渲染器和编译器之间是互相关联、互相制约的,它们共同构成一个有机整体,不同模块之间互相配合,进一步提升框架性能。

13.png