Vue源码笔记,看懂核心原理

一.目录结构

目录结构javascript

├── build --------------------------------- 构建相关的文件
├── dist ---------------------------------- 构建后文件的输出目录
├── examples ------------------------------ 存放使用Vue开发的的例子
├── flow ---------------------------------- 类型声明,使用开源项目 [Flow](https://flowtype.org/)
├── package.json -------------------------- 项目依赖
├── test ---------------------------------- 包含全部测试文件
├── src ----------------------------------- 这个是咱们最应该关注的目录,包含了源码
│   ├──platforms --------------------------- 包含平台相关的代码
│   │   ├──web -----------------------------  包含了不一样构建的包的入口文件
│   │   |   ├──entry-runtime.js ---------------- 运行时构建的入口,输出 dist/vue.common.js 文件,不包含模板(template)到render函数的编译器,因此不支持 `template` 选项,咱们使用vue默认导出的就是这个运行时的版本。你们使用的时候要注意
│   │   |   ├── entry-runtime-with-compiler.js -- 独立构建版本的入口,输出 dist/vue.js,它包含模板(template)到render函数的编译器
│   ├── compiler -------------------------- 编译器代码的存放目录,将 template 编译为 render 函数
│   │   ├── parser ------------------------ 存放将模板字符串转换成元素抽象语法树的代码
│   │   ├── codegen ----------------------- 存放从抽象语法树(AST)生成render函数的代码
│   │   ├── optimizer.js ------------------ 分析静态树,优化vdom渲染
│   ├── core ------------------------------ 存放通用的,平台无关的代码
│   │   ├── observer ---------------------- 反应系统,包含数据观测的核心代码
│   │   ├── vdom -------------------------- 包含虚拟DOM建立(creation)和打补丁(patching)的代码
│   │   ├── instance ---------------------- 包含Vue构造函数设计相关的代码
│   │   ├── global-api -------------------- 包含给Vue构造函数挂载全局方法(静态方法)或属性的代码
│   │   ├── components -------------------- 包含抽象出来的通用组件
│   ├── server ---------------------------- 包含服务端渲染(server-side rendering)的相关代码
│   ├── sfc ------------------------------- 包含单文件组件(.vue文件)的解析逻辑,用于vue-template-compiler包
│   ├── shared ---------------------------- 包含整个代码库通用的代码
复制代码

二.内容

该章节是从打包文件vue.runtime.common.dev.js中查看源码内容。html

1.入口

从打包文件中看vue源码作了什么:vue

1.package.json文件java

里的main选项说明了入口在dist/vue.runtime.common.jsnode

"main": "dist/vue.runtime.common.js",
复制代码

dev环境中引用vue实际是引用该文件vue.runtime.common.dev.jsreact

2.初始化

1.消除浏览器差别

为了抹平浏览器之间的差别,作了大量polyfill操做,例如ES6中的Setandroid

if (typeof Set !== 'undefined' && isNative(Set)) {
  // use native Set when available.
  _Set = Set;
} else {
  // a non-standard Set polyfill that only works with primitive keys.
  _Set = /*@__PURE__*/(function () {
    function Set () {
      this.set = Object.create(null);
    }
    Set.prototype.has = function has (key) {
      return this.set[key] === true
    };
    Set.prototype.add = function add (key) {
      this.set[key] = true;
    };
    Set.prototype.clear = function clear () {
      this.set = Object.create(null);
    };

    return Set;
  }());
}
...
复制代码

2. 通用函数

vue中定义了大量通用函数,以下只是一部分,日常须要找一些通用函数也能够在这里找到例子ios

function isUndef (v) {
  return v === undefined || v === null
}

function isDef (v) {
  return v !== undefined && v !== null
}

function isTrue (v) {
  return v === true
}

function isFalse (v) {
  return v === false
}
复制代码

3.浏览器嗅探

var inBrowser = typeof window !== 'undefined';
var inWeex = typeof WXEnvironment !== 'undefined' && !!WXEnvironment.platform;
var weexPlatform = inWeex && WXEnvironment.platform.toLowerCase();
var UA = inBrowser && window.navigator.userAgent.toLowerCase();
var isIE = UA && /msie|trident/.test(UA);
var isIE9 = UA && UA.indexOf('msie 9.0') > 0;
var isEdge = UA && UA.indexOf('edge/') > 0;
var isAndroid = (UA && UA.indexOf('android') > 0) || (weexPlatform === 'android');
var isIOS = (UA && /iphone|ipad|ipod|ios/.test(UA)) || (weexPlatform === 'ios');
var isChrome = UA && /chrome\/\d+/.test(UA) && !isEdge;
var isPhantomJS = UA && /phantomjs/.test(UA);
var isFF = UA && UA.match(/firefox\/(\d+)/);
复制代码

4.常量

定义常量,资源类型,生命周期钩子等:git

var SSR_ATTR = 'data-server-rendered';

var ASSET_TYPES = [
  'component',
  'directive',
  'filter'
];

var LIFECYCLE_HOOKS = [
  'beforeCreate',
  'created',
  'beforeMount',
  'mounted',
  'beforeUpdate',
  'updated',
  'beforeDestroy',
  'destroyed',
  'activated',
  'deactivated',
  'errorCaptured',
  'serverPrefetch'
];
复制代码

5.服务器渲染

vue加载模式分为是否服务端渲染,从process.env.VUE_ENV区分github

3.Vue构造函数

1.定义构造函数

  1. 构造函数的定义,默认调用实例方法_init
function Vue (options) {
  if (!(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword');
  }
  this._init(options);
}
复制代码

注意:建立实例的时候,调用this._init(options)才是真正的开始。。。

  1. 给构造函数插入各类实例方法:
initMixin(Vue);   //初始化相关,beforeCreate,created钩子在这里体现
stateMixin(Vue);	//状态相关实例方法定义
eventsMixin(Vue);	
lifecycleMixin(Vue);
renderMixin(Vue);
//...
initGlobalAPI(Vue);

复制代码

2.initMixin函数

做用:

  • 添加内部调用方法_init

_init方法:

Vue.prototype._init = function (options) {
      //动态组件优化 initInternalComponent
      //用proxy代理事件
      ...
      
      //设置好各个实例方法,生命周期、事件、渲染
      //注意钩子触发位置
    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);
    }
  }
复制代码

注意钩子触发位置:

  1. 先注册生命周期,事件,initRender建立节点

  2. 执行beforeCreate钩子

  3. 将选项注册为实例方法,实例属性,用observe将data里的属性注册到观察者模式

  4. 执行created钩子

  5. 进入挂载流程vm.$mount(vm.$options.el);

3.stateMixin函数

添加属性相关实例属性、实例方法

  • $data属性
  • $props属性
  • $set方法
  • $delete方法
  • $watch方法:将属性加入观察者模式

定义属性相关的实例方法:

Vue.prototype.$set = set;
  Vue.prototype.$delete = del;

//观察者模式的触发器
  Vue.prototype.$watch = function ( expOrFn, cb, options ) {
  ...
  }
复制代码

3.eventsMixin函数

1.添加事件相关实例函数:

  • $on注册事件
  • $once注册只使用一次事件的方法
  • $off注销事件方法
  • $emit触发事件方法

2.事件注册,实质是在on指令用hook:event的格式注册到观察者模式中;

var hookRE = /^hook:/;
Vue.prototype.$on = function (event, fn) {
    //...省略数组式添加事件
    (vm._events[event] || (vm._events[event] = [])).push(fn);
      if (hookRE.test(event)) {
        vm._hasHookEvent = true;
      }
}
复制代码

4.lifecycleMixin函数

生命周期混入,主要是更新,销毁;createmount是在init实例方法里触发。

1.添加实例函数

  • _update更新(内部使用)
  • $forceUpdate
  • $destroy
Vue.prototype.$forceUpdate = function () {
    var vm = this;
    if (vm._watcher) {
      vm._watcher.update();
    }
  };

  Vue.prototype.$destroy = function () {
      
  }
复制代码

5.renderMixin函数

1.添加渲染辅助实例函数

  • $nextTick
  • _render 渲染(内部使用)

2.添加vm.VNode属性

3.给vnode.parent赋值肯定组件的父子层级

6.initGlobalAPI函数

给Vue构造函数定义静态方法、属性:

function initGlobalAPI (Vue) {
  // config
  var configDef = {};
  configDef.get = function () { return config; };
  {
    configDef.set = function () {
      ...
    };
  }
  Object.defineProperty(Vue, 'config', configDef);
  //工具方法
  Vue.util = {
    warn: warn,
    extend: extend,
    mergeOptions: mergeOptions,
    defineReactive: defineReactive$$1
  };
	//设置属性,删除属性,nextTick
  Vue.set = set;
  Vue.delete = del;
  Vue.nextTick = nextTick;
	
  //添加对象到observe
  Vue.observable = function (obj) {
    observe(obj);
    return obj
  };
	
  Vue.options = Object.create(null);
  ASSET_TYPES.forEach(function (type) {
    Vue.options[type + 's'] = Object.create(null);
  });
	
  Vue.options._base = Vue;

  extend(Vue.options.components, builtInComponents);

  initUse(Vue);  //添加use方法
  initMixin$1(Vue);	//添加全局mixin方法
  initExtend(Vue);	//全局继承
  initAssetRegisters(Vue);	//全局资源方法:components、directives、filters注册方法
}
复制代码

7.$mount实例方法

在initMixin函数中,_init实例方法中用到的$mount实例方法,是进入挂载模板的入口:

//进入挂载流程
if (vm.$options.el) {
	vm.$mount(vm.$options.el);
}
复制代码
// public mount method
Vue.prototype.$mount = function (
  el,
  hydrating
) {
  el = el && inBrowser ? query(el) : undefined;
  return mountComponent(this, el, hydrating)
};
复制代码

最终找到函数mountComponent

function mountComponent ( vm, el, hydrating ) {
    //1.检查render选项、template选项、el选项看是否有可用的模板
    //...
    callHook(vm, 'beforeMount');
   	//...
      
    //2.定义updateComponent方法
    updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };
      
    //3.vm注册到观察者模式中
    new Watcher(vm, updateComponent, noop, {
        before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');
            }
        }
    }, true /* isRenderWatcher */);
      
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
     }
  }
复制代码

因此,模板的渲染和更新是靠观察者模式触发的

三.Vue中的双向数据绑定

Vue实例为它的每个data都实现了getter/setter方法,这是实现响应式的基础。关于getter/setter可查看MDN web docs。 简单来讲,就是在取值this.counter的时候,能够自定义一些操做,再返回counter的值;在修改值this.counter = 10的时候,也能够在设置值的时候自定义一些操做。initData(vm)的实如今源码中的instance/state.js

订阅中心Dep

Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};

Dep.prototype.removeSub = function removeSub (sub) {
  remove(this.subs, sub);
};
//getter中使用 收集依赖
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
//setter中使用 通知更新
Dep.prototype.notify = function notify () {
  // stabilize the subscriber list first
  var subs = this.subs.slice();
  if (!config.async) {
    // subs aren't sorted in scheduler if not running async
    // we need to sort them now to make sure they fire in correct
    // order
    subs.sort(function (a, b) { return a.id - b.id; });
  }
  for (var i = 0, l = subs.length; i < l; i++) {
    subs[i].update();
  }
};
复制代码

触发器Observer

Observer Class将每一个目标对象的键值(即data中的数据)转换成getter/setter形式,用于进行依赖收集和经过依赖通知更新。

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    if (hasProto) {
      protoAugment(value, arrayMethods);
    } else {
      copyAugment(value, arrayMethods, arrayKeys);
    }
    this.observeArray(value);
  } else {
    this.walk(value);
  }
};


复制代码

继续看walk()方法,注释中已说明walk()作的是遍历data对象中的每一设置的数据,将其转为setter/getter

/** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive$$1(obj, keys[i]);
  }
};

复制代码

那么最终将对应数据转为getter/setter的方法就是defineReactive()方法。从方法命名上也容易知道该方法是定义为可响应的,结合最开始的例子,这里调用就是defineReactive(...)如图所示:

/** * Define a reactive property on an Object. */
function defineReactive$$1 ( obj, key, val, customSetter, shallow ) {
  var dep = new Dep();

  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key];
  }

  var childOb = !shallow && observe(val);
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          childOb.dep.depend();
          if (Array.isArray(value)) {
            dependArray(value);
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      var value = getter ? getter.call(obj) : val;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (customSetter) {
        customSetter();
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) { return }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      childOb = !shallow && observe(newVal);
      dep.notify();
    }
  });
}
复制代码

监听器watcher

var uid$2 = 0;

/** * A watcher parses an expression, collects dependencies, * and fires callback when the expression value changes. * This is used for both the $watch() api and directives. */
var Watcher = function Watcher ( vm, expOrFn, cb, options, isRenderWatcher ) {
  this.vm = vm;
  if (isRenderWatcher) {
    vm._watcher = this;
  }
  vm._watchers.push(this);
  // options
  if (options) {
    this.deep = !!options.deep;
    this.user = !!options.user;
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
    this.before = options.before;
  } else {
    this.deep = this.user = this.lazy = this.sync = false;
  }
  this.cb = cb;
  this.id = ++uid$2; // uid for batching
  this.active = true;
  this.dirty = this.lazy; // for lazy watchers
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = expOrFn.toString();
  // parse expression for getter
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = noop;
      warn(
        "Failed watching path: \"" + expOrFn + "\" " +
        'Watcher only accepts simple dot-delimited paths. ' +
        'For full control, use a function instead.',
        vm
      );
    }
  }
  this.value = this.lazy
    ? undefined
    : this.get();
};
复制代码

总结

1.监听器Watcher里{对象,键值,回调函数}这样一个数据结构的实例经过实例方法Watcher.prototype.addDep添加到订阅中心Dep,记录在里面的静态属性subs里。

2.触发器Observer负责在触发getter/setter时候添加依赖depend/发送通知通知noticy

3.订阅中心负责处理粗发器发过来的信息(添加依赖depend/发送通知通知noticy)循环调用静态属性subs里的watcher实例,符合的实例会调用对应的回调函数

下图来自官方:

data

这是触发组件更新的图例,省略了Dep部分

四.属性说明

1.选项

el

能够是query函数能解析的字符串/HTMLElement 实例

提供一个在页面上已存在的 DOM 元素做为 Vue 实例的挂载目标。能够是 CSS 选择器,也能够是一个 HTMLElement 实例。

在实例挂载以后,元素能够用 vm.$el 访问。

2.实例属性

1.$data

Vue 实例观察的数据对象。Vue 实例代理了对其 data 对象属性的访问。

2.$props

当前组件接收到的 props 对象。Vue 实例代理了对其 props 对象属性的访问。

3.$el

Vue 实例使用的根 DOM 元素。

4.$options

vue实例的属性初始化后的配置集合

vm.$options = mergeOptions(
    resolveConstructorOptions(vm.constructor),
    options || {},
    vm
);
复制代码

5.$root

当前组件树的根 Vue 实例。若是当前实例没有父实例,此实例将会是其本身。

6.$slots

用来访问被插槽分发的内容。每一个具名插槽 有其相应的属性 (例如:v-slot:foo 中的内容将会在 vm.$slots.foo 中被找到)。default 属性包括了全部没有被包含在具名插槽中的节点,或 v-slot:default 的内容。

7.$scopedSlots

  1. 做用域插槽函数如今保证返回一个 VNode 数组,除非在返回值无效的状况下返回 undefined
  2. 全部的 $slots 如今都会做为函数暴露在 $scopedSlots 中。若是你在使用渲染函数,不论当前插槽是否带有做用域,咱们都推荐始终经过 $scopedSlots 访问它们。这不单单使得在将来添加做用域变得简单,也可让你最终轻松迁移到全部插槽都是函数的 Vue 3。

8.$attrs

包含了父做用域中不做为 prop 被识别 (且获取) 的特性绑定 (classstyle 除外)。当一个组件没有声明任何 prop 时,这里会包含全部父做用域的绑定 (classstyle 除外),而且能够经过 v-bind="$attrs" 传入内部组件——在建立高级别的组件时很是有用。

9.$listeners

包含了父做用域中的 (不含 .native 修饰器的) v-on 事件监听器。它能够经过 v-on="$listeners" 传入内部组件——在建立更高层次的组件时很是有用。

相关文章
相关标签/搜索