(解析)vue源碼解讀

前言

A: 爲什麼要做源碼解讀?
Q: 我們新到一個環境,第一件事情就是熟悉環境熟悉項目,這個很考驗閱讀源碼的能力以及耐心。vue是個很好的庫,知名度高,對js的學習具有向上性,所以搞清楚邏輯是有好處的。
A: 閱讀源碼的程度?
Q: 我們完全沒必要從頭到尾細細品味,只需要知道一些核心的實現就好了,畢竟vue也算是個產品,我們沒必要搞清楚一個產品,我們只需要知道產品的核心就夠了,多餘的也是業務代碼。(相對來說)

開始

Vue應用的實例化以及掛載

main.js

new Vue({                      // 初始化vue實例
  //components: { App }        // vue1.0的寫法
  render: h => h(App)          // 最先執行,返回一個符合component的對象,vue2.0的寫法
})
.$mount('#app')                // 掛載vue實例到 ‘#app’

render函數

render: h => h(App) 
就是
render:function(h){
    return h(App)
}
即
render: function (createElement) {
    return createElement(App)
}

找到Vue引用源文件(debug順序)

import Vue from 'vue'

找到

node-modules/vue

打開package.json 找到

"main": "dist/vue.runtime.common.js"

"main"是 npm模塊曝光的主要文件.

打開vue.runtime.common.js
ctrl/command + a, ctrl/command + k, ctrl/command + 1
,快捷鍵把所有方法摺疊

debug執行順序

你會發現 update執行了2次,我明明只初始化了一次vue實例,爲什麼update2次了呢?原因在下方表明。

構造函數

拉到最下面,

module.exports = Vue;

導出的是一個Vue構造函數。
當前文件搜索 Vue 找到 構造函數

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');
  }
  this._init(options); // 調用 初始化方法
}

初始化函數

var uid$3 = 0;
vm._uid = uid$3++; //每個vue實例 擁有唯一id,從0開始 ++

// 合併初始化vue實例的參數(入參options和默認參數)
vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      );

initLifecycle();// 初始化實例生命週期相關的參數
使用Object.create(null)用來獲取一個沒有原型鏈的對象類型

initEvents(); // 初始化實例事件觸發相關的參數
initRender(); // 初始化實例渲染相關的參數

//create的準備工作做好了,觸發beforeCreate的生命週期
callHook(vm, 'beforeCreate');

initState(); // 初始化實例狀態

// 狀態也初始化好了,觸發create的生命週期,所以 create和 breforCreate的區別就在 create的時候  有狀態。
callHook(vm, 'created');

至此,vue實例的初始化完成,然後掛載到節點

掛載實例到節點

// 將mount('#app') => query('#app') 查找到dom對象,賦值給vue.$el
// 觸發beforeMount的生命週期,所以beforeMount 和 create的區別就在  beforeMount的時候 有掛載節點。
callHook(vm, 'beforeMount');

// 拿到當前將要掛載的Vnode(虛擬dom對象)
vm._render()  vm.$vnode

// (更新)渲染頁面
vm._update(vm._render(), hydrating);
vm.__patch__(); 
createElm(); // 按照虛擬dom生成真實dom

createElm函數

// 這是一個遞歸方法,vue實例的初始化是創建一個根節點,然後再將render函數傳入的組件掛載,這就是流程圖執行2次update的原因。
// 已經刪減,只留主要邏輯,判斷虛擬dom 的 tag屬性
if (isDef(tag)) {
  {
    // 遍歷虛擬dom的子節點,並且創建,然後遞歸當前方法。
    createChildren(vnode, children, insertedVnodeQueue); 
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue);
    }
    insert(parentElm, vnode.elm, refElm);
  }
}
// 如果沒有子節點的,直接創建dom,然後插入
 else if (isTrue(vnode.isComment)) {
  vnode.elm = nodeOps.createComment(vnode.text);
  insert(parentElm, vnode.elm, refElm);
} else {
  vnode.elm = nodeOps.createTextNode(vnode.text);
  insert(parentElm, vnode.elm, refElm);
}
// 觸發Mount的生命週期
callHook(vm, 'mounted');

至此就是 Vue根節點初始化掛載和渲染的流程.

Vue數據更新

首先 我們改造下 app.vue,像官網一樣,我們新增一個雙向綁定的文本框。
現在我們知道了,第一次的update只是掛載了根節點,那麼我們新增了文本框的組件其實是在第二次init的時候初始化的。
我們可以着重看第二次的流程,搞清楚,數據的監聽與更新。

initData函數

function initData (vm) {
  var data = vm.$options.data;
  // 把從render函數返回的data函數對象賦值給data,然後data.call(this,this),也就是vm.data();拿到data返回值
  data = vm._data = typeof data === 'function'      
    ? getData(data, vm)
    : data || {};
  // observe data
  observe(data, true /* asRootData */);
}
function getData (data, vm) {
  pushTarget();
  try {
    return data.call(vm, vm)
  } catch (e) {
    handleError(e, vm, "data()");
    return {}
  } finally {
    popTarget();
  }
}

拿到data返回值

function observe (value, asRootData) {
  var ob;
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    shouldObserve &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    ob = new Observer(value);   // 返回一個 新建的 Observer
  }
  return ob
}

Observer對象(監聽者)

var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  def(value, '__ob__', this);
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value); 
  } else {
    this.walk(value);
  }
};
// 如果是對象,則按照key來劫持
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i++) {
    defineReactive(obj, keys[i]);
  }
};

// 如果是數組,就遍歷每個數組元素,再每個元素再判斷是否爲數組,對象
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

數據劫持

Watcher對象(訂閱者)

小結

未完待續。。。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章