vue的mount内容

本文最后更新于:1 个月前

Vue中的mount过程

Vue中的mount过程包含很多内容,因为Vue中是使用数据驱动模板的,所以我们写出来的模板是不能直接渲染到页面中的,那么mount过程中就需要解析模板转成可以被识别的内容。

vue runtime-only版本的$mount定义的位置

src/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)
}

判断是否有el和是否是浏览器环境,然后使用query函数去获取挂载点。
之后重点的挂载函数在mountComponent

src/core/instance/lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
      const name = vm._name
      const id = vm._uid
      const startTag = `vue-perf-start:${id}`
      const endTag = `vue-perf-end:${id}`

      mark(startTag)
      const vnode = vm._render()
      mark(endTag)
      measure(`vue ${name} render`, startTag, endTag)

      mark(startTag)
      vm._update(vnode, hydrating)
      mark(endTag)
      measure(`vue ${name} patch`, startTag, endTag)
    }
  } else {
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

在此函数中会使用callHook来调用我们两个生命周期(beforeMount,mounted)中定义的函数。

  • 如果没有render会使用vm.$options.render = createEmptyVNode来赋予一个render函数,随后在const vnode = vm._render()中调用render()获取对应的vnode,并调用_update来挂载实例,最后返回vm
    在_init执行的最后会调用vm.$mount(vm.$options.el)来自动挂载。

runtime with compiler版本的$mount定义的位置

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

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  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直接忽视template
  if (!options.render) {
    let template = options.template
    //如果有template则用template的内容
    if (template) {
      if (typeof template === 'string') {
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (process.env.NODE_ENV !== 'production' && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
      //如果没有template的话,就采用el的innerHtml
    } else if (el) {
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      //进入src/compiler/to-function.js
      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 

      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  return mount.call(this, el, hydrating)
}

处理过程

  1. 首先缓存下挂载的mount,之后重写$mount增加template转render的方法
  2. 然后处理el,调用query方法
    export function query (el: string | Element): Element {
      if (typeof el === 'string') {
        const selected = document.querySelector(el)
        if (!selected) {
          process.env.NODE_ENV !== 'production' && warn(
            'Cannot find element: ' + el
          )
          return document.createElement('div')
        }
        return selected
      } else {
        return el
      }
    }
  3. 获得挂载点之后,先判断挂载点是否正确,因为模板替换的时候是直接全部替换掉,所以挂载点不能在body和html的位置,一旦挂载在这两个位置,那么html的基本结构就被破坏,浏览器无法处理。
  4. 判断在options內存在已经定义好的render函数,如果存在,那么后续操作都不需要了,毕竟结果已经被使用者定义好了
  5. 判断是否存在template,如果存在,则对template进行操作,如果不存在就去el中拿到对应的innerHTML,最终结果都是拿到对应的template模板
    5.1 template如果传入的是字符串,就调用idToTemplate
    缓存的同时获取节点上的innerHTML
    5.2 template如果传入的是node节点,则直接去获取节点的innerHTML
    const idToTemplate = cached(id => {
      const el = query(id)
      return el && el.innerHTML
    })
  6. 最后template有内容的话调用compileToFunctions方法把模板转换成render函数

    src/compiler/to-function.js

    export function createCompileToFunctionFn (compile: Function): Function {
      const cache = Object.create(null)
    
      //将template转成render函数
      return function compileToFunctions (
        template: string,
        options?: CompilerOptions,
        vm?: Component
      ): CompiledFunctionResult {
        options = extend({}, options)
        const warn = options.warn || baseWarn
        delete options.warn
    
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production') {
          // detect possible CSP restriction
          try {
            new Function('return 1')
          } catch (e) {
            if (e.toString().match(/unsafe-eval|CSP/)) {
              warn(
                'It seems you are using the standalone build of Vue.js in an ' +
                'environment with Content Security Policy that prohibits unsafe-eval. ' +
                'The template compiler cannot work in this environment. Consider ' +
                'relaxing the policy to allow unsafe-eval or pre-compiling your ' +
                'templates into render functions.'
              )
            }
          }
        }
    
        // check cache
        const key = options.delimiters
          ? String(options.delimiters) + template
          : template
        if (cache[key]) {
          return cache[key]
        }
    
        // compile
        const compiled = compile(template, options)
    
        // check compilation errors/tips
        if (process.env.NODE_ENV !== 'production') {
          if (compiled.errors && compiled.errors.length) {
            if (options.outputSourceRange) {
              compiled.errors.forEach(e => {
                warn(
                  `Error compiling template:\n\n${e.msg}\n\n` +
                  generateCodeFrame(template, e.start, e.end),
                  vm
                )
              })
            } else {
              warn(
                `Error compiling template:\n\n${template}\n\n` +
                compiled.errors.map(e => `- ${e}`).join('\n') + '\n',
                vm
              )
            }
          }
          if (compiled.tips && compiled.tips.length) {
            if (options.outputSourceRange) {
              compiled.tips.forEach(e => tip(e.msg, vm))
            } else {
              compiled.tips.forEach(msg => tip(msg, vm))
            }
          }
        }
    
        // turn code into functions
        const res = {}
        const fnGenErrors = []
        res.render = createFunction(compiled.render, fnGenErrors)
        res.staticRenderFns = compiled.staticRenderFns.map(code => {
          return createFunction(code, fnGenErrors)
        })
    
        // check function generation errors.
        // this should only happen if there is a bug in the compiler itself.
        // mostly for codegen development use
        /* istanbul ignore if */
        if (process.env.NODE_ENV !== 'production') {
          if ((!compiled.errors || !compiled.errors.length) && fnGenErrors.length) {
            warn(
              `Failed to generate render function:\n\n` +
              fnGenErrors.map(({ err, code }) => `${err.toString()} in\n\n${code}\n`).join('\n'),
              vm
            )
          }
        }
    
        return (cache[key] = res)
      }
    }
    
    这里使用compile函数去将template转换成ast,然后使用generate函数将ast转换成对应的render函数

    src/compiler/index.js

    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
      }
    })
    generate函数使用genElement将ast转换成对应的结构,genElement的相关代码也在此文件中。

    src/compiler/codegen/index.js

    export function generate (
      ast: ASTElement | void,
      options: CompilerOptions
    ): CodegenResult {
      const state = new CodegenState(options)
      // fix #11483, Root level <script> tags should not be rendered.
      const code = ast ? (ast.tag === 'script' ? 'null' : genElement(ast, state)) : '_c("div")'
      return {
        render: `with(this){return ${code}}`,
        staticRenderFns: state.staticRenderFns
      }
    }
  7. 最后调用缓存下来的mount去渲染dom

本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议 ,转载请注明出处!