Vue源码一行行分析(一)整体分析

一、前言

Vue的项目也已经做了四五个了,虽然已经能够基本的做项目,但是要知其然,知其所以然,所以今天来研究一下源码,虽然自己前面也断续了看了一下源码, 但是你懂得,看源码真的看得头痛,所以就放弃了很多次,今天,就开始写这一部分,一则是为了记录自己的学习,以供自己的成长,以及后续的回顾,避免走弯路,二则则是表明自己的决心,相当于立一个flag,督促自己研究下去,好啦,闲话少叙,开工。

二、准备

这里只是下一个打包后的vue.js文件,而没有去下载vue的源码文件,因为vue的源码文件太复杂,我们先拿打包后的vue的js文件来分析,我们先了解vue的基本原理之后,我再下载他的源码文件,一步步来,步子太大,容易扯着…蛋(咳咳) ,当前vue.js的版本为v2.6.10,整体如下:
在这里插入图片描述
从上面图可以看出:vue.js整体为一个立即执行函数,学名IIFE,不知道的可以自己去百度一下,这里先不过多叙述,这个立即执行函数最大的作用是防止变量的污染,第一个参数将当前环境的this传入,第二个factory,可以理解为工厂函数,里面的typeof检验exports,module,module.exports在当前环境是否可用,不知道这三个东西的可以去看ES6阮一峰,总之就是各种校验之后调用工厂函数。

三、调试

index.html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>vue-sync</title>
  <!-- <script src="https://cdn.bootcss.com/vue/2.4.2/vue.js"></script> -->
  <script src="./vue.js"></script>
</head>

<body>
  <div id="app">
    {{msg}}
  </div>
</body>
<script>
  var vm = new Vue({
    el: '#app',
    data: {
      msg: 'hello vue'
    }
  })
</script>

</html>

从上面可以看出,创建一个Vue实例,然后在vue.js找到构造函数


  function Vue(options) {
    if (!(this instanceof Vue) // 如果没有使用new Vue创建实例的方式使用Vue,会报下面的警告
    ) {
      warn('Vue is a constructor and should be called with the `new` keyword');
    }
    this._init(options);
  }

找到this._init

Vue.prototype._init = function (options) {
      debugger
      var vm = this;
      // a uid
      vm._uid = uid$3++;

      var startTag, endTag;
      /* istanbul ignore if */
      if (config.performance && mark) {
        startTag = "vue-perf-start:" + (vm._uid);
        endTag = "vue-perf-end:" + (vm._uid);
        mark(startTag);
      }

      // a flag to avoid this being observed
      vm._isVue = true;
      // merge options
      if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else { // 合并options,因为Vue自身有option,与用户传入的option合并
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
      /* istanbul ignore else */
      {
        initProxy(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');

      /* istanbul ignore if */
      if (config.performance && mark) {
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };

可以看出,_init是放在Vue的原型链上,在上面的_init方法里已经写上了debugger,调试已经开始了,浏览器打开index.html 按F12
在这里插入图片描述
上面的config为全局配置,你可以查找var config = 就可以定位到相应的位置,先不说,具体作用请看官方API文档全局配置,而config.performance表示
设置为 true 以在浏览器开发工具的性能/时间线面板中启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持 performance.mark API 的浏览器上。 所以config.performance下方的代码是性能相关,先不去了解,不是说我们每行真的要懂,不可能的,一个框架,是经过了很多时间来磨合,要解决很多问题,所以有些我们不懂很正常,不可能一下就全懂一个框架源码,所以我们要学会去蚕食,去分解,最后掌握大部分,即可。

// 这段代码是合并options,将前面我们传入的options与框架自身的options合并,
// 并将合并options放入到Vue的$options上
 if (options && options._isComponent) {
        // optimize internal component instantiation
        // since dynamic options merging is pretty slow, and none of the
        // internal component options needs special treatment.
        initInternalComponent(vm, options);
      } else {
      // 将vm.constructor传入,即将当前实例的构造函数传入
        vm.$options = mergeOptions(
          resolveConstructorOptions(vm.constructor),
          options || {},
          vm
        );
      }
function resolveConstructorOptions(Ctor) { // Ctor为Vue方法
    var options = Ctor.options;
    // 有super属性,说明Ctor是Vue.extend构建的子类 这里就涉及到api extend,我们这里先不说extend
    //extend后续再讲
    // 这里就可以看出 当new Vue时是没有super属性的,当调用Vue.extend生成子类继承Vue,会设置super属性
    if (Ctor.super) {
      var superOptions = resolveConstructorOptions(Ctor.super);
      var cachedSuperOptions = Ctor.superOptions;
      if (superOptions !== cachedSuperOptions) {
        // super option changed,
        // need to resolve new options.
        Ctor.superOptions = superOptions;
        // check if there are any late-modified/attached options (#4976)

        var modifiedOptions = resolveModifiedOptions(Ctor);
        // update base extend options
        if (modifiedOptions) {
          extend(Ctor.extendOptions, modifiedOptions);
        }
        options = Ctor.options = mergeOptions(superOptions, Ctor.extendOptions);
        if (options.name) {
          options.components[options.name] = Ctor;
        }
      }
    }
    return options
  }
// 合并options
 function mergeOptions(
    parent,
    child,
    vm
  ) {
    {
    //检测用户传入的options里面的component属性是否符合html5规范,不符合会警告
      checkComponents(child);
    }
	// 如果child为方法,让child等于child里面的options
    if (typeof child === 'function') {
      child = child.options;
    }
	// 以下三个是验证pros,inject directive,这里先没看,先不看
    normalizeProps(child, vm);
    normalizeInject(child, vm);
    normalizeDirectives(child);

    // Apply extends and mixins on the child options,
    // but only if it is a raw options object that isn't
    // the result of another mergeOptions call.
    // Only merged options has the _base property.
    if (!child._base) {
      if (child.extends) {
        parent = mergeOptions(parent, child.extends, vm);
      }
      if (child.mixins) {
        for (var i = 0, l = child.mixins.length; i < l; i++) {
          parent = mergeOptions(parent, child.mixins[i], vm);
        }
      }
    }

    var options = {};
    var key;
    for (key in parent) {
      mergeField(key);
    }
    for (key in child) {
      if (!hasOwn(parent, key)) {
        mergeField(key);
      }
    }
    function mergeField(key) {
      var strat = strats[key] || defaultStrat;
      options[key] = strat(parent[key], child[key], vm, key);
    }
    return options
  }

在这里插入图片描述
合并之后获得的options
在这里插入图片描述

可以看出parent是Vue自带的option,而child是我们自己定义的,vm为this,主要是合并的代码,这里应该不难,一步步 调试,就可以看出变化。将options合并到vue的$options上之后。
在这里插入图片描述
下面进来代理拦截initProxy,而proxy是ES6的语法,语法详情可以看阮一峰ES6 proxy,对this对象进行拦截,其中hasProxy验证当前环境是否可以使用Proxy

  initProxy = function initProxy(vm) {
      if (hasProxy) { // 当环境能使用proxy 为true
        // determine which proxy handler to use
        var options = vm.$options;
        var handlers = options.render && options.render._withStripped
          ? getHandler
          : hasHandler;
        vm._renderProxy = new Proxy(vm, handlers); // 当访问vm的时候,会先进入handlers拦截过滤
      } else {
        vm._renderProxy = vm;
      }
    };
  }
// 当获取的时候触发,判断key是否是字符串 并且是否属于对象属性,如果不通过 则警告
var getHandler = {
      get: function get(target, key) {
        if (typeof key === 'string' && !(key in target)) {
          if (key in target.$data) { warnReservedPrefix(target, key); }
          else { warnNonPresent(target, key); }
        }
        return target[key]
      }
    };
// 判断key是否是保留字 第一个字符是下划线为保留字,如果是则警告, 不是则通过
 var hasHandler = { // 在Proxt中,has在 for...in中会使用到,意思就是说循环遍历的时候会触发
      has: function has(target, key) { // target为目标对象
        var has = key in target;
        var isAllowed = allowedGlobals(key) ||
          (typeof key === 'string' && key.charAt(0) === '_' && !(key in target.$data));
        if (!has && !isAllowed) {
          if (key in target.$data) { warnReservedPrefix(target, key); }
          else { warnNonPresent(target, key); }
        }
        return has || !isAllowed
      }
    };

就是说在使用vm的使用,会先调用initProxy设置的代理过滤,如果不满足条件的话,会警告你的,后面会单独开一章来举列子说明吧,现在将的比较笼统,只要知道这个是设置拦截过滤了,不满足条件的话会警告的,我们继续调试。
然后就到了关键部分了,如下如所示,这就不得不讲到Vue的生命周期了。
在这里插入图片描述
相信大家对这张图片不陌生吧。下面的图片就是对我们上面截图图片的流程化的介绍,我们一一分析
在这里插入图片描述
开始我们new Vue对应上图头部
在这里插入图片描述

	... //前面代码省略
  	vm._self = vm;
      initLifecycle(vm); // 初始化生命周期 其实就是一些变量的定义,详情可以自己看,不难
      initEvents(vm); // 初始化事件
      initRender(vm); // 初始化渲染 里面代码看不懂,以后研究
      callHook(vm, 'beforeCreate'); // 执行beforeCreate钩子函数
      initInjections(vm); //初始化注入 涉及到inject的用法
      initState(vm); // vue双向绑定相关
      initProvide(vm); // 初始化 provide 与上面的inject相关
      callHook(vm, 'created'); // 执行created钩子,这个时候data数据已经完成绑定,所以这就是为什么我们
      							//一般在created调用方法的原因了。
       /* istanbul ignore if */
      if (config.performance && mark) { // 设置为 true 以在浏览器开发工具的性能/时间线面板中
     //启用对组件初始化、编译、渲染和打补丁的性能追踪。只适用于开发模式和支持
     // performance.mark API 的浏览器上。
        vm._name = formatComponentName(vm, false);
        mark(endTag);
        measure(("vue " + (vm._name) + " init"), startTag, endTag);
      }

      if (vm.$options.el) { // 判断是否有el选项,没有的话 自行调用vm.$mount也可以渲染
        vm.$mount(vm.$options.el);
      }

下方为$mount方法

  Vue.prototype.$mount = function (
    el,
    hydrating
  ) {
    el = el && query(el);

    /* istanbul ignore if */
    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) { // 判断是否有render
      var template = options.template;
      if (template) {// 判断是否有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) {// 既没有render也没有template 使用外部模板
        template = getOuterHTML(el);
      }
      if (template) {
        /* istanbul ignore if */
        if (config.performance && mark) {
          mark('compile');
        }

        var ref = compileToFunctions(template, { // 与AST虚拟语法树有关 后续将
          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;

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

mount.call(this, el, hydrating)调用如下:

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

mountComponen如下:

  function mountComponent (
    vm,
    el,
    hydrating
  ) {
    vm.$el = el;
    if (!vm.$options.render) {
      vm.$options.render = createEmptyVNode;
      {
        /* istanbul ignore if */
        if ((vm.$options.template && vm.$options.template.charAt(0) !== '#') ||
          vm.$options.el || el) {
          warn(
            'You are using the runtime-only build of Vue where the template ' +
            'compiler is not available. Either pre-compile the templates into ' +
            'render functions, or use the compiler-included build.',
            vm
          );
        } else {
          warn(
            'Failed to mount component: template or render function not defined.',
            vm
          );
        }
      }
    }
    callHook(vm, 'beforeMount'); // 执行beforeMount

    var updateComponent;
    /* istanbul ignore if */
    if (config.performance && mark) {
      updateComponent = function () {
        var name = vm._name;
        var id = vm._uid;
        var startTag = "vue-perf-start:" + id;
        var endTag = "vue-perf-end:" + id;

        mark(startTag);
        var vnode = vm._render();
        mark(endTag);
        measure(("vue " + name + " render"), startTag, endTag);

        mark(startTag);
        vm._update(vnode, hydrating);
        mark(endTag);
        measure(("vue " + name + " patch"), startTag, endTag);
      };
    } else {
      updateComponent = function () {
        vm._update(vm._render(), hydrating);
      };
    }

    // we set this to vm._watcher inside the watcher's constructor
    // since the watcher's initial patch may call $forceUpdate (e.g. inside child
    // component's mounted hook), which relies on vm._watcher being already defined
    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);
    hydrating = false;

    // manually mounted instance, call mounted on self
    // mounted is called for render-created child components in its inserted hook
    if (vm.$vnode == null) {
      vm._isMounted = true;
      callHook(vm, 'mounted'); // 执行mounted
    }
    return vm
  }

上面的挖掘就到此告一段落 你现在不需要知道他们干了什么,实际上上面一大部分都是为了将模板渲染成AST,然后转换成VNode,在转换成html,大致是这个样子,你现在只要知道流程,如上面贴的生命周期流程,现在你应该懂了官方的生命周期是什么意思吧,以及他们相应的钩子,会直接触发的原理的吧。

四、总结

看了上面的分析,你也应该了解很多了,总结一下吧,new Vue -> mergeOptions合并options -> initProxy设置代理 ->初始化生命周期initLifecycle->初始化事件initEvents ->初始化渲染initRender->执行beforeCreate生命钩子->初始化注入initInjections->绑定数据initState->
初始化provide initProvide ->执行created生命钩子->询问是否有el ->没有退出,有则继续询问有没有render->没有询问有没有template->没有使用外部模板->执行beforeMount生命钩子->执行mounted生命钩子

没有画流程图,简单的用箭头表示,偷点懒,拿着官方的生命周期图,对着代码一步步走,一目了然,我偷不偷懒无所谓,下篇文章:Vue源码一行行分析(二)双向绑定原理

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值