Vue首次渲染过程

1,035 阅读4分钟

一、加载完整版Vue入口文件(src/platform/web/entry-runtime-with-compiler.js)
二、初始化静态成员(src/core/index.js)
  • src/core/index.js
  • 调用initGlobalAPI(Vue)方法, 给Vue的构造函数, 添加静态方法
    • initGlobalAPI(Vue)位置/src/core/global-api/index.js
      • 初始化Vue.config对象
      • 设置keep-alive组件
      • 注册Vue.use()用来注册组件
      • 注册Vue.mixin()混入
      • 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
      • 注册 Vue.directive()、 Vue.component()、Vue.filter()
export function initGlobalAPI (Vue: GlobalAPI) {
  ...
  // 初始化 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

  ...
  // 初始化 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)
}
  • 加载(src/platforms/web/runtime/index.js) 该文件通过extend给vue全局注册指令(v-model和v-show)和组件(Transition和TransitionGroup), 给Vue原型上添加_patch_函数, 作用是将虚拟DOM转换成真实DOM,调用patch函数会判断浏览器环境
// /src/platforms/web/runtime/index.js

// 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
三、初始化实例成员

  • _init()
  • 位置 src/core/instance/index.js
  • 定义了构造函数,调用了this._init(options)方法
  • 为Vue中混入了常用的实例成员
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 实例混入实例成员
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
  • initMixin(Vue)
    • 作用:注册 vm 的 _init() 方法(Vue的原型上添加),初始化 vm
  • stateMixin(Vue)
    • 注册 vm 的 data/data/props/set/set/delete/$watch
  • eventsMixin(Vue)
    • 初始化事件相关方法,on/on/once/off/off/emit
  • lifecycleMixin(Vue)
    • 初始化生命周期相关的混入方法,_update/forceUpdate/forceUpdate/destroy
  • renderMixin(Vue)
    • 混入 render,$nextTick/_render
四、继续执行src/platforms/web/entry-runtime-with-compiler.js
  • 重写Vue的$mount的方法,增加新功能
/**
 * 把模版编译成render函数
 * @param {*}
 * @return {*}
 */
Vue.prototype.$mount = function (
  el?: string | Element,
  // 非ssr情况下为 false,ssr 时候为true
  hydrating?: boolean
): Component {
  // 获取 el 对象
  el = el && query(el)
...
  const options = this.$options
  // resolve template/el and convert to render function
  // 把 template/el 转换成 render 函数
  if (!options.render) {
    let template = options.template
    // 如果模板存在
    ...
      options.render = render
      options.staticRenderFns = staticRenderFns
      ...
      }
    }
  }
  // 调用 mount 方法,渲染 DOM
  return mount.call(this, el, hydrating)
}
  • 判断是否有render选项, 如果没有render选项,则会把模板template编译成render函数, 然后调用mount方法(/src/platforms/web/runtime/index.js的$mount),渲染DOM
  • Vue增加静态成员compile方法,把html字符串编译成render函数
if (!options.render) {
  let template = options.template
    if (template) {
      ...
    }
}

Vue.compile = compileToFunctions
export default Vue
五、执行 _init(),继续初始化实例成员

  • 当静态成员和实例成员都初始化完成之后,接着调用Vue的构造函数,在构造函数中调用_init()方法
  • _init是在initMixin中初始化的,主要对Vue实例初始化
export function initMixin (Vue: Class<Component>) {
  // 给 Vue 实例增加 _init() 方法
  // 合并 options / 初始化操作
  Vue.prototype._init = function (options?: Object) {
    ...
    // vm 的生命周期相关变量初始化
    // $children/$parent/$root/$refs
    initLifecycle(vm)
    // vm 的事件监听初始化, 父组件绑定在当前组件上的事件
    initEvents(vm)
    // vm 的编译render初始化
    // $slots/$scopedSlots/_c/$createElement/$attrs/$listeners
    initRender(vm)
    // beforeCreate 生命钩子的回调
    callHook(vm, 'beforeCreate')
    // 把 inject 的成员注入到 vm 上
    initInjections(vm) // resolve injections before data/props
    // 初始化 vm 的 _props/methods/_data/computed/watch
    initState(vm)
    // 初始化 provide
    initProvide(vm) // resolve provide after data/props
    // created 生命钩子的回调
    callHook(vm, 'created')
	...
  }
}
  • initState()
  • 初始化vm的 _props/methods/_data/computed/watch
// /vue/src/core/instance/state.js
export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  } 
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
- initProps

在instance/state.js中,首先获取了Vue实例中的$options,然后判断options中是否有props,methods,data以及computed和watch这些属性,如果有的话,通过initProps进行初始化 initProps(vm, opts.props)接收了两个参数,一个是Vue实例,一个是Props属性,我们跳转到initProps函数中,首先给Vue实例定义了一个_Props对象, 并且把它存储到了常量里面

const props = vm._props = {}

然后,遍历PropsOptions的所有属性,它其实就是initProps方法中的第二个参数,遍历每个属性,然后通过defineReactive注入到Props这个对象上,这个props其实就是vm._props所有的成员都会通过defineReactive转化为get和set,最后在Props对象上存储,

注意

在开发模式中,如果我们直接给这个属性赋值的话,会发出一个警告

生产环境中直接通过defineReactive把props中的属性转化成get和set

最后判断了props属性是否在Vue实例中存在,不存在通过Proxy这个函数把我们的属性注入到Vue的实例中

在 Proxy 中,通过调用Object.defineProperty(target, key,sharePropertyDefinition)

总结:initProps 的作用就是把我们的Props成员转化成响应式数据,并且注入到Vue实例里面中

  • 在initMethods

initMethods作用就是把选项的methods注入到vue实例,在注入之前,会先判断我们命名是否在Props中存在,并且判断了命名的规范,不建议_和$开头

  • 在initMethods(vm, opts.methods)中,也是接收两个参数,Vue实例和选项中的methods,首先获取了选项中的Props,接着遍历methods所有属性,接着判断当前的环境是否是开发环境会判断methods是否是functicon

  • 接着判断methods方法的名称是否在Props对象中存在,存在就会发送一个警告,警告在属性在Props中已经存在,因为Props和methods最终都要注入到Vue实例上,不能出现同名

  • 下面继续判断key是否在Vue中存在,并且调用了isReserved(key),判断我们的key是否以_开头或$开头

  • 最后把methods注入到Vue实例上来,注入的时候会判断是否是function,如果不是返回noop,是的话把函数返回bind(methods[key], vm)

  • initData(vm)

  • 当options中有data选项时,会调用initData(vm)

  • 当没有的时候此时会给vm初始化一个_data属性observe(vm._data = {}, true)然后调用observe函数,observe是响应式中的一个函数

  • 在initData中获取了options的data选项,判断了data选项是否是function,如果是调用getData(data,vm)

  • 接着获取data中的所有属性,同时获取了props,methods中所有的属性

// src/core/instance/state.js
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods

最后做一个响应式处理

observe(data, true)

在_init函数的最后,又调用了$mount来挂载整个页面(首次渲染)

// src/core/instance/init.js

if (vm.$options.el) {
  vm.$mount(vm.$options.el)
}
六、总结
  • 在首次渲染之前,首先进行Vue初始化,初始化静态成员和实例成员
  • 当初始化结束之后,要调用Vue的构造函数new Vue(),在构造函数中调用了_init()方法,这个方法相当于我们整个Vue的入口
  • 在_init方法中,最终调用了mount,一共有两个mount,一共有两个mount,第一个定义在entry-runtime-with-compiler.js文件中,也就是我们的入口文件mount,这个mount,这个mount()的核心作用是帮我们把模板编译成render函数,但它首先会判断一下当前是否传入了render选项,如果没有传入的话,它会去获取我们的template选项,如果template选项也没有的话,他会把el中的内容作为我们的模板,然后把模板编译成render函数,它是通过compileToFunctions()函数,帮我们把模板编译成render函数的,当把render函数编译好之后,它会把render函数存在我们的options.render中。
  • 接着会调用src/platforms/web/runtime/index.js文件中的mount方法,在这个中首先会重新获取el,因为如果是运行时版本的话,是不会走entryruntimewithcompiler.js这个入口中获取el,所以如果是运行时版本的话,我们会在runtime/index.jsmount方法,在这个中首先会重新获取el,因为如果是运行时版本的话,是不会走entry-runtime-with-compiler.js这个入口中获取el,所以如果是运行时版本的话,我们会在runtime/index.js的mount()中重新获取el。
  • 接下来调用mountComponent(),这个方法在src/core/instance/lifecycle.js中定义的,在mountComponent()中,首先会判断render选项,如果没有render选项,但是我们传入了模板,并且当前是开发环境的话会发送一个警告,目的是如果我们当前使用运行时版本的Vue,而且我们没有传入render,但是传入了模版,告诉我们运行时版本不支持编译器。接下来会触发beforeMount这个生命周期中的钩子函数,也就是开始挂载之前。
  • 然后定义了updateComponent(),在这个函数中,调用vm._render和vm._update,vm._render的作用是生成虚拟DOM,vm._update的作用是将虚拟DOM转换成真实DOM,并且挂载到页面上
  • 创建Watcher对象,在创建Watcher时,传递了updateComponent这个函数,这个函数最终是在Watcher内部调用的。在Watcher内部会用了get方法,当Watcher创建完成之后,会触发生命周期中的mounted钩子函数,在get方法中,会调用updateComponent()
  • 挂载结束,最终返回Vue实例。