• home > webfront > ECMAS > vue >

    vue编译原理(0):​Vue的编译器模块

    Author:zhoulujun Date:

    Vue的编译器模块相对独立且简单Vue 项目中的编译器相关的代码就在 entry-runtime-with-compiler jsentry-runtime js 文件是 Vue 用于

    Vue的编译器模块相对独立且简单

    Vue 项目中的 编译器相关的代码就在 entry-runtime-with-compiler.js

    • entry-runtime.js 文件是 Vue 用于构建 仅包含运行时 的源码文件

    • entry-runtime-with-compiler.js 是用于构建 同时包含编译器和运行时 的全功能文件

    entry-runtime-with-compiler.js 文件里的关键代码是为 Vue 的 prototype 扩展了一个 $mount 方法,并将模板编译相关的工作都封装在了这个 $mount 方法里。

    深扒$mount方法的内部实现

    例如下面一段 html 模板:

    <div id="index"><span>{{msg}}</span></div>

    开发者可以通过如下操作使用 Vue 将上面这段模板编译成 render 函数:

    let vm = new Vue({
        data: {
            msg: 'hello',
        }
    });
    
    // 实例化 Vue 时 new Vue(options) 传入的 options 可通过 vm.$options 访问
    console.log(vm.$options.render); // Console 输出:undefined
    
    vm.$mount('#index');
    console.log(vm.$options.render);    
    /* Console 输出:
     *  ƒ anonymous() {
     *      with(this){return _c('div',{attrs:{"id":"index"}},[_c('div',[_v(_s(msg))])])}
     *  }
     */

    可以看到在调用 $mount 方法之后已经生成了 Vue 的 render 函数。

    new Vue({
        el: '#index',
        data: {
            msg: 'hello',
        },
    });

    这两种写法是完全等价的。实际上,如果在实例化 Vue 的时候提供了 el 选项,Vue 也是在内部调用 $mount 方法进行编译的。

    接下来就看看 $mount 方法的具体是怎么实现的,为了更加清晰地描述思路,以下均使用伪代码进行书写:

    /**
     * 作用:将 Vue 的 html 模板编译成 render 函数。
     *
     * 通过将 $mount 方法定义在 Vue 的 prototype 上,
     * 使得每一个 new 出来的 Vue 实例都能使用 $mount 方法。
     */
    Vue.prototype.$mount = function (el){
        // options 是 new Vue(options) 提供的实参 options
        const options = this.$options;
    
        // 优先使用实例化 Vue 时提供 render 函数
        if (options.render) {
            // 已经是 render 函数了,因此不用做任何操作
            return this;
    
        // 如果没有提供 render 函数,则优先使用提供的 template 选项
        }else if(options.template){
            template = getOuterHTML(options.template);
    
        // 如果既没有提供 render 函数,又没有 template 选项,就使用 el 选项
        }else{
            template = getOuterHTML(el);
        }
    
        // 编译 html 模板生成 render 函数,并赋给 options 的 render 选项
        // 这也是为什么上面在调用 $mount 方法之后 vm.$options.render 的值发生了变化
        options.render = compileToFunctions(template);
    
        return this;
    }
    
    // 负责兼容多样化的输入形式并返回要处理的 html模板片段
    function getOuterHTML(){/*...*/}
    // 负责将 html模板片段编译成 render 函数
    function compileToFunctions(el){/*...*/}

    可以看到,如果实例化 Vue 的时候同时提供了 render、template、el 选项中的多个,则 Vue 使用的优先级是 render > template > el

    getOuterHTML 函数

    上面的 getOuterHTML 函数所做的工作就是兼容你使用 Vue 的各种姿势,比如:

    1. { el: '#index' }

    2. { el: document.querySelector('#index') }

    3. { template: '#index' }

    4. { template: '<div>{{msg}}</div>'}

    你可以传 CSS 选择器,也可以直接传 DOM, 还可以传 html 片段,怎么玩你说了算。getOuterHTML 函数的返回值是 DOM 的 outerHTML,总之,它负责得到 html 模板片段。

    至此一切仍然是在扯淡,上面的都只是前戏,现在还没进入真正的编译阶段。眼贼的同学估计已经看到了,上面的 compileToFunctions 函数才是真刀实枪负责编译的。

    compileToFunctions 函数

    接下来就扒进去看看 compileToFunctions 是怎么把 getOuterHTML 获得的 html 模板片段编译成 render 函数的。

    compileToFunctions 函数编译模板的过程主要分为三步:

    1. 将 html 模板解析成抽象语法树(AST)。

    2. 对 AST 做优化处理。

    3. 根据 AST 生成 render 函数。

    什么是AST抽象语法树

    抽象语法树(Abstract Syntax Tree) 是源代码语法结构的抽象表示,并以树这种数据结构进行描述。AST 属编译原理范畴,有比较成熟的理论基础,因此被广泛运用在对各种程序语言(JavaScript, C, Java, Python等等)的编译处理中。Vue 同样也是使用 AST 作为中间形式完成对 html 模板的编译。

    Vue构建 AST 的一般过程

    AST 的一般解析过程

    通常程序语言解析成 AST 的过程会分为两步:

    • 词法分析(Lexical Analysis)

    • 语法分析(Syntax Analysis)

    拿咱最熟悉的 JavaScript 来说吧,比如下面一段程序:

    let a = 1

    词法分析器会把代码的字符序列转换为单词序列(tokens)。经过词法分析后就能得到如下一个词素列表:

    [
        { type: 'Keyword', value: 'let' },
        { type: 'Identifier', value: 'a' },
        { type: 'Punctuator', value: '=' },
        { type: 'Numeric', value: '1' }
    ]

    语法分析器会在词法分析的基础上将单词序列(tokens)组合成各类语法短语(语句、表达式等)。经过语法分析后即可得到 AST 的 JSON 格式:

    {
        type: "Program",
        body: [
            {
                type: "VariableDeclaration",
                declarations: [
                    {
                        type: "VariableDeclarator",
                        id: {
                            type: "Identifier",
                            name: "a"
                        },
                        init: {
                            type: "Literal",
                            value: 1,
                            raw: "1"
                        }
                    }
                ],
                kind: "let"
            }
        ],
        sourceType: "script"
    }

    以下是使用 Esprima 工具对 JS 代码进行词法分析和语法分析的结果。其它工具还有: 在线的AST生成工具、 AST树形图预览工具

    JavaScript AST语法分析结果

    Vue 构建的 AST

    ,Vue 的 html 模板比较特殊,因为它根本算不上是一门语言,而是基于 HTML 的声明式绑定。因此,Vue 生成的 AST 类似于大家已经非常熟悉且非常成熟的 DOM 树,实际上 Vue 也确实是仿照着 DOM 树进行解析的。只要你熟悉 DOM 树,Vue 生成的 AST 是灰常好看且简单的。

    最后再次强调的一点是,Vue 编译器的编译结果是一个函数——Vue 的 render 函数,AST 只是方便处理的中间形式

    AST到依赖收集

    template被编译后,会形成AST,在执行render()函数过程中就会触发data.a的getter,并且这个过程是惰性收集的(如newValue虽然用到 了a,但如果它没有被调用执行,就不会触发getter,也就不会被添加到data.a的dep.subs里)

    现在,假设template变成了这样子:

    <template>
        <div>I am {{a}},plus 1 is {{newValue}}</div>
    </template>

    那么,可以看到就对应了两个观察者函数:计算属性newValue和render()函数,它们会被包装为两个watcher。

    在执行render()函数渲染的过程中,访问了data.a,从而使得data.a的dep.subs里加入了[email protected]

    又访问了计算属性newValue,计算属性里访问了data.a,使得data.a的dep.subs里加入了[email protected]。所以data.a的dep.subs里就有了[[email protected], [email protected]]

    为什么访问特定数据就使能让数据的deps.subs里加入了watcher呢?

    这是因为,在访问getter之前,就已经进入了某个watcher的上下文了,所以有一件事情是可以保证的:Watcher类的实例watcher已经准备好了,并且已经调用了watcher.get(),Dep.target是有值的

    所以,我们看到getter里进行依赖收集的写法是dep.depend(),并没有传入什么参数,这是因为,我们只需要把Dep.target加入当前dep.subs里就好了。

    但是我们又发现,Dep.prototype.depend()的实现是:

    depend() {
        Dep.target.addDep(this);
    }

    为什么depend()的时候,不直接把Dep.target加入dep.subs,而是调用了Dep.target.addDep呢?

    这是因为,我们不能无脑地直接把当前watcher塞入dep.subs里,我们要保证dep.subs里的每个watcher都是唯一的。

    Dep.target是Watcher类实例,调用dep.depend()相当于调用了watcher.addDep方法,所以我们再来看一下这个方法里做了什么事情:

    Watcher.prototype.addDep = function (dep) {
        var id = dep.id
        if (!this.newDepIds[id]) {
            this.newDepIds[id] = true
            this.newDeps.push(dep)
            if (!this.depIds[id]) {
                dep.addSub(this)
            }
        }
    }

    概括起来就是:判断本轮计算中是否收集过这个依赖,收集过就不再收集,没有收集过就加入newDeps。同时,判断有无缓存过依赖,缓存过就不再加入到dep.subs里了。

    3、setter里进行的,则是在值变更后,通知watcher进行重新计算。由于setter能访问到闭包中dep,所以就能获得dep.subs,从而知道有哪些watcher依赖于当前数据,如果自己的值变化了,通过调用dep.notify(),来遍历dep.subs里的watcher,执行每个watcher的update()方法,让每个watcher进行重新计算。



    参考文章:

    大白话Vue源码系列(02):编译器初探 https://www.cnblogs.com/iovec/p/vue_02.html



    转载本站文章《vue编译原理(0):​Vue的编译器模块》,
    请注明出处:https://www.zhoulujun.cn/html/webfront/ECMAScript/vue/8446.html