6. 5. Smart Mattress

From (a) small dishes of chicken Vue source reading – new Vue () to do something, we say the entrance file is platforms/web/entry – runtime. Js, but this is actually the runtime Vue, actually Vue whole also contains a compiler (compile), Since Vue allows us to declare components as template strings, rendering still requires a render function, and the vue compiler mainly does a function that converts template strings into render functions.

A call to $mount during initialization mounts the component instance

Vue.prototype._init = function (options) {...// Initialize a bunch of things
  // Mount the component instance
  if(vm.$options.el) { vm.$mount(vm.$options.el); }}Copy the code

The flow chart

1. Vue compiler entry file

platforms/web/entry-runtime-with-compiler.js

Prototype.$mount,template there are three ways to write template, for these three ways need to do a uniform conversion to the template string

  1. String template
var vm = new Vue({
  el: '#app'.template: 
      
Template string
}) Copy the code
  1. The selector matches the elementinnerHTMLThe template
<div id="app">
  <div>test1</div>
  <script type="x-template" id="test">
    <p>test</p>
  </script>
</div>
var vm = new Vue({
  el: '#app'.template: '#test'
})
Copy
Copy the code
  1. domElement matches the elementinnerHTMLThe template
<div id="app">
  <div>test1</div>
  <span id="test"><div class="test2">test2</div></span>
</div>
var vm = new Vue({
  el: '#app'.template: document.querySelector('#test')})Copy the code

To summarize

  • Uniformly converted to template strings
  • generaterenderandstaticRenderFnsfunction
  • As with runtime logic, the original is called$mountFunction of the mount
// platforms/web/runtime/index.js
Vue.prototype.$mount = function (el? : string | Element,hydrating? : boolean) :Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

// platforms/web/entry-runtime-with-compiler.js
// $mount is already declared at runtime/index. It is a function that does not need to be compiled
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el? : string | Element,hydrating? : boolean) :Component {
  el = el && query(el)

  // ...
  const options = this.$options
  
  if(! options.render) {let template = options.template
    if (template) {
      if (typeof template === 'string') {
        // Selector matches
        if (template.charAt(0) = = =The '#') {
          template = idToTemplate(template)
          
          // ...
        }
      // DOM elements match
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        // ...
        return this
      }
    // If template is not passed, use el as the root node to create the template
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      // ...
      
      // Generate the render function
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV ! = ='production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
      options.render = render
      options.staticRenderFns = staticRenderFns
      // ...}}// Call the original logic
  return mount.call(this, el, hydrating)
}
Vue.compile = compileToFunctions
Copy the code

2. Generation of render function

The flow chart

compileToFunctions

Convert the template string to the Render function

This function takes a template string, a compile configuration, and a VM instance

Vue.prototype.$mount = function () {...if(! options.render) {var template = options.template;
    if (template) {
      var ref = compileToFunctions(template, {
          outputSourceRange: "development"! = ='production'.shouldDecodeNewlines: shouldDecodeNewlines,
          shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
          // Plain text inserts delimiters
          delimiters: options.delimiters,
          // Whether to keep comments in the template
          comments: options.comments
        }, this);
        varrender = ref.render; }... }}Copy the code

Now we’re going to go to compileToFunctions where does this function come from

createCompiler

Build compiler

// platforms/web/compiler/index.js
const { compile, compileToFunctions } = createCompiler(baseOptions)

// compiler/index.js
// The argument passed is the core code for the entire compilation
export const createCompiler = createCompilerCreator(function baseCompile (template: string,options: CompilerOptions) :CompiledResult {
  // Parse templates into abstract syntax trees
  const ast = parse(template.trim(), options)
  // The syntax tree will be optimized if the parameter configuration is optimized
  if(options.optimize ! = =false) {
    optimize(ast, options)
  }
  // The modified AST is then regenerated back to the code
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})
Copy the code

createCompilerCreator

The builder that generates the compiler

The createCompilerCreator function has only one purpose. It uses the idea of partial functions to cache the base compiler method baseCompile and return a programmer generator.

// compiler/create-compiler.js
export function createCompilerCreator (baseCompile: Function) :Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (template: string,options? : CompilerOptions) :CompiledResult {
      const finalOptions = Object.create(baseOptions)
      const errors = []
      const tips = []

      let warn = (msg, range, tip) = > {
        (tip ? tips : errors).push(msg)
      }

      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 the other attributes
        for (const key in options) {
          if(key ! = ='modules'&& key ! = ='directives') {
            finalOptions[key] = options[key]
          }
        }
      }

      finalOptions.warn = warn

      // The actual compiler function is executed
      const compiled = baseCompile(template.trim(), finalOptions)
      // ...
      compiled.errors = errors
      compiled.tips = tips
      return compiled
    }

    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile)
    }
  }
}
Copy the code

createCompileToFunctionFn

CreateCompileToFunctionFn using the concept of closure, the compiled template cache, the cache before compiled the results will be preserved, the cache can be used to avoid repeated compile waste caused by the performance. CreateCompileToFunctionFn will eventually compileToFunctions method returns.

// compiler/to-function.js
 function createCompileToFunctionFn (compile) {
    var cache = Object.create(null);

    return function compileToFunctions (template,options,vm) { options = extend({}, options); ...// Caching is useful to avoid wasting performance by compiling the same template repeatedly
      const key = options.delimiters
      ? String(options.delimiters) + template
      : template
      if (cache[key]) {
        return cache[key]
      }
      // Execute the compile method
      varcompiled = compile(template, options); ...// turn code into functions
      var res = {};
      var fnGenErrors = [];
      // The compiled function body string is passed as an argument to createFunction, which returns the final render function
      res.render = createFunction(compiled.render, fnGenErrors);
      res.staticRenderFns = compiled.staticRenderFns.map(function (code) {
        returncreateFunction(code, fnGenErrors) }); ...return (cache[key] = res)
    }
  }
  
 function createFunction (code, errors) {
      try {
        return new Function(code)
      } catch (err) {
        errors.push({ err, code })
        return noop
      }
}
Copy the code

conclusion

createCompilerCreator(baseCompiler)(baseOptions).compileToFunctions(template,options,vm)
Copy the code

Here the compiler logic understanding is very uncomfortable, the author mainly refers to the instance mount process and template compilation · in-depth analysis of Vue source code (penblog.cn), here the idea of partial function refers to learning, The parse and generate functions in createCompilerCreator are the most important part of compiling, but they are so complicated that I won’t go into them here.

3. From render function to VDOM

Now we need to convert the render function to VDOM, after converting the VDOM to the real DOM and mounting it to the page, back to $mount where we started

Vue.prototype.$mount = function (el? : string | Element,hydrating? : boolean) :Component { 
    el = el && inBrowser ? query(el) : undefined 
    return mountComponent(this, el, hydrating) 
}

const mount = Vue.prototype.$mount 
Vue.prototype.$mount = function (el? : string | Element,hydrating? : boolean) :Component {
    // ...
    return mount.call(this, el, hydrating)
}
Copy the code

mountComponent

  • beforeMountandmountedCall to the hook function
  • Create an instance of Watcher where the updated callback function isupdateComponentContinue to see laterupdateComponentThe declaration and implementation of this function
// core/instance/lifecycle.js
export function mountComponent (vm: Component,el: ? Element,hydrating? : boolean) :Component {
  vm.$el = el
  // ...
  
  // Call beforeMount hook
  callHook(vm, 'beforeMount')

  updateComponent = () = > {
      vm._update(vm._render(), hydrating)
  }
  new Watcher(vm, updateComponent, noop, {
    before () {
      if(vm._isMounted && ! vm._isDestroyed) { callHook(vm,'beforeUpdate')}}},true /* isRenderWatcher */)
  hydrating = false

  if (vm.$vnode == null) {
    vm._isMounted = true
    // Call mounted hook
    callHook(vm, 'mounted')}return vm
}
Copy the code

vm._render

RenderMixin () declares the prototype method _render, which converts the render function into a Virtual DOM.

export function renderMixin (Vue: Class<Component>) {
  // ...

  Vue.prototype._render = function () :VNode {
    // ...
    try {
      currentRenderingInstance = vm
      // Generate the virtual DOM
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      // ...
      vnode = vm._vnode
    } finally {
      currentRenderingInstance = null
    }
    // ...
    return vnode
  }
}
Copy the code

Vnode = render. Call (vm._renderProxy, vm.$createElement)

vm.renderProxy

This variable is actually added to _init in the initialization instance

Vue.prototype._init = function (options? :Object) {
 // ...
 if(process.env.NODE_ENV ! = ='production') {
     // Data filtering detection in the development environment
     initProxy(vm)
   } else {
     // Production is the vue instance itself
     vm._renderProxy = vm
   }
 // ...
}
 
initProxy = function initProxy (vm) {
   if (hasProxy) {
     // determine which proxy handler to use
     const options = vm.$options
     const handlers = options.render && options.render._withStripped
       ? getHandler
       : hasHandler
     vm._renderProxy = new Proxy(vm, handlers)
   } else {
     vm._renderProxy = vm
   }
 }
Copy the code

vm.$createElement

$c and $createElement differ only in the last parameter of initRender (_init)

  • $c: internal call in the render function converted via template string
  • $createElementPass in the render function as an argument when you write it by hand
function initRender(vm) {
    // ...
   vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }
   vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
   // ...
}

// core/vdom/create-element.js
function createElement (
   context, / / vm instances
   tag, / / label
   data, // Node-related data, attributes
   children, / / child nodes
   normalizationType,
   alwaysNormalize // Distinguish between internal compiled render and handwritten render
 ) {
   If there is no data, the third parameter is used as the fourth parameter, and so on.
   if (Array.isArray(data) || isPrimitive(data)) {
     normalizationType = children;
     children = data;
     data = undefined;
   }
   // alwaysNormalize distinguishes between internal compilation and user handwritten render
   if (isTrue(alwaysNormalize)) {
     normalizationType = ALWAYS_NORMALIZE;
   }
   // How to actually generate a Vnode
   return _createElement(context, tag, data, children, normalizationType) 
 }

Copy the code

_createELement

Validation is performed on the incoming data, which guarantees subsequent VDOM generation

  • The data objectdataIt cannot be reactive data
  • tagwithisYou have to do something special when you do dynamics
  • keyThe value must be the original data type
  • nativeDOMShould not be usednativeThe modifier
  • . Others don’t read the whole story and don’t seem to make much sense

Render is divided into two functions to convert children depending on whether the render is input by the user or generated by the system

  • normalizeChildren: user input, merge of text nodes, recursive call
  • simpleNormalizeChildren: generated by the system, mainly to do an array level flattening
export function _createElement (context: Component, tag? : string | Class<Component> |Function | Object, data? : VNodeData, children? : any, normalizationType? : number) :VNode | Array<VNode> {
  // 1. Data objects cannot be reactive data defined in the Vue data attribute.
  if(isDef(data) && isDef((data: any).__ob__)) { process.env.NODE_ENV ! = ='production' && warn(
      `Avoid using observed data object as vnode data: The ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render! ',
      context
    )
    return createEmptyVNode()
  }
  // 2. Special processing is required when using is as a dynamic tag
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  if(! tag) {return createEmptyVNode()
  }
  // 3. The key value must be the original data type
  if(process.env.NODE_ENV ! = ='production'&& isDef(data) && isDef(data.key) && ! isPrimitive(data.key) ) {if(! __WEEX__ || ! ('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // The first element of the children function type is used as the default slot
  if (Array.isArray(children) &&
    typeof children[0= = ='function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  
  // This is what I said before, the difference between self-written render and framework-generated render
  // If children is a simple data type, the text virtual DOM needs to be generated
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  // ...
}

// core/vdom/helpers/normalize-children.js
// Handle the compile-generated render function
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    // If the child node is an array, perform the flattening operation to create a one-dimensional array.
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// Handle the user-defined render function
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    // Generate a literal node
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

function normalizeArrayChildren (children: any, nestedIndex? : string) :Array<VNode> {
  const res = []
  let i, c, lastIndex, last
  
  // Iterate over the child nodes
  for (i = 0; i < children.length; i++) {
    c = children[i]
    if (isUndef(c) || typeof c === 'boolean') continue
    lastIndex = res.length - 1
    last = res[lastIndex]
   
    // The current child is an array
    if (Array.isArray(c)) {
      if (c.length > 0) {
        // Recurse for children
        c = normalizeArrayChildren(c, `${nestedIndex || ' '}_${i}`)
        // If the first child of the currently traversed child node is a literal node, and the last byte point of the current literal node is also, the two are merged and the first byte point of the child node is removed
        if (isTextNode(c[0]) && isTextNode(last)) {
          res[lastIndex] = createTextVNode(last.text + (c[0]: any).text)
          c.shift()
        }
        res.push.apply(res, c)
      }
    // The current child node is a simple data type
    } else if (isPrimitive(c)) {
      // Continue merging
      if (isTextNode(last)) {
        // Merge the last text node with the last one
        res[lastIndex] = createTextVNode(last.text + c)
      } else if(c ! = =' ') {
        // If it is not a text node, create a text node for the current text and insert it at the end
        res.push(createTextVNode(c))
      }
    } else {
      // The current traversal is a text node, and the last is also a text node, then merge
      if (isTextNode(c) && isTextNode(last)) {
        res[lastIndex] = createTextVNode(last.text + c.text)
      } else {
        // Add a key value if the key value does not exist and is a list
        if (isTrue(children._isVList) &&
          isDef(c.tag) &&
          isUndef(c.key) &&
          isDef(nestedIndex)) {
          c.key = `__vlist${nestedIndex}_${i}__ `
        }
        res.push(c)
      }
    }
  }
  return res
}
Copy the code

4. Map from VDOM to real DOM

updateComponent = function () {
    // render generates the virtual DOM, update renders the real DOM
    vm._update(vm._render(), hydrating);
};
Copy the code

vm._update

The change method is added at lifecycleMixin()

function lifecycleMixin() {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        // prevVnode is the old vNode node
        var prevVnode = vm._vnode; 
        // Check whether there are old nodes to determine whether it is the first rendering or data update
        // First render
        if(! prevVnode) { vm.$el = vm.__patch__(vm.$el, vnode, hydrating,false)}else {
            // Data updatevm.$el = vm.__patch__(prevVnode, vnode); }}Copy the code

__patch__

  • reatePatchFunctionMethod passes an object as a parameter that has two properties,nodeOpsandmodules.nodeOpsEncapsulates a series of operational primitivesDOMObject method. whilemodulesDefines a hook function for a module
  • createPatchFunctionThe function has over a thousand lines and I’m not going to list them here, but basically what’s going on inside it first defines a bunch of helper methods, and the core is through calls, okaycreateElmmethodsdomOperation, create node, insert child node, recursively create a completeDOMTree and insert intoBodyIn the. And in the phase of producing reality, there will bediffAlgorithm to determine before and afterVnodeIn order to minimize the real phase of change. There will be a chapter to explain it laterdiffAlgorithm.createPatchFunctionYou just need to remember a few conclusions, and the function will be called internally wrappedDOM api, according to theVirtual DOMTo generate real nodes. If a component is encounteredVnode, the mount procedure of the child component is recursively invoked

The number.

// platforms/web/runtime/index.js
Vue.prototype.__patch__ = inBrowser ? patch : noop

// platforms/web/runtime/patch.js
export const patch: Function = createPatchFunction({ nodeOps, modules })

// Freeze a collection of methods that manipulate dom objects
 var nodeOps = /*#__PURE__*/Object.freeze({
    createElement: createElement$1.createElementNS: createElementNS,
    createTextNode: createTextNode,
    createComment: createComment,
    insertBefore: insertBefore,
    removeChild: removeChild,
    appendChild: appendChild,
    parentNode: parentNode,
    nextSibling: nextSibling,
    tagName: tagName,
    setTextContent: setTextContent,
    setStyleScope: setStyleScope
  });

// Defines the module's hook function
  var platformModules = [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
  ];

var modules = platformModules.concat(baseModules);
Copy the code
Copy the code