vue響應式原理學習(一)

jkCaptain發表於2018-11-25

1.為什麼我們把屬性定義在data、props、methods等引數裡,卻能通過this物件直接訪問呢。

原理:

因為vue內部做了代理。假如我們用this去訪問某個屬性,vue會自動去data,props,methods等引數物件裡面去查詢。所以我們開發時會發現,props裡面定義過的屬性,data不能再定義了,會丟擲警告。methods也一樣。

用過Vue都知道,Vue本身是一個建構函式,所以我們的用法是直接new Vue()。下面我們用程式碼模擬一下Vue內部的代理

(部分程式碼來源:vue專案下 src/core/instance/state.js)

// 定義一個空函式
function noop() {}  
// 定義一個公用的屬性描述物件
const sharedPropertyDefinition = {
    enumerable: true,
    configurable: true,
    get: noop,
    set: noop
}
/**
 * 定義代理函式
 * @target 當前物件
 * @sourceKey 傳入的是來源,也就是代理物件的名稱
 * @key 要訪問的屬性
 */
function proxy(target, sourceKey, key) {
    sharedPropertyDefinition.get = function proxyGetter() {
        // 示例:如果你在data中訪問this.name,那麼此時返回的是 this['_data']['name']
        // target[key] => target[source][key]
        return target[sourceKey][key];
    }
    sharedPropertyDefinition.set = function proxySetter(val) {
        target[sourceKey][key] = val;
    }
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

// 建構函式
function MyVue(options) {
    this._data = options.data || {};
    this._props = options.props || {};
    this._methods = options.methods || {};
    this.init(options);
}
MyVue.prototype.init = function(options) {
    initData(this, options.data);
    initProps(this, options.props);
    iniMethods(this, options.methods);
}

// 相關方法
function initData(vm, dataObj) {
    Object.keys(dataObj).forEach(key => proxy(vm, '_data', key));
}
function initProps(vm, propsObj) {
    Object.keys(propsObj).forEach(key => proxy(vm, '_props', key));
}
function iniMethods(vm, methodsObj) {
    Object.keys(methodsObj).forEach(key => proxy(vm, '_methods', key));
}
複製程式碼

這裡的程式碼主要是示例,並沒有判斷屬性是否重複。

測試程式碼:

let myVm = new MyVue({
    data: {
        name: 'JK',
        age: 25
    },
    props: {
        sex: 'man'
    },
    methods: {
        about() {
            console.log(`my Name is ${this.name}, age is ${this.age}, sex is ${this.sex}`);
        }
    }
});

myVm.name // 'JK'
myVm.age  // 25
myVm.sex  // 'man'
myVm.about()  // my Name is JK, age is 25, sex is man
myVm.age = 24;  
複製程式碼

具體Vue內部的處理是比較複雜的,會判斷很多邊界情況。例如data返回一個函式時需要單獨處理,例如props傳入具有default和type屬性的物件等等。

2. 如何實現一個簡易的資料響應式系統

Vue的資料響應式實現是依賴 Object.defineProperty 這個api的,這也是它不支援IE8且無法hack的原因。

據說Vue3.0改用了ES6 的 ```Proxy``,並使用TypeScript編寫。很是期待。

vue改變data之後做了什麼? 如果要說完整的一套流程,那是很多的,涉及到 watcher,render 渲染函式,VNode,Dom diff 等等。

響應式系統本身是基於觀察者模式的,也可以說是釋出/訂閱模式。 釋出/訂閱模式,就好比是你去找中介租房子。而觀察者模式呢,就好比你直接去城中村找房東租房子。 釋出/訂閱模式比觀察者模式多了個排程中心(中介)。

我這裡只是先說一下怎麼收集依賴,修改了值是怎麼通知的思路。

(部分程式碼來源:vue專案下 src/core/observer/)

丟擲任何其他的因素,我們先實現一個響應式的雛形
// 假如有一個物件是 data
let data = {
    x: 1,
    y: 2
}
// 我們把這個物件變成響應式的
for(const key in data) {
    Object.defineProperty(data, key, {
        get() {
            console.log(`我獲取了data的${key}`);
            return data[key]
        },
        set(val) {
            console.log(`我設定了data的${key}${val}`);
            data[key] = val;
        }
    })
}
複製程式碼

把這個程式碼扔到瀏覽器裡,然後獲取一下data.x,會發現,啊哦,怎麼瀏覽器一直在輸出,為什麼?

因為我在 getreturn data[key],相當於又訪問了一次 data[key], 會一直觸發 get 方法的,造成死迴圈。所以我們等會把程式碼優化下。

接下來,我們在 get 裡收集依賴,set 裡觸發響應

怎麼收集依賴,怎麼觸發響應? 熟悉觀察者模式的同學應該能馬上想到,維護一個陣列,每次觸發 get 都把對應的函式push到這個陣列,每次 set 時將對應的函式觸發。是不是很像我們自定義一個事件系統,當然Vue內部肯定不會這麼簡單。

// 定義一個 watch 函式,作用是拿到改變某個值時對應的處理函式
// Target 是全域性變數, 用於儲存對應的函式
let Target = null
function $watch (exp, fn) {
    // 將 Target 的值設定為 fn
    Target = fn;
    // 讀取欄位值,觸發 get 函式
    data[exp];
}

// dep 在 get 和 set 被閉包引用,不會被回收
// 每一個 key 都有一個屬於自己的 dep
for(const key in data) {
    const dep = [];
    // 優化死迴圈
    let val = data[key];
    Object.defineProperty(data, key, {
        get() {
            console.log(`我獲取了data的${key}`);
            // 收集依賴
            dep.push(Target);
            return val;
        },
        set(newVal) {
            console.log(`我設定了data的${key}${newVal}`);
            if (val === newVal) {
                return ;
            }
            val = newVal;
            // 觸發依賴
            dep.forEach(fn => fn());
        }
    })
}

// 監聽資料變化
$watch('x', () => console.log('x被修改'));    // 輸出 '我獲取了data的x'
data.x = 3;         // 輸出 '我設定了data的x為3', x被修改
複製程式碼

響應式是做好了,但眼尖的同學可能會發現,$watch 函式裡,竟然寫了一個固定的 data[exp],這裡的 data 是我們上一段程式碼定義的變數,在開發中,肯定不可能是固定的呀。所以再優化下, 傳入一個渲染函式,渲染函式內部觸發屬性的 get

全部程式碼:

let data = {
    x: 1,
    y: 2
}

// Target 是全域性變數, 用於儲存對應的函式
let Target = null
function $watch (exp, fn) {
    // 將 Target 的值設定為 fn
    Target = fn;
    // 如果 exp 是函式,直接執行該函式
    if (typeof exp === 'function') {
        exp();
        return;
    }
    // 讀取欄位值,觸發 get 函式
    data[exp];
}

// dep 在 get 和 set 被閉包引用,不會被回收
// 每一個 key 都有一個屬於自己的 dep
for(const key in data) {
    const dep = [];
    // 優化死迴圈
    let val = data[key];
    Object.defineProperty(data, key, {
        get() {
            console.log(`我獲取了data的${key}`);
            // 收集依賴
            dep.push(Target);
            return val;
        },
        set(newVal) {
            console.log(`我設定了data的${key}${newVal}`);
            if (val === newVal) {
                return ;
            }
            val = newVal;
            // 觸發依賴
            dep.forEach(fn => fn());
        }
    })
}

// 測試程式碼
function render () {
    return document.write(`x:${data.x}; y:${data.y}`)
}
$watch(render, render);
複製程式碼

實際上Vue內部的處理是不會這麼簡單的,例如對陣列和物件的區別處理,物件的深度遍歷等,我們這裡都還沒考慮。

還有好多問題要學習:

如何避免重複收集依賴,如何根據template模板的解析並生成渲染函式,AST的實現,v-on,v-bind,v-for等指令的內部解析。

用vue時,push,slice等api改變data時可以觸發資料響應,而直接改資料的下標或length卻不會觸發呢, Vue.$set 內部做了什麼操作,

修改完資料後,內部怎麼觸發渲染對應的dom節點。

參考

Vue技術內幕

相關文章