在正文开始之前,先了解vue基于源码构建的两个版本,一个是 runtime only ,另一个是 runtime加compiler 的版本,两个版本的主要区别在于后者的源码包括了一个编译器。
什么是编译器,百度百科上面的解释是
简单讲,编译器就是将“一种语言(通常为高级语言)”翻译为“另一种语言(通常为低级语言)”的程序。一个现代编译器的主要工作流程:源代码 (source code) → 预处理器 (preprocessor) → 编译器 (compiler) → 目标代码 (object code) → 链接器 (Linker) → 可执行程序 (executables)。
通俗点讲,编译器是一个提供了将源代码转化为目标代码的工具。更进一步理解,vue内置的编译器实现了将 .vue 文件转换编译为可执行javascript脚本的功能。
3.1.1 Runtime + Compiler一个完整的vue版本是包含编译器的,我们可以使用 template 进行模板编写。编译器会自动将模板编译成 render 函数。
// 需要编译器的版本 new Vue({ template: '<div>{{ hi }}</div>' })
3.1.2 Runtime Only而对于一个不包含编译器的 runtime-only 版本,需要传递一个编译好的 render 函数,如下所示:
// 不需要编译器 new Vue({ render (h) { return h('div', this.hi) } })
很明显,编译过程对性能有一定的损耗,并且由于加入了编译过程的代码,vue代码体积也更加庞大,所以我们可以借助webpack的vue-loader工具进行编译,将编译阶段从vue的构建中剥离出来,这样既优化了性能,也缩小了体积。
3.2 挂载的基本思路vue挂载的流程是比较复杂的,我们通过流程图理清基本的实现思路。
如果用一句话概括挂载的过程,可以描述为挂载组件,将渲染函数生成虚拟DOM,更新视图时,将虚拟DOM渲染成为真正的DOM。
详细的过程是:首先确定挂载的DOM元素,且必须保证该元素不能为 html,body 这类跟节点。判断选项中是否有 render 这个属性(如果不在运行时编译,则在选项初始化时需要传递 render 渲染函数)。当有 render 这个属性时,默认我们使用的是 runtime-only 的版本,从而跳过模板编译阶段,调用真正的挂载函数 $mount 。另一方面,当我们传递是 template 模板时(即在不使用外置编译器的情况下,我们将使用 runtime+compile 的版本),Vue源码将首先进入编译阶段。该阶段的核心是两步,一个是把模板解析成抽象的语法树,也就是我们常听到的 AST ,第二个是根据给定的AST生成目标平台所需的代码,在浏览器端是前面提到的 render 函数。完成模板编译后,同样会进入 $mount 挂载阶段。真正的挂载过程,执行的是 mountComponent 方法,该函数的核心是实例化一个渲染 watcher ,具体 watcher 的内容,另外放章节讨论。我们只要知道渲染 watcher 的作用,一个是初始化的时候会执行回调函数,另一个是当 vm 实例中监测的数据发生变化的时候执行回调函数。而这个回调函数就是 updateComponent ,这个方法会通过 vm._render 生成虚拟 DOM ,并最终通过 vm._update 将虚拟 DOM 转化为真正的 DOM 。
往下,我们从代码的角度出发,了解一下挂载的实现思路,下面只提取mount骨架代码说明。
// 内部真正实现挂载的方法 Vue.prototype.$mount = function (el, hydrating) { el = el && inBrowser ? query(el) : undefined; // 调用mountComponent方法挂载 return mountComponent(this, el, hydrating) }; // 缓存了原型上的 $mount 方法 var mount = Vue.prototype.$mount; // 重新定义$mount,为包含编译器和不包含编译器的版本提供不同封装,最终调用的是缓存原型上的$mount方法 Vue.prototype.$mount = function (el, hydrating) { // 获取挂载元素 el = el && query(el); // 挂载元素不能为跟节点 if (el === document.body || el === document.documentElement) { warn( "Do not mount Vue to <html> or <body> - mount to normal elements instead." ); return this } var options = this.$options; // 需要编译 or 不需要编译 if (!options.render) { ··· // 使用内部编译器编译模板 } // 最终调用缓存的$mount方法 return mount.call(this, el, hydrating) } // mountComponent方法思路 function mountComponent(vm, el, hydrating) { // 定义updateComponent方法,在watch回调时调用。 updateComponent = function () { // render函数渲染成虚拟DOM, 虚拟DOM渲染成真实的DOM vm._update(vm._render(), hydrating); }; // 实例化渲染watcher new Watcher(vm, updateComponent, noop, {}) }
3.3 编译过程 - 模板编译成 render 函数