详解vue初始化

965 阅读3分钟

开始一张图,内容全靠编。上图

lifecycle.png

开始

本篇文章介绍了vue.js实例被创建时经历了哪些一系列的初始化,盗用的vue官网生命周期图示展示了vue.js实力的生命周期。我们可以笼统的分为四个阶段:初始化阶段、模板编译阶段、挂载阶段与卸载阶段,每个阶段都会运行相对应的生命周期钩子的函数。下面详细介绍一下每个阶段具体干了些什么:

初始化阶段

new Vue()created钩子函数之间的阶段是初始化阶段。 在这个阶段vue实例会初始化一些属性、事件以及响应式数据,例如propsdatamethodscomputedwatchprovideinject等。 Vue类通过_init函数进行初始化,在_init函数里会执行一系列初始化流程。_init的大体实现如下:

Vue.prototype._init = function (options?: Object) {
    const vm: Component = this;
    vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
    )
    // expose real self
    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)
    }
  }
}

可以看到,_init函数会先合并options,然后依次执行initLifecycleinitEventsinitRenderinitInjectionsinitStateinitProvide初始化函数。其中会在initRender函数与initProvide函数后执行beforeCreatecreated生命周期钩子函数。

initLifecycle

initLifecycle函数主要用于初始化实例属性,如_watcher_inactive_directInactive_isMounted_isDestroyed_isBeingDestroyed等自己内部用到的属性以及$parent$root$children$refs等供外部用到的属性。

function initLifecycle (vm: Component) {
  const options = vm.$options
  // locate first non-abstract parent
  let parent = options.parent
    ......
  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
}

initEvents

initEvents函数主要将父组件在模板中使用的v-on注册的事件添加到子组件的事件系统中。在父组件的模板编译阶段,虚拟DOM会根据VNode进行对比渲染然后创建标签,如果判断此标签是组件标签,那么会将子组件实例化并给它传递一些参数,其中就包括父组件模板中使用v-on注册在子组件标签上的事件。

function initEvents (vm: Component) {
  vm._events = Object.create(null)
  vm._hasHookEvent = false
  // init parent attached events
  const listeners = vm.$options._parentListeners
  if (listeners) {
    updateComponentListeners(vm, listeners)
  }
}

initRender

initRender用来绑定createElement函数,等其他操作(还没看懂)

export function initRender (vm: Component) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  ......
}

initInjections

initInjections函数是初始化组件inject属性配置的内容。它会使用inject配置的key从当前组件读取provide内容,读取不到会读取父组件的provide配置内容,以此类推直到找到最终内容,并将key值设置成可侦测的。initInjections代码如下:

function initInjections (vm: Component) {
  // 根据inject查找内容,如果找不到就会继续向父组件查找。
  const result = resolveInject(vm.$options.inject, vm)
  if (result) {
    toggleObserving(false)
    Object.keys(result).forEach(key => {
        defineReactive(vm, key, result[key])
    })
    toggleObserving(true)
  }
}

initState

initState方法是对propsmethodsdatacomputedwatch等配置的初始化。代码如下:

function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.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)
  }
}

initState方法依次执行initPropsinitMethodsinitDatainitComputedinitWatch方法来初始化对应的配置,通过上述方法可以看到初始化的顺序,由此可以看出在data内能够访问到props配置,initState方法在initInjections方法后执行,所以在props里能够访问到inject配置。

initProvide

initProvide方法将provide配置添加上到实例的_provided属性上

export function initProvide (vm: Component) {
  const provide = vm.$options.provide
  if (provide) {
    vm._provided = typeof provide === 'function'
      ? provide.call(vm)
      : provide
  }
}

此方法会先判断一下provide配置的是否是函数,如果是函数就会先执行函数,然后将返回值赋值给vm._provided,如果不是就会直接将provide赋值给vm._provided

模板编译阶段

created钩子函数与beforeMount钩子函数之间的阶段是模板编译阶段,此阶段可以分成两个步骤:先将模板解析成AST(Abstract Syntax Tree,抽象语法树),然后再根据AST生成渲染函数。代码如下:

const createCompiler = createCompilerCreator(function baseCompile (
  template: string,
  options: CompilerOptions
): CompiledResult {
  // 模板解析成ast    
  const ast = parse(template.trim(), options)
  // 根据ast生成渲染函数
  const code = generate(ast, options)
  return {
    ast,
    render: code.render,
    staticRenderFns: code.staticRenderFns
  }
})

模板到AST

AST是一个对象,对象中的属性用来表示各个节点所需要的各种数据,比如tag表示节点名,parent表示父节点,children表示子节点。例如: 模板

    <div>
        <p>{{name}}</p>
    </div>

解析成AST后的样子为:

{
    tag: 'div',
    type: 1,
    parent: undefined,
    children: [
        {
            tag: 'p',
            type: 1,
            parent: {tag: 'div', ...},
            children: [{
                type: 2,
                text: '{{name}}'
            }]
}

上面只是简单列了一下AST的属性,其中还有更详细的,有兴趣的同学自行百度吧,这里不做详细介绍,大家能明白什么是AST就可以了。 模板转换成AST是通过各种解析器完成的,其中包括过滤器解析器、文本解析器与HTML解析器,过滤器解析器是用来解析过滤器的,文本解析器用来解析带变量的文本(例如:Hello {{name}}),而HTML解析器是解析器中最核心的模块,用来解析模板内html标签的。

AST生成渲染函数

通过createCompiler函数可以发现parse函数会将模板解析成AST,然后generate函数会根据AST生成渲染函数。

渲染函数被执行后可以生成一分VNode,而虚拟DOM可以通过这个VNode来渲染视图。generate函数将上述AST生成的渲染函数如下:

    with(this){
        return _c(
            'div',
            {},
            [
                _v('hello'+_s(name))
            ]
        )
    }

仔细观察后可以发现这个是一个嵌套的函数调用,函数_c的参数中执行了函数_v,而函数_v的参数中又执行了函数_s。函数_v其实是createElement的别名,执行createElement可以创建一个VNode

挂载阶段

beforeMount够子函数与mounted够子函数之间的阶段是挂载阶段。在这个阶段,Vue.js会将其实例挂载到DOM元素上,通俗的讲,就是将模板渲染到指定的DOM元素中。Vue.js会根据用户el配置项或者手动执行vm.$mount方法实现将实例挂载到DOM元素上。vm.$mount函数代码如下:

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

该函数首先会判断是否传入el参数,并且是否在浏览器内,根据el查找对应的DOM元素,然后执行mountComponent函数,函数mountComponent代码大体如下:

    unction mountComponent (
  vm,
  el,
  hydrating
) {
  vm.$el = el;
  if (!vm.$options.render) {
    vm.$options.render = createEmptyVNode;
    warn(
          'Failed to mount component: template or render function not defined.',
          vm
        );
  }
  callHook(vm, 'beforeMount');
    ...
  var updateComponent;
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
    ...
  new Watcher(vm, updateComponent, noop, {
    before: function before () {
      if (vm._isMounted && !vm._isDestroyed) {
        callHook(vm, 'beforeUpdate');
      }
    }
  }, true /* isRenderWatcher */);
  hydrating = false;
    ...
  vm._isMounted = true;
  callHook(vm, 'mounted');
  return vm
}

该函数第一步会判断一下是否存在渲染函数,如果不存在会将创建空节点的createEmptyVNode函数赋值给渲染函数,并打印出警告,然后执行beforeMount回调函数。

下一步通过vm._update(vm._render())执行具体的渲染操作。执行vm_render函数会得到一分最新的VNode节点树,而vm._update函数会对最新的VNode和上一次渲染用到的旧VNode进行对比并更新DOM节点,也就是执行了渲染操作。

执行完渲染操作后,通过new Watcher实现了挂载的持续性,watcher会监听vue实例,当vue实例内状态发生变化后会重新执行渲染操作。

当所有操作完成后执行mounted生命周期钩子回调函数

卸载阶段

当应用调用vm.$destroy方法后,vue.js的生命周期会进入到卸载阶段。在这个阶段,Vue.js会将自身从父组件中删除,取消实例上所有依赖的追踪并且移出所有的事件监听。

$destroy函数具体代码如下:

Vue.prototype.$destroy = function () {
    var vm = this;
    if (vm._isBeingDestroyed) {
      return
    }
    callHook(vm, 'beforeDestroy');
    vm._isBeingDestroyed = true;
    // 将自己从父组件中移除
    var parent = vm.$parent;
    if (parent && !parent._isBeingDestroyed && !vm.$options.abstract) {
      remove(parent.$children, vm);
    }
    // 取消监听
    if (vm._watcher) {
      vm._watcher.teardown();
    }
    var i = vm._watchers.length;
    while (i--) {
      vm._watchers[i].teardown();
    }
    // 移出引用,取消监听
    if (vm._data.__ob__) {
      vm._data.__ob__.vmCount--;
    }
    vm._isDestroyed = true;
    // 销毁VNode
    vm.__patch__(vm._vnode, null);
    // 触发 destroyed 回调函数
    callHook(vm, 'destroyed');
    // 关掉所有的实例监听.
    vm.$off();
    // 移出DOM元素
    if (vm.$el) {
      vm.$el.__vue__ = null;
    }
    if (vm.$vnode) {
      vm.$vnode.parent = null;
    }
  };
}

结束

以上为vue初始化时的详细过成,奈何心中没文化,只能写成这样了,供大家参考。

参考

《深入浅出Vue.js》
vue官网