编程知识 cdmana.com

Optimization of vue3 template compilation

Vue3 It's been a while since the official release , I wrote an article some time ago (《Vue Template compilation principle 》) analysis Vue The principle of template compilation of . Today's article is going to study Vue3 Under the template compilation and Vue2 The difference under , as well as VDOM Next Diff Optimization of algorithm .

Compile entry

Read about Vue3 My classmates must know Vue3 New combinations have been introduced Api, In components mount The stage calls setup Method , Then we will judge render Does the method exist , If it doesn't exist compile Methods will template Turn into render.

// packages/runtime-core/src/renderer.ts
const mountComponent = (initialVNode, container) => {
  const instance = (
    initialVNode.component = createComponentInstance(
      // ...params
    )
  )
  //  call  setup
  setupComponent(instance)
}

// packages/runtime-core/src/component.ts
let compile
export function registerRuntimeCompiler(_compile) {
  compile = _compile
}
export function setupComponent(instance) {
  const Component = instance.type
  const { setup } = Component
  if (setup) {
    // ... call  setup
  }
  if (compile && Component.template && !Component.render) {
      //  without  render  Method 
    //  call  compile  take  template  To  render  Method 
    Component.render = compile(Component.template, {...})
  }
}

This part is all about runtime-core The code in , It was mentioned in the previous article Vue Divided into full version and runtime edition . If you use vue-loader Handle .vue file , In general, I will .vue In the document template It can be directly processed into render Method .

//   Need compiler 
Vue.createApp({
  template: '<div>{{ hi }}</div>'
})

//  Unwanted 
Vue.createApp({
  render() {
    return Vue.h('div', {}, this.hi)
  }
})

The full version and runtime The difference is , The full version will introduce compile Method , If it is vue-cli The generated project will erase this part of the code , take compile The process is in the packaging stage , To optimize performance .runtime-dom Provided in registerRuntimeCompiler Method is used to inject compile Method .

Main process

In the full version of index.js in , Called registerRuntimeCompiler take compile For injection , Now let's look at the injected compile What does the method mainly do .

// packages/vue/src/index.ts
import { compile } from '@vue/compiler-dom'

//  Compile cache 
const compileCache = Object.create(null)

//  Inject  compile  Method 
function compileToFunction(
    //  Templates 
  template: string | HTMLElement,
  //  Compile configuration 
  options?: CompilerOptions
): RenderFunction {
  if (!isString(template)) {
    //  If  template  It's not a string 
    //  They think it's a  DOM  node , obtain  innerHTML
    if (template.nodeType) {
      template = template.innerHTML
    } else {
      return NOOP
    }
  }

  //  If it exists in the cache , Get directly from the cache 
  const key = template
  const cached = compileCache[key]
  if (cached) {
    return cached
  }

  //  If it is  ID  Selectors , This gets  DOM  After the element , take  innerHTML
  if (template[0] === '#') {
    const el = document.querySelector(template)
    template = el ? el.innerHTML : ''
  }

  //  call  compile  obtain  render code
  const { code } = compile(
    template,
    options
  )

  //  take  render code  Turn into  function
  const render = new Function(code)();

    //  return  render  Method at the same time , Put it in the cache 
  return (compileCache[key] = render)
}

//  Inject  compile
registerRuntimeCompiler(compileToFunction)

Talking about Vue2 The template compilation has already talked about ,compile The method is divided into three steps ,Vue3 The logic of is similar to :

  1. Template compilation , Convert template code to AST;
  2. Optimize AST, Convenient follow-up virtual DOM to update ;
  3. The generated code , take AST Into executable code ;
// packages/compiler-dom/src/index.ts
import { baseCompile, baseParse } from '@vue/compiler-core'
export function compile(template, options) {
  return baseCompile(template, options)
}

// packages/compiler-core/src/compile.ts
import { baseParse } from './parse'
import { transform } from './transform'

import { transformIf } from './transforms/vIf'
import { transformFor } from './transforms/vFor'
import { transformText } from './transforms/transformText'
import { transformElement } from './transforms/transformElement'

import { transformOn } from './transforms/vOn'
import { transformBind } from './transforms/vBind'
import { transformModel } from './transforms/vModel'

export function baseCompile(template, options) {
  //  analysis  html, Turn into  ast
  const ast = baseParse(template, options)
  //  Optimize  ast, Tag static nodes 
  transform(ast, {
    ...options,
    nodeTransforms: [
      transformIf,
      transformFor,
      transformText,
      transformElement,
      // ...  Omitted part of  transform
    ],
    directiveTransforms: {
      on: transformOn,
      bind: transformBind,
      model: transformModel
    }
  })
  //  take  ast  Convert to executable code 
  return generate(ast, options)
}

Calculation PatchFlag

The general logic here is not much different from the previous one , Mainly optimize The method has become transform Method , Some default syntax will be used transform. these transform It's the follow-up virtual DOM The key to optimization , Let's see transform Code for .

// packages/compiler-core/src/transform.ts
export function transform(root, options) {
  const context = createTransformContext(root, options)
  traverseNode(root, context)
}
export function traverseNode(node, context) {
  context.currentNode = node
  const { nodeTransforms } = context
  const exitFns = []
  for (let i = 0; i < nodeTransforms.length; i++) {
    // Transform  Will return an exit function , After processing all the child nodes, execute 
    const onExit = nodeTransforms[i](node, context)
    if (onExit) {
      if (isArray(onExit)) {
        exitFns.push(...onExit)
      } else {
        exitFns.push(onExit)
      }
    }
  }
  traverseChildren(node, context)
  context.currentNode = node
  //  Execution so  Transform  Exit function of 
  let i = exitFns.length
  while (i--) {
    exitFns[i]()
  }
}

Let's focus on transformElement The logic of :

// packages/compiler-core/src/transforms/transformElement.ts
export const transformElement: NodeTransform = (node, context) => {
  // transformElement  There's no logic being executed , Instead, it directly returns an exit function 
  //  explain  transformElement  You need to wait until all the child nodes have finished processing 
  return function postTransformElement() {
    const { tag, props } = node

    let vnodeProps
    let vnodePatchFlag
    const vnodeTag = node.tagType === ElementTypes.COMPONENT
      ? resolveComponentType(node, context)
      : `"${tag}"`
    
    let patchFlag = 0
    //  Detect node properties 
    if (props.length > 0) {
      //  Detect the dynamic part of node attributes 
      const propsBuildResult = buildProps(node, context)
      vnodeProps = propsBuildResult.props
      patchFlag = propsBuildResult.patchFlag
    }

    //  Detect child nodes 
    if (node.children.length > 0) {
      if (node.children.length === 1) {
        const child = node.children[0]
        //  Check whether the child node is dynamic text 
        if (!getStaticType(child)) {
          patchFlag |= PatchFlags.TEXT
        }
      }
    }

    //  format  patchFlag
    if (patchFlag !== 0) {
        vnodePatchFlag = String(patchFlag)
    }

    node.codegenNode = createVNodeCall(
      context,
      vnodeTag,
      vnodeProps,
      vnodeChildren,
      vnodePatchFlag
    )
  }
}

buildProps The node's properties will be traversed once , Because the internal source code involves a lot of other details , Here's the simplified code , Just keep patchFlag Related logic .

export function buildProps(
  node: ElementNode,
  context: TransformContext,
  props: ElementNode['props'] = node.props
) {
  let patchFlag = 0
  for (let i = 0; i < props.length; i++) {
    const prop = props[i]
    const [key, name] = prop.name.split(':')
    if (key === 'v-bind' || key === '') {
      if (name === 'class') {
          //  If you include  :class  attribute ,patchFlag | CLASS
        patchFlag |= PatchFlags.CLASS
      } else if (name === 'style') {
          //  If you include  :style  attribute ,patchFlag | STYLE
        patchFlag |= PatchFlags.STYLE
      }
    }
  }

  return {
    patchFlag
  }
}

The code above shows only three kinds of patchFlag The type of :

  • A node has only one text child node , And the text contains dynamic data TEXT = 1
<p>name: {{name}}</p>
  • Nodes contain variable class attribute CLASS = 1 << 1
<div :class="{ active: isActive }"></div>
  • Nodes contain variable style attribute STYLE = 1 << 2
<div :style="{ color: color }"></div>

You can see PatchFlags It's all numbers 1 after Shift left operator Calculated .

export const enum PatchFlags {
  TEXT = 1,             // 1,  Binary system  0000 0001
  CLASS = 1 << 1,       // 2,  Binary system  0000 0010
  STYLE = 1 << 2,       // 4,  Binary system  0000 0100
  PROPS = 1 << 3,       // 8,  Binary system  0000 1000
  ...
}

You can see from the code above ,patchFlag The initial value of 0, Every time the patchFlag It's all about execution | ( or ) operation . If the current node is only a dynamic text child node and has dynamic style attribute , final patchFlag by 5( Binary system :0000 0101).

<p :style="{ color: color }">name: {{name}}</p>
patchFlag = 0
patchFlag |= PatchFlags.STYLE
patchFlag |= PatchFlags.TEXT
//  Or operations : Only one of the two corresponding bits is 1, It turns out that the corresponding bit is 1.
// 0000 0001
// 0000 0100
// ------------
// 0000 0101  =>   Decimal system  5

patchFlag

We put the code above in Vue3 Run in :

const app = Vue.createApp({
  data() {
    return {
      color: 'red',
      name: 'shenfq'
    }
  },
  template: `<div>
      <p :style="{ color: color }">name: {{name}}</p>
  </div>`
})

app.mount('#app')

Last generated render The method is as follows , Basically consistent with our previous description .

function render() {}

render Optimize

Vue3 In virtual DOM Diff when , Will take out patchFlag And what needs to be done diff Type to proceed &( And ) operation , If the result is true Before entering the corresponding diff.

patchFlag  Judge

Take the previous template as an example :

<p :style="{ color: color }">name: {{name}}</p>

If at this time name Changes have taken place ,p The node enters diff Stage , This will determine patchFlag & PatchFlags.TEXT , It turns out to be true at this time , indicate p The node has text modification .

patchFlag

patchFlag = 5
patchFlag & PatchFlags.TEXT
//  Or operations : Only the corresponding two binary digits are 1 when , The result is 1.
// 0000 0101
// 0000 0001
// ------------
// 0000 0001  =>   Decimal system  1
if (patchFlag & PatchFlags.TEXT) {
  if (oldNode.children !== newNode.children) {
    //  Modify the text 
    hostSetElementText(el, newNode.children)
  }
}

But go ahead patchFlag & PatchFlags.CLASS When judging , Because the nodes are not dynamic Class, The return value is 0, So it will not be used for this node class Attributes in diff, To optimize performance .

patchFlag

patchFlag = 5
patchFlag & PatchFlags.CLASS
//  Or operations : Only the corresponding two binary digits are 1 when , The result is 1.
// 0000 0101
// 0000 0010
// ------------
// 0000 0000  =>   Decimal system  0

summary

Actually Vue3 There are a lot of performance optimizations , It's just going to be patchFlag One tenth of the content was given out ,Vue3 Before it was officially released, it was said that Diff The process goes through patchFlag To optimize performance , So I'm going to look at his optimization logic , On the whole, there are still gains .
image

版权声明
本文为[Shenfq]所创,转载请带上原文链接,感谢

Scroll to Top