玩轉Vuejs--核心原理

1、摘要:html

Vuejs是一款前端MVVM框架,利用Vuejs、webpack以及周邊一系列生態工具咱們能夠快速的構建起一個前端應用,網上對於Vue的分析大都是基於各個模塊,理解起來不夠順暢,本文將從整個執行過程出發,講一下Vuejs的核心原理。前端

 

2、版本說明:vue

Vuejs有兩種版本,一種是runtime、一種是runtime-with-compiler,對應的渲染有兩種寫法:react

一、render渲染函數:直接寫render函數,渲染時將會調用render函數渲染DOMwebpack

<div id="demo"></div>
 
 //runtime寫法
  var instance = new Vue({
    data: {
      hi: "monrning ",
      name: "Mr zhang"
    },
    render(h){ //h vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
      return h('div', this.hi + " " + this.name);
    }
  });
  instance.$mount(document.getElementById("demo"));

 

二、HTML模板語法:web

<div id="demo">{{hi}} {{name}}</div>

new Vue({
  el: "#demo"
    data: {
      hi: "monrning ",
      name: "Mr zhang"
    }
  });

runtime就是Vue運行時,不少框架的核心都是改自Vue的運行時,好比mpvue的運行部分,之後會講。算法

runtime與runtime-with-compiler的區別:express

顧名思義,前者沒有compiler,後者有compiler。ompiler是Vue的編譯模塊,能夠將HTML模板轉換成對應的AST以及render渲染函數提供給Vue使用,因此本質上能夠認爲Vue的渲染就是在調用render渲染函數,compiler的做用就是在構造渲染函數。本文不講compiler部分,只須要知道compiler會將模板構形成render函數便可,後面理解會用到。api

 

3、模塊分解:緩存

爲了便於理解,本人將Vue的核心分紅如下幾個部分:數據初始化、數據更新、異步隊列、DOM渲染(虛擬DOM)

數據初始化:初始化調用Object.defineProperty對數據進行劫持,進而監聽數據的變化,後續的更新、渲染都會依賴這一部分;

數據更新:數據監聽實際在數據初始化階段已經完成了,將這一部分獨立出來的緣由是數據初始化只作了對數據的set、get進行監聽,邏輯的部分須要在數據更新以及渲染中來看;

異步隊列:異步隊列主要是爲了解決在數據更新上觸發多個Watcher如何進行更新的問題;

DOM渲染:這一部分包含虛擬DOM,單獨做爲一個部分,其中虛擬DOM高效的diff算法、patch的概念都很重要;

說明一下,這部分的分解是按照我我的的思路總結出來的幾部分(非按順序執行部分),若是有沒法下手的狀況能夠有意識的按照這幾部分來思考一下,僅供你們借鑑使用。

ps:注意上面的初始化和更新僅僅是「數據」的部分,不要跟下面的分析弄混淆。

 

4、核心原理:

接下來以實際的執行過程來說一下Vue和核心原理,主要包括兩個階段:初始化階段、數據更新階段

如下面的代碼爲例:

<html lang="en">
<head>
  <meta charset="UTF-8">
  <title></title>
  <script src="../dist/vue.js"></script>
</head>
<body>
<div id="demo">
  <div >
    {{title}}
    {{title2}}
  </div>
  <input type="button" @click="testClick" value="click"/>
</div>
</body>
<script>
  // 這種狀況不須要
  var instance = new Vue({
    el:"#demo",
    data: {
      title: "input MVVM",
      title2: "input MVVM title2"
    },
    methods: {
      testClick(){
        this.title = Math.random();
        this.title2 = Math.random();
      }
    }
  });
</script>
</html>

首先講一下初始化階段

 一、首先進行「數據的初始化」,代碼以下:

  function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};
    if (!isPlainObject(data)) {
      data = {};
      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
    var keys = Object.keys(data);
    var props = vm.$options.props;
    var methods = vm.$options.methods;
    var i = keys.length;
    while (i--) {
      var key = keys[i];
      {
        if (methods && hasOwn(methods, key)) {
          warn(
            ("Method \"" + key + "\" has already been defined as a data property."),
            vm
          );
        }
      }
      if (props && hasOwn(props, key)) {
        warn(
          "The data property \"" + key + "\" is already declared as a prop. " +
          "Use prop default value instead.",
          vm
        );
      } else if (!isReserved(key)) {
        proxy(vm, "_data", key); //將data代理到Vue實例上
      }
    }
    // observe data
    observe(data, true /* asRootData */);
  }

能夠看到數據初始化包括兩個過程:

(1)、將 data 代理到Vue實例上,以後 data 中的數據能夠直接經過Vue實例 this 來設置或者獲取,代理過程以下: 

 vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};

//遍歷data,設置代理
proxy(vm, "_data", key);    

//proxy 過程
  function proxy (target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter () {
      return this[sourceKey][key]
    };
    sharedPropertyDefinition.set = function proxySetter (val) {
      this[sourceKey][key] = val;
    };
    Object.defineProperty(target, key, sharedPropertyDefinition);
  }

ps: 這裏先將數據掛在了vm._data 上,以後再代理到 vm 實例上,前者主要是爲了暴漏 $data 使用,最終能夠獲取數據的地方有三個: vm (實例)、vm._data、vm.$data;

(2)、劫持數據,進行數據的初始化,底層使用你們瞭解最多Object.defineProperty(官方宣稱3.0後這部分會使用Proxy來代替)

上面的 observe(data, true /* asRootData */) 最終會調用 defineReactive 進行初始化,直接看這部分:

//{title: 'MVVM'} 
function
defineReactive$$1 ( obj,//{title: 'MVVM'} key,//title val, customSetter, shallow ) { var dep = new Dep();//依賴收集器 每一個key一個 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(); //依賴收集器將當前watcher收集到依賴中 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(); //通知訂閱當前key的全部watcher } }); }

這部分是最基本的部分,必須理解,直接上圖:

    

 

 ①、Observer會調用defineReactive對數據的每一個key進行劫持;

 ②、defineReactive會爲當前key定義get、set方法,以及建立一個Dep實例,Dep能夠稱爲依賴收集器;

 ③、當(watcher)在獲取數據時,如: let title = data.title,此時就會觸發 'title' 對應的 get方法,若是Dep.target有對應的watcher,那麼經過dep.depend() 將當前watcher加入到  'title' 對應的dep中,最後返回 title 的值。

  這裏有必要看下Dep的源碼以及解釋下Dep.target的含義

  var uid = 0;

  /**
   * A dep is an observable that can have multiple
   * directives subscribing to it.
   */
  var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

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

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  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();
    }
  };

  // The current target watcher being evaluated.
  // This is globally unique because only one watcher
  // can be evaluated at a time.
  Dep.target = null;
  var targetStack = [];

  function pushTarget (target) {
    targetStack.push(target);
    Dep.target = target;
  }

  function popTarget () {
    targetStack.pop();
    Dep.target = targetStack[targetStack.length - 1];
  }

這裏Dep.target至關於一個上下文的做用(公共變量),用來存儲當前正在執行的watcher實例。獲取數據時判斷若是Dep.target(當前watcher)存在,dep.depend() 就會將當前正在獲取數據的watcher加入到依賴之中;

 ④、當數據發生變化時,如:data.title = 'xxxx',此時會觸發 'title' 對應的set方法,經過dep.notity() 通知對應dep中的watcher,watcher再進行更新;

以上就是數據初始化的過程,總結來講就是對數據進行劫持,併爲每一個key創建一個依賴,獲取數據時依賴收集對應的watcher,數據變化時通知對應的watcher進行更新。

 

2.以後進行數據的掛載,主要包括兩個部分:模版解析、建立渲染watcher完成渲染

模版解析:compiler部分

調用compiler中的compiletoFunctions將模版解析爲render渲染函數,(模板解析是運行時比較消耗性能的部分,若是已經編譯過Vue會將結果緩存起來使用)

render函數將會傳遞給下面的渲染watcher渲染DOM使用(獲取數據、建立對應的DOM、綁定事件),直觀的看一下render的邏輯。

(function anonymous(
) {
with(this){return _c('div',{attrs:{"id":"demo"}},[_c('div',[_v("\n    "+_s(title)+"\n    "+_s(title2)+"\n    ")]),_v(" "),_c('input',{attrs:{"type":"button","value":"click"},on:{"click":testClick}})])}
})

建立渲染watcher,渲染DOM:渲染watcher劃重點,Vue有不少個watcher,但只有一個watcher負責渲染,就是渲染watcher

首先講一下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.lazy = options.lazy;
  .....this.id = ++uid$2; // uid for batching
  .....
   this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      .....
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  /**
   * Evaluate the getter, and re-collect dependencies.
   */
  Watcher.prototype.get = function get () {
    pushTarget(this); 
    var value;
    var vm = this.vm;

  ......
  value = this.getter.call(vm, vm); 

  ......
  popTarget();
   this.cleanupDeps();
  return value
  };

  /**
   * Add a dependency to this directive.
   */  
  Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };
 
 Watcher.prototype.update = function update () {
  /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this); 
    }
 };

/**
* Scheduler job interface.
* Will be called by the scheduler.
*/
Watcher.prototype.run = function run () {
  if (this.active) {
    var value = this.get();
//將當前watcher 賦值到 Dep.target//調用watcher傳入的回調函數//將 watcher加入異步隊列中
    ......
  }
    ......
}

Watcher接受五個參數,如今只關注前兩個便可,一個是vm實例,一個是執行函數expOrFn。

Watcher在構造函數初始化時會執行 this.get() 方法,this.get()會執行兩個操做:pushTargt(this)、執行回調函數expOrFn

①、pushTargt(this):將當前的watcher實例賦值給Dep.target(見上面Object.defineProperty中的get方法),也便是此時 Dep.target = 'renderWatcher';

②、執行回調函數expOrFn:若是此時回調函數中若是有屬獲取數據的動做,將會先觸發Object.defineProperty中的get方法,將Dep.target(當前watcher) 加入到依賴中,以後整個回調函數執行完畢。

 

過程如圖:

 

這裏的 pushTarget 以及 Dep.target 在看源碼的時候是比較難懂的,主要是語義上沒有寫清(若是對應寫成 pushWatcher 以及 globalWatcher 可能會更清楚一些,做者應該是想寫的高內聚一些)。

接下來看一下渲染watcher,渲染watcher顧名思義就是負責渲染的watcher,說白了就是回調函數會執行上面的render渲染函數進行DOM渲染

  updateComponent = function () {
    vm._update(vm._render(), hydrating); //_render 會調用上面render進行DOM的繪製
  };

new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);

那麼根據上面Watcher邏輯,此處渲染watcher初始化時首先會將Dep.target切換到當前watcher上,以後執行回調updateComponent。updateComponent實際上就是執行render函數,render函數獲取並訂閱數據,以後建立DOM完成渲染。

因爲已經訂閱了數據,數據在發生變化時就會通知渲染watcher從新進行渲染,以後反映到DOM上。

下面是 updateComponent -> render 執行的邏輯,createElement 將會使用虛擬DOM來建立,最後映射到真實DOM,虛擬DOM的技術有機會單獨來說,不在此處展開。

with(this){return _c('div',{attrs:{"id":"demo"}},[_c('div',[_v("\n    "+_s(title)+"\n    "+_s(title2)+"\n    ")]),_v(" "),_c('input',{attrs:{"type":"button","value":"click"},on:{"click":testClick}})])}

vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };

總結一下:watcher說白了就是用了哪一個數據就會訂閱哪一個數據,數據變化時就會收到對應通知,未使用到的數據變化就不會受到通知。如渲染watcher,模版中使用了title、title2,那麼當title、title2變化時就會通知渲染watcher進行更新,其它各類watcher同理(lazy的有些特殊,後面再講);

 

接下來說一下數據更新階段

當數據發生變化時,如例子中testClick觸發數據變化,過程爲:

testClick(){
        this.title = Math.random();
        this.title2 = Math.random();
      }
 

1.數據更新觸發data中的set,依賴收集器會通知Watcher進行更新:

此處首先執行的是this.title = Math.random(),因爲渲染watcher使用了title進行渲染,那麼此處title的變化就會通知渲染watcher進行更新

//defineReactive中數據的set
dep.notify(); 

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();//watcher更新
  }
};

這裏只有一個watcher使用了title,因此subs中就是一個渲染watcher,渲染watcher就會更新。

接下來看下watcher的更新邏輯:

Watcher.prototype.update = function update () {
  /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
 };

這裏能夠看到watcher的update不會直接執行watcher的邏輯(例子中爲渲染DOM),而是執行了一個queueWatcher方法(前面的邏輯判斷先忽略),queueWatcher會將watcher放入一個隊列queue中,以後經過nextTick來執行,這就是我前面講到的異步隊列

function queueWatcher (watcher) {
    var id = watcher.id;
    console.log('watcherId='+ id + 'exporession=' + watcher.expression);
    if (has[id] == null) {
      //console.log('watcherId='+ id + 'exporession=' + watcher.expression);
      has[id] = true;
      if (!flushing) {
        queue.push(watcher);
      } else { 
        // if already flushing, splice the watcher based on its id
        // if already past its id, it will be run next immediately.
        var i = queue.length - 1;
        while (i > index && queue[i].id > watcher.id) {
          i--;
        }
        queue.splice(i + 1, 0, watcher);
      }
      // queue the flush
      if (!waiting) {
        waiting = true;

        if (!config.async) {
          flushSchedulerQueue();
          return
        }
        nextTick(flushSchedulerQueue);
      }
    }
  }

nextTick能夠簡單當成一個異步函數來看,好比setTimeout,主要目的就是把要執行的操做放到下一個執行週期中,好比例子中testClick整個執行週期完成後纔會執行此處的邏輯,watcher中的邏輯纔會真正執行,這樣就避免了屢次觸發watcher以及觸發了多個watcher更新形成的重複渲染問題。

var queue = [ watcher ];

//nextTick flushSchedulerQueue
setTimetout(function(){
  for(var i = 0; i < queue.length; i++){
        queue[i].run(); //watcher.run()
  }
} , 0);

ps:這裏的異步隊列中涉及到了nextTick以及多個watcher執行順序的問題,本文爲了方便理解只講了一種watcher--渲染watcher,後面講其它watcher的時候一塊兒來說。

以上就是數據更新階段的邏輯。

 

5、總結:

本文從Vue的初始化以及更新兩個方面出發講了下Vue的核心邏輯,主要目的是爲了幫助你們瞭解整個流程,介紹過程屏蔽了很多內容,後面有機會再展開。另外,若是有沒看過源碼能夠從我上面劃分的幾個部分來開,可能會事半功倍。

相關文章
相關標籤/搜索