【Vue】Vue.js源码剖析—new Vue()发生了什么?

大家好,我是小伞,下面是自己对Vue.js源码中new Vue()部分的整理,如有错误欢迎点出哦。

源码直通车

vue.js源码github直通车:https://github.com/vuejs/vue

正题引入

在 Vue.js 中我们可以采用简洁的模板语法来声明式的将数据渲染为 DOM:

<div id="app">
  {{ message }}
div>
var app = new Vue({
  el: '#app',
  data: {
    message: 'Hello Vue!'
  }
})

最终我们会看到在页面上渲染出 Hello Vue这么神奇的效果,仅仅在HTML部分使用一个插值就可以将message渲染到页面,而且改变message页面的信息也跟着变化(这个自己可以试试),对比jQuery操作DOM元素的步骤,大大简化了代码量。那不多BB,从上面的代码可以看出new Vue肯定是’始作俑者’,那么我们就从入口代码开始分析,看看 new Vue 背后发生了哪些事情?

源码分析

我们都知道,new 关键字在 Javascript 语言中代表实例化是一个对象,而 Vue 实际上是一个Class类,类在 Javascript 中是用 Function 来实现的,那我们来看看雨溪大神的源码部分src/core/instance/index.js

 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.prototype._init

在定义vue 方法之后,会调用一系列vue 方法的封装工作,执行new vue 后,会执行this._init 方法,此方法便是 initMixin 封装到vue函数原型中的,下面进入initMixin方法src/core/instance/init.js

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   合并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)     // 初始化渲染
  callHook(vm, 'beforeCreate')
  initInjections(vm) 
  initState(vm)    // 初始化 data、props、computed、watcher 等等
  initProvide(vm) 
  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)
  }
  /* mount 为将数据模型vdom挂载到真实dom上 */
  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

为了让大家知道源码中重点部分是什么意思,我在上面进行了中文注释,Vue 初始化主要就干了几件事情,合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等,最后初始化完成检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM;此过程我们重点关注initState以及 最后执行的mount

分析iniState,再分析initData

initState即为初始化定义在构造函数中的用户定义的所有属性值,包括data props computed等,以下是initState源码部分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)
  }
}

主要是对你的构造函数中所定义的 常用属性:data, watch,computed 进行初始化 下面我们看下对data的初始化 initData代码src/core/instance/state.js

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }
  // proxy data on instance
  const keys = Object.keys(data)
  const props = vm.$options.props
  const methods = vm.$options.methods
  let i = keys.length
  while (i--) {
    const key = keys[i]
    if (process.env.NODE_ENV !== 'production') {
      if (methods && hasOwn(methods, key)) {
        warn(
          `Method "${key}" has already been defined as a data property.`,
          vm
        )
      }
    }
    if (props && hasOwn(props, key)) {
      process.env.NODE_ENV !== 'production' && warn(
        `The data property "${key}" is already declared as a prop. ` +
        `Use prop default value instead.`,
        vm
      )
    } else if (!isReserved(key)) {
      proxy(vm, `_data`, key)
    }
  }
  // observe data
  observe(data, true /* asRootData */)
}

initDate这里将data的所有属性进行遍历挂载到vm的_data属性下,中间的代码 //proxy data on instance ,即为对所有data下的属性设置代理,然后使用Object.defineProperty 把这些属性全部转为 getter/setter。同时每一个实例对象都有一个watcher实例对象,他会在模板编译的过程中,用getter去访问data的属性,watcher此时就会把用到的data属性记为依赖,这样就建立了视图与数据之间的联系。之后我们渲染视图的数据依赖发生改变(即数据的setter被调用)的时候,watcher会对比前后两个的数值是否发生变化,然后确定是否通知视图进行重新渲染这样就实现了所谓的数据对于视图的驱动;回到源码中通过代理我们可以通过this.xx访问到this._data.xx,达到了访问我们所定义的data下的值,最后,observe 方法(也就是我们说的响应式处理)对所有data属性,进行了监听,data改变驱动视图。

最后执行的mount

在初始化完成后,检测到如果有 el 属性,则调用 vm.$mount 方法挂载 vm,挂载的目标就是把模板渲染成最终的 DOM

总结

通过上面的分析,大家是不是对new vue 所发生的一切以及对vue 方法的封装已经基本明了呢。

总结一下大概过程:new Vue() → 执行Vue.prototype._init方法 → 进行Vue初始化 → 初始化完成,调用 vm.$mount 方法挂载 vm将模版渲染成最终的 DOM

你可能感兴趣的:(【Vue/React】)