VUE源码解析 -- 编译流程解析

337 阅读8分钟

编译:编译就是把高级语言变成计算机可以识别的2进制语言, 但是我们这里所说的Vue的编译指的是: 把template模板字符串转换成render渲染函数的过程。

通过上面所说的我们就应该知道看源码的什么地方了:应该就是定义render函数的地方了, 通常我们使用Vue的话有两种方式:

  1. 自己定义render函数: new Vue({ render:h=>h(App) }).$mount("#root")

  2. 使用template new Vue({ template:"

    Hello World!
    " }).$mount("#root")

通过上面的使用方法我们可以看到不管自定义render还是直接使用template模板方式,最后都需要调用$mount方法,

mount方法就是把我们的Vue实例挂载到页面中,我们应该也可以预料到了生成render函数也应该是在这个方法内了,下面我们来看一下mount方法就是把我们的Vue实例挂载到页面中,我们应该也可以预料到了 生成render函数也应该是在这个方法内了, 下面我们来看一下mount方法的定义:

  // entry-runtime-with-compiler.js 
  const mount = Vue.prototype.$mount
  // 带编译器的$mount
  // 也就是说如果我们new Vue的时候使用了template属性,就需要调用这个$mount函数
  Vue.prototype.$mount = function (
    el?: string | Element,
    hydrating?: boolean
  ): Component {
    // 获取根节点
    // query方法 如果document.querySelector(el)存在就直接返回,不存在就createElement("div")返回
    el = el && query(el)

    /* istanbul ignore if */
    if (el === document.body || el === document.documentElement) {
      process.env.NODE_ENV !== 'production' && warn(
        `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
      )
      return this
    }

    const options = this.$options
    // resolve template/el and convert to render function
    // 没有render函数
    if (!options.render) {
      // 获取template 模板字符串
      let template = options.template
      // 如果有的话 就执行。。。
      if (template) {
        // template:#app
        // 获取页面中id为app的节点的innerHTML的内容
        if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            // 获取页面中id为app的节点的innerHTML的内容
            //类似这种用法: <script type="text/x-template" id="app">...</script>
            template = idToTemplate(template)
          }
        } else if (template.nodeType) {
          template = template.innerHTML
        } else {
          return this
        }
      } else if (el) {// 没有template 的话 获取el.outerHTML
        template = getOuterHTML(el)
      }
      if (template) {
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
          mark('compile')
        }
        // 根据template模板生成render函数
        const { render, staticRenderFns } = compileToFunctions(template, {
          outputSourceRange: process.env.NODE_ENV !== 'production', // 参数啥意思?
          shouldDecodeNewlines, // 这两个参数应该是判断节点的参数会不会在浏览器中被转义的
          shouldDecodeNewlinesForHref, 
          delimiters: options.delimiters, // 改变纯文本插入分隔符。<span>{{这里的"{{ }}"应该就是分割符 }}}}</span>
          comments: options.comments // 当设为 true 时,将会保留且渲染模板中的 HTML 注释。默认行为是舍弃它们。
        }, this)
        options.render = render
        options.staticRenderFns = staticRenderFns
      }
    }
    // 最后调用不包括compiler的mount方法
    return mount.call(this, el, hydrating)
  }

通过上面的代码我们可以看出来转换成render函数的方法就是 compileToFunctions 这个方法,下面我们来看一下具体的流程是怎样的:

我们根据流程图来大概解释一下:

  1. creatCompiler函数用来生成complie和compileToFunctions函数
  2. complie函数调用baseComplie函数来生成render字符串函数体(baseComplie转换成的render为字符串函数体,类似这种:"with(this){return _c('div',[_c('h1',[_v("我是父组件")])],2)}")
  3. complieToFunction函数用来把render字符串函数体转换为render函数

下面我们在结合代码来看一下这个逻辑,基本上就是几个高阶函数的相互调用:

// platforms/web/compiler/index.js  --- compileToFunctions函数
import { createCompiler } from 'compiler/index'

const { compile, compileToFunctions } = createCompiler(baseOptions)

//  compiler/index.js  --  createCompiler函数
import { createCompilerCreator } from './create-compiler'
// createCompilerCreator 高阶函数 返回另一个函数
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 模板解析阶段 用正则等方式解析template模板中的指令,class、style等数据,形成ast(语法树)。
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 遍历AST,找出其中的静态节点/ 静态根节点。并打上标记
    optimize(ast, options)
  }
  // 将AST转换成render字符串函数体
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

// compiler/create-compiler.js   --- createCompilerCreator函数
export function createCompilerCreator (baseCompile: Function): Function {
  // 返回createCompiler函数
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (
      template: string,
      options?: CompilerOptions // 具体里面有哪些参数,请查看CompilerOptions类型定义的地方(flow文件夹里面有)
    ): CompiledResult {
      const finalOptions = Object.create(baseOptions)
      
      // 合并一下配置参数
      if (options) {
        // merge custom modules
        if (options.modules) {
          finalOptions.modules =
            (baseOptions.modules || []).concat(options.modules)
        }
        // merge custom directives
        if (options.directives) {
          finalOptions.directives = extend(
            Object.create(baseOptions.directives || null),
            options.directives
          )
        }
        // copy other options
        for (const key in options) {
          if (key !== 'modules' && key !== 'directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn
      // 调用baseCompile函数 
      const compiled = baseCompile(template.trim(), finalOptions)
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}

// compiler/to-function.js  createCompileToFunctionFn函数

export function createCompileToFunctionFn (compile: Function): Function {
  // 闭包的方式缓存不同模板的render函数
  const cache = Object.create(null)

  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn
    
    // check cache
    // key =  "{,}<div>
    //     <h1>我是父组件</h1>
    // </div>
    // "
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    // 如果已经存在了就直接返回
    if (cache[key]) {
      return cache[key]
    }

    // compile
    const compiled = compile(template, options)

    // turn code into functions
    const res = {}
    const fnGenErrors = []
    // complied.render:"with(this){return _c('div',[_c('h1',[_v("我是父组件")]),_v(" "),_c('h2',[_v("msg:"+_s(msg))])])}" 他是这个东西。。
    // createFunction方法就是把这个字符串转换成函数,
    res.render = createFunction(compiled.render, fnGenErrors)

    ...

    return (cache[key] = res)
  }
}

通过上面的讲解我们应该清楚将template转换成render渲染函数的逻辑主要是在baseComplier函数中的,下面我们来重点讲解一下这个函数:

function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 模板解析阶段 用正则等方式解析template模板中的指令,class、style等数据,形成ast(语法树)。
  const ast = parse(template.trim(), options)
  if (options.optimize !== false) {
    // 优化阶段,遍历AST,找出其中的静态节点/ 静态根节点。并打上标记
    optimize(ast, options)
  }
  // 代码生成阶段。将AST转换成渲染函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
}

可以看到里面的逻辑也比较清晰,大致分为三个流程:

  1. parse函数:把我们写的模板字符串(template)中的节点通过js对象的方式来描述出来,用于后面来生成对应render函数

  2. optimize函数:遍历我们上面得到的astElement对象,然后找出其中的静态节点/静态根节点,并打上标记

  3. generate函数: 根据上面的对象生成render函数(用于生成VNode节点)

下面我们来逐一讲解一下每个函数的具体逻辑:

parse函数

我们来看一下parse函数的代码,看见里面主要的函数就是parseHTML函数,下面我们来结合代码看一下具体逻辑:

// compiler/parser/index.js 
export function parse (
  template: string,
  options: CompilerOptions
): ASTElement | void {
  // 标识符
  delimiters = options.delimiters

  const stack = []
  let root
  let currentParent
  let inVPre = false
  let inPre = false
  let warned = false

  parseHTML(template, {
    warn,
    expectHTML: options.expectHTML,
    isUnaryTag: options.isUnaryTag,
    canBeLeftOpenTag: options.canBeLeftOpenTag,
    shouldDecodeNewlines: options.shouldDecodeNewlines,
    shouldDecodeNewlinesForHref: options.shouldDecodeNewlinesForHref,
    shouldKeepComment: options.comments,
    outputSourceRange: options.outputSourceRange,
    /*
      tag:标签名
      attrs:标签属性
      unary:是否是自闭合标签
      start:开始索引
      end:结束索引
    */ 

    start (tag, attrs, unary, start, end) {
      // 创建一个tag类型的AST节点
      let element: ASTElement = createASTElement(tag, attrs, currentParent)
      // v-for指令
      processFor(element)
      // v-if指令
      processIf(element)
      //v-once指令: el.once = true 
      processOnce(element)
      // root是根节点,第一次调用start钩子函数的时候 默认是root,后面再调用的时候会通过下面的closeElement方法添加到root.children中
      if (!root) {
        root = element
      }
      // 非自闭合标签,
      // currentParent:当前的节点
      if (!unary) {
        currentParent = element
        stack.push(element)
      } else {
        closeElement(element)
      }
    },
    end (tag, start, end) {
      const element = stack[stack.length - 1]
      // pop stack
      stack.length -= 1
      currentParent = stack[stack.length - 1]
      closeElement(element)
    },
    chars (text: string, start: number, end: number) {
      const children = currentParent.children
      if (text) {
        let res
        let child: ?ASTNode
        // {{item}} 走这个节点
        if (!inVPre && text !== ' ' && (res = parseText(text, delimiters))) {
          child = {
            type: 2,
            expression: res.expression,
            tokens: res.tokens,
            text
          }
        } else if (text !== ' ' || !children.length || children[children.length - 1].text !== ' ') {
          child = {
            type: 3,
            text
          }
        }
        if (child) {
          children.push(child)
        }
      }
    },
    comment (text: string, start, end) {
      if (currentParent) {
        const child: ASTText = {
          type: 3,
          text,
          isComment: true
        }
        currentParent.children.push(child)
      }
    }
  })
  return root
}

通过上面我们可以看到parse主要就是调用parseHTML函数,下面我们来通过一张图来理解一下parseHTML函数的作用: 首先我们能看到parseHTML函数调用的时候会有四个钩子函数:

  1. start函数
    当正则匹配到开始标签的时候调用, 根据开始标签的tagName生成astElement节点对象 处理开始标签中的属性:v-for/v-if/attrs等,
  2. end函数
    正则匹配到结束标签的时候调用 调用closeElement方法 把当前结束的标签添加到对应父标签的children中
  3. chars函数
    正则匹配到当前节点为文本节点时调用 生成文本astElement节点对象
  4. comment函数
    正则匹配到注释节点时调用 生成注释节点对象

下面我们来通过一张图来理解一下具体template的编译过程:

总结一下:
parse函数就是把template转换成astElement节点

optimize函数

标记一下节点是否为静态节点/静态根节点

  1. 静态节点:
function isStatic (node: ASTNode): boolean {
  if (node.type === 2) { // expression  指的是 <h1>{{msg}}</h2>中的{{msg}}节点
    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) // 说明只包含基础的属性(像v-if/v-for、slot等都会给node增加属性的)
  ))
}

符合上面条件的节点就是静态节点

  1. 静态根节点:
 function markStaticRoots (node: ASTNode, isInFor: boolean) {
  if (node.type === 1) {
    // 这个节点时静态节点,并且children 并不仅仅只有text文本 
    if (node.static && node.children.length && !(
      node.children.length === 1 &&
      node.children[0].type === 3
    )) {
      node.staticRoot = true
      return
    } else {
      node.staticRoot = false
    }
    // 同样还要判断子节点里面是不是符合这个条件
    if (node.children) {
      for (let i = 0, l = node.children.length; i < l; i++) {
        markStaticRoots(node.children[i], isInFor || !!node.for)
      }
    }
    // if if-else if-else-if 等条件渲染的时候的 不显示的节点  同样的道理
    if (node.ifConditions) {
      for (let i = 1, l = node.ifConditions.length; i < l; i++) {
        markStaticRoots(node.ifConditions[i].block, isInFor)
      }
    }
  }
}

generate函数

把我们生成的astElement节点对象生成render字符串函数体:

具体的代码我们这里就不在详细讲述了,里面的代码逻辑不复杂,自己感兴趣的话可以看一下,我们来举例看一下我们的template转换之后生成的render字符串函数体:

//<div>    


//<p v-for="(item,idx) in list" :key="idx">{{item}}</p>    --->  "_l((list),function(item,idx){return _c('p',{key:idx},[_v(_s(item))])})"

//    <h1>我是父组件</h1>                                   --->      "_c('h1',[_v("我是父组件")])"

//    <h2 ref="msgCon">msg:{{msg}}</h2>                    --->      "_c('h2',{ref:"msgCon"},[_v("msg:"+_s(msg))])"

//    <p v-if="inpVal === 'abc'">我是v-if的元素,只有inpVal==='abc'的时候才会显示</p>  --->   "_c('p',[_v("我是v-if的元素,只有inpVal==='abc'的时候才会显示")])"

//    <h2>计算属性msgDouble: {{strDouble}}</h2>            --->     "_c('h2',[_v("计算属性msgDouble: "+_s(strDouble))])"

//    <input type="text" v-model="inpVal"/>               --->    "_c('input',{directives:[{name:"model",rawName:"v-model",value:(inpVal),expression:"inpVal"}],attrs:{"type":"text"},domProps:{"value":(inpVal)},on:{"input":function($event){if($event.target.composing)return;inpVal=$event.target.value}}})"

//    <ComponentA :foo='msg' @changeFoo="changeMsg"/>     --->     "_c('ComponentA',{attrs:{"foo":msg},on:{"changeFoo":changeMsg}})"


//</div>

//我们template模板如下: 
<template>
  <div>
      <p v-for="(item,idx) in list" :key="idx">{{item}}</p>
      <h1>我是父组件</h1>
      <h2 ref="msgCon">msg:{{msg}}</h2>
      <p v-if="inpVal">我是v-if的元素,只有inpVal==='abc'的时候才会显示</p>
      <h2>计算属性msgDouble: {{strDouble}}</h2>
      <input type="text" v-model="inpVal"/>
      <ComponentA :foo='msg' @changeFoo="changeMsg"/>
  </div>
</template>

// 转换之后的render字符串函数体如下:
`with(this){
    return _c('div',
              [
                  _c('h1',[_v("我是父组件")]),
                  _v(" "),
                  _c('h2',{ref:"msgCon"},[_v("msg:"+_s(msg))]),
                  _v(" "),
                  (inpVal)?_c('p',[_v("我是v-if的元素,只有inpVal==='abc'的时候才会显示")]):_e(),
                  _v(" "),
                  _c('h2',[_v("计算属性msgDouble: "+_s(strDouble))]),
                  _v(" "),
                  _c('input',{directives:[{name:"model",rawName:"v-model",value:(inpVal),expression:"inpVal"}],attrs:{"type":"text"},domProps:{"value":(inpVal)},on:{"input":function($event){if($event.target.composing)return;inpVal=$event.target.value}}}),
                  _v(" "),
                  _c('ComponentA',{attrs:{"foo":msg},on:{"changeFoo":changeMsg}})
              ],2)
  }`

  //里面我们能够看到好多_c、_l等这种内部变量,_C对应的就是createElement函数,下面我们来看一下这些内部变量的定义:
  export function installRenderHelpers (target: any) {
    target._o = markOnce
    target._n = toNumber
    target._s = toString
    target._l = renderList
    target._t = renderSlot
    target._q = looseEqual
    target._i = looseIndexOf
    target._m = renderStatic
    target._f = resolveFilter
    target._k = checkKeyCodes
    target._b = bindObjectProps
    target._v = createTextVNode
    target._e = createEmptyVNode
    target._u = resolveScopedSlots
    target._g = bindObjectListeners
    target._d = bindDynamicKeys
    target._p = prependModifier
  }

以上就是生成的render函数,我们后面渲染页面的时候就可以通过render函数生成VNode节点,然后使用Vue提供的update方法把节点渲染到页面中了。

总结

别着急慢慢看,没看明白就缓缓再看,多看几遍总会明白的。