Vue2響應式原理與實現

1、Vue的初始化

Vue本質上是一個暴露在全局的名爲Vue的函數,在使用的時候經過new這個Vue函數來建立一個Vue實例,而且會傳入一個配置對象
Vue函數內須要作的事情就是根據傳入的配置對象進行初始化。如:數組

// src/index.js
function Vue(options) {
    this._init(options);
}

這裏經過this調用了_init()方法,這個this就是建立的Vue實例對象,可是目前Vue實例上並無這個_init()方法,因此咱們須要給Vue的prototype上添加一個_init()方法。爲了方便模塊管理,咱們須要專門建一個單獨的init.js用於作初始化的工做。init.js中須要暴露一個initMix()方法,該方法接收Vue以便在Vue的prototype上添加原型方法,如:app

// src/init.js
export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        // 這裏進行Vue的初始化工做
    }
}
// src/index.js
import {initMixin} from "./init";
function Vue(options) {
    this._init(options);
}
initMixin(Vue); // 傳入Vue以便在其prototype上添加_init()方法

此時_init()方法就能拿到用戶傳入的options配置對象,而後開始進行初始化工做:
將options對象掛載到Vue實例的\$options屬性上;
初始化狀態數據;
判斷用戶有沒有傳el屬性,若是傳了則主動進行調用\$mount()方法進行掛載,若是沒有傳,那麼須要用戶本身調用\$mount()方法進行掛載。函數

// src/init.js
export function initMixin(Vue) {
    Vue.prototype._init = function(options) {
        const vm = this;
        vm.$options = options; // 將options掛載到Vue實例的$options屬性上
        // beforeCreate 這裏執行Vue的beforeCreate生命週期
        initState(vm); // 進行狀態的初始化
        // created 這裏執行Vue的created生命週期
        if (options.el) { // 若是配置了el對象,那麼就要進行mount
            vm.$mount(options.el); // 主動調用$mount()進行掛載
        }
    }
    Vue.prototype.$mount = function(el) {
        // 這裏進行掛載操做
    }
}

2、Vue狀態數據的初始化

接下來就是進行狀態的初始化,即實現initState()方法,狀態的初始化是一個獨立複雜的過程,咱們須要將其單獨放到一個state.js中進行,主要就是根據options中配置的屬性進行特定的初始化操做,如:工具

export function initState(vm) {
    const options = vm.$options;
    if (options.data) { // 若是配置了data屬性
        initData(vm);
    }
    if (options.computed) { // 若是配置了計算屬性
        initComputed(vm);
    }
    if (options.watch) { // 若是配置了用戶的watch
        initWatch(vm);
    }
}
function initData(vm) { 
    // 這裏進行data屬性的初始化
}

function initComputed(vm) {
    // 這裏進行computed計算屬性的初始化
}
function initWatch(vm) {
    // 這裏進行用戶watch的初始化
}

3、將data數據變成響應式的

data屬性的初始化是Vue響應式系統的核心,即對data對象中的每個屬性進行觀測監控。用戶傳入的data多是一個對象也多是一個返回對象的函數。因此須要對data的類型進行判斷,若是是函數,那麼傳入Vue實例並執行這個函數拿到返回的對象做爲用於觀測的data。同時爲了方便Vue實例操做data中的數據,還須要將data中的屬性一必定義到Vue實例上,如:性能

// src/state.js 實現initData()方法
import {proxy} from "./utils/index";
import {observe} from "./observer/index";
function initData(vm) {
    let data = vm.$options.data; // 多是一個函數
    // 給Vue實例添加一個_data屬性和$data屬性保存用戶的data
    data = vm._data = vm.$data = typeof data === "function" ? data.call(vm) : data;
    for (let key in data) { // 遍歷data的全部屬性
        proxy(vm, "_data", key); // 將data中的屬性代理到Vue實例上,方便操做data
    }
    observe(data); // 對數據進行觀察
}

上面用到了一個proxy工具方法,用於將data中的屬性代理到Vue實例上,其內部主要就是經過Object.defineProperty()方法,將data中的屬性代理到Vue實例上,如:this

// src/utils/index.js
export function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[source][key];
        },
        set(newVal) {
            vm[source][key] = newVal;
        }
    });
}

這裏的vm[source]就是vm._data對象也就是用戶傳入的data,這樣當用戶經過Vue實例去操做數據的時候,實際上操做的就是用戶傳入的data對象prototype

接着就是對整個data數據進行觀測了,進行數據觀測的時候,這個數據必須是對象或者數組,不然不進行觀測,若是這個對象中某個key的屬性值也未對象,那麼也須要對其進行觀測,因此這裏會存在一個遞歸操做,這也是影響Vue性能的重要緣由。數據觀測也是一個獨立複雜的過程,須要對其單獨管理,如:代理

// src/observer/index.js
import {isObject} from "../utils/index";
export function observe(data) {
    if (!isObject(data)) { // 僅觀察對象和數組
        return;
    }
    // 若是要觀測的數據是一個對象或者數組,那麼給其建立一個Observer對象
    return new Observer(data);
}
// src/utils/index.js
export function isObject(data) {
    return data && typeof data === "object";
}

接下來Vue會給符合對象或者數組的data進行觀測,給其建立一個Observer對象,觀測的時候,對象和數組的處理會有所不一樣,對於對象而言,遍歷對象中的每一個屬性並將其定義成響應式便可;對於數組而言,因爲數組可能存在很是多項,爲了不性能影響,不是將數組的全部索引定義成響應式的,而是對數組中屬於對象或者數組的元素進行觀測code

// src/observer/index.js
class Observer {
    constructor(data) {
        this.data = data;
        def(data, "__ob__", this); // 給每一個被觀察的對象添加一個__ob__屬性,若是是數組,那麼這個數組也會有一個__ob__屬性
        if (Array.isArray(data)) { // 對數組進行觀測
            data.__proto__ = arrayMethods; // 重寫數組方法
            this.observeArray(data); // 遍歷數組中的每一項值進行觀測
        } else {
            this.walk(data); // 對對象進行觀測
        }
    }
    walk(data) {
        for (let key in data) { // 遍歷對象中的全部key,並定義成響應式的數據
            defineReactive(data, key, data[key]);
        }
    }
    observeArray(arr) {
        arr && arr.forEach((item) => {
            observe(item); // 對數組中的每一項進行觀測
        }
    }
}
function defineReactive(data, key, value) {
    let ob = observe(value); // 對傳入的對象的屬性值進行遞歸觀測
    Object.defineProperty(data, key, {
        get() {
            return value;
        },
        set(newVal) {
            if (newVal === value) {
                return;
            }
            observe(newVal); // 若是用戶修改了值,那麼也要對用戶傳入的新值觀測一下,由於可能傳入的是一個對象或者數組,對新值修改的時候才能檢測到
            value = newVal;
        }
    })
}

對數組的觀測,主要就是要從新那些會改變原數組的方法,如: pushpopshiftunshiftsortreversesplice以便數組發生變化後可以給觀察者發送通知,而且push、unshift、splice會給數組新增元素,咱們還須要知道新增的是什麼數據,須要對這些新增的數據進行觀測。server

const arrayProto = Array.prototype; // 獲取數組的原型對象
export const arrayMethods = Object.create(arrayProto); // 根據數組的原型對象建立一個新的原型對象,避免方法無限循環執行
const methods = [
    "push",
    "pop",
    "shift",
    "unshift",
    "splice",
    "sort",
    "reverse"
];

methods.forEach((method) => {
    arrayMethods[method] = function(...args) {
        const result = arrayProto[method].apply(this, args); // 執行數組的上本來的方法
        const ob = this.__ob__;
        let inserted; // 用於記錄用戶給數組插入的新元素
        switch(method) {
            case "push":
            case "unshift":
                inserted = args;
                break;
            case "splice":
                inserted = args.slice(2);// 對應splice第三個參數纔是用戶要插入的數據
                break;
            default:
                console.log("攔截的方法不存在");
        }
        if (inserted) { // 數組方法內,惟一能拿到的就是數組這個數據,因此咱們須要給觀察的數組對象添加一個key,值爲Observer對象,才能拿到Observer對象上的方法
            ob.observeArray(inserted); // 對插入的新元素進行觀測
        }
        return result;
    }
});
相關文章
相關標籤/搜索