Vue源码学习(二)-----自定义组件以及生命周期源码解析

1、Vue插件的功能

  • 添加全局方法或者属性,例如vue-custom-element;
  • 添加全局资源:指令/过滤器或者过渡等;
  • 通过全局混入来添加一些组件选项,例如vue-router;
  • 添加vue实例方法,将其加到原型上;
  • 一个组件库,提供自己的API,同时提供上面提到的一个或者多个功能,例如Element-ui提供自己的API;

2、Vue的钩子函数

  • 源码中(/src/core/instance/index.js),Vue是一个构造函数,所以我们需要使用new的方式创建一个关于Vue的实例,如果是生产环境或者不是Vue的实例,将发出警告,然后通过Vue实例上的_init()方法进行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')
      }
      this._init(options)
    }
  • _init()方法是Vue通过prototype来实现的一个原型属性,一共干了三件事:
    Vue.prototype._init = function (options) {
        const vm = this
        vm.$options = mergeOptions(
            resolveConstructorOptions(vm.constructor),
            options || {},
            vm
        )
        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')
     
        if (vm.$options.el) {
          vm.$mount(vm.$options.el)
        }
      }
    • 首先,将vue实例赋值给了vm,并且把用户传入的options,和当前构造函数的options,以及父级构造函数的options进行合并。将合并后的options赋值给$options,并且将这个$options挂载到vue实例上;
      • 注意:options的合并方式mergeOptions:

        • resolveConstructorOptions就相当于Vue.options是在initGlobalAPI(Vue)中定义的值;
          //  src/core/global-api/index.js
          export function initGlobalAPI (Vue: GlobalAPI) {
            // Vue.options通过Object.create(null)创建了一个空对象,然后遍历ASSET_TYPES
            Vue.options = Object.create(null)
            ASSET_TYPES.forEach(type => {
              Vue.options[type + 's'] = Object.create(null)
             // Vue.options.components = {}
              // Vue.options.directives = {}
              // Vue.options.filters = {}
            })
           // 把一些内置组件扩展到 Vue.options.components 上,这也就是为什么我们在其它组件中使用这些组件不需要注册的原因
            extend(Vue.options.components, builtInComponents)
            // ...
          }
           
          //  ASSET_TYPES 的定义在 src/shared/constants.js
          export const ASSET_TYPES = [
            'component',
            'directive',
            'filter'
        • 首先递归把 extends 和 mixins 合并到 parent 上。然后创建一个空对象options,遍历 parent,把parent中的每一项通过调用 mergeField函数合并到空对象options里,接着再遍历 child,把存在于child里但又不在 parent中 的属性继续调用 mergeField函数合并到空对象options里,最后,options就是最终合并后得到的结果,将其返回。值得一提的是 mergeField 函数,它不是简单的把属性从一个对象里复制到另外一个对象里,而是根据被合并的不同的选项有着不同的合并策略;
          /**
           * Merge two option objects into a new one.
           * Core utility used in both instantiation and inheritance.
           */
          export function mergeOptions (
            parent: Object,
            child: Object,
            vm?: Component
          ): Object {
           
            if (typeof child === 'function') {
              child = child.options
            }
            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
          }
    • 然后,Vue调用了initLifecycle(vm)、initEvents(vm)、initRender(vm)这三个方法,用于初始化生命周期、事件和渲染函数,且发生在beforeCreated钩子函数之前,在Vue中几乎所有的钩子(errorCaptured除外)函数执行都是通过callHook(vm: Component, hook: string) 来调用的;
      • 初始化生命周期:

        首先是$parent的挂载:逻辑是这样的,如果当前组件不是抽象组件并且存在父级,那么就通过while循环来向上循环,如果当前组件的父级是抽象组件并且也存在父级,那就继续向上查找当前组件父级的父级,直到找到第一个不是抽象类型的父级时,将其赋值vm.$parent,同时把该实例自身添加进找到的父级的$children属性中。这样就确保了在子组件的$parent属性上能访问到父组件实例,在父组件的$children属性上也能访问子组件的实例。

        其次是$root的挂载:逻辑是这样的,首先会判断如果当前实例存在父级,那么当前实例的根实例$root属性就是其父级的根实例$root属性,如果不存在,那么根实例$root属性就是它自己。

        最后就是初始化一些其他的属性

        //   src/core/instance/lifecycle.js
        export function initLifecycle (vm: Component) {
          const options = vm.$options
         
          // locate first non-abstract parent
          let parent = options.parent
          if (parent && !options.abstract) {
            while (parent.$options.abstract && parent.$parent) {
              parent = parent.$parent
            }
            parent.$children.push(vm)
          }
         
          vm.$parent = parent
          vm.$root = parent ? parent.$root : vm
         
          vm.$children = []
          vm.$refs = {}
         
          vm._watcher = null
          vm._inactive = null
          vm._directInactive = false
          vm._isMounted = false
          vm._isDestroyed = false
          vm._isBeingDestroyed = false
        }
    • 最后,会判断用户有没有传入el选项,如果有,则调用$mount函数进入模板编译与挂载阶段,如果没有传入el选项,则不进入下一个生命周期阶段,需要用户手动执行vm.$mount方法才进入下一个生命周期阶段;
  • callHook()方法根据传入的hook从实例中拿到对应的回调函数数组(/packages/vue-template-compiler/browser.js下的 LIFECYCLE_HOOKS),然后再执行,步骤如下:
    • 把所有同类钩子先合并成数组(一个实例通过mixins可能有很多个相同钩子),然后存放在 vm.$options,类似于mixin,所以当Vue.mixin和用户在实例化Vue时,如果设置了同一个钩子函数,那么在触发钩子函数时,就需要同时触发这个两个函数,所以转换成数组就是为了能在同一个生命周期钩子列表中保存多个钩子函数,然后遍历执行钩子函数数组
      /**
       * Hooks and props are merged as arrays.
       */
      function mergeHook (parentVal,childVal):  {
        return childVal
          ? parentVal
            ? parentVal.concat(childVal)
            : Array.isArray(childVal)
              ? childVal
              : [childVal]
          : parentVal
      }
       
      LIFECYCLE_HOOKS.forEach(hook => {
        strats[hook] = mergeHook
      })
       
      //    src/shared/constants.js
       
      export const LIFECYCLE_HOOKS = [
        'beforeCreate',
        'created',
        'beforeMount',
        'mounted',
        'beforeUpdate',
        'updated',
        'beforeDestroy',
        'destroyed',
        'activated',
        'deactivated',
        'errorCaptured'
      ]
      View Code
    • 使用
      //callHook的作用就是执行用户自定义的钩子函数,并将钩子中this指向指为当前的组件实例
      export function callHook(vm:Component, hook:String) {
          //vm为当前vue实例,hook为钩子函数名称
          pushTarget(); //为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
        //获取对相应的钩子函数内容
          var handlers = vm.$options[hook];
          if (handlers) {
              for (var i = 0, j = handlers.length; i < j; i++) {
                  try {
                      // 直接调用
                      handlers[i].call(vm);
                  } catch (e) {
                      handleError(e, vm, (hook + " hook"));
                  }
              }
          }
          // 钩子函数事件emit
          if (vm._hasHookEvent) {
              vm.$emit('hook:' + hook);
          }
          popTarget();//为了避免在某些生命周期钩子中使用 props 数据导致收集冗余的依赖
      }

3、插件的开发流程

  • 如果插件是一个对象,必须暴露一个install方法,如果是一个函数,自己则会被作为install方法
    Myplugin.install = (Vue,options) => {
    //添加全局方法或者属性
    Vue.globalMethod = () => {}
    //添加全局资源
    Vue.directive(directiveName,{})
    //注入组件选项
    Vue.mixin({})
    //添加实例方法
    Vue.prototype.commonMethod = () => {}

          注意:首先根据installed属性来判断插件是否被注册,防止二次注册,然后利用Vue.mixin使插件在beforeCreate(即组件实例化之前)注册,在destoryed中销毁组件,最后利用defineProperty;让复制关系为已读,且不会被人为修改,结尾还是用Vue.component方法注册router-view与router-link两个组件;

  • 在new Vue()(即组件实例化之前)使用全局方法Vue.use(),执行时自动执行install方法,用来安装插件,多次调用,只会注册一次;
    注意:Vue上定义use方法,参数可以是Object或者Function,逻辑:先判断插件是否注册过,防止重复注册,然后使用工具类的方法toArray拿到use中从1-剩余的参数,最后根据是
    Object或者Function执行对应的install方法,并记录该组件已经被注册过(将参数放入一个数组中)。

4、遇到的问题

  暂无

5、参考文章

  • https://blog.csdn.net/leelxp/article/details/107091403
posted @ 2020-10-19 16:25  北栀女孩儿  阅读(356)  评论(0编辑  收藏  举报