vue源码讲解系列之一------声明式渲染new Vue()过程发生了什么

vue源码解析

声明式渲染
用模板语法声明式地将数据渲染至DOM

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

数据和DOM建立关联,数据是响应式的。
new Vue 发生了什么
new关键字将创建一个实例,所以Vue是一个构造函数。
我们打开vue源文件,找到core/instance/index.js文件,可以看到以下代码,这就是找的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'

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方法。Vue方法首先判断当前是否是生产环境,判断有没有用new关键字创建Vue实例(如果new了实例,this会指向实例,实例instanceof Vue 返回true)。然后调用_init方法。_init中的options参数就是上述{el: '#app',data: {message: 'Hello Vue!'}}部分。

调用initMixin方法,查看initMixin方法,看到之前flow语法,详情:vuejs源码讲解从flow.js开始——一个JavaScript 静态类型检测工具

initMixin接收一个类作为参数,在类(Vue)的原型上添加了_init方法。_init接收可选的接口为Object的options参数。在这个_init方法内,主要是初始化(vm、生命周期、事件等)和合并options。

export function initMixin (Vue: Class<Component>) {
  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)
    }

    // 避免被观察的标志
    vm._isVue = true
    // 合并 options
    if (options && options._isComponent) {
        //优化内部组件实例化
        //因为动态选项合并非常慢,
        //内部组件选项需要特殊处理。
      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) // 在data/pops之前注入
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    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) {   // 判断el,执行$mount 挂载Dom
      vm.$mount(vm.$options.el)
    }
  }
}

假设在vue项目初始化的时候我们选择了runtime版本,首先来看entry-runtime-with-compiler.js,在这里我们可以看到判断el将实例挂载到#app节点上的操作, m o u n t 。 mount。 mountmount在Vue原型上创建,判断el是不是body和HTML标签,如果是而且在生产环境则警告用户不能将模板挂载在body和HTML标签上。如果options上没有render就开始检测如果el传进来的是字符串,那么检测是不是#开头,再根据环境和存不存在这个DOM元素来判断有没有模板。如果el不是字符串,那就检测用户编写的模板是不是DOM节点( document.getElementByID(’#id’))。处理最后的结果都是赋值给template变量,为了转化为render函数。【详情看下一篇render】

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  or  - 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, {
        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)
}

上述idToTemplate方法中判断DOM元素存不存在调用的是query方法,该方法主要使用querySelector查找的dom元素。

export function query (el: string | Element): Element {
  if (typeof el === 'string') {
    const selected = document.querySelector(el)
    if (!selected) {
      process.env.NODE_ENV !== 'production' && warn(
        'Cannot find element: ' + el
      )
      return document.createElement('div')
    }
    return selected
  } else {
    return el
  }
}

我们找到initState方法,initState方法调用了一些初始化props、methods、data、computed、watch的方法。

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)
  }
}

然后我们找到initData方法,该方法首先判断data是不是函数,因为我们既可以data(){}添加data数据,也可以用data:{}形式添加。如果是函数则调用getData方法,接收一个接口为Function的data参数和Vue实例,改变data中this上下文,返回data。

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 */)
}

export function getData (data: Function, vm: Component): any {
  // #7573 disable dep collection when invoking data getters
  pushTarget()
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, `data()`)
    return {}
  } finally {
    popTarget()
  }
}

至此我们知道了执行new Vue主要发生了什么。

相关链接:
vuejs源码讲解从flow.js开始——一个JavaScript 静态类型检测工具

vue源码讲解系列之二------ render()函数、VNode虚拟节点的实现

你可能感兴趣的:(vue)