Vue编译器源码分析(一)

一直对Vue的编译compile函数挺感兴趣的,接下来我将会尝试把我了解的用系列文章的形式总结分享。
前期工作先把 Vue 源码的目录结构搞清楚,详细目录介绍如下。

├── scripts ------------------------------- 构建相关的文件
│   ├── git-hooks ------------------------- git钩子的目录
│   ├── alias.js -------------------------- 别名配置
│   ├── config.js ------------------------- rollup配置文件
│   ├── build.js -------------------------- 对 config.js 中所有的rollup配置进行构建
│   ├── ci.sh ----------------------------- 持续集成运行的脚本
│   ├── release.sh ------------------------ 用于自动发布新版本的脚本
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放一些使用Vue开发的应用案例
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── packages ------------------------------ 存放独立发布的包的目录
├── test ---------------------------------- 包含所有测试文件
├── src ----------------------------------- 源码
│   ├── compiler -------------------------- 编译器代码的存放目录将 template 编译为 render 函数
│   ├── core ------------------------------ 存放通用的,与平台无关的代码
│   │   ├── observer ---------------------- 响应系统,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 虚拟DOM创建(creation)和打补丁(patching)的代码
│   │   ├── instance ---------------------- Vue构造函数设计相关的代码
│   │   ├── global-api -------------------- Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 抽象出来的通用组件
│   ├── server ---------------------------- 服务端渲染(server-side rendering)的相关代码
│   ├── platforms ------------------------- 平台特相关代码,不同平台的不同构建的入口文件存放地
│   │   ├── web --------------------------- web平台
│   │   │   ├── entry-runtime.js ---------- 运行时构建的入口,不包含模板(template)到render函数的编译器。
│   │   │   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,在 entry-runtime 的基础上添加了模板(template)到render函数的编译器
│   │   │   ├── entry-compiler.js --------- vue-template-compiler 包的入口文件
│   │   │   ├── entry-server-renderer.js -- vue-server-renderer 包的入口文件
│   │   │   ├── entry-server-basic-renderer.js -- 输出 packages/vue-server-renderer/basic.js 文件
│   │   ├── weex -------------------------- 混合应用
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 包含整个代码库通用的代码
├── package.json --------------------------  你懂得
├── yarn.lock ----------------------------- yarn 锁定文件
├── .editorconfig ------------------------- 针对编辑器的编码风格配置文件
├── .flowconfig --------------------------- flow 的配置文件
├── .babelrc ------------------------------ babel 配置文件
├── .eslintrc ----------------------------- eslint 配置文件
├── .eslintignore ------------------------- eslint 忽略配置
├── .gitignore ---------------------------- git 忽略配置

宏观的理论上讲讲compile编译分成 parse、optimize 与 generate 三个阶段,最终需要得到 render function。
parse
parse 会用正则等方式解析 template 模板中的指令、class、style等数据,形成AST。

optimize
optimize 的主要作用是标记 static 静态节点,这是 Vue 在编译过程中的一处优化,后面当 update 更新界面时,会有一个 patch 的过程, diff 算法会直接跳过静态节点,从而减少了比较的过程,优化了 patch 的性能。

generate
generate 是将 AST 转化成 render function 字符串的过程,得到结果是 render 的字符串以及 staticRenderFns 字符串。

在经历过 parse、optimize 与 generate 这三个阶段以后,组件中就会存在渲染 VNode 所需的 render function 了。

为了让大家更好理解,接下来我们用CND上下载的代码来分析。 源码下载地址

开始吧,灵魂三问?

1:如果我新建一个Vue的实例那编译什么?

为了搞清楚这个问题看这: Vue生命周期

Vue编译器源码分析(一)_第1张图片
image.png

通过这个图你是否理解了?

当你new Vue() 新建一个Vue实例的时候会查看你是否有template选项,有就把template编译到render函数,没有就将el外部的HTML作为template编译。(注:组件、单文件组件后续再讲)

回归到源码:

Vue.prototype.$mount = function(
        el,
        hydrating
    ) {
        el = el && query(el);

        /* istanbul ignore if */
        if (el === document.body || el === document.documentElement) {
            warn(
                "Do not mount Vue to  or  - mount to normal elements instead."
            );
            return this
        }

        var options = this.$options;
        // resolve template/el and convert to render function
        if (!options.render) {
            var template = options.template;
            if (template) {
                if (typeof template === 'string') {
                    if (template.charAt(0) === '#') {
                        template = idToTemplate(template);
                        /* istanbul ignore if */
                        if (!template) {
                            warn(
                                ("Template element not found or is empty: " + (options.template)),
                                this
                            );
                        }
                    }
                } else if (template.nodeType) {
                    template = template.innerHTML;
                } else {
                    {
                        warn('invalid template option:' + template, this);
                    }
                    return this
                }
            } else if (el) {
                template = getOuterHTML(el);
            }
            if (template) {
                /* istanbul ignore if */
                if (config.performance && mark) {
                    mark('compile');
                }

                var ref = compileToFunctions(template, {
                    shouldDecodeNewlines: shouldDecodeNewlines,
                    shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
                    delimiters: options.delimiters,
                    comments: options.comments
                }, this);
                var render = ref.render;
                var staticRenderFns = ref.staticRenderFns;
                options.render = render;
                options.staticRenderFns = staticRenderFns;

                /* istanbul ignore if */
                if (config.performance && mark) {
                    mark('compile end');
                    measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
                }
            }
        }
        return mount.call(this, el, hydrating)
    };

解读下上面代码:

1:如果传递了el参数,那么就使用query函数获取到指定的DOM元素并重新赋值给el变量,这个元素我们称之为挂载点。接着是一段if语句块,检测了挂载点是不是元素或者元素,如果是的话那么在非生产环境下会打印警告信息,警告你不要挂载到元素或者元素。为什么不允许这么做呢?那是因为挂载点的本意是组件挂载的占位,它将会被组件自身的模板替换掉,而元素和元素是不能被替换掉的。

2:接下来的if语句检测是否包含render选项,即是否包含渲染函数。如果渲染函数存在那么直接调用运行时版$mount函数进行挂载工作(后续专门的篇幅来讲这块)。

3:如果options.render不存在?接下来的 if 语句块的代码只有一个目的:使用 template 或 el 选项构建渲染函数。

总结下整个过程:

A:如果template选项不存在,那么使用el元素的outerHTML作为模板内容

B:如果template选项存在

且template的类型是字符串

  • 第一个字符是 #,那么会把该字符串作为 css 选择符去选中对应的元素,并把该元素的 innerHTML 作为模板
  • 第一个字符不是 #,那就用 template 自身的字符串值作为模板

且template的类型是元素节点

  • 则使用该元素的innerHTML作为模板

若template既不是字符串又不是元素节点,那么在非生产环境会提示开发者传递的template选项无效

经过以上逻辑的处理之后,理想状态下此时template变量应该是一个模板字符串,将来用于渲染函数的生成。

接下来我们将把重心放在:

var ref = compileToFunctions(template, {
                    shouldDecodeNewlines: shouldDecodeNewlines,
                    shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
                    delimiters: options.delimiters,
                    comments: options.comments
                }, this);

待续...Vue编译器源码分析(二)

你可能感兴趣的:(Vue编译器源码分析(一))