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

207 阅读3分钟

让给各位见笑了,这个只是记录的这段时间对《Vue.js 设计与实现》学习的记录,如果在某些方面不妥,还请您帮忙指出。谢谢😊

一、从这章节,我学到了哪些

  1. Vue有哪几种声明式描述UI
  2. 什么是渲染函数、渲染器、编译器,他们之间是怎样相辅相成,以及怎么实现的
  3. 组件本质是什么
  4. 虚拟DOM是什么
  5. 模板的工作原理

二、分别介绍一下上面的知识点

1. 声明式地描述UI

其实Vue3声明式地描述UI也是高度模板原生来写的,一下是对比

描述内容声明式UI原生UI
DOM元素<com></com><div></div>
属性描述<com id="app" :class="dynamicClass"></com><div id="app"></div>
事件描述<com @click="handleClick"></com>需要js addEventListener
层级关系<com><span></span></com><div><span></span></div>

2.JavaScript对象描述

  • 模板声明式: <h1 @click="handler"><span></span></h1>

  • JavaScript对象描述:

    const title = {
        // 标签
        tag: 'h1',
        // 标签属性
        props: {
            onClick: handler
        },
        // 子节点
        children: [
            { tag: 'span' }
        ]
    }
    
  • 模板式声明与JavaScript对象描述特点

    • 模板式声明书写直观,简单
    • JavaScript对象描灵活性高,比如动态的h1~h6标签,对象式更加灵活
  • 虚拟DOM:
    其实JavaScript对象描述就是虚拟DOM,只是这里描述的比较简单而已

3. 渲染函数、渲染器、编译器

  • 渲染函数:h

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

    其实 h 函数返回的就是虚拟DOM

  • 渲染器:将虚拟DOM渲染成真是的DOM

    function render(vnode, container) {
      // 使用 vnode.tag 作为标签名称创建DOM元素
      const el = document.createElement(vnode.tag);
      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.appendChildren(document.createElement(vnode.children));
      } else if (Array.isArray(vnode.children)) {
        //   递归调用 render 函数渲染子节点,使用当前元素 el 作为挂在节点
        vnode.children.forEach((child) => render(child));
      }
    
      //   将元素添加到挂载点下
      container.appendChildren(el);
    }
    

    渲染器的精髓:根据变更点,通过Diff算法找出并更新变更点

  • 编译器:将模板编译为渲染函数
    模板:

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

    编译为渲染函数:

    render(h) {
        return h("div", { onClick: handler }, "click me");
    },
    

    其实上面就是模板的工作原理
    模板 / 手写渲染函数 → 编译器编译成渲染函数 → 渲染函数生成虚拟DOM → 渲染器将虚拟DOM渲染成真是DOM并挂在到容器中(#app)

  • 相辅相成
    组件实现依赖渲染器,模板编译依赖编译器,编译后的代码根据渲染器和虚拟DOM的设计决定的,各个模块之间是相互制约、相互关联共同构成一个有机整体。

    比如:编译器让渲染器提高性能。
    编译器将 模板 / 渲染函数 编译成虚拟DOM的时候,标记哪些是动态属性patchFlag,告知渲染器哪些是可能变更点,从而在Diff的时候降低工作量,提升性能。

4. 组件的本质

组件:一组虚拟DOM元素的封装,可以是返回的虚拟DOM,或者对象内含有一个函数返回虚拟DOM,被渲染函数渲染出来的称之为subtree

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

function mountElement(vnode, container) {
  // 使用 vnode.tag 作为标签名称创建DOM元素
  const el = document.createElement(vnode.tag);
  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.appendChildren(document.createElement(vnode.children));
  } else if (Array.isArray(vnode.children)) {
    //   递归调用 render 函数渲染子节点,使用当前元素 el 作为挂在节点
    vnode.children.forEach((child) => render(child));
  }

  //   将元素添加到挂载点下
  container.appendChildren(el);
}

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