vue響應式原理學習(一)

1.爲何咱們把屬性定義在data、props、methods等參數裏,卻能經過this對象直接訪問呢。

原理:javascript

由於vue內部作了代理。假如咱們用this去訪問某個屬性,vue會自動去data,props,methods等參數對象裏面去查找。因此咱們開發時會發現,props裏面定義過的屬性,data不能再定義了,會拋出警告。methods也同樣。vue

用過Vue都知道,Vue自己是一個構造函數,因此咱們的用法是直接new Vue()。下面咱們用代碼模擬一下Vue內部的代理java

(部分代碼來源:vue項目下 src/core/instance/state.js)api

// 定義一個空函數
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的緣由。dom

聽說Vue3.0改用了ES6 的 ```Proxy``,並使用TypeScript編寫。非常期待。函數

vue改變data以後作了什麼? 若是要說完整的一套流程,那是不少的,涉及到 watcher,render 渲染函數,VNode,Dom diff 等等。oop

響應式系統自己是基於觀察者模式的,也能夠說是發佈/訂閱模式。 發佈/訂閱模式,就比如是你去找中介租房子。而觀察者模式呢,就比如你直接去城中村找房東租房子。 發佈/訂閱模式比觀察者模式多了個調度中心(中介)。

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

(部分代碼來源: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技術內幕

相關文章
相關標籤/搜索