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" 傳入內部組件——在建立更高層次的組件時很是有用。

相關文章
相關標籤/搜索