Vue实例的源码解析 (实例一)

本文深入解析Vue实例的创建过程,包括数据处理、虚拟DOM生成和真实节点挂载的三个阶段。通过示例详细阐述每个阶段的具体步骤,涉及构造函数、模板编译、数据代理、vnode创建及元素挂载等关键环节。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Vue实例的源码解析

接下来我将用两个例子来讲解一个vm实例从无到有再到渲染页面的过程,可能比较复杂,望君能沉静下来。

这里只讨论了关于实例的创建以及虚拟节点的创建和挂载部分,并没有讨论生成实例后的数据处理部分。关于数据处理部分我会在后续的文章中详细介绍

示例一:

示例一的代码如下:

<body>
    <div id="app">
        <span>{
  {message}}</span>
    </div>
    <script src="./vue-dev/dist/vue.js"></script>
    <script>
        var vm = new Vue({
     
            el:'#app',
            data:{
     
                message:123
            }
        })
    </script>
</body>

这个实例很简单,没有用到嵌套组件。接下来我们来讲解一下具体的流程。每一个组件实例都会经历三个阶段(在没有经过数据更改的情况下),分别是:数据处理阶段生成vnode阶段生成真实节点挂载阶段

数据处理阶段:

当我们执行new Vue时候,会执行Vue构造函数:

function Vue (options) {
    
  //options是我们传入的配置项
  //这个就是大名鼎鼎的vue构造函数,所有的vue项目的开始的地方
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
    //这里的this 其实使 vm实例,看是否用了new Vue
  ) {
   
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  //这里的this是我们执行new Vue 时构造函数内部生成的实例对象,也可以理解为vm
  this._init(options)
  //从这个函数进入我们用initMixin(Vue)初始化添加的_init函数
}

Vue构造函数传入我们的配置项options。此时vm实例已经创建完成,然后调用this._init(options)来初始化我们的数据。

vm._init()函数位于src/core/instance/init.js。具体代码如下:

 Vue.prototype._init = function (options?: Object) {
   
    //定义一个vm并指向this
    const vm: Component = this
    // a uid
    //为这个vm实例添加一个唯一的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
    // a flag to avoid this being observed
    vm._isVue = true
    // merge options
    // _isComponent是内部创建子组件时才会添加为true的属性
    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 {
   
      //合并options
      vm.$options = mergeOptions(
        //resolveConstructorOptions函数在后面有定义
        //向该函数传入的是vm.constructor也就是Vue
        resolveConstructorOptions(vm.constructor),
        options || {
   },
        vm
      )
      //vm.$options合并两项
    }
    /* 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) // resolve injections before data/props
    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) {
   
      vm.$mount(vm.$options.el)
    }
  }

这段代码非常好懂,首先是将我们的vm实例上挂载uid来标识每一个组件实例,然后添加_isVue。表示的是Vue组件,然后判断是否是一个组件,虽然说我们的根组件也是组件,但是它这里所说的组件其实表示的是子组件。例如你的模板中引入了一个组件。然显然我们的根实例不是子组件那种类型的,然后就会走else分支,该分支的作用是调用mergeOptions函数来合并Vue.options和我们传入的options,合并成一个总的options然后添加到vm.$options上。接着就是进行数据代理处理。当处理完之后会执行:

  initLifecycle(vm)
    initEvents(vm)
    initRender(vm)
    callHook(vm, 'beforeCreate')
    initInjections(vm) // resolve injections before data/props
    initState(vm)
    initProvide(vm) // resolve provide after data/props
    callHook(vm, 'created')

这些都是进行一些事件和数据的初始化的,在初始化的过程中会伴随着一些钩子函数的执行。组件的数据处理就集中在这里,我们这里不对数据处理的具体操作进行展开,因为后续会单独的讲解数据处理的部分。当vue数据处理完成后,就进入到了下一个阶段:生成vnode阶段

生成vnode阶段:

当数据处理完之后会执行:

 if (vm.$options.el) {
   
      vm.$mount(vm.$options.el)
    }

因为根组件实例的$el是存在的,所以会执行vm.$mount(vm.$options.el)这个函数。该函数位于:./src/platforms/web/entry-runtime-with-complier.js文件中。具体的代码如下:

Vue.prototype.$mount = function (
  el?: string | Element,//我们传入的el类型有两种,一种是字符串'#app',另一种是一个元素对象,例如document.getElementById('app')
  hydrating?: boolean
): Component {
     
  el = el && query(el)//query函数主要是返回一个元素对象,如果我们传入的el存在,那么就返回该元素的对象形式,如果不存在,那么就会默认是一个div元素对象

  /* istanbul ignore if */
  if (el === document.body || el === document.documentElement) {
   
    //这里告诉我们el不能是body和html。原因是它会发生覆盖,这样就会将原来的模板完全覆盖掉。
    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//这里的this指向的是vm实例对象
  // resolve template/el and convert to render function
  if (!options.render) {
   
    //如果我们没有render函数。那么会进入该区域代码。
    let template = options.template //获取模板
    if (template) {
   
      //如果存在template配置项,
      if (typeof template === 'string') {
   //如果配置项的类型为字符串。
        if (template.charAt(0) === '#') {
   //这里我们只处理template为#xxx的格式的模板,也就是类似于template:'#app'这种
          template = idToTemplate(template)//该函数返回的是template模板内部的节点的字符串形式。
          /* istanbul ignore if */
          //这里是template的错误处理
          if (process.env.NODE_ENV !== 'production' && !template) {
   
            warn(
              `Template element not found or is empty: ${
     options.template}`,
              this
            )
          }
        }
      } else if (template.nodeType) {
   
        //如果我们传入的template是一个节点对象,那么获取该节点对象中的innerHTML,然会的也是字符串形式
        template = template.innerHTML
      } else {
   
        //不是以上两种格式,那么抛出错误
        if (process.env.NODE_ENV !== 'production') {
   
          warn('invalid template option:' + template, this)
        }
        return this
      }
    } else if (el) {
   
      //如果template配置项不存在,那么获取el.outerHTML当作我们的template。返回的也是字符串类型
      template = getOuterHTML(el)
    }
    
    if (template) {
   
      //这是处理好的template。这种template的来源有两种,第一种是我们自己设置的,另一种就是el.outerHTML来充当template
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   
        mark('compile')
      }


      //接下来的代码是进行编译,将我们的模板编译成以js描述的对象,即虚拟DOM,然后将虚拟DOM转化为render(渲染函数)。
      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')
      }
    }
  }
  //如果我们没有render函数,其实通过对template进行编译,我们就会获得render函数,
  //然后调用mount.call()函数。
  //如果我们自己有render函数,那么我们就可以直接调用mount.call函数,不需要去进行编译。
  return mount.call(this, el, hydrating)//this -> vm ; el -> 元素对象
}

该函数做的事情很多,我们来具体分析,首先是获取到我们的el元素节点,然后判断我们的el是否是body/html。因为会发生覆盖问题,所以Vue不允许我们的根节点是body/html。然后获取vm实例上的options.render函数,因为我们没有定义render函数,所以就会去判断是否有template模板,我们也没有定义该模板,所以就会走else if的分支:

//如果template配置项不存在,那么获取el.outerHTML当作我们的template。返回的也是字符串类型
      template = getOuterHTML(el)

因为我们没有定义template模板,所以Vue就把我们的根节点作为了我们的template模板(字符串形式)。此时vm实例对象就有template模板了,所以会执行:

 const {
    render, staticRenderFns } = compileToFunctions(template, {
   
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this)
 //将生成的render函数挂载到options.render上
      options.render = render
      options.staticRenderFns = staticRenderFns 

这段代码是干什么的呢?它的作用是将我们的tempalte通过Vue内置的模板编译器把它变为一个render函数。然后将该函数挂载到vm.options.render上面去。compileToFunctions函数的内部比较复杂,有兴趣的话可以自己研究研究。这里提醒一下render函数是一个函数,不是vdom

当这些执行完之后就会执行mount.call(this, el, hydrating)函数,可能你会问,mount函数是什么。这里简要说一下,Vue官方给我们提供了两个版本,第一个版本是我们的runtime + complier版本,另一个版本是runtimeOnly版本,这两个版本有什么不同呢?前者是允许我们写template的,或者说是允许我们添加el属性的,因为该版本内置的有编译器,而后者是让我们写render函数的,并不会进行模板编译处理,而mount函数是后者的挂载函数,为什么前者引用了呢。原因是因为我们前者虽然没有render函数,但是通过模板编译还是会生成,生成之后仍需要挂载,所以还是会用到mount函数。好了我们来看该函数内部的代码实现:

//  /web/runtime/index.js
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
   
  el = el && inBrowser ? query(el) : undefined//这里之所以对再一次的对el进行判断,是因为这里的$mount是runtime-only版本的,所以对你传入的el进行判断。
  return mountComponent(this, el, hydrating)
}

首先还是先获取el元素节点,然后执行mountComponent()函数。代码很简单,不过多介绍,我们接下来看mountComponent函数。该函数的具体代码如下:

//./src/core/instance/lifecycle.js
export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
   
  vm.$el = el//将el元素对象挂载到vm.$el上,也就是说,vm.$el是在执行$mount的时候挂载上去的。宏观的将,它是在created钩子函数之后,beforeMount钩子函数之前被挂载的。
  if (!vm.$options.render) {
   //这里的vm.$options.render是处理之后的render函数,也就是说,如果我们如果不传入render函数或者编译后的虚拟DOM无法生成render函数,那么vm.$options.render都为false
    vm.$options.render = createEmptyVNode//如果在上述中为真,那么我们就给vm.$options.render赋值一个由空虚拟DOM组成的渲染函数。
    if (process.env.NODE_ENV !== 'production') {
   
      /* istanbul ignore if */
      //这里的警告就是你用了runtime-only,但是你写了template/el那么就会报错,它只能接收render函数。这是版本问题
      if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
        vm.$options.el || el) {
   
        warn(
          'You are using the runtime-only build of Vue where the template ' +
          'compiler is not available. Either pre-compile the templates into ' +
          'render functions, or use the compiler-included build.',
          vm
        )
      } else {
   
        warn(
          'Failed to mount component: template or render function not defined.',
          vm
        )
      }
    }
  }
  //触发berforeMount钩子函数
  callHook(vm, 'beforeMount')

  let updateComponent
  /* istanbul ignore if */
  //这和运行性能有关,暂时可以忽视
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
   
 		......
  } else {
   
    //最终定义updateComponent函数
    updateComponent = () => {
   
      //vm._update是在lifecycleMixin(Vue)中定义的
      //vm._render是在renderMixin中定义的。
      //hydrating:false
      //该函数的执行其实是在new Watcher()中执行的,我们暂时只关注它的执行,不去关注在什么地方触发。
      vm._update(vm._render(), hydrating)
    }
  }

  // we set this to vm._watcher inside the watcher's constructor
  // since the watcher's initial patch may call $forceUpdate (e.g. inside child
  // component's mounted hook), which relies on vm._watcher being already defined
  new Watcher(vm, updateComponent, noop, {
   
    before () {
   
      if (vm._isMounted && !vm._isDestroyed) {
   
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)
  hydrating = false

  // manually mounted instance, call mounted on self
  // mounted is called for render-created child components in its inserted hook
  if (vm.$vnode == null) {
   
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

el节点挂载到vm实例的$el属性上,然后判断vm.$options.render函数是否存在。因为vm实例已经通过编译根节点获取到render函数了,所以向下执行。在这里触发beforeMount钩子函数。

接着就定义updateComponent函数,其具体代码如下:

  updateComponent = () => 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值