美文网首页
从源码的角度分析Vue 独立构建和运行时构建

从源码的角度分析Vue 独立构建和运行时构建

作者: hello_小丁同学 | 来源:发表于2020-11-21 12:59 被阅读0次

vue有两种不同的构建方式:

  • 独立构建:包含模板编译器,渲染过程 HTML字符串 → render函数 → VNode → 真实DOM节点
  • 运行时构建:不包含模板编译器,渲染过程 render函数 → VNode → 真实DOM节点

使用方式的差异

下面通过具体的代码进行分析:
项目使用webpack打包
在main.js创建vue实例的时候,一般有以下方式

  1. 指定template模板
import Vue from 'vue'
new Vue({
    el: '#app',
    template: "<div>{{msg}}</div>",
    data: {
        msg: 'hhh'
    }
})
  1. 使用渲染函数
import Vue from 'vue'
import App from './pages/App.vue'
new Vue({
    render: h => h(App)
}).$mount('#app')

如果通过是通过import的方式引入Vue,在使用第一种方式的时候会报错,第二种方式是可以成功运行的,提示:


image.png

这是因为vue默认引入的包是运行时版本的,不包含模板编译器,也就没有办法处理template字符串。可以在webpack配置文件中如下配置,引入完整版的vue包:

  resolve: {
    alias: {
        vue: 'vue/dist/vue.js'
    },
  },

这种方式就属于独立构建,会在浏览器运行的时候对template进行编译处理。
这种方式不推荐,具体原因看下面分析。

如果细心一点会发现,上面第二种创建Vue实例的方式,会引入一个App.vue,这个文件也是包含template模板的,那为何可以正常运行呢,是因为vue-loader会使用vue-template-compiler在打包阶段就会把vue文件里的template模板编译成渲染函数。这种在打包阶段就编译完成的方式,要比在运行时再去编译性能更好。
vue-template-compiler的说明:


image.png

进入Vue源码

vue源码版本是2.6.11
独立构建使用的是带有编辑器版本的Vue,挂载函数$mount是在中定义的src/platform/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
  if (!options.render) {
    let template = options.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
      }
    } else if (el) {
      template = getOuterHTML(el)
    }
    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)
}

整体逻辑是,若是render函数,会将template处理成渲染函数,这里包含两种情况:1、有template属性,就直接处理成渲染函数,如果没有,若没有则会把el节点作为template,然后处理成渲染函数,在函数末尾调用mount.call(this, el, hydrating),这个是在src/platform/web/runtime/index.js 中定义的。若是采用运行时构建会挂载vue时直接调用这里的$mount,

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

// devtools global hook
/* istanbul ignore next */
if (inBrowser) {
  setTimeout(() => {
    if (config.devtools) {
      if (devtools) {
        devtools.emit('init', Vue)
      } else if (
        process.env.NODE_ENV !== 'production' &&
        process.env.NODE_ENV !== 'test'
      ) {
        console[console.info ? 'info' : 'log'](
          'Download the Vue Devtools extension for a better development experience:\n' +
          'https://github.com/vuejs/vue-devtools'
        )
      }
    }
    if (process.env.NODE_ENV !== 'production' &&
      process.env.NODE_ENV !== 'test' &&
      config.productionTip !== false &&
      typeof console !== 'undefined'
    ) {
      console[console.info ? 'info' : 'log'](
        `You are running Vue in development mode.\n` +
        `Make sure to turn on production mode when deploying for production.\n` +
        `See more tips at https://vuejs.org/guide/deployment.html`
      )
    }
  }, 0)
}

最终会调用 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
}

在本文最开始导致的报错信息也是在这个函数里面定义的。这里会创建一个Watcher实例,同时会在Watcher实例的get方法中执行作为参数传入的updateComponent,updateComponent里面主要是调用vm._render()生成虚拟dom和vm._update(vnode, hydrating)将虚拟dom渲染到页面上。下面看下代码细节
先看vm._render(),这个函数位于src/core/instance/render.js

Vue.prototype._render = function (): VNode {
    const vm: Component = this
    const { render, _parentVnode } = vm.$options

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }

主要是执行我们传入的render函数,同时把vm.$createElement作为参数参过去,就是这里的h

render: h => h(App)

createElement最终返回的是vnode类型的实例,也就是常说的虚拟dom,
下面再看vm._update(vnode, hydrating),这个函数位于src/core/instance/lifecycle.js

  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
      vm.$parent.$el = vm.$el
    }
    // updated hook is called by the scheduler to ensure that children are
    // updated in a parent's updated hook.
  }

最终渲染函数位于src/core/vdom/patch.js文件,其实就是根据虚拟dom,调用js原生方法创建真实dom。

参考:
https://vue-js.com/learn-vue/lifecycle/templateComplie.html#_1-%E5%89%8D%E8%A8%80
https://www.open-open.com/lib/view/open1524302857219.html#articleHeader0
https://ustbhuangyi.github.io/vue-analysis/v2/data-driven/mounted.html#%E6%80%BB%E7%BB%93

相关文章

  • 从源码的角度分析Vue 独立构建和运行时构建

    vue有两种不同的构建方式: 独立构建:包含模板编译器,渲染过程 HTML字符串 → render函数 → VNo...

  • Vue.js知识点

    独立构建和运行时构建 有两种构建方式,独立构建和运行构建。它们的区别在于前者包含模板编译器而后者不包含。 模板编译...

  • vue独立构建和运行构建

    有两种构建方式,独立构建和运行构建。它们的区别在于前者包含模板编译器而后者不包含。 模板编译器:模板编译器的职责是...

  • vue中踩的坑(转)

    vue2.x1.独立构建vs运行时构建在按照vue1.0的配置配置好webpack后,会出现Failed to m...

  • 深入Vue - 源码目录及构建过程分析

    摘要: Vue源码阅读第一步。 原文:深入vue - 源码目录及构建过程分析 公众号:前端小苑 Fundebug经...

  • Vue源码之目录结构

    Vue版本:2.6.9 源码结构图 Vue 不同的构建版本对比 术语解释 完整版:同时包含编译器和运行时的版本。 ...

  • 2021-03-30 vue-small

    Vue的响应式系统构建 Vue比较复杂的,直接进入源码分析构建的每一个流程会让理解变得困难 对Dep,Watche...

  • spring-beans 深入之spring源码构建

    我觉得不分析源码的讲解都不是好的讲解所以我还是直接从源码开始分析。首先来讲解构建源码项目:源码构建过程其实还是比较...

  • 从源码的角度分析Vue视图更新和nexttick机制

    这篇文章从vue源码的角度分析,为什么在this.$nextTick回调里面才能看到视图更新了。 TL;DR th...

  • Vue源码——准备工作

    源码目录 Vue.js 的源码都在 src 目录下,结构如下: 源码构建 Vue.js 源码是基于 Rollup ...

网友评论

      本文标题:从源码的角度分析Vue 独立构建和运行时构建

      本文链接:https://www.haomeiwen.com/subject/zjnsiktx.html