Vue源码分析——new Vue()干了什么

前言

本文通过vue2源码,浅析vue2中的new Vue()发生了什么,涉及了初始化、挂载、虚拟dom、真实dom的生成。

js 复制代码
// vue2中new Vue()使用
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

初始化

入口文件,传入配置项,调用_init方法进行初始化。

js 复制代码
// src\core\instance\index.ts
function Vue(options) {
  if (__DEV__ && !(this instanceof Vue)) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }

  this._init(options)
}

//@ts-expect-error Vue has function type
initMixin(Vue) // 初始化Vue构造函数中,调用的this._init()方法
//@ts-expect-error Vue has function type
stateMixin(Vue)
//@ts-expect-error Vue has function type
eventsMixin(Vue)
//@ts-expect-error Vue has function type
lifecycleMixin(Vue)
//@ts-expect-error Vue has function type
renderMixin(Vue)

this._init()方法主要步骤:

  1. 合并配置项;
  2. 初始化生命周期、初始化事件中心、初始化渲染、调用beforeCreate钩子,初始化 injectionstate(datapropscomputed...)、provide 、调用create钩子;
  3. 最后通过判断有无vm.$options.el进行vm.$mount挂载。

this._init()源码:

js 复制代码
// 初始化混入
export function initMixin(Vue: typeof Component) {
  Vue.prototype._init = function (options?: Record<string, any>) {
    const vm: Component = this
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    // 性能检测
    if (__DEV__ && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

    // 一个标志,将其标记为 Vue 实例,而无需执行实例
    vm._isVue = true
    // 避免实例添加observer
    vm.__v_skip = true
    // 影响范围
    vm._scope = new EffectScope(true /* detached */)
    vm._scope._vm = true
    // 合并配置:业务逻辑以及组件的一些特性全都放到了vm.$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 as any)
    } else {// 顶层的vm
      vm.$options = mergeOptions(
        /*一些内置组件(keep-alive...)和指令(show、model...)...*/
        resolveConstructorOptions(vm.constructor as any),
        options || {},
        vm
      )
    }
    /* istanbul ignore else */
    // 初始化vm._renderProxy 为后期渲染做准备  
    // 开发环境并支持proxy vm._renderProxy = new Proxy(vm)
    if (__DEV__) {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }
    // expose real self
    vm._self = vm
    // 初始化生命周期
    initLifecycle(vm)
    // 初始化事件
    initEvents(vm)
    // 初始化渲染
    initRender(vm)
    // 调用beforeCreate回调
    callHook(vm, 'beforeCreate', undefined, false /* setContext */)
    // 初始化injections
    initInjections(vm) // resolve injections before data/props
    // 初始化state
    initState(vm)
    // 初始化provide
    initProvide(vm) // resolve provide after data/props
    // 调用create回调
    callHook(vm, 'created')

    /* istanbul ignore if */
    // 性能检测
    if (__DEV__ && config.performance && mark) {
      vm._name = formatComponentName(vm, false)
      mark(endTag)
      measure(`vue ${vm._name} init`, startTag, endTag)
    }
    // 如果有el 没有el 不挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

挂载

生成render函数------vm.prototype.$mount()

这段代码是上面_init()方法最后调用的vm.$mount()方法。这个方法主要作用是若配置项中没有render方法则将template作为编译函数的参数生成该方法,最后调用runtime时的mount方法(后面会讲到)。

mount方法主要步骤如下:

  1. 判断el是否是bodyhtml标签,如果是直接返回,中断挂载。若是开发环境,会发出警告。
  2. 如果配置项中不存在render函数,接下来会通过template得到html的内容,若配置中没有template,则通过el来获取。最后根据上面获取到的html内容对其进行编译,最终生成render函数并将其挂载在vm.$options.render中。 编译主要通过三个方法。
    • parse方法是通过大量的正则表达式对html字符串进行解析,将标签、指令、属性等转化为抽象语法树AST
    • optimize方法是遍历AST,标记节点类型,比如对静态节点进行标记,方便在页面更新渲染时进行diff比较时,直接跳过这些静态节点,优化runtime的性能。
    • generate方法是最终的AST转化为render函数字符串。
  3. 调用runtime时的mount方法。

mount源码:

js 复制代码
// src\platforms\web\runtime-with-compiler.ts
// runtime moute函数
const mount = Vue.prototype.$mount

Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)
  /* istanbul ignore if */
  // 1. 不允许挂载在html 或者 body身上
  if (el === document.body || el === document.documentElement) {
    __DEV__ &&
      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
  // 不存在render
  if (!options.render) {
    let template = options.template
    /*---------------------------- 2. 初始化template ------------------------------*/
    if (template) {
      if (typeof template === 'string') {
        // 如果模板开头是# 说明是一个id选择器 通过idToTemplate获取相应的innerHtml
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
          /* istanbul ignore if */
          if (__DEV__ && !template) {
            warn(
              `Template element not found or is empty: ${options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {// 是node对象 直接获取innerHTML
        template = template.innerHTML
      } else {// 不合法配置项
        if (__DEV__) {
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
      // @ts-expect-error
      template = getOuterHTML(el)
    }

    /*----------------------------- 3. 将模板转化为render函数 --------------------------- */
    if (template) {
      /* istanbul ignore if */
      // 性能检测
      if (__DEV__ && config.performance && mark) {
        mark('compile')
      }

      // 3.1把模板转换成render函数 得到ast抽象树获得的render函数
      const { render, staticRenderFns } = compileToFunctions(
        template,
        {
          outputSourceRange: __DEV__,
          shouldDecodeNewlines,
          shouldDecodeNewlinesForHref,
          delimiters: options.delimiters,
          comments: options.comments
        },
        this
      )
      // 3.2保存渲染函数 with(this){return _c('div',{attrs:{\"id\":\"app\"}},[_v(\"\\n......
      options.render = render
      options.staticRenderFns = staticRenderFns

      /* istanbul ignore if */
      // 性能检测
      if (__DEV__ && config.performance && mark) {
        mark('compile end')
        measure(`vue ${this._name} compile`, 'compile', 'compile end')
      }
    }
  }
  // 4. 调用$mount方法
  return mount.call(this, el, hydrating)
}

完成挂载------mountComponent

这是runtime时的mount方法。最后直接调用了mountComponent方法。所以直接看mountComponent方法吧。

js 复制代码
// src\platforms\web\runtime\index.ts
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && inBrowser ? query(el) : undefined
  return mountComponent(this, el, hydrating)
}

mountComponent方法完成了vuebeforeMountmounted的过程。主要步骤如下:

  1. render函数是生成dom的关键,所以在一开始会先判断是否存在,如果不存在创建一个空的虚拟dom
  2. 调用生命周期钩子beforeMount
  3. 生成updateComponent方法,用于触发页面的更新;
  4. 生成watcherOptions配置,里面before方法会触发beforeUpdate钩子,将会在触发updateComponent前调用;
  5. 生成组件更新的watcher,前面两部分都是为了这部分做准备。new Watcher()后会触发updateComponent方法的调用,生成页面虚拟dom,将watcher加入到影响页面变化data的依赖收集器中,这样当data发生变化时,就会触发页面更新,最终进行dom diff,生成真实dom(详细的后面会说到);
  6. 调用生命周期钩子mounted

mountComponent方法源码如下:

js 复制代码
// src\core\instance\lifecycle.ts
export function mountComponent(
  vm: Component,
  el: Element | null | undefined,
  hydrating?: boolean
): Component {
  vm.$el = el
  // 不存在render 创建一个空的vnode 开发环境给出警告
  if (!vm.$options.render) {
    // @ts-expect-error invalid type
    vm.$options.render = createEmptyVNode
    if (__DEV__) {
      // 省略vue警告
    }
  }
  // 调用beforeMount钩子
  callHook(vm, 'beforeMount')

  // 初始化updateComponent值
  let updateComponent
  
  if (__DEV__ && config.performance && mark) {
    // 省略开发环境下性能检测代码
  } else {
    updateComponent = () => {
      // vm._render()得到vnode,vm._update更新页面
      vm._update(vm._render(), hydrating)
    }
  }

  // 页面更新前会调用before函数,触发beforeUpdate钩子
  const watcherOptions: WatcherOptions = {
    before() {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }
    
  if (__DEV__) {
    watcherOptions.onTrack = e => callHook(vm, 'renderTracked', [e])
    watcherOptions.onTrigger = e => callHook(vm, 'renderTriggered', [e])
  }

  // 创建组件更新的watcher,后面会加入到影响页面变化data的deps中
  new Watcher(
    vm,
    updateComponent,
    noop,
    watcherOptions,
    true /* isRenderWatcher */
  )
  hydrating = false

  // flush buffer for flush: "pre" watchers queued in setup()
  const preWatchers = vm._preWatchers
  if (preWatchers) {
    for (let i = 0; i < preWatchers.length; i++) {
      preWatchers[i].run()
    }
  }

  // 完成挂载 触发钩子mounted
  if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

更新机制形成------new Watcher()

这里主要分析上面new Watcher()后,如何触发updateComponent方法的调用。因此只涉及部分Watcher类的代码。

new Watcher()构造方法里会初始化一些属性,最重要的是将this.getter = expOrFn,将updateComponent方法赋给了this.getter。最后初始化this.value时,调用了this.get()

new Watcher()构造方法源码如下:

js 复制代码
/**
 * vm:实例
 * expOrFn:updateComponent方法(触发页面更新)
 * cb: function noop(a?: any, b?: any, c?: any) {}
 * options:上面的watcherOptions
 * isRenderWatcher:渲染类的watcher
 */
 constructor(
    vm: Component | null,
    expOrFn: string | (() => any),
    cb: Function,
    options?: WatcherOptions | null,
    isRenderWatcher?: boolean
  ) {
    // 将watcher挂载到vm._watcher上
    if ((this.vm = vm) && isRenderWatcher) {
      vm._watcher = this
    }
    
    // w记录所有的watcher
    w.push(this)
    
    // 根据options初始化一些属性
    if (options) {
      this.deep = !!options.deep
      this.user = !!options.user
      this.lazy = !!options.lazy
      this.sync = !!options.sync
      this.before = options.before
      if (__DEV__) {
        this.onTrack = options.onTrack
        this.onTrigger = options.onTrigger
      }
    } else {
      this.deep = this.user = this.lazy = this.sync = false
    }
    this.cb = cb
    this.id = ++uid // uid for batching
    this.active = true
    this.post = false
    this.dirty = this.lazy // for lazy watchers
    this.deps = []
    this.newDeps = []
    this.depIds = new Set()
    this.newDepIds = new Set()
    this.expression = __DEV__ ? expOrFn.toString() : ''
    
    // render类型expOrFn肯定是一个方法 所以this.getter就是页面更新方法了
    if (isFunction(expOrFn)) {
      this.getter = expOrFn
    } else {
      //...
    }
    
    // 因为render类型的watcher lazy值不会是true(只有computed才会是) 所以接下来会调用get方法
    this.value = this.lazy ? undefined : this.get()
  }

get方法中,可以看到这行代码value = this.getter.call(vm, vm)。就是这里调用了updateComponent方法。调用了updateComponent方法会触发vm._update(vm._render(), hydrating)。所以接下来分析vm._render方法。

get()方法如下:

js 复制代码
get() {
    // 相当于Dep.target=this
    pushTarget(this)
    let value
    const vm = this.vm
    try {
      // 调用this.getter方法,即调用了`updateComponent`方法
      value = this.getter.call(vm, vm)
    } catch (e: any) {
      if (this.user) {
        handleError(e, vm, `getter for watcher "${this.expression}"`)
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value)
      }
    
      popTarget()
      this.cleanupDeps()
    }
    return value
  }

生成虚拟dom------vm._render()

_render()函数,这里就不展示完整源码了,只看重点部分。这里主要是调用了vm.$options.render的方法,该方法用于生成虚拟dom。它可以是用户自定义的,也可以是编译而成的。最后返回虚拟dom

js 复制代码
// 主要作用执行render 得到vnode
  Vue.prototype._render = function (): VNode {
    // ...
    const vm: Component = this
    const { render, _parentVnode } = vm.$options
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      setCurrentInstance(vm)
      currentRenderingInstance = vm
      // 重点!!!调用render函数 得到vnode
      // vm._renderProxy在支持proxy且开发环境下是new Proxy(vm) 其他情况为vm
      // vm.$createElement是真正生成虚拟dom的函数
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e: any) {
      // ...
    } finally {
      currentRenderingInstance = null
      setCurrentInstance()
    }
    // ...
    return vnode
  }

用户自定义render函数

这种情况是用户自定义render函数。上面调用render方法处,传了一个vm.$createElement函数作为参数,所以hvm.$createElement

js 复制代码
new Vue({
  router,
  render: h => h(App),
}).$mount('#app')

找到vm.$createElement源码发现最终调用的是createElement方法。

js 复制代码
// src\core\instance\render.ts
// 这段代码在initRender中

// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
// @ts-expect-error
// 用于用户自己手写的render函数
vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

编译生成的render函数

这段html代码通过编译后生成的render函数如下。

js 复制代码
<div id="app">
    {{num1}}
    {{hello}}
    <button @click="changeNum">num1</button>
    <button @click="changeNum2">num2</button>
    <div v-for="(item,idx) of arr" :key="item">
        {{item}}
    </div>
    <comp :msg="msg" @log-msg="logMsg"></comp>
</div>

代码中,使用了with(this){}说明{}所有引用都指向this对象(即vm)。所以代码中的_c = vm._c,因此最终其实也是调用了createElement函数生成虚拟dom。在执行这段代码时,会读取到data中的属性,比如num1arr等等,读取时会调用这些属性的getter方法,在getter方法中会判断到Dep.target中存在值,会将该值存入依赖收集器,从而完成依赖收集。属性发生改变时,触发setter方法,通过depwatcher最终会调用updateComponent方法进行页面的更新。

js 复制代码
// src\core\instance\render.ts
// 这段代码在initRender中

// 用于编译后产生的render函数
vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

如果对里面_s_l函数感兴趣可以找到这段代码来看看。

js 复制代码
// src\core\instance\render-helpers\index.ts
export function installRenderHelpers(target: any) {
target._o = markOnce
target._n = toNumber
target._s = toString
target._l = renderList
target._t = renderSlot
target._q = looseEqual
target._i = looseIndexOf
target._m = renderStatic
target._f = resolveFilter
target._k = checkKeyCodes
target._b = bindObjectProps
target._v = createTextVNode
target._e = createEmptyVNode
target._u = resolveScopedSlots
target._g = bindObjectListeners
target._d = bindDynamicKeys
target._p = prependModifier
}

createElement

js 复制代码
const SIMPLE_NORMALIZE = 1 // 浅扁平
const ALWAYS_NORMALIZE = 2 // 深扁平

export function createElement(
  context: Component,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,/** 使用哪种扁平化方式 */
  alwaysNormalize: boolean/** 当render函数是手写的时候为true */
): VNode | Array<VNode> {
  // 实际上是判断data是否存在,如果不存在参数向前进一个
  if (isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE
  }
  return _createElement(context, tag, data, children, normalizationType)
}

_creatElement方法是创建vnode的核心。

  1. 规范childrenchildren按照normalizationType类型进行扁平化处理,这个目的是规范children,对于同一深度层次的元素,不管是单个元素还是该元素在数组中,只要是属于同一深度层次,都扁平化于一个一维数组中。
  2. 生成vnode:如果是普通标签,直接new Vnode()Vue组件则需要通过createComponent生成vnode
js 复制代码
// src\core\vdom\create-element.ts
/**
 * 生成虚拟dom
 * @param context vm
 * @param tag 标签(div、p、ul...)
 * @param data 标签上的属性
 * @param children 子节点
 * @param normalizationType 
 * @returns Vnode 虚拟dom
 */
export function _createElement(
  context: Component,
  tag?: string | Component | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // vnode data不能为响应式对象,有__ob__代表这个对象为响应式对象
  if (isDef(data) && isDef((data as any).__ob__)) {
    __DEV__ &&
      warn(
        `Avoid using observed data object as vnode data: ${JSON.stringify(
          data
        )}\n` + 'Always create fresh vnode data objects in each render!',
        context
      )
    return createEmptyVNode()
  }
  // object syntax in v-bind

  // <Component :is=""/>
  if (isDef(data) && isDef(data.is)) {
    tag = data.is
  }
  
  if (!tag) {
    // in case of component :is set to falsy value
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (__DEV__ && isDef(data) && isDef(data.key) && !isPrimitive(data.key)) {
    warn(
      'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
      context
    )
  }
  // support single function children as default scoped slot
  if (isArray(children) && isFunction(children[0])) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根据normalizationType对应不同扁平化处理方式
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 深层扁平化
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 浅层扁平化
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 标签为字符串类型
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (
        __DEV__ &&
        isDef(data) &&
        isDef(data.nativeOn) &&
        data.tag !== 'component'
      ) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      // 创建普通标签的vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag),
        data,
        children,
        undefined,
        undefined,
        context
      )
    } else if (
      (!data || !data.pre) &&
      isDef((Ctor = resolveAsset(context.$options, 'components', tag)))
    ) {
      // component
      // 为组件,调用createComponent创建vnode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 直接创建vnode
      vnode = new VNode(tag, data, children, undefined, undefined, context)
    }
  } else {
    // direct component options / constructor
    // tag为组件构造函数或组件选项,创建Vue组件
    vnode = createComponent(tag as any, data, context, children)
  }
  if (isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

创建Vue组件------createComponent

createComponent作用是返回Vue组件的虚拟dom。同时在这过程中,会构造子类构造函数(这里会调用_init方法完成组件初始化)、安装组件钩子函数。

源码位置src\core\vdom\create-component.ts

这部分可以直接看这篇文章【Vue源码】8.组件化-createComponent

生成真实dom------vm._update(vnode)

vm._update方法源码如下,在源码写了注释,这里就不展开了。

js 复制代码
// src\core\instance\lifecycle.ts
 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    // 存储前面的el
    const prevEl = vm.$el
    // 存储前面的vnode
    const prevVnode = vm._vnode
    // 存储活动的实例
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates 更新
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // update __vue__ reference
    // 更新指向
    if (prevEl) {
      prevEl.__vue__ = null
    }
    if (vm.$el) {
      vm.$el.__vue__ = vm
    }
    // if parent is an HOC, update its $el as well
    let wrapper: Component | undefined = vm
    while (
      wrapper &&
      wrapper.$vnode &&
      wrapper.$parent &&
      wrapper.$vnode === wrapper.$parent._vnode
    ) {
      wrapper.$parent.$el = wrapper.$el
      wrapper = wrapper.$parent
    }
  }

总结

总体来说,整个过程可以分为四部分,分别是初始化、挂载、生成虚拟节点、生成真实节点,最后调用mounted钩子。

  1. 初始化:合并配置项,初始化生命周期、事件、datacomputedwatch等等。完成了beforeCreatecreated的整个过程。
  2. 挂载:如果options.render不存在,将template编译生成render函数。接下来就是建立更新机制,创建watcher,通过watcher中的get方法,调用组件更新updateComponent函数。
  3. 生成虚拟dom:调用更新函数,会调用vm._render方法,这个方法会调用vm.$options.render方法用于生成虚拟dom
  4. 生成真实dom:通过vm._render方法得到虚拟dom后,会作为vm._update()方法的参数去生成vnode相应的真实dom
相关推荐
安冬的码畜日常41 分钟前
【D3.js in Action 3 精译_029】3.5 给 D3 条形图加注图表标签(上)
开发语言·前端·javascript·信息可视化·数据可视化·d3.js
小白学习日记1 小时前
【复习】HTML常用标签<table>
前端·html
程序员大金2 小时前
基于SpringBoot+Vue+MySQL的装修公司管理系统
vue.js·spring boot·mysql
丁总学Java2 小时前
微信小程序-npm支持-如何使用npm包
前端·微信小程序·npm·node.js
yanlele2 小时前
前瞻 - 盘点 ES2025 已经定稿的语法规范
前端·javascript·代码规范
懒羊羊大王呀2 小时前
CSS——属性值计算
前端·css
DOKE3 小时前
VSCode终端:提升命令行使用体验
前端
xgq3 小时前
使用File System Access API 直接读写本地文件
前端·javascript·面试
用户3157476081353 小时前
前端之路-了解原型和原型链
前端