1. 程式人生 > >vue2.x原始碼理解

vue2.x原始碼理解

也不知道哪股風潮,鑽研原始碼竟成了深入理解的標配。我只想說一句,說的很對

準備工作

  1. 從GitHub上面下載vue的原始碼(https://github.com/vuejs/vue
  2. 瞭解下Flow,Flow 是 facebook 出品的 JavaScript 靜態型別檢查工具。Vue.js 的原始碼利用了 Flow 做了靜態型別檢查
  3. vue.js 原始碼目錄設計,vue.js的原始碼都在 src 目錄下(\vue-dev\src)
    src
    ├── compiler # 編譯相關
    ├── core # 核心程式碼
    ├── platforms # 不同平臺的支援
    ├── server # 服務端渲染
    ├── sfc # .vue 檔案解析
    ├── shared # 共享程式碼

    core 目錄:

    包含了 Vue.js 的核心程式碼,包括內建元件、全域性 API 封裝,Vue 例項化、觀察者、虛擬 DOM、工具函式等等。這裡的程式碼可謂是 Vue.js 的靈魂

    platform目錄:Vue.js 是一個跨平臺的 MVVM 框架,它可以跑在 web 上,也可以配合 weex 跑在 natvie 客戶端上。platform 是 Vue.js 的入口,2 個目錄代表 2 個主要入口,分別打包成執行在 web 上和 weex 上的 Vue.js。比如現在比較火熱的mpvue框架其實就是在這個目錄下面多了一個小程式的執行平臺相關內容。

在這裡插入圖片描述

  1. vue2.0的生命週期分為4主要個過程:
    4.1 create:建立---例項化Vue(new Vue) 時,會先進行create。
    4.2 mount:掛載---根據el, template, render方法等屬性,會生成DOM,並新增到對應位置。
    4.3 update:更新---當資料發生變化後,更新DOM。
    4.4 destory:銷燬---銷燬時執行

new Vue()發生了什麼

在vue的生命週期上第一個就是 new Vue() 建立一個vue例項出來,對應到原始碼在\vue-dev\src\core\instance\index.js


import { initMixin } from './init'
import { stateMixin } from './state'
import { renderMixin } from './render'
import { eventsMixin } from './events'
import { lifecycleMixin } from './lifecycle'
import { warn } from '../util/index'

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

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

export default Vue

可以通過index.js中的程式碼看到,其實就是一個function,在es5中實現class的方式,在function vue中還加入了if判斷,表示vue必須通過new關鍵字進行例項化。這裡有個疑問就是為什麼vue中沒有使用es6的方式進行定義?通過看下面的方法可以得到解答。

function vue下定義了許多Mixin這種方法,並且把vue類當作引數傳遞進去,下面來進入initMixin(Vue)下,來自import { initMixin } from './init',選取了部分程式碼如下


export function initMixin (Vue: Class<Component>) {
  Vue.prototype._init = function (options?: Object) {
    const vm: Component = this
    // a uid
    vm._uid = uid++

    let startTag, endTag
    /* istanbul ignore if */
    if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
      startTag = `vue-perf-start:${vm._uid}`
      endTag = `vue-perf-end:${vm._uid}`
      mark(startTag)
    }

可以看到initMixin方法就是往vue的原型上掛載了一個_init方法,其他的Mixin也是同理,都是往vue的原型上掛載各種方法,而最開始建立vue類時通過es5 function的方式建立也是為了後面可以更加靈活操作,可以將方法寫入到各個js檔案,不用一次寫在一個下面,更加方便程式碼後期的維護,這個也是選擇es5建立的原因。

當呼叫new Vue的時候,事實上就呼叫的Vue原型上的_init方法.

vue 初始化主要就幹了幾件事情,合併配置,初始化生命週期,初始化事件中心,初始化渲染,初始化 data、props、computed、watcher 等等

Vue的雙向繫結原理

從index.js入口分析後,越往裡發現各個檔案之間的引用理不亂剪還亂,於是乎從原來的看原始碼變成模仿著寫雛形,這種方式可能會理解的更加深刻一些,和大家共勉。

vue中的雙向資料是通過資料劫持(Object.defineProperty())結合釋出者-訂閱者模式來實現的,Object.defineProperty()方法會直接在一個物件上定義一個新屬性,或者修改一個已經存在的屬性, 並返回這個物件。

Object.defineProperty()使用

做個小案例,定義一個svue.js檔案,定義一個book物件,並賦值輸出


var Book = {
    name: 'vue權威指南'
  };
console.log(Book.name);  // vue權威指南

得到的結果就是“vue權威指南”,如果想要在執行console.log(book.name)的同時,直接給書名加個書名號怎麼做?可以使用Object.defineProperty()來完成,修改後的程式碼


var Book = {}
var name = '';
Object.defineProperty(Book, 'name', {
  set: function (value) {
    name = value;
    console.log('你取了一個書名叫做' + value);
  },
  get: function () {
    return '《' + name + '》'
  }
})
 
Book.name = 'vue權威指南';  // 你取了一個書名叫做vue權威指南
console.log(Book.name);  // 《vue權威指南》

通過Object.defineProperty()對Book物件的name屬性的get和set進行了重寫操作,當訪問name屬性時會觸發get執行。

動手模擬寫資料雙向繫結

實現mvvm主要包含兩個方面,資料變化更新檢視,檢視變化更新資料。分為3個步驟來做:

(1) 實現一個監聽器Observer,用來劫持並監聽所有屬性,如果有變動的,就通知訂閱者
Observer是一個數據監聽器,其實現核心方法就是前文所說的Object.defineProperty( )。如果要對所有屬性都進行監聽的話,那麼可以通過遞迴方法遍歷所有屬性值,並對其進行Object.defineProperty( )處理。

對應到原始碼的目錄是:/vue-dev/src/core/observer/index.js


function defineReactive(data, key, val) {
    observe(val); // 遞迴遍歷所有子屬性
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            return val;
        },
        set: function(newVal) {
            val = newVal;
            console.log('屬性' + key + '已經被監聽了,現在值為:“' + newVal.toString() + '”');
        }
    });
}
 
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};
 
var library = {
    book1: {
        name: ''
    },
    book2: ''
};
observe(library);
library.book1.name = 'vue權威指南'; // 屬性name已經被監聽了,現在值為:“vue權威指南”
library.book2 = '沒有此書籍';  // 屬性book2已經被監聽了,現在值為:“沒有此書籍”

因為訂閱者是有很多個,所以我們需要有一個訊息訂閱器Dep來專門收集這些訂閱者,然後在監聽器Observer和訂閱者Watcher之間進行統一管理的,所以要修改下程式碼


function defineReactive(data, key, val) {
    observe(val); // 遞迴遍歷所有子屬性
    var dep = new Dep(); 
    Object.defineProperty(data, key, {
        enumerable: true,
        configurable: true,
        get: function() {
            if (Dep.target) { //是否需要新增訂閱者(Dep.target後類中加入)
                dep.addSub(Dep.target); // 在這裡新增一個訂閱者
            }
            return val;
        },
        set: function(newVal) {
            if (val === newVal) {
                return;
            }
            val = newVal;
            console.log('屬性' + key + '已經被監聽了,現在值為:“' + newVal.toString() + '”');
            dep.notify(); // 如果資料變化,通知所有訂閱者
        }
    });
}
function observe(data) {
    if (!data || typeof data !== 'object') {
        return;
    }
    Object.keys(data).forEach(function(key) {
        defineReactive(data, key, data[key]);
    });
};
 
function Dep () {
    this.subs = []; //訂閱者的list
}
Dep.prototype = {
    addSub: function(sub) {
        this.subs.push(sub);
    },
    notify: function() {
        this.subs.forEach(function(sub) {
            sub.update();
        });
    }
};

在setter函式裡面,如果資料變化,就會去通知所有訂閱者,訂閱者們就會去執行對應的更新的函式
(2) 實現一個訂閱者Watcher,可以收到屬性的變化通知並執行相應的函式,從而更新檢視


function Watcher(vm, exp, cb) {
    this.cb = cb;
    this.vm = vm;
    this.exp = exp;
    this.value = this.get();  // 將自己新增到訂閱器的操作
}
 
Watcher.prototype = {
    update: function() {
        this.run();
    },
    run: function() {
        var value = this.vm.data[this.exp];
        var oldVal = this.value;
        if (value !== oldVal) {
            this.value = value;
            this.cb.call(this.vm, value, oldVal);
        }
    },
    get: function() {
        Dep.target = this;  // 快取自己
        var value = this.vm.data[this.exp]  // 強制執行監聽器裡的get函式
        Dep.target = null;  // 釋放自己
        return value;
    }
};

簡單版的Watcher設計完畢,這時候只要將Observer和Watcher關聯起來,就可以實現一個簡單的雙向繫結資料了,定義index.js


function SelfVue (data, el, exp) {
    this.data = data; //傳遞進來的{}物件資料
    observe(data);//監聽
    el.innerHTML = this.data[exp];  // 初始化模板資料的值 this.data[name]
    //訂閱者 function更新會在watcher.js中回撥
    new Watcher(this, exp, function (value) {
        el.innerHTML = value;
    });
    return this;
}

定義index.html引入以上3個js檔案進行測試


<!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>Document</title>
</head>
<body>
    <h1 id="name">{{name}}</h1>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
    var ele = document.querySelector('#name');
    var selfVue = new SelfVue({
        name: 'hello world'
    }, ele, 'name');
 
    window.setTimeout(function () {
        console.log('name值改變了');
        selfVue.data.name = '66666666';
    }, 2000);
 
</script>
</html>

(3) 實現一個解析器Compile,可以掃描和解析每個節點的相關指令,並根據初始化模板資料以及初始化相應的訂閱器
雖然上面已經實現了一個雙向資料繫結的例子,但是整個過程都沒有去解析dom節點,而是直接固定某個節點進行替換資料的,所以接下來需要實現一個解析器Compile來做解析和繫結工作


function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    init: function () {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },
    nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;
        while (child) {
            // 將Dom元素移入fragment中
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    compileElement: function (el) {
        var childNodes = el.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;

            if (self.isTextNode(node) && reg.test(text)) {  // 判斷是否是符合這種形式{{}}的指令
                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);  // 繼續遞迴遍歷子節點
            }
        });
    },
    compileText: function(node, exp) {
        var self = this;
        var initText = this.vm[exp];
        this.updateText(node, initText);  // 將初始化的資料初始化到檢視中
        new Watcher(this.vm, exp, function (value) { // 生成訂閱器並繫結更新函式
            self.updateText(node, value);
        });
    },
    updateText: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
}

修改index.js


function SelfVue (options) {
    var self = this;
    this.vm = this;
    this.data = options.data;

    Object.keys(this.data).forEach(function(key) {
        self.proxyKeys(key);
    });

    observe(this.data);
    new Compile(options.el, this.vm);
    return this;
}

SelfVue.prototype = {
    proxyKeys: function (key) {
        var self = this;
        Object.defineProperty(this, key, {
            enumerable: false,
            configurable: true,
            get: function proxyGetter() {
                return self.data[key];
            },
            set: function proxySetter(newVal) {
                self.data[key] = newVal;
            }
        });
    }
}

修改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>Document</title>
</head>
<body>
    <div id="app">
        <h2>{{title}}</h2>
        <h1>{{name}}</h1>
    </div>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
    var selfVue = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: ''
        }
    });
 
    window.setTimeout(function () {
        selfVue.title = '你好';
    }, 2000);
 
    window.setTimeout(function () {
        selfVue.name = 'canfoo';
    }, 2500);
 
</script>
</html>

現在已經可以解析出{{}}的內容,如果想要支援更多的指令,繼續完善compile.js


function Compile(el, vm) {
    this.vm = vm;
    this.el = document.querySelector(el);
    this.fragment = null;
    this.init();
}

Compile.prototype = {
    init: function () {
        if (this.el) {
            this.fragment = this.nodeToFragment(this.el);
            this.compileElement(this.fragment);
            this.el.appendChild(this.fragment);
        } else {
            console.log('Dom元素不存在');
        }
    },
    nodeToFragment: function (el) {
        var fragment = document.createDocumentFragment();
        var child = el.firstChild;
        while (child) {
            // 將Dom元素移入fragment中
            fragment.appendChild(child);
            child = el.firstChild
        }
        return fragment;
    },
    compileElement: function (el) {
        var childNodes = el.childNodes;
        var self = this;
        [].slice.call(childNodes).forEach(function(node) {
            var reg = /\{\{(.*)\}\}/;
            var text = node.textContent;

            if (self.isElementNode(node)) {  
                self.compile(node);
            } else if (self.isTextNode(node) && reg.test(text)) {
                self.compileText(node, reg.exec(text)[1]);
            }

            if (node.childNodes && node.childNodes.length) {
                self.compileElement(node);
            }
        });
    },
    compile: function(node) {
        var nodeAttrs = node.attributes;
        var self = this;
        Array.prototype.forEach.call(nodeAttrs, function(attr) {
            var attrName = attr.name;
            if (self.isDirective(attrName)) {
                var exp = attr.value;
                var dir = attrName.substring(2);
                if (self.isEventDirective(dir)) {  // 事件指令
                    self.compileEvent(node, self.vm, exp, dir);
                } else {  // v-model 指令
                    self.compileModel(node, self.vm, exp, dir);
                }
                node.removeAttribute(attrName);
            }
        });
    },
    compileText: function(node, exp) {
        var self = this;
        var initText = this.vm[exp];
        this.updateText(node, initText);
        new Watcher(this.vm, exp, function (value) {
            self.updateText(node, value);
        });
    },
    compileEvent: function (node, vm, exp, dir) {
        var eventType = dir.split(':')[1];
        var cb = vm.methods && vm.methods[exp];

        if (eventType && cb) {
            node.addEventListener(eventType, cb.bind(vm), false);
        }
    },
    compileModel: function (node, vm, exp, dir) {
        var self = this;
        var val = this.vm[exp];
        this.modelUpdater(node, val);
        new Watcher(this.vm, exp, function (value) {
            self.modelUpdater(node, value);
        });

        node.addEventListener('input', function(e) {
            var newValue = e.target.value;
            if (val === newValue) {
                return;
            }
            self.vm[exp] = newValue;
            val = newValue;
        });
    },
    updateText: function (node, value) {
        node.textContent = typeof value == 'undefined' ? '' : value;
    },
    modelUpdater: function(node, value, oldValue) {
        node.value = typeof value == 'undefined' ? '' : value;
    },
    isDirective: function(attr) {
        return attr.indexOf('v-') == 0;
    },
    isEventDirective: function(dir) {
        return dir.indexOf('on:') === 0;
    },
    isElementNode: function (node) {
        return node.nodeType == 1;
    },
    isTextNode: function(node) {
        return node.nodeType == 3;
    }
}

修改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>Document</title>
</head>
<body>
    <div id="app">
        <h2>{{title}}</h2>
        <input v-model="name">
        <h1>{{name}}</h1>
    </div>
</body>
<script src="observer.js"></script>
<script src="watcher.js"></script>
<script src="compile.js"></script>
<script src="index.js"></script>
<script type="text/javascript">
    var selfVue = new SelfVue({
        el: '#app',
        data: {
            title: 'hello world',
            name: ''
        }
    });
 
</script>
</html>

就能看到v-model的效果了

未完待續

來源:https://segmentfault.com/a/1190000015846104