阅读 775

每天一道面试题 vue 初始化做了什么

前言 面试者众多 如何在其中脱颖而出 干掉一大批面试者 至少有在某个领域有深度 在这里和后面写的文章里给所有的求职者一份小白都能够看懂的 vue 技术栈源码剖析 不管是你在面试中还是在项目中都能够从容应对

首先一个简单的 vue 组件案例

     <html>
         <head></head>
         <body>
             <div id="app">hello {{msg}}</div>
          <script>
            export default {
                el:'app',
                data () {
                    return {
                      msg:"Vue"
                    }
                  }
            }
    </script>
         </body>
     </html>
复制代码

最终在网页上呈现出一段文字 hello Vue

并且当我们想修改视图中的文字时候 是需要修改 data 中的属性 就能够动态的修改视图上的文字

我们都知道如果不靠 vue 的话 我们用普通 js 做到同样的效果的话你需要操作 dom 元素 获取 id 节点 然后再添加文字

那么 vue 它到底帮我们做了什么 能让我们这么方便不需要写 js 代码就能够轻易操作视图

围绕着这点 我们来开启遨游 vue 源码的旅程 首先来查看 vue 初始化做了什么

查看源码注意事项

这里的 vue 版本是 2.6 正式版 由于 3.0 的正式版是向下兼容 2.6 的 开发人员学习和它的生态还有一段过

渡期

vue 的源码结构复杂 在查看源码的过程中 我们只看主线过程 跳过支线步骤 搞清楚 vue 的主要工作

vue 会有很多构建的版本 打包过程 会根据用户的命令 执行对应的版本文件打包 这里说的是在 web 平台下 完整版的打包入口文件 entry-running-with-compiler.js

vue 初始化

在入口文件中 主要做了两件事情:

  • 重写了 $mount 方法增加新的功能 首先判断有没有 render 如果没有 将 template 编译成 render 函数 , 如果有调用 mount 渲染到 DOM
  • 注册 vue 静态方法 complier 将 HTML 字符串编译成 render
src/platforms/web/entry-running-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' /* vue 的构造函数文件*/
import { query } from './util/index'
import { compileToFunctions } from './compiler/index'
import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'

const idToTemplate = cached(id => {
  const el = query(id)
  return el && el.innerHTML
})
// 保留 Vue 实例的 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  // 非ssr情况下为 false,ssr 时候为true
  hydrating?: boolean
): Component {
  // 获取 el 对象
  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
  }

  const options = this.$options
  // resolve template/el and convert to render function
  // 把 template/el 转换成 render 函数
  if (!options.render) {
    let template = options.template
    // 如果模板存在
    if (template) {
      if (typeof template === 'string') {
        // 如果模板是 id 选择器
        if (template.charAt(0) === '#') {
          // 获取对应的 DOM 对象的 innerHTML
          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) {
        // 如果模板是元素,返回元素的 innerHTML
        template = template.innerHTML
      } else {
        if (process.env.NODE_ENV !== 'production') {
          warn('invalid template option:' + template, this)
        }
        // 否则返回当前实例
        return this
      }
    } else if (el) {
      // 如果没有 template,获取el的 outerHTML 作为模板
      template = getOuterHTML(el)
    }
    if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
      // 把 template 转换成 render 函数
      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')
      }
    }
  }
  // 调用 mount 方法,渲染 DOM
  return mount.call(this, el, hydrating)
}

/**
 * Get outerHTML of elements, taking care
 * of SVG elements in IE as well.
 */
 /*与主线无关 这里先省略*/
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

复制代码

在这个文件 vue 做了一系列的判断后最终主要目的是把 DOM 渲染到视图上 但在这里并没有看到 vue 的构造函数在做了什么 我们再看导入的 ./runtime/index.js 文件

src/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 } 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
// 判断是否是关键属性(表单元素的 input/checked/selected/muted)
// 如果是这些属性,设置el.props属性(属性不设置到标签上)
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
// 在 vue.options 中的属性是可以在全局中使用的
extend(Vue.options.directives, platformDirectives)
extend(Vue.options.components, platformComponents)

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

// public mount method
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)
}

export default Vue


复制代码

它主要做了下面几件事

  • 注册了只供内部使用的属性

    • 它在 vue 的构造函数里面先注册了 只供内部使用的属性 外部很少作用到 包括注册了 是否是保留的标签 是否是保留的属性 及传入的参数是否是 HTML 特有的标签和属性
  • 注册了指令和组件

    • 在构造函数里添加注册与平台相关的全局组件 说明了这个指令和组件 是 web 平台特有的 切实全局的 extend方法将后面参数赋值到前面参数 注册全局的指令 v-show v-model 和 v-transition 组件 且都可以全局访问·
  • 注册了 path 函数

    • 先是判断是不是浏览器环境 如果是添加 path 方法 它的作用是将 虚拟 DOM 转换成正式 DOM
  • 注册了 $mount

    • 这里给 vue 的实例上注册了 $mount 调用了 mountComponent 方法把 DOM 渲染界面上

可以看到到最后依然找不到 vue 的构造函数 继续往下看 导入的 core/index.js

src/core/index.js
import Vue from './instance/index'
import { initGlobalAPI } from './global-api/index'
import { isServerRendering } from 'core/util/env'
import { FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 给 vue 构造函数注册了静态方法
initGlobalAPI(Vue)
//下面这三个监听属性是在 SSR 中用的
Object.defineProperty(Vue.prototype, '$isServer', {
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
  get () {
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
  value: FunctionalRenderContext
})
// 添加了 vue 的版本
Vue.version = '__VERSION__'

export default Vue


复制代码

在这里重点看 initGlobalAPI 我们在 vscode 中按住 Ctrl + 鼠标左键 跳转到该文件

src/core/global-api/index



/* @flow */

import config from '../config'
import { initUse } from './use'
import { initMixin } from './mixin'
import { initExtend } from './extend'
import { initAssetRegisters } from './assets'
import { set, del } from '../observer/index'
import { ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import { observe } from 'core/observer/index'

import {
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
  // config
  const configDef = {}
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
    configDef.set = () => {
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.
  // 这些工具方法不视作全局API的一部分,除非你已经意识到某些风险,否则不要去依赖他们
  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive
  }
  // 静态方法 set/delete/nextTick
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
    observe(obj)
    return obj
  }
  // 初始化 Vue.options 对象,并给其扩展
  // components/directives/filters
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  // 设置 keep-alive 组件
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、 Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}


复制代码

这个文件里 它主要做注册静态的事情 注册了静态成员 vue.config 注册了 util 的方法 注册了 set / delete / nextTick 注册了 让一个对象变成响应式的数据 。。。。的静态方法

最后依然没有看的 vue 的构造函数 我们回到 running/index.js 文件再来看 导入的 ./instance/index' 再次 Ctrl + 鼠标右键

src/core/instance/index
import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'
// 此处不用 class 的原因是因为方便后续给 Vue 实例混入实例成员
//判断是否是生产环境并且 this 不是执行 vue 说明没有用 new 去创建实例 提示警告如果都是 调用 init 函数 后面都是在 vue 的原型上添加方法$data/$props/$set/$delete/$watch 事件 $on/$once/$off/$emit 生命周期相关的方法
function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用 _init() 方法
  this._init(options)
}
// 注册 vm 的 _init() 方法,初始化 vm
initMixin(Vue)
// 注册 vm 的 $data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

export default Vue


复制代码

在这里种终于可以看到 vue 的构造函数了 它主要做两件事创建构造函数 设置 vue 实例成员

最后我们用思维导图来梳理一下整个流程

总结

yuque_diagram.jpg

看到最后 vue 就是把每个功能切割成不同的模块 这样方便我们将来的查看和维护

文章分类
前端
文章标签