Vue源码解析系列——数据驱动篇:$mount执行过程

准备

vue版本号2.6.12,为方便分析,选择了runtime+compiler版本。

回顾

如果有感兴趣的同学可以看看我之前的源码分析文章,这里呈上链接:《Vue源码分析系列:目录》

runtime-with-compiler的$mount

首先映入眼帘的缓存了是Vue原型上的$mount方法。然后重新定义了$mount

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

//..somecode

import Vue from './runtime/index'

//..somecode

//缓存原先的$mount方法(./runtime/index)
const mount = Vue.prototype.$mount
//重新定义$mount方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
	/*...somecode*/
}

下面的$mount中的代码有些多,我们一块一块看。
这里按照Vue使用者传入的el来查询出响应的DOM元素。并判断是否为body或者html根元素。

//查询el DOM对象
  el = el && query(el)

  /* istanbul ignore if */
  //判断el是否为body或者html根元素
  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
  }

这下面的一整块代码都是为了编译出render函数,毕竟最后Vue只会执行render函数。
具体表现为:判断template并解析成一个DOM字符串,编译template成render函数。

const options = this.$options

  // resolve template/el and convert to render function
  /**
   * 以下是对render函数解析
   * 如果没有定义render,就转换template为render
   * 具体表现为:判断template并解析成一个DOM字符串,编译template成render函数
   * 最终调用mount
   */
  //是否有定义render函数
  if (!options.render) {
    let template = options.template
    /**
     * 这边一整块都是来获取template
     * 最后获得的template就是一个DOM字符串
     */
    //是否有定义template
    if (template) {
      //如果template是个字符串,获取html字符串
      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) {
        //如果是个dom元素,直接获取innerHTML
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      //如果没有定义template但是定义了el,就获取el的outerHTML赋值给template
      template = getOuterHTML(el)
    }
     /**
     * 以下为编译template相关
     */
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      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)

最终会调用mount并返回,这个mount就是之前缓存下来的$mount
进入原先定义的$mount。在'./runtime/index'文件中。
进入'./runtime/index'

runtime 的 $mount

这里在Vue的原型上定义了$mount方法。方法返回了mountComponent执行后的值,进入mountComponent

/*src/platforms/web/runtime/index.js*/

//...somecode

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

//...somecode

mountComponent

找到mountComponent方法的定义。
这个方法是用来挂载组件,也就是挂载DOM。先后调用了两个生命周期函数beforeMountmounted

/* src/core/instance/lifecycle.js */
/**
 * 挂载组件
 *
 * @param vm
 * @param el
 * @param hydrating
 * @returns {Component}
 */
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  //缓存el元素
  vm.$el = el
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode
    if (process.env.NODE_ENV !== 'production') {
      /* istanbul ignore if */
      //如果用了runtime-only版本的vue,并且定义了template,会报如下警告
      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) {
    /**
     * vue性能埋点
     * 可忽略
     */
    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: ( arg1:VNode , arg2:SSR相关 ) => {}
    updateComponent = () => {
      vm._update(vm._render(), hydrating)
    }
  }

以上代码的核心的在updateComponent方法的定义,这个方法负责调用vm._update。之后将updateComponent放入一个新建的渲染watcher实例。

watcher

进入watcher的定义可以看到将传入的updateComponent放入了实例属性的getter上。
注:下面的expOrFn就是传入的updateComponent,详情可见wathcerconstructor

/* src/core/observer/watcher.js */
// parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn
    } else {
      this.getter = parsePath(expOrFn)
      if (!this.getter) {
        this.getter = noop
        process.env.NODE_ENV !== 'production' && warn(
          `Failed watching path: "${expOrFn}" ` +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        )
      }
    }

watcherget实例方法上可以看到this.getter的调用

get () {
    pushTarget(this)
    let value
    const vm = this.vm
    try {
    //这里调用了getter,也就是updateComponent
      value = this.getter.call(vm, vm)
    } catch (e) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

然后我们再回到mountComponent

if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm

最后在vm实例上标记了_isMounted,调用了生命周期函数mount
至此,整个$mount过程分析完毕。

总结

其实$mount最终都会执行./runtime/index.js里面的$mount。这里的$mount才是真正的mount执行过程。而entry-runtime-width-compiler.js里面的$mount其实就是将template编译成了render函数,然后丢给真正的$mount去执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

爱学习的前端小黄

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值