• 技术文章 >web前端 >Vue.js

    Vue实例要怎么挂载?聊聊实例挂载的过程

    青灯夜游青灯夜游2022-01-07 19:33:32转载118
    Vue实例要怎么挂载?下面本篇文章带大家了解一下Vue实例挂载的过程,希望对大家有所帮助。

    Vue2目前已经出现在大众视野中非常久了,甚至Vue3都已经开始投入使用,所有大家对于Vue2的掌握程度要逐步加深,即使你不想面大厂,也要试着到达源码级的。Vue实例的挂载过程是一个面试中高频出现的考点,今天带着大家根据源码一步步探析!

    一、思考

    我们都听过知其然知其所以然这句话。

    那么不知道大家是否思考过new Vue()这个过程中究竟做了些什么?

    过程中是如何完成数据的绑定,又是如何将数据渲染到视图的等等。

    二、分析

    首先找到Vue的构造函数

    源码位置: 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)
    }

    options是用户传递过来的配置项,如data、methods等常用的方法。

    Vue构建函数调用_init方法,但我们发现本文件中并没有此方法,但仔细可以看到文件下方定义了很多初始化方法。

    initMixin(Vue);     // 定义 _init
    stateMixin(Vue);    // 定义 $set $get $delete $watch 等
    eventsMixin(Vue);   // 定义事件  $on  $once $off $emit
    lifecycleMixin(Vue);// 定义 _update  $forceUpdate  $destroy
    renderMixin(Vue);   // 定义 _render 返回虚拟dom

    首先可以看到initMixin方法,发现该方法在Vue原型上定义了_init方法。

    源码位置: 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
        // 合并属性,判断初始化的是否是组件,这里合并主要是 mixins 或 extends 的方法
        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 { // 合并vue属性
          vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
          )
        }
        /* istanbul ignore else */
        if (process.env.NODE_ENV !== 'production') {
          // 初始化proxy拦截器
          initProxy(vm)
        } else {
          vm._renderProxy = vm
        }
        // expose real self
        vm._self = vm
        // 初始化组件生命周期标志位
        initLifecycle(vm)
        // 初始化组件事件侦听
        initEvents(vm)
        // 初始化渲染方法
        initRender(vm)
        callHook(vm, 'beforeCreate')
        // 初始化依赖注入内容,在初始化data、props之前
        initInjections(vm) // resolve injections before data/props
        // 初始化props/data/method/watch/methods
        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)
        }
      }

    仔细阅读上面的代码,我们得到以下的结论:

    initState方法是完成 props/data/method/watch/methods的初始化。

    源码位置:src/core/instance/state.js

    export function initState (vm: Component) {
      // 初始化组件的watcher列表
      vm._watchers = []
      const opts = vm.$options
      // 初始化props
      if (opts.props) initProps(vm, opts.props)
      // 初始化methods方法
      if (opts.methods) initMethods(vm, opts.methods)
      if (opts.data) {
        // 初始化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的方法为initData,它与initState在同一文件上

    function initData (vm: Component) {
      let data = vm.$options.data
      // 获取到组件上的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
            )
          }
        }
        // 属性名不能与state名称重复
        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)) { // 验证key值的合法性
          // 将_data中的数据挂载到组件vm上,这样就可以通过this.xxx访问到组件上的数据
          proxy(vm, `_data`, key)
        }
      }
      // observe data
      // 响应式监听data是数据的变化
      observe(data, true /* asRootData */)
    }

    仔细阅读上面的代码,我们可以得到以下结论:

    关于数据响应式在这就不展示详细说明了 上文提到挂载方法是调用vm.$mount方法

    源码:

    Vue.prototype.$mount = function (
      el?: string | Element,
      hydrating?: boolean
    ): Component {
      // 获取或查询元素
      el = el && query(el)
    
      /* istanbul ignore if */
      // vue 不允许直接挂载到body或页面文档上
      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
      // resolve template/el and convert to render function
      if (!options.render) {
        let template = options.template
        // 存在template模板,解析vue模板文件
        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')
          }
          /**
           *  1.将temmplate解析ast tree
           *  2.将ast tree转换成render语法字符串
           *  3.生成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')
          }
        }
      }
      return mount.call(this, el, hydrating)
    }

    阅读上面代码,我们能得到以下结论:

    template的解析步骤大致分为以下几步:

    生成render函数,挂载到vm上后,会再次调用mount方法

    源码位置:src/platforms/web/runtime/index.js

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

    调用mountComponent渲染组件

    export function mountComponent (
      vm: Component,
      el: ?Element,
      hydrating?: boolean
    ): Component {
      vm.$el = el
      // 如果没有获取解析的render函数,则会抛出警告
      // render是解析模板文件生成的
      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 {
            // 没有获取到vue的模板文件
            warn(
              'Failed to mount component: template or render function not defined.',
              vm
            )
          }
        }
      }
      // 执行beforeMount钩子
      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 {
        // 定义更新函数
        updateComponent = () => {
          // 实际调⽤是在lifeCycleMixin中定义的_update和renderMixin中定义的_render
          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
    }

    阅读上面代码,我们得到以下结论:

    updateComponent方法主要执行在vue初始化时声明的render, update方法

    render的作用主要是生成vnode

    源码位置:src/core/instance/render.js

    // 定义vue 原型上的render方法
    Vue.prototype._render = function (): VNode {
        const vm: Component = this
        // render函数来自于组件的option
        const { render, _parentVnode } = vm.$options
    
        if (_parentVnode) {
            vm.$scopedSlots = normalizeScopedSlots(
                _parentVnode.data.scopedSlots,
                vm.$slots,
                vm.$scopedSlots
            )
        }
    
        // set parent vnode. this allows render functions to have access
        // to the data on the placeholder node.
        vm.$vnode = _parentVnode
        // 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.
            currentRenderingInstance = vm
            // 调用render方法,自己的独特的render方法, 传入createElement参数,生成vNode
            vnode = render.call(vm._renderProxy, vm.$createElement)
        } catch (e) {
            handleError(e, vm, `render`)
            // return error render result,
            // or previous vnode to prevent render error causing blank component
            /* istanbul ignore else */
            if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
                try {
                    vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
                } catch (e) {
                    handleError(e, vm, `renderError`)
                    vnode = vm._vnode
                }
            } else {
                vnode = vm._vnode
            }
        } finally {
            currentRenderingInstance = null
        }
        // if the returned array contains only a single node, allow it
        if (Array.isArray(vnode) && vnode.length === 1) {
            vnode = vnode[0]
        }
        // return empty vnode in case the render function errored out
        if (!(vnode instanceof VNode)) {
            if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
                warn(
                    'Multiple root nodes returned from render function. Render function ' +
                    'should return a single root node.',
                    vm
                )
            }
            vnode = createEmptyVNode()
        }
        // set parent
        vnode.parent = _parentVnode
        return vnode
    }

    _update主要功能是调用patch,将vnode转成为真实DOM,并且更新到页面中

    源码位置:src/core/instance/lifecycle.js

    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
        const vm: Component = this
        const prevEl = vm.$el
        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
        if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
          vm.$parent.$el = vm.$el
        }
        // updated hook is called by the scheduler to ensure that children are
        // updated in a parent's updated hook.
      }

    三、结论

    【相关推荐:vue.js教程

    以上就是Vue实例要怎么挂载?聊聊实例挂载的过程的详细内容,更多请关注php中文网其它相关文章!

    声明:本文转载于:掘金社区,如有侵犯,请联系admin@php.cn删除
    专题推荐:Vue实例 实例挂载
    上一篇:推荐 20 个近期比较“火热”的Vue项目(值得收藏) 下一篇:解析源码,看看 vue 编译器是怎么生成渲染函数的!

    相关文章推荐

    • vue和微信小程序的区别有哪些• 带你深入了解vue2中的 v-model,看看如何让组件支持该语法• 推荐 20 个近期比较“火热”的Vue项目(值得收藏)• 简单聊聊Vue3中的Hook特性(总结分享)• vue怎样解决axios请求出现前端跨域问题(实例详解)

    全部评论我要评论

  • 取消发布评论发送
  • 1/1

    PHP中文网