# 整体流程

# 初始化定义

在之前的总结中我们知道Vue会根据不同的环境(Web,Weex)找到不同的入口文件,最终都会去import Vue的构造函数,我们需要理解Vue构造函数执行的整个流程。

我们以entry-runtime-with-compiler.js 作为分析的入口,逐层寻找,最后通过分析,我们可以得到一份简单的定义流程图 An image

# initMixin

从之前的分析中,我们得知initMixin主要用来定义_init方法

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");
  }
  // 执行_init,同时传入用户的options
  this._init(options);
}

当我们使用的时候,则会执行_init

new Vue({
  el: "#app",
  data: {
    msg: "hello world",
  },
});

然后我们看下_init是如何定义的

Vue.prototype._init = function (options) {
    const vm: Component = this;
    // 合并用户传入的options和Vue构造函数中设置的options
    if (options && options._isComponent) {
      initInternalComponent(vm, options);
    } else {
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm,
      );
    }
    // render代理
    if (process.env.NODE_ENV !== "production") {
      initProxy(vm);
    } else {
      vm._renderProxy = vm;
    }
    vm._self = vm;
    // vm生命周期相关变量初始化
    // $children/$parent/$root/$refs
    initLifecycle(vm);
    // vm事件监听初始化,父组件绑定在当前组件上的事件
    initEvents(vm);
    // vm的render初始化
    // $slots/$scopeSlots/_c/$createElement/$attrs/$listeners
    initRender(vm);
    // 钩子函数执行
    callHook(vm, "beforeCreate");
    // 将inject上的成员注入到vm上
    initInjections(vm);
    // 初始化vm的props/methods/data/computed/watch
    initState(vm);
    // 初始化provide
    initProvide(vm);
    // 钩子函数执行
    callHook(vm, "created");


    // 调用$mount挂载
    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
  };
};

状态相关的,$mount等我们在具体执行的时候再去分析。

# stateMixin

stateMixin主要用来定义Vue实例上的属性和方法,包括$data,$props,$set,$delete,$watch

import { set, del } from "../observer/index";
export function stateMixin(Vue) {
  // 定义$data, $props
  const dataDef = {};
  dataDef.get = function () {
    return this._data;
  };
  const propsDef = {};
  propsDef.get = function () {
    return this._props;
  };
  Object.defineProperty(Vue.prototype, "$data", dataDef);
  Object.defineProperty(Vue.prototype, "$props", propsDef);

  // 定义$set, $delete, $watch
  Vue.prototype.$set = set;
  Vue.prototype.$delete = del;
  Vue.prototype.$watch = function () {};
}

$data$props: 通过代理的方式获取到_data_props,同时不能set改变对应的数据,在开发模式会抛出warning

if (process.env.NODE_ENV !== "production") {
  dataDef.set = function () {
    warn(
      "Avoid replacing instance root $data. " +
        "Use nested data properties instead.",
      this
    );
  };
  propsDef.set = function () {
    warn(`$props is readonly.`, this);
  };
}

`$set``delete`: 和全局的`Vue.set`,`Vue.delete`为同一个方法,主要是处理响应式数据,我们后面会详细分析。
`$watch`: 通过一个`watcher`实例来监听,后续会详细介绍。

流程图:

An image

# eventsMixin

eventsMixin主要用来定义事件相关的实例方法:$on,$once,$off,$emit

仔细看源码其实就是发布-订阅模式来处理一些事件

# 定义$on

const hookRE = /^hook:/;
Vue.prototype.$on = function (event, fn) {
  const vm = this;
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$on(event[i], fn);
    }
  } else {
    (vm._events[event] || (vm._events[event] = [])).push(fn);
    if (hookRE.test(event)) {
      vm._hasHookEvent = true;
    }
  }
  return vm;
};

initMixin中会初始化事件相关的initEvents,定义了初始化的_events对象

export 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);
  }
}

分析:首先判断事件是否为数组,如果不是,则定义为fn_events中,否则则会遍历数组,递归的去调用$on方法。

# 定义$emit

Vue.prototype.$emit = function (event) {
  const vm = this;
  let cbs = vm._events[event];
  if (cbs) {
    cbs = cbs.length > 1 ? toArray(cbs) : cbs;
    const args = toArray(arguments, 1);
    const info = `event handler for "${event}"`;
    for (let i = 0, l = cbs.length; i < l; i++) {
      invokeWithErrorHandling(cbs[i], vm, args, vm, info);
    }
  }
  return vm;
};

分析:我们从_events中获取到对应的方法,然后遍历进行调用。这边用了invokeWithErrorHandling来包裹,更好的处理错误。

# 定义$off

Vue.prototype.$off = function (event, fn) {
  const vm = this;
  // all
  if (!arguments.length) {
    vm._events = Object.create(null);
    return vm;
  }
  // array of events
  if (Array.isArray(event)) {
    for (let i = 0, l = event.length; i < l; i++) {
      vm.$off(event[i], fn);
    }
    return vm;
  }
  // specific event
  const cbs = vm._events[event];
  if (!cbs) {
    return vm;
  }
  if (!fn) {
    vm._events[event] = null;
    return vm;
  }
  // specific handler
  let cb;
  let i = cbs.length;
  while (i--) {
    cb = cbs[i];
    if (cb === fn || cb.fn === fn) {
      cbs.splice(i, 1);
      break;
    }
  }
  return vm;
};

分析:主要是事件的取消订阅,首先如果不传参数,则取消所有的事件监听,如果只传了event,则取消该event下面的所有事件监听,如果传了eventfn,则取消对应fn下面的事件监听。

# 定义$once

Vue.prototype.$once = function (event, fn) {
  const vm = this;
  function on() {
    vm.$off(event, on);
    fn.apply(vm, arguments);
  }
  on.fn = fn;
  vm.$on(event, on);
  return vm;
};

分析:将fn定义在内部的on函数上,可以在后续的移除操作中使用这个回调函数。执行完on函数后直接off可以确保fn只执行一次,可以学习一下这种设计模式,有效保证了函数的私有性和封装性。

//cb.fn是once定义
if (cb === fn || cb.fn === fn) {
  cbs.splice(i, 1);
  break;
}

# lifecycleMixin

lifecycleMixin主要用来定义生命周期相关的方法,包括_update,$forceUpdate,$destory

export function lifecycleMixin(Vue) {
  Vue.prototype._update = function () {};

  // 实例方法
  Vue.prototype.$forceUpdate = function () {
    if (this._watcher) {
      this._watcher.update();
    }
  };
  Vue.prototype.$destroy = function () {};
}

生命周期的相关功能会在后面详细展开

# renderMixin

renderMixin 主要用来定义一堆私有方法,还有nextTick,_render

export function renderMixin(Vue) {
  // 挂载各种私有方法,例如this._c,this._v等
  installRenderHelpers(Vue.prototype);

  Vue.prototype.$nextTick = function (fn) {
    return nextTick(fn, this);
  };
  Vue.prototype._render = function () {};
}

installRenderHelpers :定义私有方法

export function installRenderHelpers(target: any) {
  target._o = markOnce;
  target._n = toNumber;
  target._s = toString;
  target._l = renderList;
  target._t = renderSlot;
  target._q = looseEqual;
  target._i = looseIndexOf;
  target._m = renderStatic;
  target._f = resolveFilter;
  target._k = checkKeyCodes;
  target._b = bindObjectProps;
  target._v = createTextVNode;
  target._e = createEmptyVNode;
  target._u = resolveScopedSlots;
  target._g = bindObjectListeners;
  target._d = bindDynamicKeys;
  target._p = prependModifier;
}

nextTick: 和全局的nextTick()为同一个方法。

_render():编译模版为VNode

# initGlobalAPI

定义完实例相关的方法后,initGlobalAPI主要用来挂载全局的方法。

export function initGlobalAPI(Vue: GlobalAPI) {
  // config
  const configDef = {};
  configDef.get = () => config;
  // 初始Vue.config
  Object.defineProperty(Vue, "config", configDef);

  Vue.util = {
    warn,
    extend,
    mergeOptions,
    defineReactive,
  };

  // 定义静态方法set/delete/nextTick
  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;

  // 2.6 explicit observable API
  Vue.observable = (obj) => {
    observe(obj);
    return obj;
  };

  // 初始化Vue.options, 设置原型为null的对象,提高性能
  Vue.options = Object.create(null);

  // 给Vue.options定义 componets/directives/filters为空对象
  ASSET_TYPES.forEach((type) => {
    Vue.options[type + "s"] = Object.create(null);
  });

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue;

  // 设置keep-alive组件
  extend(Vue.options.components, builtInComponents);

  // 注册Vue.use()来注册插件
  initUse(Vue);
  // 注册Vue.mixin()来实现混入
  initMixin(Vue);
  // 注册Vue.extend()基于传入的options返回一个组件的构造函数
  initExtend(Vue);
  // 注册Vue.directives(),Vue.component(),Vue.filter()
  initAssetRegisters(Vue);
}

整体的逻辑其实很清晰:

An image

我们分析的是Web平台的代码,所以还要看入口runtime+compile的逻辑

platforms/web/runtime/index.js:

主要是定义了一些工具方法,同时扩展了directivescomponents,加入了v-model,v-show,Transition组件,TransitionGroup组件,还在原型中定义了 __patch__方法用来更新虚拟DOM,定义了$mount用来挂载Dom

Vue.config.mustUseProp = mustUseProp;
Vue.config.isReservedTag = isReservedTag;
Vue.config.isReservedAttr = isReservedAttr;
Vue.config.getTagNamespace = getTagNamespace;
Vue.config.isUnknownElement = isUnknownElement;

extend(Vue.options.directives, platformDirectives);
extend(Vue.options.components, platformComponents);

Vue.prototype.__patch__ = inBrowser ? patch : noop;

Vue.prototype.$mount = function () {};

platfomrs/web/entry-runtime-with-compiler.js

主要是重写了$mount,用来渲染template转成render函数,同时定义了 Vue.compile 函数用来编译成Vnode

const mount = Vue.prototype.$mount;
Vue.prototype.$mount = function (el, hydrating) {
  // ...
  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;
  return mount.call(this, el, hydrating);
};
// 定义compile
Vue.compile = compileToFunctions;

# 首次渲染

上面分析的所有都是Vue相关的定义,当我们new Vue的时候,才是代码执行,进行首次渲染的逻辑,按照分析,我们得出一个流程图

An image