最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • 从创建实例到模板编译前Vue都做了些什么?

    正文概述 掘金(兔走)   2021-07-26   373

    写在前面

    Vue构造函数的创建过程 一文中介绍了 Vue构造函数 的创建过程,其中第一个对 Vue构造函数 进行成员添加的就是 initMixin(Vue),该调用内创建了 _init 方法。可以说,Vue实例 的大门就是 _init 方法,因此,我们从这个方法入手,一步一步剖析 vm 是如何生成的。

    /*
    <div id="app">
      <div>
        <h1>{{msg}}</h1>
        <div id="extendUse"></div>
        <child-component :msg="msg" />
      </div>
    </div>
    */
    // Vue.extend 手动挂载组件
    const ExtendUse = Vue.extend({
      props: ['msg'],
      template: `<h2>ExtendUse said: {{msg}}</h2>`,
    })
    const ChildExtend = new ExtendUse({
      propsData: {
        msg: 'hello Extend'
      }
    }).$mount('#extendUse')
    // Vue.component 自动挂载组件
    const ChildComponent = Vue.extend({
      props: ['msg'],
      template: `<h2>child's father said: {{msg}}</h2>`,
    })
    Vue.component('child-component', ChildComponent)
    // Vue 实例
    const vm = new Vue({
      el: '#app',
      data: {
        msg: 'hello Vue',
      }
    })
    

    按照惯例,我先将 _init 的源码简写一下,并且划分一下步骤:

    Vue.prototype._init = function (options) {
      // 步骤 - 1
      const vm = this
      vm._uid = uid++
      vm._isVue = true
      // 步骤 - 2
      if (options && options._isComponent) {
        initInternalComponent(vm, options)
      } else {
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        )
      }
      // 步骤 - 3
      if (process.env.NODE_ENV !== 'production') {
        initProxy(vm)
      } else {
        vm._renderProxy = vm
      }
      vm._self = vm
      // 步骤 - 4
      initLifecycle(vm)
      initEvents(vm)
      initRender(vm)
      callHook(vm, 'beforeCreate')
      initInjections(vm)
      initState(vm)
      initProvide(vm)
      callHook(vm, 'created')
      // 步骤 - 5
      if (vm.$options.el) {
        vm.$mount(vm.$options.el)
      }
    }
    

    步骤 - 1

    在这段代码中,我们首要了解的是 vm 指向了谁? 从它的调用 function Vue(options) { this._init(options) } 中我们可以了解到,Vue 是一个构造函数,而 _init 方法又不是该构造函数的静态方法,因此 this 指向了实例,所以 const vm = this 这一步其实就是将实例本身赋值给了 vm。
    再具体点?const app = new Vue(options) 这里的 app 就是 vm 的值。
    接着,在实例身上添加上 _uid 和 _isVue 的标识,记录当前实例是第 (_uid + 1) 个 Vue 实例对象。

    步骤 - 2

    这段代码是一个条件判断,判断的是是否是一个组件。那怎样的存在算是一个组件呢?Vue.extend 手动挂载上去的算不算一个组件呢?
    我就先兜个底,只有存在于 vm.options.components 中的才算组件,且能进入 if 判断之中。这就意味着,要么 Vue.component(id, definition) 定义的对象,要么 new Vue({ components: { id: definition } }) 定义的对象,其他方式都不具备 _isComponent 属性。因此,即使是 Vue.component 方法内同样也调用了的 Vue.extend 方法,在手动挂载时也不算作组件,不具备 _isComponent 属性。
    那这段代码的意义是什么呢?简而言之,就是将 Vue构造函数 中的成员变量 options 和我们传入的 options 合并然后挂载到实例的 $options 属性上。

    initInternalComponent

    不知道大家想没想过,组件为什么会进到 _init 中来,哪儿定义了它也可以进来进行初始化操作?
    如果你没有忘记 Vue.component和Vue.extend都做了些什么? 一文中讲过,Vue.component 中会调用一次 Vue.extend,将生成的实例存入 vm.options.components 之中。而在 Vue.extend 中我们曾定义过一个 VueComponent构造函数,这个构造函数继承了 Vue构造函数,因此也有 _init 方法,在 VueComponent构造函数 的 constructor 中又恰好调用了 this._init(options),所以套了一层又一层,剥丝抽茧后你应该就能明白为什么组件可以进来了吧。
    那么,这个方法内做了些什么?当你进入函数题你会发现,options 中哪来的这么多属性???我丢,见都没见过啊,一脸懵逼逐渐变成N脸懵逼。所以我们先按下不表,因为其中涉及到模板编译部分,扯得太远回不来就麻烦了。

    resolveConstructorOptions

    既然不讲组件的 options 合并,那总得讲讲 new (Vue.extend(extendOptions))(options) / new Vue(options) 的 options 合并啊,讲讲讲,这不就来了么。先声明一下,以下 vm 指的是 Vue构造函数 的实例,componentVM 指的是 Vue.extend 返回的构造函数的实例。
    这个方法本质上是为了得到 Vue构造函数 身上的 options。
    如果传入的 constructor 没有 super 属性,则说明当前实例是 vm,直接返回 vm.constructor.options。
    如果传入的 constructor 拥有 super 属性,则说明当前实例是 componentVM,那么当前的 vm.constructor.options 就是 VueComponent.options。如果 VueComponent.superOptions 和 VueComponent.super.options 不相等时会将 VueComponent.superOptions 更新,得到最新的 VueComponent.options 然后返回,否则直接返回 VueComponent.options。

    mergeOptions

    首先,我们需要了解这个方法做了什么。

    function mergeOptions (parent, child, vm) {
      if (process.env.NODE_ENV !== 'production') {
        checkComponents(child)
      }
      if (typeof child === 'function') {
        child = child.options
      }
      normalizeProps(child, vm)
      normalizeInject(child, vm)
      normalizeDirectives(child)
      const extendsFrom = child.extends
      if (extendsFrom) {
        parent = mergeOptions(parent, extendsFrom, vm)
      }
      if (child.mixins) {
        for (let i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], vm)
        }
      }
      const options = {}
      let key
      for (key in parent) {
        mergeField(key)
      }
      for (key in child) {
        if (!hasOwn(parent, key)) {
          mergeField(key)
        }
      }
      function mergeField (key) {
        const strat = strats[key] || defaultStrat
        options[key] = strat(parent[key], child[key], vm, key)
      }
      return options
    }
    

    checkComponents

    判断 (vm | componentVM).options.components: { id: definition } 的 id 是否等于 slot / component 或其他原生 HTML 标签,如果是则报错。

    normalizeProps

    当 (vm | componentVM) 中传入的 options 中存在 props 时激活该方法,目的是为了格式化 props 中的属性。
    如果 options.props 是一个数组,则将 options.props 的值统一成 { item: { type: null } }。
    如果 options.props 是一个对象,则将 options.props 的值统一成 { key: { type: value } }。
    当 key 是以短横线命名法( name-space )命名时将其转换成驼峰命名法( nameSpace )

    // 如果 options.props 是一个数组
    new Vue({
      props: ['msg', 'aeo-rus']
    })
    /*
    props: {
      msg: {
        type: null
      },
      aeoRus: {
        type: null
      }
    }
    */
    // 如果 options.props 是一个对象
    new Vue({
      props: {
        msg: {
          type: String,
          default: 'hello Vue'
        },
        'aeo-rus': {
          default: 'aeorus'
        },
      }
    })
    /*
    props: {
      msg: {
        type: String,
        default: 'hello Vue'
      },
      aeoRus: {
        default: 'aeorus'
      }
    }
    */
    

    normalizeInject

    当 (vm | componentVM) 中传入的 options 中存在 inject 时激活该方法,目的是为了格式化 inject 中的属性。
    如果 options.inject 是一个数组,则将 options.inject 的值统一成 { item: { from: item } }。
    如果 options.inject 是一个对象,则将 options.inject 的值统一成 { key: { from: value } } ( 当 value 也是对象时则统一成 { from: key, value.k: value.v } )。

    new Vue({
      inject: ['onload', 'reload']
    })
    /*
    inject: {
      onload: {
        from: 'onload'
      },
      reload: {
        from: 'reload'
      }
    }
    */
    new Vue({
      inject: {
        onload: {
          from: 'onlaunch',
          default: 'onload'
        },
        reload: 'reload'
      }
    })
    /*
    inject: {
      onload: {
        from: 'onlaunch',
        default: 'onload'
      },
      reload: {
        from: 'reload'
      }
    }
    */
    

    normalizeDirectives

    当 (vm | componentVM) 中传入的 options 中存在 directives 时激活该方法,目的是为了格式化 directives 中的指令。
    将 options.directives 的值统一成 { key: { bind: value, update: value } } ( 有可能 value 是一个方法 ) 。

    new Vue({
      directives: {
        focus: {
          inserted(el) {
            el.focus()
          }
        }
      }
    })
    /*
    directives: {
      focus: {
        inserted: el => {
          el.focus()
        }
      }
    }
    */
    new Vue({
      directives: {
        focus: el => {
          el.focus()
        }
      }
    })
    /*
    directives: {
      focus: {
        bind: el => {
          el.focus()
        },
        update: el => {
          el.focus()
        }
      }
    }
    */
    

    extends / mixins

    当 (vm | componentVM) 中传入的 options 中存在 extends / mixins 时进入条件判断,递归 mergeOptions 方法,将 extends / mixins 的对象 ( 实质上就是 options ) 与 (vm | componentVM) 进行合并。

    mergeField

    const options = {}
    let key
    for (key in parent) {
      mergeField(key)
    }
    for (key in child) {
      if (!hasOwn(parent, key)) {
        mergeField(key)
      }
    }
    function mergeField (key) { 
      const strat = strats[key] || defaultStrat
      options[key] = strat(parent[key], child[key], vm, key)
    }
    

    这个方法极其复杂,本质上就是合并了构造函数身上的属性和 options 上的属性。根据 key 的不同判断是返回 options 上的属性抑或以构造函数身上的属性的值为原型创造的新的拥有 options 上的属性的值的对象。

    // 比如当 key 为 components 时返回
    options = {
      components: {
        ChildComponent,
        __proto__: {
          components: {
            KeepAlive,
            Transition,
            TransitionGroup,
          },
        },
      },
    }
    

    步骤 - 3

    我们可以看到在这段代码中多了一个新的属性 _renderProxy,从字面意义上来看应该称之为渲染代理对象,事实上它确实参与了渲染流程,在调用 render 方法获取 vnode 时会将 render 方法的内部指针指向它。
    但随之而来产生了两个问题: 1.为什么要有渲染代理对象?2.为什么开发环境和生产环境要用不同的渲染代理对象?
    其实并不复杂,我们通过以下代码来进行解答:

    /*
    目录: core/instance/render.js
    vnode = render.call(vm._renderProxy, vm.$createElement)
    */
    render(h) {
      h('div', 'hello vue')
    }
    

    我们通过以上代码可以发现,h函数 其实就是 vm.$createElement,但是因为调用的时候是直接使用 h() 来调用的,因此它内部的指针应该指向 window 而不是当前实例,所以我们需要通过 call 的方式将内部指针指向当前实例,如此就可以使用 this 获取到该实例身上 options 中其他的属性了。
    那么开发环境和生产环境为什么要用不同的渲染代理对象呢?我们可以发现在开发环境中其实是调用了 initProxy 方法创建的渲染代理对象,其中判断了是否存在 Proxy 对象,通过 Proxy 对象拦截 this.xxx 中的 xxx 是否是一个非法的属性,有利于我们开发时的操作不谨慎。但是生产环境时就没必要了,毕竟我们不会将错误保留到线上。

    步骤 - 4

    这又是一连串的调用,像极了 core/global-api/index.js 中的操作,但是请不要忽略入参,core/global-api/index.js 中传入的是 Vue,而这里传入的是 vm。这意味着,core/global-api/index.js 中是对 Vue构造函数 进行成员的添加,而这里是对 vm 进行属性的添加。

    initLifecycle

    初始化一系列属性,如果当前实例是组件则将 parent/parent / parent/root 绑定上组件所归属的 vm。

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options,
      _renderProxy,
      _self,
      /* new add start */
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      /* new add end */
    }
    

    initEvents

    初始化事件中心,如果当前实例是组件则会对父组件的事件监听进行重新绑定。

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options,
      _renderProxy,
      _self,
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      /* new add start */
      _events: {},
      _hasHookEvent: {},
      /* new add end */
    }
    

    initRender

    初始化存放和生成 虚拟DOM 的属性和方法。
    如果当前实例不是组件,则将 attrs/attrs / attrs/listeners 设置为空对象添加到实例上。
    如果当前实例是组件,则会将父组件 虚拟DOM 上的 attrs 添加响应式后放到自身实例的 attrs属性上;再将父组件的事件监听添加响应式后放到自身实例的attrs 属性上;再将父组件的事件监听添加响应式后放到自身实例的 attrs属性上;再将父组件的事件监听添加响应式后放到自身实例的listeners 属性上。

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options,
      _renderProxy,
      _self,
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      _events: {},
      _hasHookEvent: {},
      /* new add start */
      $vnode: null,
      _vnode: null,
      $slots,
      $scopedSlots,
      _c() {},
      $createElement() {},
      $attrs,
      $listeners,
      /* new add end */
    }
    

    beforeCreate

    调用 beforeCreate 生命周期。

    initInjections

    由于 步骤 - 2 -> resolveConstructorOptions -> mergeOptions -> normalizeInject 这一过程中在实例的 $options 中挂载了 inject 这个属性的缘故,这个方法中就不需要再进行添加,只是单纯地为 inject 中的对象添加了响应式。

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options: {
        /* update start */
        inject: {}
        /* update end */
      },
      _renderProxy,
      _self,
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      _events: {},
      _hasHookEvent: {},
      $vnode: null,
      _vnode: null,
      $slots,
      $scopedSlots,
      _c() {},
      $createElement() {},
      $attrs,
      $listeners,
    }
    

    initState

    这一步大家肯定熟,只要在网上看过 Vue 源码解析啊响应式原理啊之类视频的应该都了解,这里就是网传的 Vue 的 constructor 中的内容。
    即初始化 props / methods / data / computed / watch。

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options: {
        inject: {}
      },
      _renderProxy,
      _self,
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      _events: {},
      _hasHookEvent: {},
      $vnode: null,
      _vnode: null,
      $slots,
      $scopedSlots,
      _c() {},
      $createElement() {},
      $attrs,
      $listeners,
      /* new add start */
      _watchers,
      _props, // 如果 options 上有 props
      ...options.methods, // 如果 options 上有 methods
      ...options.data, // 如果 options 上有 data
      _data: {
        ...options.data
      }, // 如果 options 上有 data
      ...options.computed, // 如果 options 上有 computed
      /* new add end */
    }
    

    initProvide

    vm = {
      __proto__: {
        ...Vue,
      },
      _uid,
      _isVue,
      $options: {
        inject: {}
      },
      _renderProxy,
      _self,
      $parent,
      $root,
      $children: [],
      $refs: {},
      _watcher: null,
      _inactive: null,
      _directInactive: false,
      _isMounted: false,
      _isDestroyed: false,
      _isBeingDestroyed: false,
      _events: {},
      _hasHookEvent: {},
      $vnode: null,
      _vnode: null,
      $slots,
      $scopedSlots,
      _c() {},
      $createElement() {},
      $attrs,
      $listeners,
      _watchers: [], // 如果 options 上有 watch 则会存在 Watcher 的实例
      _props, // 如果 options 上有 props
      ...options.methods, // 如果 options 上有 methods
      ...options.data, // 如果 options 上有 data
      _data: {
        ...options.data
      }, // 如果 options 上有 data
      ...options.computed, // 如果 options 上有 computed
      /* new add start */
      _provided, // 如果 options 上有 provide
      /* new add end */
    }
    

    created

    调用 created 生命周期。

    步骤 - 5

    到目前为止,我们都只是初始化的工作,大部分都是挂载某某属性,要说真的做了什么业务相关的事情,那大概就是 initState 这部分了,其中为数据添加了响应式,当有计算属性和侦听器时还顺便做了依赖收集。
    但是,我们是否还没看到对于 DOM 的处理?对了,这就是为什么在 beforeCreate 和 created 生命周期里无法获取 this.$refs 的原因,我们还没有进入模板编译,那么 DOM 自然就还没有生成,没有生成的东西怎么可能在这些钩子里被获取呢?
    自此,_init 就告一段落了,接下来我们即将进入下一个流程 ———— 模板编译 -> ゲットスタート ( get start )。


    起源地下载网 » 从创建实例到模板编译前Vue都做了些什么?

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元