vuejs设计与实现-vuejs3的设计思路

141 阅读3分钟

声明式地描述ui

vuejs中使用模板来声明式地描述UI, 也支持通过h函数调用返回虚拟dom(组件的render函数)描述ui.

const title = {
    tag: 'h1',
    props: {
        onClick: hanlder,
    },
    children: [
        { tag: 'span' }
    ]
}

// template 对应模版  h函数: 辅助创建虚拟dom的工具函数
<h1 @click="handler"><span></span></h1>

// 渲染函数
export default {
    render(){
        return h('h1', { onClick: hanlder })
    }
}

初识渲染器

渲染器将虚拟dom渲染为真实dom, 分为三步:

  • 创建元素: 把vnode.tag作为标签名称来创建dom元素
  • 添加属性和事件: 遍历 vnode.props对象, onXxx 代表事件, 调用addEventListener绑定事件处理函数
  • 处理children: 如果是数据, 则递归调用renderer继续渲染; 如果是字符串则创建一个文本节点添加到元素内

更新节点时应该只更新有变化的地方, 而不需要重新走一遍创建流程. 后文会深入了解, 总归就是通过一些dom操作来完成渲染工作.

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

组件的本质

虚拟dom是用来描述真实ui的js对象, 而组件就是一组dom元素的封装

// 定义一个函数来代表组件, 而返回值就是组件要渲染的内容.
const MyComponent = function(){
    return {
        tag: 'div',
        props: {
            onClick(){ ... },
        },
        children: 'click me'
    }
}

const vnode = {
    tag: MyComponent
}

// 对 renderer 进行补充
function renderer(vnode, container){
    if(typeof vnode.tag === 'string'){
        // vnode描述的是标签元素
        mountElement(vnode, container)
    } 
    else if(typeof vnode.tag === 'function'){
        // vnode描述的是组件
        mountComponent(vnode, container)
    }    
    
}

function mountComponent(vnode, container){
    // 获取组件要渲染的内容
    const subtree = vnode.tag()
    // 递归地调用 renderer 渲染 subtree
    renderer(subtree, container)
}
// 组件也可以是对象而不是函数, 通过对象的render方法同样知道组件要渲染的内容(虚拟dom)

模板的工作原理

渲染器是将虚拟dom渲染成真实dom, 而编译器是将模版编译为渲染函数. 对于编译器来说,模板就是一个字符串, 它会分析该字符串并生成一个对应的渲染函数.

以熟悉的vue文件为例, 一个 .vue 文件就是一个组件.

<template>
    <div @click="hanlder">click me</div>
</template>
<script>
export default {
    data(){ ... },
    methods: {
        hanlder(){ ... }
    },
    // render(){ ... 编译器会把模板内容编译成渲染函数, 并添加到组件对象上. }
}
</script>

vuejs是各个模块组成的有机整体

组件的实现依赖渲染器, 模板的编译依赖编译器, 并且编译后的代码是根据渲染器和虚拟dom的设计决定的.

// 模版
<div id="foo" :class="cls" />

// 编译器将上述模版编译成渲染函数
render(){
    // return h('div', { id: 'foo', class: cls })
    return {
        tag: 'div',
        props: {
            id: 'foo',
            class: cls
        },
        patchFlags: 1 // 假设 1 代表class是动态的
    }
}

cls是一个变量. 渲染器需要找到变化的内容进行更新, 经过编译器编译, 生成的虚拟dom中多出了一个patchFlags属性, 渲染器看到这个属性就可以快速定位哪些内容需要更新. 虚拟dom对象中包含多种数据字段, 每个字段都有一定的含义.


总结

模板会被编译器的程序编译为渲染函数, 虚拟dom对象会被渲染器渲染为真实dom元素. 他们是共同构成一个有机的整体, 相互配合进而提升性能.