美文网首页
Vue源码解读(二):初始化和挂载

Vue源码解读(二):初始化和挂载

作者: 明么 | 来源:发表于2021-09-12 20:32 被阅读0次

    初始化流程

    new Vue

    我们在使用 Vue 的时候,首页就是先 new Vue(...) ;在上一章中通过分析构建流程,我们得出入口文件 src/platforms/web/entry-runtime-with-compiler.js ,通过入口文件,我们一步一步找到 Vue 构造函数定义所在:

    // src/platforms/web/entry-runtime-with-compiler.js
    // ...
    import Vue from './runtime/index'
    // ...
    
    // src/platforms/web/runtime/index.js
    import Vue from 'core/index'
    // ...
    
    // src/core/index.js
    import Vue from './instance/index'
    import { initGlobalAPI } from './global-api/index'
    import { isServerRendering } from 'core/util/env'
    import { FunctionalRenderContext } from 'core/vdom/create-functional-component'
    // 初始化全局 API
    initGlobalAPI(Vue)
    // ...
    
    // src/core/instance/index.js
    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'
    // 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')
      }
      // 调用 Vue.prototype_init 方法,该方法是在 initMixin 中定义的
      this._init(options)
    }
    // 定义 Vue.prototype_init 方法
    initMixin(Vue)
    /**
     * 定义:
     *   Vue.prototype.$data
     *   Vue.prototype.$props
     *   Vue.prototype.$set
     *   Vue.prototype.$delete
     *   Vue.prototype.$watch
     */
    stateMixin(Vue)
    /**
     * 定义 事件相关的 方法:
     *   Vue.prototype.$on
     *   Vue.prototype.$once
     *   Vue.prototype.$off
     *   Vue.prototype.$emit
     */
    eventsMixin(Vue)
    /**
     * 定义:
     *   Vue.prototype._update
     *   Vue.prototype.$forceUpdate
     *   Vue.prototype.$destroy
     */
    lifecycleMixin(Vue)
    /**
     * 定义:
     *   Vue.prototype.$nextTick
     *   Vue.prototype._render
     */
    renderMixin(Vue)
    export default Vue
    

    _init

    // src/core/instance/init.js
    export function initMixin (Vue: Class<Component>) {
      Vue.prototype._init = function (options?: Object) {
        const vm: Component = this
        // 每个实例都保存一个 _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
        // 处理组件配置项
        if (options && options._isComponent) {
          // 每个子组件初始化时走这里,这里只做了一些性能优化
          // 将组件配置对象上的一些深层次属性放到 vm.$options 选项中,以提高代码的执行效率
          initInternalComponent(vm, options)
        } else {
          // 合并选项,合并默认选项和自定义选项
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        // 设置代理,将 vm 实例上的属性代理到 vm._renderProxy
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        // 初始化实例关系属性,$parent、$children、$refs、$root等
        initLifecycle(vm)
        // 初始化自定义事件,处理父组件传递的事件和回调
        initEvents(vm)
        // 解析组件的插槽信息,得到 vm.$slot,处理渲染函数(_render),得到 vm.$createElement 方法,即 h 函数
        initRender(vm)
        // 调用 beforeCreate 钩子函数
        callHook(vm, 'beforeCreate')
        // 初始化组件的 inject 配置项,得到 result[key] = val 形式的配置对象,然后对结果数据进行响应式处理,并代理每个 key 到 vm 实例
        initInjections(vm)
        // 数据响应式核心,处理 props、methods、data、computed、watch
        initState(vm)
        // 解析组件配置项上的 provide 对象,将其挂载到 vm._provided 属性上
        initProvide(vm) // resolve provide after data/props
        // 调用 created 钩子函数
        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)
        }
      }
    }
    

    上面代码很清晰的看出初始化都做了哪些事情,在初始化的最后,如果有 el 属性,则会自动调用 vm.$mount 进行挂载,否则我们就需要手动调用 $mount。接下里就进入了挂载阶段。

    Vue 实例挂载

    $mount

    入口文件 src/platforms/web/entry-runtime-with-compiler.js

    /* @flow */
    import config from 'core/config'
    import { warn, cached } from 'core/util/index'
    import { mark, measure } from 'core/util/perf'
    import Vue from './runtime/index'
    import { query } from './util/index'
    import { compileToFunctions } from './compiler/index'
    import { shouldDecodeNewlines, shouldDecodeNewlinesForHref } from './util/compat'
    const idToTemplate = cached(id => {
      const el = query(id)
      return el && el.innerHTML
    })
    /**
     * 编译器的入口
     * 进行预编译,最终将模版编译成 render 函数
     */
    // 缓存原型上的方法
    const mount = Vue.prototype.$mount
    // 重新定义该方法
    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && query(el)
      // 不能挂载在 body、html 这样的根节点上
      if (el === document.body || el === document.documentElement) {
        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
      /**
       *   若没有 render方法,则解析 template 和 el,并转换为 render 函数
       *   优先级:render > template > el
       */
      if (!options.render) {
        let template = options.template
        // template
        if (template) {
          if (typeof template === 'string') {
            if (template.charAt(0) === '#') {
              // { template: '#app' },以 id 为 ‘app’ 的节点,作为挂载节点
              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 是一个正常的元素,获取其 innerHtml 作为模版
            template = template.innerHTML
          } else {
            if (process.env.NODE_ENV !== 'production') {
              warn('invalid template option:' + template, this)
            }
            return this
          }
        } else if (el) {
          // 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, {
            // 在非生产环境下,编译时记录标签属性在模版字符串中开始和结束的位置索引
            outputSourceRange: process.env.NODE_ENV !== 'production',
            shouldDecodeNewlines,
            shouldDecodeNewlinesForHref,
            // 界定符,默认 {{}}
            delimiters: options.delimiters,
            // 是否保留注释
            comments: options.comments
          }, this)
          // 将两个渲染函数放到 this.$options 上
          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)
    }
    /**
     * Get outerHTML of elements, taking care
     * of SVG elements in IE as well.
     */
    function getOuterHTML (el: Element): string {
      if (el.outerHTML) {
        return el.outerHTML
      } else {
        const container = document.createElement('div')
        container.appendChild(el.cloneNode(true))
        return container.innerHTML
      }
    }
    Vue.compile = compileToFunctions
    export default Vue
    

    从上面代码可以看出,不管定义 render 方法还是 eltemplate 属性,最终的目的就是得到 render 渲染函数。然后保存在 options 上。

    编译模板,得到 render 渲染函数,通过调用 compileToFunctions 方法,这个到编译器的时候再一块看。

    最后调用原型上的 $mount ,定义在 src/platform/web/runtime/index.js

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      el = el && inBrowser ? query(el) : undefined
      return mountComponent(this, el, hydrating)
    }
    

    实际调用 mountComponent ,定义在 src/core/instance/lifecycle.js

    mountComponent

    // src/core/instance/lifecycle.js
    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
        if (process.env.NODE_ENV !== 'production') {
          /* istanbul ignore if */
          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
            )
          }
        }
      }
      callHook(vm, 'beforeMount')
      let updateComponent
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        updateComponent = () => {
          const name = vm._name
          const id = vm._uid
          const startTag = `vue-perf-start:${id}`
          const endTag = `vue-perf-end:${id}`
          mark(startTag)
          const vnode = vm._render()
          mark(endTag)
          measure(`vue ${name} render`, startTag, endTag)
          mark(startTag)
          vm._update(vnode, hydrating)
          mark(endTag)
          measure(`vue ${name} patch`, startTag, endTag)
        }
      } else {
        // 执行 vm._render() 函数,得到 虚拟 DOM,并将 vnode 传递给 _update 方法,接下来就该到 patch 阶段了
        updateComponent = () => {
          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
     
     // vm.$vnode 表示 Vue 实例的父虚拟 Node,所以它为 Null 则表示当前是根 Vue 的实例
      if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
      }
      return vm
    }
    

    mountComponent 内定义了 updateComponent 方法,然后实例化一个Watcher,同时将 updateComponent 作为参数传入,在 Watcher 的回调函数中被调用。Watcher 在这里主要是初始化和数据变化时,执行回调函数。

    最后设置 vm._isMounted = true ,表示实例已挂载。

    updateComponent 的调用会执行 vm._updatevm._rendervm._render 获取虚拟DOM,vm._update 更新视图。

    上面代码出现了三个生命周期钩子 beforeMountbeforeUpdatemounted ;也就是说,在执行 vm._render() 之前,执行了 beforeMount 钩子函数;在执行完 vm._update() 把虚拟DOM转换真实 DOM 后,执行 mounted 钩子函数;后续若数据变化时,通过 _isMounted 标记,表示已挂载则执行 beforeUpdate 钩子函数。

    这里值得注意的是,在 mounted 钩子执行前有个判断,只有在父虚拟 Node 为 null 的时候执行。只有 new Vue 才会走到这里,如果是组件的话,它的父虚拟 Node 是存在的。组件的 mounted 在别的地方。

    相关链接

    Vue源码解读(预):手写一个简易版Vue

    Vue源码解读(一):准备工作

    Vue源码解读(二):初始化和挂载

    Vue源码解读(三):响应式原理

    Vue源码解读(四):更新策略

    Vue源码解读(五):render和VNode

    Vue源码解读(六):update和patch

    Vue源码解读(七):模板编译(待续)

    如果觉得还凑合的话,给个赞吧!!!也可以来我的个人博客逛逛 https://www.mingme.net/

    相关文章

      网友评论

          本文标题:Vue源码解读(二):初始化和挂载

          本文链接:https://www.haomeiwen.com/subject/yjbtgltx.html