vue2源码-- 初始化阶段

Posted 在厕所喝茶

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了vue2源码-- 初始化阶段相关的知识,希望对你有一定的参考价值。

目录

概述

初始化阶段所做的工作大致分为两部分:第一部分是new vue(),也就是创建一个vue实例;第二部分就是为创建好的vue实例初始化一些事件、属性、响应式数据等

new vue()阶段

  • 利用instanceof,检查当前实例是否通过new关键字初始化的
  • 调用原型链上面_init方法,进行初始化,这个方法是在initMixin函数中添加上去的
    • vue实例赋值给vm,并且吧用户传递的options选项和当前构造函数的options属性,以及父级构造函数的options属性进行合并,得到一个新的属性,并挂载到vm.$options
    • 通过调用一些初始化函数来vue实例初始化一些属性,事件,响应式数据等
      • initLifecycle,初始化组件实例关系属性:挂载一些属性, p a r e n t , parent, parent,root,$children 等
      • initEvents,初始化自定义事件:将父组件向子组件注册的事件注册到子组件的实例中
      • initRender,初始化渲染相关的: s l o t s , slots, slotsscopedSlots,createElement, a t t r s , attrs, attrslisteners 等数据
      • 调用beforeCreate生命周期函数
      • initInjections,初始化 inject
      • initState,初始化 props,methods,data,computed,watch
      • initProvide,初始化 provide
      • 调用created生命周期
    • 所有初始化工作完成之后,最后,会判断用户是否传入了el选项,如果传入了,则调用$mounted函数进入模板编译和挂载阶段。如果没有传入,则不进入下一个生命周期阶段,需要用户手动调用$mounted函数才会进入到下一个生命周期阶段

源码:

// vue是一个函数,通过new去实例化
function Vue(options) 
  // 一定要通过new去实例化vue
  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);

export function initMixin(Vue: Class<Component>) 
  // vue初始化的时候主要有:合并配置,初始化生命周期,初始化事件中心,初始化渲染,初始化data,props,computed,watcher等
  Vue.prototype._init = function (options?: Object) 
    const vm: Component = this;
    // 唯一标识
    vm._uid = uid++;

    // 标识是vue实例,避免被数据劫持
    vm._isVue = true;
    // merge options
    if (options && options._isComponent) 
      // 组件的合并策略
      initInternalComponent(vm, options);
     else 
      // 用户传递的options,当前构造函数的options,父级构造函数的options
      // 合并配置之后挂载到$options
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || ,
        vm
      );
      // 合并之后
      // vm.$options = 
      //   components:  ,
      //   created: [
      //     function created() 
      //       console.log('parent created')
      //     
      //   ],
      //   directives:  ,
      //   filters:  ,
      //   _base: function Vue(options) 
      //     // ...
      //   ,
      //   el: "#app",
      //   render: function (h) 
      //     //...
      //   
      // 
    
    if (process.env.NODE_ENV !== "production") 
      // 这个initProxy是代理vm上面的数据,数据被设置之后,会做相应的检查,不符合要求的会报错警告
      initProxy(vm);
     else 
      vm._renderProxy = vm;
    
    vm._self = vm;
    // 初始化组件实例关系属性:挂载一些属性,$parent,$root,$children等
    initLifecycle(vm);
    // 初始化自定义事件:将父组件向子组件注册的事件添加到子组件实例的_events中
    // 初始化事件函数initEvents实际上初始化的是父组件在模板中使用v-on或@注册的监听子组件内触发的事件。
    initEvents(vm);
    // 初始化渲染相关的:$slots,$scopedSlots,createElement,$attrs,$listeners等数据
    initRender(vm);
    // 调用生命周期函数
    callHook(vm, "beforeCreate");
    // 初始化inject
    initInjections(vm); // resolve injections before data/props
    // 初始化props,methods,data,computed,watch
    initState(vm);
    // 初始化provide
    initProvide(vm); // resolve provide after data/props
    // 调用生命周期
    callHook(vm, "created");
    // 如果传入了el参数
    if (vm.$options.el) 
      // 挂载dom
      vm.$mount(vm.$options.el);
    
  ;

initLifecycle 函数

这个函数主要是初始化一些属性包括以$开头给外部使用的属性,以_开头给内部使用的属性

查找当前组件的直接父组件(不能是抽象组件,会直接跳过抽象组件),并把自身实例添加到父组件的$children中。添加$root根节点组件

源码:

// 给vue实例挂载一些属性,并设置了默认值
export function initLifecycle(vm: Component) 
  const options = vm.$options;

  // 挂载$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;

initEvents 函数

该函数内部实际上会在实例上面新增一个_events属性的空对象,用来存储事件,接着调用updateComponentListeners函数,将父组件向子组件注册的事件注册到子组件实例的_events对象中

模板编译解析的时候,会调用processAttrs方法解析标签中的属性,如果是指令,会解析出属性的修饰符,然后如果判断出是事件指令,就会根据修饰符对事件名做处理,如果是浏览器原生事件,就添加到nativeEvents属性中,如果是自定义事件,就会添加到events属性中,并把回调函数的字符串保留到对应的事件中。最后在创建组件的时候,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。

换句话说:实例初始化阶段调用的初始化事件函数 initEvents 实际上初始化的是父组件在模板中使用 v-on 或@注册的监听子组件内触发的事件

源码:

// 父组件给子组件的注册事件中,把自定义事件传给子组件,在子组件实例化的时候进行初始化;而浏览器原生事件是在父组件中处理。
export function initEvents(vm: Component) 
  vm._events = Object.create(null);
  vm._hasHookEvent = false;
  // init parent attached events
  // 获取父组件v-on监听器事件
  const listeners = vm.$options._parentListeners;
  if (listeners) 
    // 将父组件向子组件注册的事件添加到子组件实例的_events中
    updateComponentListeners(vm, listeners);
  

initInjections 函数

该函数主要是从当前组件起,不断向上游父级组件的_provided属性中查找是否提供了该数据对应的值,直到找到为止。如果没有找到,就看是否提供了默认值,有就使用默认值,没有就抛出异常。最后把查找出来的key-val键值对添加到当前实例上

源码:

export function initInjections(vm: Component) 
  // 获取规范格式后的inject对象
  const result = resolveInject(vm.$options.inject, vm);
  if (result) 
    // 把shouldObserve 置为false,
    // 目的是为了告诉observe函数仅仅把键值对加到当前实例,不需要转化为响应式
    // 这也响应了的文档的,provide 和 inject 绑定并不是可响应的
    // 当然,如果传入了一个可监听的对象,那么该对象的属性还是可响应的
    toggleObserving(false);
    Object.keys(result).forEach((key) => 
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== "production") 
        defineReactive(vm, key, result[key], () => 
          // 禁止修改inject的值,如果inject的是对象,修改对象里面的属性不会报错
          warn(
            `Avoid mutating an injected value directly since the changes will be ` +
              `overwritten whenever the provided component re-renders. ` +
              `injection being mutated: "$key"`,
            vm
          );
        );
       else 
        defineReactive(vm, key, result[key]);
      
    );
    // 恢复shouldObserve为true
    toggleObserving(true);
  

initState 函数

该函数首先添加一个_watchers的属性,用来存储当前实例上面的所有watcher实例

注意:从 Vue 2.0 版本起,Vue 不再对所有数据都进行侦测,而是将侦测粒度提高到了组件层面,对每个组件进行侦测,所以在每个组件上新增了 vm._watchers 属性,用来存放这个组件内用到的所有状态的依赖,当其中一个状态发生变化时,就会通知到组件,然后由组件内部使用虚拟 DOM 进行数据比对,从而降低内存开销,提高性能

然后开始按顺序初始化propsmethodsdatacomputedwatcher

源码:

export function initState(vm: Component) 
  // 给实例添加一个_watchers,用来存储当前实例的所有watcher实例
  // vue2并不会对所有数据进行拦截检测,不然数据越多,绑定的依赖就越多,会造成内存开销很大
  // 所以vue2将监测粒度上升到组件层面,所以新增了一个_watchers属性
  // 当一个数据发生变化时,会通知到组件,由组件内部使用虚拟dom进行数据对比
  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);
  

initProps 函数

  • 检验 props 的值是否合法。其中布尔类型的比较特殊

1、父组件没有传入prop属性,并且也没有默认属性,就设置为false
2、父组件传入了prop属性,属性值为空字符串或者属性名和属性值相等,并且prop属性不存在string类型或者boolean类型的优先级比string类型的优先级高,就设置为true

  • 如果prop属性不在this上面,就将prop属性代理到this上面

源码:

function initProps(vm: Component, propsOptions: Object) 
  // 父组件传进来的props数据
  const propsData = vm.$options.propsData || ;
  // 所有设置了props变量的属性都会保存一份到_props中
  const props = (vm._props = );
  // 缓存props对象中的key,更新的时候只需要遍历keys就可以拿到所有props的key
  const keys = (vm.$options._propKeys = []);
  // 判断当前组件是否为根组件
  const isRoot = !vm.$parent;
  // 不是根组件不需要将数据转化为响应式
  // 根组件的props代理发生在vue.extend中
  if (!isRoot) 
    toggleObserving(false);
  
  // 遍历props的键值对
  for (const key in propsOptions) 
    // 将建名添加到keys中
    keys.push(key);
    // 校验值
    const value = validateProp(key, propsOptions, propsData, vm);
    /* istanbul ignore else */
    if (process.env.NODE_ENV !== "production") 
      // 检查key是否为html的保留属性
      const hyphenatedKey = hyphenate(key);
      if (
        isReservedAttribute(hyphenatedKey) ||
        config.isReservedAttr(hyphenatedKey)
      ) 
        warn(
          `"$hyphenatedKey" is a reserved attribute and cannot be used as component prop.`,
          vm
        );
      
      // 将key-value添加到_props
      defineReactive(props, key, value, () => 
        // 禁止组件修改props值
        if (!isRoot && !isUpdatingChildComponent) 
          warn(
            `Avoid mutating a prop directly since the value will be ` +
              `overwritten whenever the parent component re-renders. ` +
              `Instead, use a data or computed property based on the prop's ` +
              `value. Prop being mutated: "$key"`,
            vm
          );
        
      );
     else 
      defineReactive(props, key, value);
    
    // 如果key不在当前实例的vm上
    if (!(key in vm)) 
      // 将数据到this上面
      proxy(vm, `_props`, key);
    
  
  toggleObserving(true);

initMethods 函数

  • 检查methods中的每一个方法必须是一个函数

  • 检查 methods 中的方法是否跟 props 中的重名

  • 如果实例中有对应的 key 值,并且以_或者$开头的,提示命名不规范

  • 将方法代理到 this 上面

源码:

function initMethods(vm: Component, methods: Object) 
  const props = vm.$options.props;
  for (const key in methods) 
    if (process.env.NODE_ENV !== "production") 
      if (typeof methods[key] !== "function") 
        // methods里面的必须是函数
        warn(
          `Method "$key" has type "$typeof methods[
            key
          ]" in the component definition. ` +
            `Did you reference the function correctly?`,
          vm
        );
      
      if (props && hasOwn(props, key)) 
        // 检查methods中的方法是否跟props中的重名
        warn(`Method "$key" has already been defined as a prop.`, vm);
      
      // isReserved函数是用来判断字符串是否以_或$开头。
      if (key in vm && isReserved(key)) 
        // 如果实例中有对应的key值,并且以_或者$开头的,提示命名不规范
        warn(
          `Method "$key" conflicts with an existing Vue instance method. ` +
            `Avoid defining component methods that start with _ or $.`
        );
      
    
    // 将方法代理到this上面
    vm[key] =
      typeof methods[key] !== "function" ? noop : bind(methods[key], vm);
  

initData 函数

  • 获取 data 对象,如果 data 不是一个对象,则报错警告

  • 检查是否跟methodsprops有重名的

  • 将不以_或者$开头的属性代理到 this 上面

  • data对象转化为响应式对象

源码:

function initData(vm: Component) 
  let data = vm.$options.data;
  // 获取data对象
  data = vm._data = typeof data === "function" ? getData(data, vm) : data || ;
  if (!isPlainObject(data)) 
    // data必须是一个对象,否则警告
    data = ;
    process.env.NODE_ENV !== "production" &&
      warn(
        "data functions should return an object:\\n" +
          "https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function",
        vm
      );
  
  // proxy data on instance
  const keys = Object.keys(data);
  const props = vm.$options.props;
  const methods = vm.$options.methods;
  let i = keys.length;
  while (i--) 
    const key = keys[i];
    if (process.env.NODE_ENV !== "production") 
      // 检查是否跟methods有重名的
      if (methods && hasOwn(methods, key)) 
        warn(
          `Method "$key" has already been defined as a data property.`,
          vm
        );
      
    
    if (props && hasOwn(props, key)) 
      // 检查是否跟props有重名的
      process.env.NODE_ENV !== "production" &&
        warn(
          `The data property "$key" is already declared as a prop. ` +
            `Use prop default value instead.`,
          vm
        );
     else if (!isReserved(key)) 
      // 将不以_或者$开头的属性代理到this上面
      proxy(vm, `_data`, key)以上是关于vue2源码-- 初始化阶段的主要内容,如果未能解决你的问题,请参考以下文章

vue2源码-- 模板编译阶段

vue2源码-- 挂载阶段

vue2.0源码解析(上)

vue2源码-- computed 计算属性的实现

vue2源码-- computed 计算属性的实现

vue2.x源码学习