Vue.js 设计与实现读书简记(二)

214 阅读5分钟

前言

最近偶尔刷到Vue.js 设计与实现这本书的推荐, 发现是尤雨溪推荐的, Vue.js 官方团队成员霍春阳写的一本书. 看了书本简介(以下),刚好可以深入研究一下 Vue.js 3, 就入手了。

本书基于 Vue.js 3,从规范出发,以源码为基础,并结合大量直观的配图,循序渐进地讲解 Vue.js 中各个功能模块的实现,细致剖析框架设计原理。全书共18章,分为六篇,主要内容包括:框架设计概览、响应系统、渲染器、组件化、编译器和服务端渲染等。通过阅读本书,对 Vue.js 2/3具有上手经验的开发人员能够进一步理解 Vue.js 框架的实现细节,没有Vue.js使用经验但对框架设计感兴趣的前端开发人员,能够快速掌握 Vue.js 的设计原理。

这读书简记只是自我的读书笔记总结与汇总,方便自己日后回忆翻看,所以内容可能比较随意简洁,毕竟详细内容可以看原书。

接上一节

2. 框架设计的核心要素

框架设计的思考:

  1. 提供构建产物
  2. 产物模块格式
  3. 错误信息提示
  4. 开发版与生产版区别
  5. 热更新
  6. 打包体积
  • 提升用户的开发体验,提供友好的错误信息提示, 准确的定位错误位置
  • 控制框架代码的体积,通过 rollup.js 插件预定义, __DEV__ 常量控制生产环境与开发环境代码。达到开发环境中为用户提供友好的警告信息的同时,不会增加生产环境代码的体积。
  • 框架要做到良好的 Tree-Shaking,实现 Tree-Shaking 前提是模块必须是 ESM(ES Module), 因为 Tree-Shaking 依赖 ESM 的静态结构。
  • 框架应该输出怎样的构建产物
  • 特性开关
  • 错误处理
  • 良好的 TypeScript 类型支持

3. Vue.js 3 的设计思路

Vue.js 3 是声明式 UI 框架

  1. 模版描述
<div></div>
<div @click="dynamicID"></div>
  1. JS 描述
const title = {
    tag: 'h1', 
    props: {
        onclick: handler
    },
    children: [
            {tag: 'span'}
        ]
    }

渲染器

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

graph TD
虚拟DOM --> 渲染器 --> 真实DOM
function renderer(vnode, continer) {
     // 使用 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.substring(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))
     }
     // 将元素添加到挂载点
     continer.appendChild(el)
   }

这里 renderer 函数接收如下二个参数

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

renderer(vnode, document.body) // body 作为挂载点

renderer 思路

  1. 创建元素
  2. 为元素添加属性和事件
  3. 处理 children
const vnode = {
    tag: 'div', 
    props: {
        onclick: ()=>alert('hello')
    },
    children: 'click again' // 从 click me 改成 click again
    }

上面小小的修改, 渲染器只需要精确定的找到 vnode 对象的变更点并更新变更内容.

组件的本质

虚拟 DOM 除了可以描述真实 DOM 外,还可以描述组件。

组件就是一组 DOM 元素的封装

组件 MyConpnent 的返回值就是代表组件渲染的内容

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

定义虚拟 DOM 描述组件 MyConpnent, 虚拟 DOM 对象中的 tag 属性存储组件函数

const vnode = {
  tag: MyConpnent
}

渲染组件

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

mountElement 递归的调用 renderer 渲染

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

上面描述组件的时候是组件函数。但同时组件就一定时函数吗?当然不是,组件还可以是对象来表达。

// MyConpnent 是一个对象
const MyConpnent = {
    render() {
        return{
            tag: 'div', 
            props: {
                onclick: () => alert('hello')
            },
            children: 'click me'
            }
        }
    }

修改渲染器判断条件

function renderer(vnode, continer) {
     if (typeof vnode.tag === 'string') {
       mountElement(vnode, continer)
     } else if (vnode.tag === 'object')) { // 如果是对象, 说明 vnode 描述的是组件
       mountElement(vnode, continer)
     }
   }

接着修改 mountElement 函数

function mountElement(vnode, continer) {
     // vnode.tag 是组件对象, 调用它的 render 函数得到组件要渲染内容(虚拟 DOM)
     const subtree = vnode.tag.render()
     // 递归的调用 renderer 渲染 subtree
     renderer(subtree, continer)
   }

模版的工作原理

无论是手写虚拟 DOM (渲染函数) 还是使用模板,都属于声明式地描述 UI,并且 Vue 同时支持这两种 UI 表示方式。那么模板是怎么工作的呢? ---> 编译器 编译器与渲染器一样, 只是一段程序而已, 不过工作内容不同.

编译器作用是将模板编译为渲染函数

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

模板--->渲染函数

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

.vue 文件就是一个组件

<template>
  <div @click='handler'>
  click me
  </div>
</template>
<script>
  export default {
  data() {/*···*/},
  methods:{/*···*/},
}
</script>

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

export default {
    data() {/*···*/},
    methods: {
        handler: () => {/*···*/}
    },
    render() {
        return h('div', { onClick: handler }, 'click me')
    } 
}

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

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

Vue.js 模板会被一个叫作编译器的程序编译为渲染函数, 最后, 编译器、渲染器都是 Vue.js 的核心组成部分, 它们共同构成一个有机的整体, 不同模块之间互相配合, 进一步提升框架性能.

未完待续