Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Vue源码系列——Vue 的初始化 #15

Open
webproblem opened this issue Jul 6, 2019 · 0 comments
Open

Vue源码系列——Vue 的初始化 #15

webproblem opened this issue Jul 6, 2019 · 0 comments
Labels

Comments

@webproblem
Copy link
Owner

webproblem commented Jul 6, 2019

Vue源码系列--初始化

最近在看 Vue 的源码架构,打算在公司组织 Vue 源码的分享会,所以准备做一系列关于 Vue 源码的技术输出。

目录结构

先来大致看下 vue 目录结构,这里只列出 src 目录下的文件结构。

├── src
	├── compiler -------------------------------- 编译器代码,将 template 模板编译成 render 函数
	├── core ------------------------------------ 核心代码,主要包括响应式原理,vdom,全局 API 等
	├── platforms ------------------------------- 平台代码
		├── web --------------------------------- web 端代码
		├── weex -------------------------------- 移动端混合开发代码
	├── server ----------------------------------- ssr 服务端渲染
	├── sfc -------------------------------------- .vue 单文件组件解析
	├── shared ----------------------------------- 通用代码,定义的一些工具函数和常量

Vue 的入口文件

分析 Vue 源码首先要找到 Vue 的入口文件,从入口开始,一步步深入了解实现原理。在跟目录下的 package.json 文件中找到构建配置。

  "scripts": {
    "dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev",
    "dev:cjs": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-cjs-dev",
    "dev:esm": "rollup -w -c scripts/config.js --environment TARGET:web-runtime-esm",
    "dev:test": "karma start test/unit/karma.dev.config.js",
    "dev:ssr": "rollup -w -c scripts/config.js --environment TARGET:web-server-renderer",
    "dev:compiler": "rollup -w -c scripts/config.js --environment TARGET:web-compiler ",
    "dev:weex": "rollup -w -c scripts/config.js --environment TARGET:weex-framework",
    "dev:weex:factory": "rollup -w -c scripts/config.js --environment TARGET:weex-factory",
    "dev:weex:compiler": "rollup -w -c scripts/config.js --environment TARGET:weex-compiler ",
    "build": "node scripts/build.js",
    "build:ssr": "npm run build -- web-runtime-cjs,web-server-renderer",
    "build:weex": "npm run build -- weex",
    "test": "npm run lint && flow check && npm run test:types && npm run test:cover && npm run test:e2e -- --env phantomjs && npm run test:ssr && npm run test:weex",
    "test:unit": "karma start test/unit/karma.unit.config.js",
    "test:cover": "karma start test/unit/karma.cover.config.js",
    "test:e2e": "npm run build -- web-full-prod,web-server-basic-renderer && node test/e2e/runner.js",
    "test:weex": "npm run build:weex && jasmine JASMINE_CONFIG_PATH=test/weex/jasmine.js",
    "test:ssr": "npm run build:ssr && jasmine JASMINE_CONFIG_PATH=test/ssr/jasmine.js",
    "test:sauce": "npm run sauce -- 0 && npm run sauce -- 1 && npm run sauce -- 2",
    "test:types": "tsc -p ./types/test/tsconfig.json",
    "lint": "eslint src scripts test",
    "flow": "flow check",
    "sauce": "karma start test/unit/karma.sauce.config.js",
    "bench:ssr": "npm run build:ssr && node benchmarks/ssr/renderToString.js && node benchmarks/ssr/renderToStream.js",
    "release": "bash scripts/release.sh",
    "release:weex": "bash scripts/release-weex.sh",
    "release:note": "node scripts/gen-release-note.js",
    "commit": "git-cz"
  }

可以看到,针对不同模块的构建输出设置了不同的构建脚本,这里只看 dev 脚本的,也就是运行 npm run dev。运行脚本的时候,会找到 scripts/config.js 执行,然后在 config.js 中找到 web-full-dev 的配置。

可以看到,构建时的入口文件是 web/entry-runtime-with-compiler.js 。这里的 web 其实是配置的相对路径的别名,相关的配置都写在 scripts/alias.js 中。

顺着路口文件一路找下去,可以看到 Vue 被定义在 src/core/instance/index.js 中。

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'

/**
 * Vue 构造器
 * @param {*} options
 */
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')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

这个文件的主要作用是定义了一个 Vue 构造器,里面通过执行 _init 函数进行初始化,这也说明了 Vue 必须通过 new 关键字来初始化。Vue 构造器接收的 options 就是在 new Vue 时传入的配置项。

new Vue 的过程

通过内部定义的 Vue 构造器可以看到,new Vue 时,内部会通过调用 _init 函数进行初始化。_init 函数是挂载在 Vue 原型上的方法,代码定义在 initMixin 中,也就是 src/core/instance/init.js。

// 挂载到 Vue 原型上
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 性能分析
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // 合并配置项
    if (options && options._isComponent) {
      // optimize internal component instantiation
      // since dynamic options merging is pretty slow, and none of the
      // internal component options needs special treatment.
      initInternalComponent(vm, options)
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化渲染
    initRender(vm)
    // beforeCreate 生命周期钩子函数阶段
    callHook(vm, 'beforeCreate')
    // 初始化 injections
    initInjections(vm) // resolve injections before data/props
    // 初始化 props,methods,data 等
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // created 生命周期钩子函数阶段
    callHook(vm, 'created')

    /* istanbul ignore if */
    // 性能分析
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }

    // 挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

_init 函数的作用主要是合并初始化配置项,初始化事件监听,初始化渲染等操作。可以看到的是,initState 函数也就是初始化配置项是在 beforeCreate 钩子函数阶段后,created 钩子函数阶段前完成的,所以在 beforeCreate 钩子函数阶段是不能访问和操作数据的。

合并配置选项

// 合并配置项
if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
} else {
    vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
    )
}

这段代码就是用来合并配置选项的。这里只考虑根实例的初始化,组件实例的初始化暂不考虑,也就是 new Vue 初始化。所以代码会进入到 else 代码块,执行 mergeOptions 函数进行合并。mergeOptions 函数接受 3 个参数,第一个参数执行一个函数并得到返回值,其值就相当于是 Vue.options。第二个参数就是传入的配置项,第三个参数是 Vue 实例。Vue.options 定义在 src/core/global-api/index.js 中。

首先给 Vue.options 赋值了一个空对象,然后通过遍历 ASSET_TYPES 数组给 Vue.options 添加属性。ASSET_TYPES 定义在 src/shared/constants.js 中。

export const ASSET_TYPES = [
  'component',
  'directive',
  'filter'
]

然后再执行拷贝函数将 KeepAlive 组件赋值给 components 属性值。最终 Vue.options 的值如下:

Vue.options = {
    components: {
        KeepAlive
    },
    directives: {},
    filters: {}
}

到此就明白了 mergeOptions 函数的参数类型了。接下来看下 mergeOptions 函数是怎么合并配置选项的。

export function mergeOptions (
  parent: Object,
  child: Object,
  vm?: Component
): Object {
  if (process.env.NODE_ENV !== 'production') {
    checkComponents(child)
  }

  if (typeof child === 'function') {
    child = child.options
  }

  // 规范化 props
  normalizeProps(child, vm)
  // 规范化 inject
  normalizeInject(child, vm)
  // 规范化 directives
  normalizeDirectives(child)

  // Apply extends and mixins on the child options,
  // but only if it is a raw options object that isn't
  // the result of another mergeOptions call.
  // Only merged options has the _base property.
  if (!child._base) {
    if (child.extends) {
      parent = mergeOptions(parent, child.extends, vm)
    }
    if (child.mixins) {
      for (let i = 0, l = child.mixins.length; i < l; i++) {
        parent = mergeOptions(parent, child.mixins[i], vm)
      }
    }
  }
  
  // 合并配置项代码
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options
}

mergeOptions 函数首先会对一些配置项进行规范化,比如 props,inject 依赖注入,directives 自定义指令。拿props 为例,这个属性是用来父组件向子组件传参用的,其值的写法有很多种。

// 第 1 种:数组形式
props: ['size', 'disabled']

// 第 2 种
props: {
    size: String,
    disabled: Boolean    
}

// 第 3 种
props: {
    size: {
        type: String   
    },
    disabled: {
        type: Boolean
    }    
}

对于第 1 种和第 2 种写法,在 Vue 内部会统一规范化成第 3 种形式的写法。规范化函数如下:

function normalizeProps (options: Object, vm: ?Component) {
  const props = options.props
  if (!props) return
  const res = {}
  let i, val, name

  // props 是数组或对象的情况
  if (Array.isArray(props)) {
    i = props.length
    while (i--) {
      val = props[i]
      if (typeof val === 'string') {
        name = camelize(val)
        res[name] = { type: null }
      } else if (process.env.NODE_ENV !== 'production') {
        warn('props must be strings when using array syntax.')
      }
    }
  } else if (isPlainObject(props)) {
    for (const key in props) {
      val = props[key]
      name = camelize(key)
      res[name] = isPlainObject(val)
        ? val
        : { type: val }
    }
  } else if (process.env.NODE_ENV !== 'production') {
    warn(
      `Invalid value for option "props": expected an Array or an Object, ` +
      `but got ${toRawType(props)}.`,
      vm
    )
  }
  options.props = res
}

对选项的值规范化后,就开始进入到选项合并阶段了,首先会遍历 Vue 内部提供的默认配置项并自行 mergeField 函数,再接着遍历传入的配置项执行 mergeField 函数。mergeField 函数时关键,函数内部会针对不同的配置项进行相应的合并处理。

// 合并配置项代码
  const options = {}
  let key
  for (key in parent) {
    mergeField(key)
  }
  for (key in child) {
    if (!hasOwn(parent, key)) {
      mergeField(key)
    }
  }
  function mergeField (key) {
    const strat = strats[key] || defaultStrat
    options[key] = strat(parent[key], child[key], vm, key)
  }
  return options

strats 是定义的一个空对象,Vue 内部会将配置项合并函数作为属性挂载到这个对象上,包括 data 选项合并函数,生命周期选项合并函数,watch,props,methods 等选项合并函数。Vue 的配置项合并规则在官方文档中解释得很清楚了,合并规则就是:

  • 数据对象在内部会进行递归合并,并在发生冲突时以组件数据优先。
  • 同名钩子函数将合并为一个数组,因此都将被调用。另外,混入对象的钩子将在组件自身钩子之前调用。
  • 值为对象的选项,例如 methodscomponents 和 directives,将被合并为同一个对象。两个对象键名冲突时,取组件对象的键值对。

整个 options.js 的作用都是用来合并配置项的。

Vue 的挂载

合并配置项完成后,接着就会进行一些初始化。

// 初始化生命周期
initLifecycle(vm)
// 初始化事件监听
initEvents(vm)
// 初始化渲染
initRender(vm)
// beforeCreate 生命周期钩子函数阶段
callHook(vm, 'beforeCreate')
// 初始化 injections
initInjections(vm) // resolve injections before data/props
// 初始化 props,methods,data 等配置项
initState(vm)
// 初始化 provide
initProvide(vm) // resolve provide after data/props
// created 生命周期钩子函数阶段
callHook(vm, 'created')

需要注意的是,对于 props,methods,data 等配置项的初始化是发生在 beforeCreate 钩子函数之后, created 钩子函数之前的,所以,在 beforeCreate 钩子函数阶段是不能访问好操作数据的,必须是至少在 created 钩子函数阶段才能访问和操作。

完成了上面的初始化之后,就会进入到挂载阶段。

// 挂载
if (vm.$options.el) {
    vm.$mount(vm.$options.el)
}

$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
  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 函数,再把 VDOM 转换成真实的 DOM 元素放入到 document 文档中。这里有一个需要知道的点就是,如果配置项中没有定义 render 配置项,就会选取 template 配置项作为模板编译成 render 函数,如果 template 配置项也不存在,那就直接选取 外部 HTML 作为模板进行编译。优先级是 ender 函数选项-template选项-外部HTML。最后会通过调用定义在 src\core\instance\lifecycle.js 中的 mountComponent 函数转换成真实 DOM。

在 mountComponent 函数内部,会进入 beforeMount 钩子函数阶段和 mounted 钩子函数阶段,需要注意的是,vue 的挂载完成是在 beforeMount 钩子函数之后和 mounted 钩子函数之前发生的,所以 vm.$el 至少是在 mounted 钩子函数阶段才能访问到。

Vue 生命周期

  • 在 beforeCreate 阶段也就是实例初始化但是还没创建完成的时候,数据监测和事件初始化还没完成,不能对任何数据访问操作。
  • created 阶段,实例创建完成,数据监测和事件初始化也都完成,可以访问和操作 data 数据,但是在这一步,$el 还获取不到,这一步可以开始一些数据请求的操作。
  • 实例创建完成后,就进入到挂载阶段,在挂载阶段就可以访问 $el 的值了。要说明的是进入挂载阶段前,会先判断传入的配置项中是否有 el 属性,如果有才会继续初始化,如果没有初始化会暂停,等到执行了 vm.$mount(el) 后才会继续。在 beforeMount 阶段,内部会找到 template 配置项作为模板进行编译成 render 函数,如果没有template 配置项,则使用外部 HTML 作为模板,如果有直接配置 render 函数,则直接使用配置的 render 函数,优先级是:render 函数选项-template选项-外部HTML。并且在 beforeMount 阶段,$el 获取的值还是双大括号的 vue 语法,只是个占位符,内容还没有被替换掉。
  • 当执行完 render function之后,到了mounted阶段,el被创建的vm.$el替换掉,挂载完成。到这里为止,初始化过程中会触发上面这些钩子函数,更新和销毁钩子函数都需要手动触发的。
  • 当修改 data 选项中的数据时,会触发 beforeUpdate 和 updated,虚拟 DOM 按照 DIFF 算法重新渲染和打补丁,最后 DOM 更新完成后进入到 updated 阶段。
  • beforeDestory 是在vue实例销毁之前调用,在这里,实例仍然可以访问操作,一般在这一步中进行:销毁定时器、解绑全局事件、销毁插件对象等操作。
  • destoryed 在实例销毁后调用,所有指令都将会解绑,事件监听移除,子实例也会销毁。

总结

new Vue的时候,内部会调用 _init 函数进行初始化,主要是初始化事件监听,初始化渲染,初始化配置项,实例挂载等操作。初始化的时候,会执行 beforeCreate, created, beforeMount, mounted 生命周期钩子函数。

@webproblem webproblem added the Vue label Jul 6, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant