Skip to content

Latest commit

 

History

History
287 lines (227 loc) · 7.91 KB

07-view-render-compiler.md

File metadata and controls

287 lines (227 loc) · 7.91 KB

View Render - Compiler

This article belongs to the series Read Vue Source Code.

In this article, we will learn:

  • Parser
  • Optimizer
  • Generator

This article will focus on the compiler part.

// `createCompilerCreator` allows creating compilers that use alternative
// parser/optimizer/codegen, e.g the SSR optimizing compiler.
// Here we just export a default compiler using the default parts.
export const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  const ast = parse(template.trim(), options)
  optimize(ast, options)
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

According to the code of baseCompile, template is first converted to AST by parse(), then goes to optimize() and generate() to generate render and staticRenderFns.

Parser

Parser will parse the template and build the AST. AST is Abstract Syntax Tree. Before parsing, template is only a string. After parsing, we understand the meaning of the template and convert that into a tree for later usage.

Open compiler/parser/index.js, look through the code, the parse() function defines some helpers and calls parseHTML() to do the real parsing.

This article is not intended to teach you how to write a parser, so we will skip details. If you are really interested in them just read this file.

For now, we just treat parser as a black box. Although we won't go into details, I still want to show you the final AST generated by parser, this is interesting and useful for understanding later functions.

In order to get the AST, we need to modify Vue's source code.

If you use Git to clone Vue, the default branch is dev, which will generate this error if you build Vue files and use them in your project:

- [email protected]
- [email protected]

This may cause things to work incorrectly. Make sure to use the same version for both.

We need to go master branch, checkout the latest release and build:

git checkout master
git pull
git checkout v2.3.4
npm install
npm run build

The code on master branch is a little different from dev branch because dev is the latest version, some changes have not been merged into master.

Then add one line to baseCompiler():

...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ADD THIS LINE
optimize(ast, options)
const code = generate(ast, options)
...

Here is a small demo written in .vue file:

<template>
  <div id="app">
    {{ newName ? newName + 'true' : newName + 'false' }}
    <span>This is static node</span>
  </div>
</template>

<script>
export default {
  name: 'app',
  data () {
    return {
      name: 'foo'
    }
  },
  computed: {
    newName () {
      return this.name + 'new!'
    }
  }
}
</script>

If you write <template> tag inside your normal HTML file, Vue will compile it at runtime. If you put it inside .vue file, during the building process vue-template-compiler will be used to pre-compile it before publishing to the internet.

Run npm run build to generate the modified Vue project. Copy package/vue-template-compiler/build.js to our demo's node_modules/vue-template-compiler, then use the modified vue-template-compiler to build it. I got this in my terminal:

{  
  type:1,
  tag:'div',
  attrsList:[  
    {  
      name:'id',
      value:'app'
    }
  ],
  attrsMap:{  
    id:'app'
  },
  parent:undefined,
  children:[  
    {  
      type:2,
      expression:'"\\n  "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n  "',
      text:'\n  {{ newName ? newName + \'true\' : newName + \'false\' }}\n  '
    },
    {  
      type:1,
      tag:'span',
      attrsList:[  

      ],
      attrsMap:{  

      },
      parent:[  
        Circular
      ],
      children:[  
        Array
      ],
      plain:true
    }
  ],
  plain:false,
  attrs:[  
    {  
      name:'id',
      value:'"app"'
    }
  ]
}

That's the AST parser generates. Easy to understand by both human and machine.

After parsing, Vue uses the optimizer to extract static parts. Why? Because static parts won't change, so we can extract them and make render process lighter.

Optimizer

Default optimizer located at compiler/optimizer.js. It walks through the AST and finds out static parts. Same to the parser, we just treat it as a black box and see the output.

...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ADD THIS LINE
optimize(ast, options)
console.log(ast) // <-- ADD THIS LINE
const code = generate(ast, options)
...

Below is the output of the second console.log():

{  
  type:1,
  tag:'div',
  attrsList:[  
    {  
      name:'id',
      value:'app'
    }
  ],
  attrsMap:{  
    id:'app'
  },
  parent:undefined,
  children:[  
    {  
      type:2,
      expression:'"\\n  "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n  "',
      text:'\n  {{ newName ? newName + \'true\' : newName + \'false\' }}\n  ',
      static:false
    },
    {  
      type:1,
      tag:'span',
      attrsList:[  

      ],
      attrsMap:{  

      },
      parent:[  
        Circular
      ],
      children:[  
        Array
      ],
      plain:true,
      static:true,
      staticInFor:false,
      staticRoot:false
    }
  ],
  plain:false,
  attrs:[  
    {  
      name:'id',
      value:'"app"'
    }
  ],
  static:false,
  staticRoot:false
}

Compare these two ASTs, you can tell the differences. After optimizing, all nodes in AST are marked with static flags. So where are they used?

The answer is––

Generator

Generator located in compiler/codegen/index.js. Again, let's focus on the output.

...
const ast = parse(template.trim(), options)
console.log(ast) // <-- ADD THIS LINE
optimize(ast, options)
console.log(ast) // <-- ADD THIS LINE
const code = generate(ast, options)
console.log(code) // <-- ADD THIS LINE
...
{  
  render:'with(this){return _c(\'div\',{attrs:{"id":"app"}},[_v("\\n  "+_s(newName ? newName + \'true\' : newName + \'false\')+"\\n  "),_c(\'span\',[_v("This is static node")])])}',
  staticRenderFns:[  

  ]
}

The result is a string render and an array staticRenderFns. Seems it doesn't generate static render functions for such a simple text node.

render is a string, maybe a little surprising but reasonable after you think about it. Compiler works without browser environment, it just accepts a string and output a compiled string.

Here are some important points:

  • with(this). Recall that render is used in this line vnode = render.call(vm._renderProxy, vm.$createElement), thus that this refers to the component. That's the reason why you can use properties without this in template
  • _c and _v. What's that? After global searching, we can find they are two functions set to vm._c and Vue.prototype._v during initRender(). Why _c is set to vm? Checkout the comments above that line
  • Notice that the implement of _c and _v is dynamic based on the platform. But their names are the same in render. This is a kind of abstract to make your code more generic

Compiler also has a function called compileToFunction(), it just converts the string render to a function and return.

Next Step

Now you know what compiler is, how it compiles template to the final render "function".

In next article, we will turn to the browser and see how Vue uses the generated render function and __patch__() to update your webpage.

Read next chapter: View Rendering - Patch.

Practice

Find the definition of vm._c, trace the origin implementation and tell what is returned by executing this function.