阅读 vue 2.5.2 源码产生的疑问点及解释

207 阅读5分钟

1、vue源码中的函数、变量、关键字的解读

  • Vue:Vue构造函数,用于创建Vue实例。
  • Vue实例:通过Vue构造函数创建的实例,它具有一些内置的属性和方法。
  • 模板(template):Vue中用于渲染视图的声明性HTML模板。
  • 渲染函数(render function):在Vue中,将模板转化为虚拟DOM的JavaScript函数。
  • 虚拟DOM(Virtual DOM):Vue内部使用的一种表示DOM节点树的JavaScript对象。
  • 生命周期钩子函数(lifecycle hooks):Vue实例在创建、更新、销毁等不同阶段触发的一系列回调函数,可以在这些回调函数中执行一些特定的逻辑。
  • 指令(directive):在模板中使用的带有v-前缀的特殊属性,用于在DOM元素上添加特殊的行为。
  • 计算属性(computed property):Vue实例中用于派生其他属性的属性,可以缓存计算结果以提高性能。
  • 监听器(watcher):用于监听Vue实例中数据变化的对象,当数据变化时执行相应的回调函数。
  • 组件(component):在Vue中封装了一些可重用的UI组件或功能模块,可以用来构建更复杂的用户界面。

2、方法或者属性定义,根据单词分析大概含义再带着目的阅读

  • lifecycle:生命周期相关的一些方法和钩子函数,包括生命周期的触发顺序、生命周期函数的具体作用等
  • state:Vue 实例状态相关的一些方法,包括数据劫持相关的方法、watch 监听相关的方法等
  • render:与渲染相关的一些方法和函数,包括 Vue 的渲染过程、虚拟 DOM 相关的方法、渲染函数的使用等
  • events:与事件相关的一些方法和函数,包括事件绑定、事件触发、事件冒泡等
  • directive:指令相关的一些方法和函数,包括自定义指令的注册、使用和实现等
  • filter:过滤器相关的一些方法和函数,包括自定义过滤器的注册、使用和实现等
  • mixin:混入相关的一些方法和函数,包括混入对象的合并、覆盖规则等
  • component:组件相关的一些方法和函数,包括组件注册、组件通信等
  • template:模板相关的一些方法和函数,包括模板解析、模板编译、模板渲染等
  • compiler:编译相关的一些方法和函数,包括模板编译器的实现、AST 抽象语法树的生成等
  • observer:观察者相关的一些方法和函数,包括数据的双向绑定实现原理、响应式数据的实现等

3、/platforms/web/entry-runtime-with-compiler.js和/platforms/web/runtime/index.js中的$mount是什么关系?

  • 问题1: entry-runtime-with-compiler.js为什么要重新创建$mount?
  • 问题2: 那entry-runtime-with-compiler.js和web/runtime/index.js中的$mount是什么关系?

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

/* @flow */

import config from 'core/config'
import { warn, cached } from 'core/util/index'
import { mark, measure } from 'core/util/perf'

import Vue from './runtime/index'
import { query } from './util/index'
import { shouldDecodeNewlines } from './util/compat'
import { compileToFunctions } from './compiler/index'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})

const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 判断 el 是否存在,并将其转换为 DOM 元素(两者需都满足)
  el = el && query(el)

  /**
   * 这段代码的作用是防止将 Vue 实例挂载到 document.body 或 document.documentElement 上。
   * 这是因为 Vue 在挂载时会将模板编译成真正的 DOM 元素,并替换挂载点的内容,如果将 Vue 实例挂载到 <body> 或 <html> 上,可能会破坏原有的文档结构。
   * 因此,这里进行了判断,如果挂载点是 <body> 或 <html>,则会发出警告,并返回当前 Vue 实例。
   */
  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `不要将 Vue 挂载到 <html> 或 <body> 元素上 - 而是挂载到普通元素。`
    )
    return this
  }

  /**
   * 这行代码是获取 Vue 实例的配置对象,也就是 new Vue(options) 中传递的参数,
   * 它在 Vue 实例化时就会被保存在 $options 中,后续可以通过 this.$options 访问这些参数。
   * 在这个方法中,获取 $options 的目的是为了检测 Vue 实例是否包含 render 函数并赋值给 vm.$options.render,以便后续使用。
   */
  const options = this.$options

  /**
   *
    下面这段代码主要是用于将 template 或 el 转化为 render 函数(在Vue中,将模板转化为虚拟DOM的JavaScript函数。)。
    首先会判断 options 中是否已经有了 render 函数,如果有,则直接使用该函数,否则会继续处理 template 和 el。
   */
  if (!options.render) {
    let template = options.template
    if (template) {
      // 如果有 template,则会进行处理,将其转换为 render 函数,转换的过程包括了将 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('无效的模版选项:' + template, this)
        }
        return this
      }
    } else if (el) {
      // 如果没有 template,但是有 el,则会根据 el 获取到对应的 DOM 元素,然后再对该元素的 innerHTML 进行编译,将其转换为渲染函数并进行缓存。
      template = getOuterHTML(el)
    }
    // 最后,如果成功获取到了渲染函数
    if (template) {
      // 则将其保存到 options 中的 render 属性中。
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }

      /**
       * 这段代码的作用是将 template 编译为 render 函数,
       * 然后将编译得到的 render 函数添加到 Vue 实例的 options 对象中,以便后续使用。
       * 具体来说,这段代码首先调用 compileToFunctions 方法
       * 将 template 编译为 render 函数和 staticRenderFns 数组,
       * 其中 render 函数表示模板的渲染函数,
       * 而 staticRenderFns 数组表示静态节点的渲染函数。
       * 接着,将 render 函数和 staticRenderFns 数组添加到 Vue 实例的 options 对象中,
       * 以便在渲染组件时使用。
       * 这样,当 Vue 实例渲染模板时,就可以直接使用编译得到的 render 函数进行渲染了。
       */
      const { render, staticRenderFns } = compileToFunctions(template, {
        shouldDecodeNewlines,
        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')
      }
    }
  }

  // 最后,调用 mount 方法,将 Vue 实例挂载到指定的 DOM 元素上。
  return mount.call(this, el, hydrating)
}

/**
 * 该函数用于获取一个元素的outerHTML,即该元素的HTML标签及其内容。
 * 如果元素的outerHTML属性存在,则直接返回该属性值,
 * 如果元素的outerHTML属性不存在,创建一个div元素作为容器,将该元素的克隆节点附加到容器中,最后返回容器的innerHTML。
 */
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

export default Vue

/platforms/web/runtime/index.js

/* @flow */

import Vue from 'core/index'
import config from 'core/config'
import { extend, noop } from 'shared/util'
import { mountComponent } from 'core/instance/lifecycle'
import { devtools, inBrowser, isChrome } from 'core/util/index'

import {
  query,
  mustUseProp,
  isReservedTag,
  isReservedAttr,
  getTagNamespace,
  isUnknownElement
} from 'web/util/index'

import { patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'

// install platform specific utils
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
/**
 * 这段代码是 Vue.js 中的 $mount 方法。
 * 它是 Vue.js 应用程序的入口点之一,用于将 Vue 实例挂载到一个 DOM 元素上,从而启动应用程序的渲染过程。
 * 具体来说,这个方法会将 el 参数转化为一个 DOM 元素,并将其作为挂载点传递给 mountComponent 方法。
 * mountComponent 方法是实际执行 Vue 实例挂载的方法。
 * 它会执行一系列的初始化操作:
 * 包括创建虚拟 DOM、编译模板、生成渲染函数、执行渲染函数等。
 * 最终,它会将渲染得到的 DOM 元素插入到挂载点上,完成 Vue 实例的挂载。
 */
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 */
Vue.nextTick(() => {
  if (config.devtools) {
    if (devtools) {
      devtools.emit('init', Vue)
    } else if (process.env.NODE_ENV !== 'production' && isChrome) {
      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' &&
    config.productionTip !== false &&
    inBrowser && 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)

export default Vue

问题1:(entry-runtime-with-compiler.js为什么要重新创建$mount?)

entry-runtime-with-compiler.js 中重新创建 Vue.prototype.$mount 函数,主要是为了在有编译器的情况下,能够编译模板并生成 render 函数,并将其挂载到选项中,最终调用 mountComponent 函数来挂载组件。

这是因为在有编译器的情况下,我们可以通过将模板编译成渲染函数的方式来优化应用程序的性能。在这种情况下,我们需要在挂载组件之前先编译模板并生成渲染函数,然后将其挂载到选项中。

重新创建 $mount 函数可以确保我们能够在组件实例上使用 $mount 方法来挂载组件,而不需要手动编译模板和生成渲染函数。同时也允许用户在运行时动态编译模板并将其挂载到组件实例上。

问题2:(那entry-runtime-with-compiler.js和web/runtime/index.js中的$mount是什么关系?)

entry-runtime-with-compiler.jsweb/runtime/index.js 中的 $mount 具有相同的功能,但实现方式略有不同。

entry-runtime-with-compiler.js 中的 $mount 方法主要是用于编译器生成的带有模板(template)的 Vue 应用程序,它会首先将模板(template)编译成渲染函数(render function),然后再执行 web/runtime/index.js 中的 $mount 方法。因此,entry-runtime-with-compiler.js 中的 $mount 实际上是对 web/runtime/index.js$mount 方法的封装。

web/runtime/index.js 中的 $mount 方法则是用于运行时版本的 Vue 应用程序,它接收一个 el 参数和一个可选的 hydrating 参数,然后将 Vue 实例挂载到 el 元素上。

简单来说,entry-runtime-with-compiler.js 中的 $mount 方法会在编译过程中将模板(template)编译成渲染函数(render function),而 web/runtime/index.js 中的 $mount 方法则是直接将 Vue 实例挂载到 DOM 上。

4、new Vue() 都发生了什么?

instance/index.js

  1. new Vue() 从instance/index.js开始看,看见了Vue的庐山面目本质上是个构造函数
  2. instance/index.js 初始化一些混合函数,如initMixin、stateMixin、lifecycleMixin等,向Vue原型上混入一些属性和方法。
  3. 调用_init(option)方法(Vue构造函数中 instalce/init.js)。

instalce/init.js

  1. 向vm上定义uid
  2. 合并配置:将传入的option和本身的配置进行合并,传入的option类似 {el: '#app', data: {message: 'msg'}}。
  3. 初始化工作:initLifecycle(vm)、initEvents(vm)、initRender(vm)、执行beforCreate、initState(vm)、initProvide(vm)、执行created
  4. 进行挂载dom:vm.mount(vm.mount(vm.options.el),在这行代码执行完,页面上能看见渲染的真实dom。

接下来重点分析initState()具体做了哪些事情

instalce/state.js

  1. 初始化工作:initProps、initMethods、initData、initComputed、initWatch 初始化这些我们常用的属性和方法。
  2. initData():首先data进行检测必须是function或者object。其次要确定method和props中没有和data中定义重复的key,如果有抛出警告,如果没有重复的key,检测当前key是不是保留字($或_开头的),不是的话,将key代理到vm实例上。最后使用observe(data, true /* asRootData */)将data设置为响应式对象,以便data属性更新时,对应更新视图。

总结:new Vue时调用了init方法,进行配置的合并,还有一些生命周期、事件、state等初始化工作。初始化完成后,会执行$mount,将组件挂载,渲染成真实dom。

为什么可以在mounted中使用this获取到data中的属性?

因为,在init方法中,调用initState(vm)时,初始化了initData。将option.data中的所有非保留字的key通过proxy代理到了vm实例上。而mounted生命周期是在initData()方法执行之后,$mount挂载dom之后执行的,this指向了vue实例vm,vm在initData()时代理了data中所有符合规范的key。所以说,mounted中可以使用this获取到data中的属性。