vue 源码学习 --- 总纲篇

555 阅读3分钟

vue 生成文件格式说明

vue 是通过 rollup 进行打包的,rollup 可以通过配置 output 的 format 值打包成不同格式的文件

// rollup.config.js  
export default {
    input: 'package/vue/index.ts',
    output: {
        file: 'dist/index.js',
        format: 'amd',//amd cjs esm iife umd
        name: 'Vue',
    }
}
// package/vue/index.ts
const Vue={
    createApp(){}
};
export default Vue;

amd : 异步模块定义,用于像RequireJS这样的模块加载器

cjs : CommonJS,适用于 Node 和 Browserify/Webpack

esm : 将软件包保存为 ES 模块文件,在现代浏览器中可以通过 <script type=module> 标签引入

iife : 一个立即执行函数的功能,适合作为<script>标签

umd – 通用模块定义,以amdcjsiife 为一体

// dist/index.js// cjs 格式
'use strict';
const Vue={
    createApp(){}
};
module.exports = Vue;
​
// esm 格式
const Vue={
    createApp(){}
};
export { Vue as default };
​
// amd 格式
define((function () { 'use strict';
    const Vue={
        createApp(){}
    };
    return Vue;
}));
​
// iife 格式
var Vue = (function () {
    'use strict';
    const Vue={
        createApp(){}
    };
    return Vue;
})();
​
// umd 格式
(function (global, factory) {
    typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
    typeof define === 'function' && define.amd ? define(factory) :
    (global = typeof globalThis !== 'undefined' ? globalThis : global || self, global.Vue = factory());
})(this, (function () { 'use strict';
    const Vue={
        createApp(){}
    };
    return Vue;
}));

vue 的设计思路

声明式框架

对于视图(UI)框架来说,从范式的角度看,通常分为命令式框架和声明式框架。命令式框架关注的是过程,声明式框架关注的是结果。

例如,要实现如下如下功能:

  1. 获取 id 为 #app 的 div 标签
  2. 设置文本内容是 'hello world'
  3. 绑定点击事件,点击打印 'OK'

用声明式的方式翻译成代码

const div = document.querySelector('#app');
div.innerText = 'hello world';
div.addEventListener('click',()=>{console.log('ok')})

声明式方式只在乎结果,只需要通过如下一行代码就完成上述功能,但是功能怎么实现的,这个是框架需要考虑的事情。

<div @click="console.log('ok')" >hello world</div>

上面的代码就是 vue 的模板,vue 就是典型的声明式框架,除了 vue 外,react angular 都是声明式的框架。

编译器

在说明声明式框架时提到过 vue 的模板 <div @click="console.log('ok')" >hello world</div> 。可以看出它和 HTML 的书写方式基本一致,但仅是看上去不一样。对于 vue 的编译器来说,模板就是一个普通的字符串

编译器的功能就是把模板里的字符串转变为渲染函数,渲染函数执行后会得到一个JavaScript 对象,该对象也就是虚拟DOM

下面用一个 .vue 文件简单说明下编译器的工作原理

<template>
    <div @click="handler">
        <span>{{name}}</span>
    </div>
</template>
<script>
    export default{
        data(){
            return {
                name:'a'
            }
        },
        methods:{
            handler(){}
        }
    }
</script>

通过编译器的编译,模板将会编译成渲染函数并添加到 <script> 标签快的组件对象上,最终在浏览器运行的代码如下:

export default{
    data(){
        return {
            name:'a'
        }
    },
    methods:{
        handler(){}
    },
    render(h){
        return h('div',{onClick:handler},[h('span',this.name)])//返回的就是虚拟 DOM
    }
}

将 h 函数的结果打印出来可以得到虚拟DOM,在Vue2中对象包含的属性如下:

VNode {tag: 'div', data: {…}, children: Array(1), text: undefined, elm: undefined, …}
​
// children: Array(1) 的结果如下
VNode {tag: 'span', data: undefined, children: Array(1), text: undefined, elm: span, …}

顺便说一点,通过 .vue 文件打包后得到的代码就是 render 函数,因此打包后的文件是不带渲染器的。

渲染器

当了解了虚拟 DOM 其实就是描述真实 DOM 的对象后,那 vue 又是怎么把虚拟 DOM 变成真实 DOM 渲染到页面上的呢?答案是渲染器,那么渲染器是如何工作的,下面通过一个简单的例子来说明。

假设有如下虚拟 DOM:

const VNode = {
    tag: 'div',
    props: {
        onClick: () => {
            console.log('ok')
        }
    },
    text: undefined,
    children: [{
        tag: 'span',
        text: undefined,
        children: [{
            tag: undefined,
            text: 1,
            children: undefined,
        }]
    }]
};

编写一个渲染器函数 renderer

function renderer(VNode, container) {
    let el;
    // 创建元素
    VNode.tag && (el = document.createElement(VNode.tag));
    // 为元素条件属性和事件
    VNode.props && Object.keys(VNode.props).forEach(keys => {
        if (/^on/s.test(keys)) {
            const eventType = keys.slice(2).toLowerCase();
            el.addEventListener(eventType, () => {
                VNode.props[keys].call(VNode);
            })
        }
    });
    // 处理 children
    if (Array.isArray(VNode.children)) {
        VNode.children.forEach(child => {
            renderer(child, el)
        })
    }
    // 元素/文本挂载
    if (VNode.text) {
        container.innerText = VNode.text;
    } else {
        container.appendChild(el)
    }
}
renderer(VNode,document.body);

在浏览器中运行这段代码,点击 div 标签会打印出 ok。可以看出渲染器其实就是操作 DOM API来完成渲染工作。当然 vue 的渲染器不仅仅是创建 DOM 节点这么简单,其精髓是在节点更新阶段的 patch 算法。

组件的本质

初步了解了编译器,虚拟 DOM 和渲染器后,我们知道了 DOM 元素会先经编译器转化为 render 函数,render 函数执行得到虚拟 DOM,虚拟 DOM 经渲染器创建成真实的 DOM。

在 vue 的使用过程中,最常用的是组件,组件在模板中也是通过标签的形式展示的。但在本质上 组件其实就是一组 DOM 元素的封装

const myComponent1 =function(){
    return {
        tag: 'div',
        props: {
            onClick: () => {
                console.log('ok')
            }
        },
        text: undefined,
        children: [{
            tag: 'span',
            text: undefined,
            children: [{
                tag: undefined,
                text: 1,
                children: undefined,
            }]
        }]
    }
}
const VNode={
    tag:myComponent1
}

上面的例子中,组件是通过一个函数来表示的,函数执行后得到的是虚拟DOM,虚拟DOM描述的就是一组 DOM 元素。当然除了函数外通过对象也是可以用来描述组件的。下面看下如何用对象描述组件:

const myComponent = {
    render(){
       return {
            tag: 'div',
            props: {
                onClick: () => {
                    console.log('ok')
                }
            },
            text: undefined,
            children: [{
                tag: 'span',
                text: undefined,
                children: [{
                    tag: undefined,
                    text: 1,
                    children: undefined,
                }]
            }]
        }
    }
}
const VNode={
    tag:myComponent2
}

为了兼容组件的渲染,渲染器 renderer 函数需要做如下修改:

function renderer(VNode, container) {
    // 判断是组件还是普通元素
    if (typeof VNode.tag === 'function') {
        mountComponent(VNode,container);
    } else if (typeof VNode.tag === "object") {
        mountComponent(VNode,container);
    }else if(typeof VNode.tag === 'string'){
        mountElement(VNode, container)
    }
}
function mountComponent(VNode,container){
    if(typeof VNode.tag === 'function'){
        const VNodes = VNode.tag();
        mountElement(VNodes, container);
    }else{
        const VNodes = VNode.tag.render();
        mountElement(VNodes, container);
    }
}
function mountElement(VNode, container) {
    let el;
    // 创建元素
    VNode.tag && (el = document.createElement(VNode.tag));
    // 为元素条件属性和事件
    VNode.props && Object.keys(VNode.props).forEach(keys => {
        if (/^on/s.test(keys)) {
            const eventType = keys.slice(2).toLowerCase();
            el.addEventListener(eventType, () => {
                VNode.props[keys].call(VNode);
            })
        }
    });
    // 处理 children
    if (Array.isArray(VNode.children)) {
        VNode.children.forEach(child => {
            mountElement(child, el)
        })
    }
    // 元素/文本挂载
    if (VNode.text) {
        container.innerText = VNode.text;
    } else {
        container.appendChild(el)
    }
​
}
renderer(VNode, document.querySelector('#app1'));
renderer(VNode1, document.querySelector('#app2'));
renderer(VNode2, document.querySelector('#app3'));

响应式系统

响应式系统可以说是 vue 的驱动器,初始化时读取模板中的数据触发 getter,此时收集渲染函数;模板中的数据被修改触发 setter,此时执行收集到的渲染函数。关于响应式系统接下来会通过手写一个响应式系统加深理解。