之前我们分析过模板到真实 DOM 渲染的过程,中间有⼀个环节是把模板编译成 render 函数,这个 过程我们把它称作编译。 虽然我们可以直接为组件编写 render 函数,但是编写 template 模板更加直观,也更符合我们的 开发习惯。 Vue.js 提供了 2 个版本,⼀个是 Runtime + Compiler 的,⼀个是 Runtime only 的,前者是包含编译代码 的,可以把编译过程放在运⾏时做,后者是不包含编译代码的,需要借助 webpack 的 vue-loader 事 先把模板编译成 render 函数。 这⼀章我们就来分析编译的过程,对编译过程的了解会让我们对 Vue 的指令、内置组件等有更好的理 解。不过由于编译的过程是⼀个相对复杂的过程,我们只要求理解整体的流程、输⼊和输出即可,对 于细节我们不必抠太细。有些细节⽐如对于 slot 的处理我们可以在之后去分析插槽实现的时候再详 细分析。
编译⼊⼝
那么接下来的章节我会带⼤家去逐步分析这⼏个过程。
总结
编译⼊⼝逻辑之所以这么绕,是因为 Vue.js 在不同的平台下都会有编译的过程,因此编译过程中的依 赖的配置 baseOptions 会有所不同。⽽编译过程会多次执⾏,但这同⼀个平台下每⼀次的编译过程 配置⼜是相同的,为了不让这些配置在每次编译过程都通过参数传⼊,Vue.js 利⽤了函数柯⾥化的技巧 很好的实现了 baseOptions 的参数保留。同样,Vue.js 也是利⽤函数柯⾥化技巧把基础的编译过程 函数抽出来,通过 createCompilerCreator(baseCompile) 的⽅式把真正编译的过程和其它逻辑如 对编译配置处理、缓存处理等剥离开,这样的设计还是⾮常巧妙的。
parse
流程图
总结
那么⾄此, parse 的过程就分析完了,看似复杂,但我们可以抛开细节理清它的整体流程。 parse 的⽬标是把 template 模板字符串转换成 AST 树,它是⼀种⽤ JavaScript 对象的形式来描述整个模 板。那么整个 parse 的过程是利⽤正则表达式顺序解析模板,当解析到开始标签、闭合标签、⽂本 的时候都会分别执⾏对应的回调函数,来达到构造 AST 树的⽬的。 AST 元素节点总共有 3 种类型, type 为 1 表⽰是普通元素,为 2 表⽰是表达式,为 3 表⽰是纯⽂ 本。其实这⾥我觉得源码写的不够友好,这种是典型的魔术数字,如果转换成⽤常量表达会更利于源 码阅读。 当 AST 树构造完毕,下⼀步就是 optimize 优化这颗树。
optimize
codegen
⾮常经典的事件中⼼的实现,把所有的事件⽤ vm._events 存储起来,当执⾏ vm.$on(event,fn) 的时候,按事件的名称 event 把回调函数 fn 存储起来 vm._events[event].push(fn) 。当执⾏ vm.$emit(event) 的时候,根据事件名 event 找到所有的回调函数 let cbs = vm._events[event] ,然后遍历执⾏所有的回调函数。当执⾏ vm.$off(event,fn) 的时候会移除指 定事件名 event 和指定的 fn 当执⾏ vm.$once(event,fn) 的时候,内部就是执⾏ vm.$on ,并 且当回调函数执⾏⼀次后再通过 vm.$off 移除事件的回调,这样就确保了回调函数只执⾏⼀次。 所以对于⽤户⾃定义的事件添加和删除就是利⽤了这⼏个事件中⼼的 API。需要注意的事⼀ 点, vm.$emit 是给当前的 vm 上派发的实例,之所以我们常⽤它做⽗⼦组件通讯,是因为它的回 调函数的定义是在⽗组件中,对于我们这个例⼦⽽⾔,当⼦组件的 button 被点击了,它通过 this.$emit('select') 派发事件,那么⼦组件的实例就监听到了这个 select 事件,并执⾏它的 回调函数——定义在⽗组件中的 selectHandler ⽅法,这样就相当于完成了⼀次⽗⼦组件的通讯。 总结 那么⾄此我们对 Vue 的事件实现有了进⼀步的了解,Vue ⽀持 2 种事件类型,原⽣ DOM 事件和⾃定义 事件,它们主要的区别在于添加和删除事件的⽅式不⼀样,并且⾃定义事件的派发是往当前实例上派 发,但是可以利⽤在⽗组件环境定义回调函数来实现⽗⼦组件的通讯。另外要注意⼀点,只有组件节 点才可以添加⾃定义事件,并且添加原⽣ DOM 事件需要使⽤ native 修饰符;⽽普通元素使⽤ .native 修饰符是没有作⽤的,也只能添加原⽣ DOM 事件。