概论

本文主要讲vue源码分析 – 模板编译

以v2.5.16版本为分析例子(https://github.com/vuejs/vue/tree/v2.5.16)

vue的核心可以分为四个大块

  • 数据处理和双向绑定
  • 模板编译
  • 虚拟DOM
  • 组件化

vue的源码目录

  • circleci:是一个持续集成与部署服务。vue使用了这个服务来部署项目,该文件夹下为circleci部署所需的配置文件
    benchmarks:这个文件夹里面,都是作者对于vue的性能测试。其中在性能测试代码中,有一个api大家可以关注下,window.performance.now(),该api经常在衡量代码运行时间,运行效率时被用到

  • example: 相当于vue的使用说明,里面都是尤大写的vue使用的小demo

  • flow: flow是一个用来进行静态类型检查的工具。它的作用是让现有的JavaScript语法可以事先作类型的声明(定义),然后在开发过程中去进行自动检查。vue使用了该工具。该文件夹中,都是作者定义的静态类型。

  • test: 测试用例

  • src: vue的所有重要代码,都写在了这里面。详细说一下其下的各个文件夹。

    • compiler:模板解析模板编译的相关代码,也是本次组会要跟大家进行解读的部分
    • core
      • components: 全局组件定义的代码
      • global-api: 添加在vue对象上的方法。比如Vue.use,Vue.enxtend等
      • instance: vue实例相关的内容,比如生命周期,事件等。
      • observer: vue双向绑定部分的代码
      • vdom: 虚拟dom相关的代码
      • util: vue的工具方法
    • platforms:一个是web平台,我们常用的就是这个。一个是weex平台,weex是一个用于开发原生应用的框架,也可以把它视作vue-native。
    • server:服务端渲染的相关代码。
    • shared: vue共享的工具和方法。
    • sfc: .vue 文件解析

模板编译代码解读

export const createCompiler = createCompilerCreator(function baseCompile(
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    optimize(ast, options)
  }
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

vue模板编译的三个重要阶段都在上面的baseCompile函数中,这三个阶段分别是:

  • 生成AST
  • 优化静态内容
  • 生成render。

a,生成AST

AST简介

AST即抽象语法树,我们的代码运行在浏览器的时候,浏览器首先就会把我们的代码解析为AST然后再进一步把语法树转化为字节码或者直接生成机器码。所以他对于浏览器很重要,同样的对于开发者来说,AST也很重要,我们通过ast可以精准定位到代码的任何地方,从而可以对代码进行一系列操作。比如代码的语法检查,压缩以及代码结构的改变等等。webpack、UglifyJS等工具的核心都是通过ast来操作代码的。语法树记录了其对应代码的所有信息。

JavaScript Parser

代码解析成AST,是通过js Parser来实现的,不同的parser,生成的AST格式是不同的。比如chrome和firefox,由于jsParser不同,其生成的AST格式也不同,优化AST的格式有时也是浏览器厂商提高浏览器效率的一种方式。

这个网站可以即时看到不同parser解析出的ast

const ast = parse(template.trim(), options)

b,优化静态内容

静态内容就是和数据没有关系,当数据更新时,不需要刷新的内容。比如{ { text } }就是非静态内容,当数据变化的时候,这里的内容也会被更新。
baseCompile函数中,通过下面这行代码实现了静态内容的优化

if (options.optimize !== false) {
    optimize(ast, options)
  }

这其中的optimize函数,其实主要做了两件事:

  • 标记静态&非静态节点
  • 标记静态根结点
markStatic(root) // 标记所有静态&非静态节点

markStaticRoots(root, false) // 标记静态根节点
  • 标记静态&非静态结点

其标记的思路是从最底层往上一步一步的去标记静态节点。先标记最底层的叶子结点,再往上标记其父节点。一旦当前节点被标记为非静态节点,那么他所有的父节点都会被标记为非静态节点。

每个结点是否为静态的判断依据是:isStatic(node)=true && 子节点是静态节点

isStatic函数:

function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression
    return false
  }
  if (node.type === 3) { // text
    return true
  }
  return !!(node.pre || (
    !node.hasBindings && // no dynamic bindings
    !node.if && !node.for && // not v-if or v-for or v-else
    !isBuiltInTag(node.tag) && // not a built-in
    isPlatformReservedTag(node.tag) && // not a component
    !isDirectChildOfTemplateFor(node) &&
    Object.keys(node).every(isStaticKey) // node中的所有属性  是否isStaticKey()为true
  ))
}

其实就是先判断当前结点是否为表达式,是的话直接返回false,再判断是会否为纯为本,是的话直接返回true,如果都不是,再去判断是否绑定了v-for、v-if等属性。

  • 标记静态根结点

与标记静态&非静态结点的方向正好相反,标记静态跟结点是从根结点,到叶子节点,上往下去标记的。

每个结点是否为静态结点的标记依据是:

该结点为静态结点&&(结点有多个子节点||有一个子节点,但是这个子节点不是纯文本)

以上就是标记跟结点的实现思路。

至此所有静态/非静态信息就标记完了。这些标记在每次vue更新DOM时都会起到很重要的优化作用,因为遇到静态节点,就知道,其自身以及子节点内容都无需更新,可直接跳过,由此很大的提高了vue的更新速度。

c,生成render字符串

baseCompile函数中,下面这行为生成render字符串的代码

const code = generate(ast, options)  

文档参考:https://ustbhuangyi.github.io/vue-analysis/prepare/directory.html