Vue源码解析:Vue生命周期之从生到死(五)

讲过了生命周期的初始化阶段(出生阶段),现在也是该走向成长了,也就是vue该进入到下一个阶段——模版编译阶段,也就是这个时候要调用vm.$mount()函数了,这个函数的调用标志着初始化的结束,下一个阶段的开始。

 

所以,我们来讲讲vue的模版编译,其实,vue存在俩个版本,完整版和运行时版本,其区别就是有没有模版编译这一部分内容,完整版本是包含了模版编译这一部分。 

完整版本:vue完整版本包含了编译器,会根据template模版编译生成渲染函数的代码,源码中就是render函数。

运行时版本:通过自己手写render函数,去定义渲染过程,这个时候并不需要完整的模版编译过程。同样,也可以借助vue-loader来对*.vue文件进行编译。所以这部分内容,都可以交给webpack插件来完成。

其实,模版编译的过程,对于性能的问题影响的非常大,加入了模版编译,使得vue整个包体积增大,性能降低,所以,在开发中一般不会将模版编译这部分内容加入到vue中,而是将模版编译交给webpack去处理,这样不仅仅减小了生成环境包的体积,也在性能方面有了很大的提高。

那么模版编译阶段到底干了什么呢?

刚我们讲过,vm.$mount方法的调用标志着模版编译的开始,由于vue有俩个版本,所以vm.$mount方法也有俩个版本,但是归根结底还是一个版本,下面我们就来看看vm.$mount到低是何方神圣?

完整版vm.$mount

var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el,hydrating) {
  // 省略获取模板及编译代码

  return mount.call(this, el, hydrating)
}

运行时版本vm.$mount

Vue.prototype.$mount = function (el,hydrating) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};

可以看到在运行时版本中,vm.$mount方法内部获取到el选项对应的DOM元素后直接调用mountComponent函数进行挂载操作。而完整版本的$mount定义之前,先将Vue原型上的$mount方法先缓存起来,记作变量mount。其实在源码中,是先定义只包含运行时版本的$mount方法,再定义完整版本的$mount方法,所以此时缓存的mount变量就是只包含运行时版本的$mount方法。这是因为不管是那个版本,最终都会进入到挂载阶段,而完整版本只是比运行时版本多了个模版编译,所以,完整版本要先缓存一次$mount,这样等模版编译完成之后,可以直接调用$mount。

vm.$mount的源码实现:

//    dist/vue.js
var mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el,hydrating) {
  el = el && query(el);
  if (el === document.body || el === document.documentElement) {
    warn(
      "Do not mount Vue to <html> or <body> - mount to normal elements instead."
    );
    return this
  }

  var options = this.$options;
  // resolve template/el and convert to render function
  if (!options.render) {
    var template = options.template;
    if (template) {
      if (typeof template === 'string') {
          if (template.charAt(0) === '#') {
            template = idToTemplate(template);
            /* istanbul ignore if */
            if (!template) {
              warn(
                ("Template element not found or is empty: " + (options.template)),
                this
              );
            }
          }
      } else if (template.nodeType) {
        template = template.innerHTML;
      } else {
        {
          warn('invalid template option:' + template, this);
        }
        return this
      }
    } else if (el) {
      template = getOuterHTML(el);
    }
    if (template) {
      if (config.performance && mark) {
        mark('compile');
      }

      var ref = compileToFunctions(template, {
        outputSourceRange: "development" !== 'production',
        shouldDecodeNewlines: shouldDecodeNewlines,
        shouldDecodeNewlinesForHref: shouldDecodeNewlinesForHref,
        delimiters: options.delimiters,
        comments: options.comments
      }, this);
      var render = ref.render;
      var staticRenderFns = ref.staticRenderFns;
      options.render = render;
      options.staticRenderFns = staticRenderFns;

      if (config.performance && mark) {
        mark('compile end');
        measure(("vue " + (this._name) + " compile"), 'compile', 'compile end');
      }
    }
  }
  return mount.call(this, el, hydrating)
};

从整体的代码结构可以看到,一共有三部分内容:

1、根据传入的el参数获取DOM元素;

el参数可以是元素,也可以是字符串类型的元素选择器,所以调用query函数来获取到el对应的DOM元素。query方法的作用根据是不是字符串,获取到不同的dom。另外,这里还多了一个判断,就是判断获取到el对应的DOM元素如果是bodyhtml元素时,将会抛出警告。这是因为Vue会将模板中的内容替换el对应的DOM元素,如果是bodyhtml元素时,替换之后将会破坏整个DOM文档,所以不允许elbodyhtml

2、在用户没有手写render函数的情况下获取传入的模板template

首先获取用户传入的template选项赋给变量template,如果变量template存在,则接着判断如果template是字符串并且以##开头,则认为templateid选择符,则调用idToTemplate函数获取到选择符对应的DOM元素的innerHTML作为模板。如果template不是字符串,那就判断它是不是一个DOM元素,如果是,则使用该DOM元素的innerHTML作为模板,如果既不是字符串,也不是DOM元素,此时会抛出警告:提示用户template选项无效。如果变量template不存在,表明用户没有传入template选项,则根据传入的el参数调用getOuterHTML函数获取外部模板,

3、将获取到的template编译成render函数;

模板编译成渲染函数是在compileToFunctions函数中进行的,该函数接收待编译的模板字符串和编译选项作为参数,返回一个对象,对象里面的render属性即是编译好的渲染函数,最后将渲染函数设置到$options上。 

如果说模版编译是成长阶段,那么挂载就一定是壮年阶段。那么vue的挂载阶段干了什么呢?

vue的挂载阶段主要的工作是创建Vue实例并用其替换el选项对应的DOM元素,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。

上面我们讲到,只有在模版编译完成之后,才会吊起vm.$mount函数,而vm.$mount里的重要的处理就是调用了mountComponent函数,接下来,我们看看这个函数具体做了什么?

//    src/core/instance/lifecycle.js
export function mountComponent (vm,el,hydrating) {
    vm.$el = el
    if (!vm.$options.render) {
        vm.$options.render = createEmptyVNode
    }
    callHook(vm, 'beforeMount')

    let updateComponent

    updateComponent = () => {
        vm._update(vm._render(), hydrating)
    }
    new Watcher(vm, updateComponent, noop, {
        before () {
            if (vm._isMounted) {
                callHook(vm, 'beforeUpdate')
            }
        }
    }, true /* isRenderWatcher */)
    hydrating = false

    if (vm.$vnode == null) {
        vm._isMounted = true
        callHook(vm, 'mounted')
    }
    return vm
}

首先会判断实例上是否存在渲染函数,如果不存在,则设置一个默认的渲染函数createEmptyVNode,该渲染函数会创建一个注释类型的VNode节点。

然后调用callHook函数来触发beforeMount生命周期钩子函数。

该钩子函数触发后标志着正式开始执行挂载操作。

接下来定义了一个updateComponent函数,在该函数内部,首先执行渲染函数vm._render()得到一份最新的VNode节点树,然后执行vm._update()方法对最新的VNode节点树与上一次渲染的旧VNode节点树进行对比并更新DOM节点(即patch操作),完成一次渲染。

也就是说,如果调用了updateComponent函数,就会将最新的模板内容渲染到视图页面中,这样就完成了挂载操作的一半工作。因为在挂载阶段不但要将模板渲染到视图中,同时还要开启对模板中数据(状态)的监控,当数据(状态)发生变化时通知其依赖进行视图更新。这就是挂载后的处理,我们可看到,函数内部创建了一个watcher实例。并将定义好的updateComponent函数传入。要想开启对模板中数据(状态)的监控。这个watcher就是我们在数据侦测阶段的watcher,这样就又实现了数据的可观测,这样一来,就可以走响应式的那一段逻辑了,更新依赖了。

从出生,走过了成年,壮年,也必将走进老年,走完一生,这就是vue的销毁阶段。那么vue的销毁阶段干了什么呢?

当调用了vm.$destroy方法,Vue实例就进入了销毁阶段,该阶段所做的主要工作是将当前的Vue实例从其父级实例中删除,取消当前实例上的所有依赖追踪并且移除实例上的所有事件监听器。也就是说,当这个阶段完成之后,当前的Vue实例的整个生命流程就全部走完了。

我们来看看vm.$destroy函数干了什么?

//    src/core/instance.lifecycle.js
Vue.prototype.$destroy = function () {
  const vm: Component = this
  if (vm._isBeingDestroyed) {
    return
  }
  callHook(vm, 'beforeDestroy')
  vm._isBeingDestroyed = true
  // remove self from parent
  const parent = vm.$parent
  if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
    remove(parent.$children, vm)
  }
  // teardown watchers
  if (vm._watcher) {
    vm._watcher.teardown()
  }
  let i = vm._watchers.length
  while (i--) {
    vm._watchers[i].teardown()
  }
  // remove reference from data ob
  // frozen object may not have observer.
  if (vm._data.__ob__) {
    vm._data.__ob__.vmCount--
  }
  // call the last hook...
  vm._isDestroyed = true
  // invoke destroy hooks on current rendered tree
  vm.__patch__(vm._vnode, null)
  // fire destroyed hook
  callHook(vm, 'destroyed')
  // turn off all instance listeners.
  vm.$off()
  // remove __vue__ reference
  if (vm.$el) {
    vm.$el.__vue__ = null
  }
  // release circular reference (##6759)
  if (vm.$vnode) {
    vm.$vnode.parent = null
  }
}

首先判断当前实例的_isBeingDestroyed属性是否为true,因为该属性标志着当前实例是否处于正在被销毁的状态,如果它为true,则直接return退出函数,防止反复执行销毁逻辑。

接着,触发生命周期钩子函数beforeDestroy,该钩子函数的调用标志着当前实例正式开始销毁。这个时候就进入了真正的销毁阶段:

首先将当前实例从父级实例中删除。如果当前实例有父级实例,同时该父级实例没有被销毁并且不是抽象组件,那么就将当前实例从其父级实例的$children属性中删除,即将自己从父级实例的子实例列表中删除。把自己从父级实例的子实例列表中删除之后,接下来就开始将自己身上的依赖追踪和事件监听移除。

依赖有俩部分:实例自身依赖其他数据,需要将实例自身从其他数据的依赖列表中删除;实例内的数据对其他数据的依赖(如用户使用$watch创建的依赖),也需要从其他数据的依赖列表中删除实例内数据。首先执行vm._watcher.teardown()将实例自身从其他数据的依赖列表中删除,teardown方法的作用是从所有依赖向的Dep列表中将自己删除。我们知道,所有实例内的数据对其他数据的依赖都会存放在实例的_watchers属性中,所以我们只需遍历_watchers,将其中的每一个watcher都调用teardown方法,从而实现移除实例内数据对其他数据的依赖。接下来移除实例内响应式数据的引用、给当前实例上添加_isDestroyed属性来表示当前实例已经被销毁,同时将实例的VNode树设置为null。然后触发生命周期钩子函数destroy。调用实例的vm.$off方法(关于该方法在后面介绍实例方法时会详细介绍),移除实例上的所有事件监听器。最后,移除相关的属性引用。

此时,vue走完了自己光辉的一生。。。

vue的一生,和我们每个人的一生其实极为相似,从出生开始,就注定要经历磨难,所以,vue在初始化的时候,做了相当多的工作,可以说是从小就历经磨难。正因为如此,未来才会走的相对顺利,所以,骚年们,请永远别相信那似容易的事情,做起来却未必容易,做好每一件小事,你才会像vue一样,经得起磨难,才能走完完美的一生。。。。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值