美文网首页前端面试题目解析
Vue 实例挂载发生什么

Vue 实例挂载发生什么

作者: JerisonPaul | 来源:发表于2021-09-06 12:55 被阅读0次

    一、思考

    我们都听过知其然知其所以然这句话
    那么不知道大家是否思考过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)
        }
      }
    

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

    • 在调用beforeCreate之前,数据初始化并未完成,像dataprops这些属性无法访问到

    • 到了created的时候,数据已经初始化完成,能够访问dataprops这些属性,但这时候并未完成dom的挂载,因此无法访问到dom元素

    • 挂载方法是调用vm.$mount方法

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

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

    • 初始化顺序:propsmethodsdata

    • data定义的时候可选择函数形式或者对象形式(组件只能为函数形式)

    关于数据响应式在这就不展开详细说明

    上文提到挂载方法是调用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)
    }
    

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

    • 不要将根元素放到body或者html

    • 可以在对象中定义template/render或者直接使用templateel表示元素选择器

    • 最终都会解析成render函数,调用compileToFunctions,会将template解析成render函数

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

    • html文档片段解析成ast描述符

    • ast描述符解析成字符串

    • 生成render函数

    生成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
    }
    

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

    • 会触发boforeCreate钩子
    • 定义updateComponent渲染页面视图的方法
    • 监听组件数据,一旦发生变化,触发beforeUpdate生命钩子

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

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

    三、结论

    • new Vue的时候调用会调用_init方法

      • 定义 $set$get$delete$watch 等方法
      • 定义 $on$off$emit$off等事件
      • 定义 _update$forceUpdate$destroy生命周期
    • 调用$mount进行页面的挂载

    • 挂载的时候主要是通过mountComponent方法

    • 定义updateComponent更新函数

    • 执行render生成虚拟DOM

    • _update将虚拟DOM生成真实DOM结构,并且渲染到页面中
      参考文献:https://www.cnblogs.com/gerry2019/p/12001661.html

    相关文章

      网友评论

        本文标题:Vue 实例挂载发生什么

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