Vue響應(yīng)式原理(Object.defineProperty)全過(guò)程解析

大致流程

  1. 發(fā)生在beforeCreate和created之間initState(vm)中的defineProperty
  2. 發(fā)生在beforeMount和mounted之間的Dep和Watcher的初始化
  3. 發(fā)生在beforeUpdate前到updated觸發(fā)厘托,這期間Watcher的相關(guān)變化

第一步:數(shù)據(jù)初始化

在new一個(gè)Vue實(shí)例時(shí)澄干,其實(shí)只執(zhí)行了一個(gè)this._init(options)

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

在_init方法中实辑,包含以下這些操作,其中initState包含對(duì)data和props的處理過(guò)程;initData包含了對(duì)data創(chuàng)建觀察者的observe函數(shù)

Vue.prototype._init = function (options) {
    ...
    initLifecycle(vm);
    initEvents(vm);
    initRender(vm);
    callHook(vm, 'beforeCreate');
    initInjections(vm); 
    initState(vm);
    initProvide(vm);
    callHook(vm, 'created');
    ...
}
function initState (vm) {
    ...
    if (opts.data) {
        initData(vm);
    } else {
        observe(vm._data = {}, true);
    ...
}
function initData (vm) {
    var data = vm.$options.data;
    data = vm._data = typeof data === 'function' // 這行代碼解釋了平時(shí)為啥data為什么支持函數(shù)式的
        ? getData(data, vm)
        : data || {};
    ...
    proxy(vm, "_data", key);// 將data綁定到vue的this上
    ...
    observe(data, true);
}

這里observe(data)會(huì)return一個(gè)Observe類(lèi)的實(shí)例

function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
        return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
        ob = value.__ob__;
    } else if (
        shouldObserve &&
        !isServerRendering() &&
        (Array.isArray(value) || isPlainObject(value)) &&
        Object.isExtensible(value) &&
        !value._isVue
    ) {
        ob = new Observer(value);
    }
    if (asRootData && ob) {
        ob.vmCount++;
    }
    return ob
}

Observe類(lèi)將傳進(jìn)來(lái)的參數(shù)進(jìn)行遞歸調(diào)用杨拐,最終都會(huì)調(diào)用this.walk

var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
        if (hasProto) {
            protoAugment(value, arrayMethods);
        } else {
            copyAugment(value, arrayMethods, arrayKeys);
        }
        this.observeArray(value);
    } else {
        this.walk(value);
    }
};

Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
        defineReactive$$1(obj, keys[i]);
    }
};

終于看見(jiàn)和definePropoty長(zhǎng)得差不多的defineReactive棕所,其實(shí)defineReactive就是創(chuàng)建響應(yīng)式對(duì)象,是對(duì)definePropoty的一層封裝辨宠,到這里響應(yīng)式數(shù)據(jù)的初始化就算完成了遗锣,完整代碼如下:

function defineReactive$$1 (
    obj,
    key,
    val,
    customSetter,
    shallow
) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
        return
    }

    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();
                if (childOb) {
                    childOb.dep.depend();
                    if (Array.isArray(value)) {
                        dependArray(value);
                    }
                }
            }
            return value
        },
        set: function reactiveSetter (newVal) {
            var value = getter ? getter.call(obj) : val;
            if (newVal === value || (newVal !== newVal && value !== value)) {
                return
            }
        
            if (process.env.NODE_ENV !== 'production' && customSetter) {
                customSetter();
            }
      
            if (getter && !setter) { return }
            if (setter) {
                setter.call(obj, newVal);
            } else {
                val = newVal;
            }
            childOb = !shallow && observe(newVal);
                dep.notify();
            }
      });
}

第二步:建立依賴(lài)

建立依賴(lài)其實(shí)就是觸發(fā)Object.defineProperty中定義get的一個(gè)過(guò)程,我們都知道get是在獲取對(duì)象值的時(shí)候觸發(fā)的函數(shù)嗤形,在vue運(yùn)行過(guò)程中精偿,get的觸發(fā)是在beforeMount和mounted這兩個(gè)聲明周期之間,這里就不去羅列模板解析過(guò)程了赋兵,大致就是一個(gè)template => AST => render函數(shù) => Vnode => DOM的過(guò)程笔咽,這里接著最上面created聲明周期后的部分進(jìn)行,執(zhí)行了$mount

Vue.prototype._init = function (options) {
    initState(vm);
    initProvide(vm); 
    callHook(vm, 'created');

    if (vm.$options.el) {
      vm.$mount(vm.$options.el);
    }
}

Vue.prototype.$mount = function (
    el,
    hydrating
) {
    el = el && inBrowser ? query(el) : undefined;
    return mountComponent(this, el, hydrating)
};

function mountComponent (
    vm,
    el,
    hydrating
) {
    vm.$el = el;
    ...
    callHook(vm, 'beforeMount');
    ...
    // 這里有一段updateComponent的定義
    ...
    new Watcher(vm, updateComponent, noop, {
        before: function before () {
            if (vm._isMounted && !vm._isDestroyed) {
                callHook(vm, 'beforeUpdate');
            }
        }
    }, true );
    ...
    if (vm.$vnode == null) {
        vm._isMounted = true;
        callHook(vm, 'mounted');
    }
    return vm
}

從上圖可以看出beforeMount和mounted之間其實(shí)就定義了一個(gè)名為updateComponent(它是Watcher里的一個(gè)回調(diào)霹期,發(fā)生在Watcher的get中)叶组,然后new了一個(gè)Watcher。
這里主要講講Dep和Watcher历造,先介紹Watcher和Watcher是如何作為target定義到Dep上的:

var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
) {
    this.vm = vm;
    if (isRenderWatcher) {
        vm._watcher = this;
    }
    vm._watchers.push(this);
  
    if (options) {
        // mounted階段new的那個(gè)Watcher里只有before字段甩十,其他初始化全都是false
        this.deep = !!options.deep;
        this.user = !!options.user;
        this.lazy = !!options.lazy;
        this.sync = !!options.sync;
        this.before = options.before;
    } else {
        this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = process.env.NODE_ENV !== 'production'
        ? expOrFn.toString()
        : '';
  
    if (typeof expOrFn === 'function') {
        this.getter = expOrFn;
    } else {
        this.getter = parsePath(expOrFn);
        if (!this.getter) {
            this.getter = noop;
            process.env.NODE_ENV !== 'production' && warn(
            "Failed watching path: \"" + expOrFn + "\" " +
            'Watcher only accepts simple dot-delimited paths. ' +
            'For full control, use a function instead.',
            vm
            );
        }
    }
    this.value = this.lazy
        ? undefined
        : this.get();
};

上面的最后調(diào)用了this.get()船庇,在get()函數(shù)里利用pushTarget把Watcher 作為 target 定義在了Dep上,并且執(zhí)行了this.getter.call(vm, vm);這里的getter是Watcher構(gòu)造函數(shù)的第二個(gè)參數(shù)expOrFn侣监,內(nèi)容為vm._update(vm._render(), hydrating)鸭轮,也就是觸發(fā)了頁(yè)面的渲染

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

Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
        value = this.getter.call(vm, vm);
    } catch (e) {
        if (this.user) {
            handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
        } else {
            throw e
        }
    } finally {
    
        if (this.deep) {
            traverse(value);
        }
        popTarget();
        this.cleanupDeps();
    }
    return value
};

在vm._update的渲染過(guò)程中,因?yàn)橐昧薲ata中的數(shù)據(jù)橄霉,所以會(huì)觸發(fā)第一階段中defineProperty為data內(nèi)數(shù)據(jù)設(shè)置的get函數(shù)窃爷,代碼如下:如果Dep.target存在會(huì)調(diào)用dep.depend()(Dep.target其實(shí)是一個(gè)Watcher)

function defineReactive$$1 (){
    Object.defineProperty(obj, key, {
        ...
        var dep = new Dep();
        ...
        get: function reactiveGetter () {
            var value = getter ? getter.call(obj) : val;
            if (Dep.target) {
                dep.depend();
                ...
            }
        }
    }
}

再看看Dep類(lèi),在defineReactive中會(huì)new一個(gè)Dep的實(shí)例酪劫,這里subs是一個(gè)裝watcher的數(shù)組吞鸭,一般在不自定義watch的前提下,這個(gè)數(shù)組里都只有一個(gè)Watcher

var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
};

Dep.prototype.depend = function depend () {
    if (Dep.target) {
        Dep.target.addDep(this); // 根據(jù)上面的描述這里Dep.target就是Watcher
    }
};

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

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

忽視掉和響應(yīng)式數(shù)據(jù)無(wú)關(guān)的部分覆糟,到這里基本就是mount結(jié)束的地方了刻剥,總結(jié)下都干了什么,觸發(fā)beforeMount生命周期滩字,new了一個(gè)Watcher對(duì)象造虏,渲染模板,觸發(fā)數(shù)據(jù)的get初始化麦箍,對(duì)每個(gè)響應(yīng)式數(shù)據(jù)的Dep實(shí)例進(jìn)行依賴(lài)收集漓藕,然后觸發(fā)Mounted生命周期。

第三步:派發(fā)更新

當(dāng)有響應(yīng)式的數(shù)據(jù)被改變時(shí)挟裂,觸發(fā)set函數(shù)享钞,調(diào)用dep.notify()

set: function reactiveSetter (newVal) {
    var value = getter ? getter.call(obj) : val;
    if (newVal === value || (newVal !== newVal && value !== value)) {
        return
    }
    if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter();
    }
    if (getter && !setter) { return }
    if (setter) {
        setter.call(obj, newVal);
    } else {
        val = newVal;
    }
    childOb = !shallow && observe(newVal);

    dep.notify();
}

這里subs就是一個(gè)裝Watcher的數(shù)組(在沒(méi)有綁定自定義Watcher的簡(jiǎn)單的Vue對(duì)象中,這個(gè)數(shù)組的長(zhǎng)度是1)诀蓉,所以就等于是調(diào)用了當(dāng)前vue對(duì)象對(duì)應(yīng)Watcher的update()

Dep.prototype.notify = function notify () {
    var subs = this.subs.slice();
    if (process.env.NODE_ENV !== 'production' && !config.async) {
        subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
        subs[i].update();
    }
};

Watcher的update()會(huì)對(duì)根據(jù)Watcher初始化傳入的option中sync字段進(jìn)行一個(gè)判斷栗竖,如果是true的直接觸發(fā)run(),如果不是會(huì)進(jìn)行一個(gè)隊(duì)列的操作渠啤。因?yàn)槲覀冊(cè)?mount過(guò)程中new Watcher時(shí)傳的option只有before字段狐肢,所以其他lazy,sync等字段都是false沥曹,所以這里會(huì)產(chǎn)生一個(gè)隊(duì)列份名,用于存放Watcher

Watcher.prototype.update = function update () {
    if (this.lazy) { //這里是false
        this.dirty = true;
    } else if (this.sync) { // 這里是false
        this.run();
    } else {
        queueWatcher(this);
    }
};

這個(gè)隊(duì)列會(huì)先判斷之前是否添加過(guò)這個(gè)watcher,如果沒(méi)有則添加妓美,并會(huì)有一個(gè)針對(duì)id的排序插入

function queueWatcher (watcher) {
    var id = watcher.id;
    if (has[id] == null) {
        has[id] = true;
        if (!flushing) {
            queue.push(watcher);
        } else {
     
            var i = queue.length - 1;
            while (i > index && queue[i].id > watcher.id) {
                i--;
            }
            queue.splice(i + 1, 0, watcher);
        }
        
        if (!waiting) {
            waiting = true;

            if (process.env.NODE_ENV !== 'production' && !config.async) {
                flushSchedulerQueue();
                return
            }
            nextTick(flushSchedulerQueue);
        }
    }
}

flushSchedulerQueue僵腺,首先會(huì)對(duì)隊(duì)列中的Watcher進(jìn)行排序,然后觸發(fā)option中的before壶栋,也就是beforeUpdate的生命周期函數(shù)想邦,然后執(zhí)行Watcher.run()

function flushSchedulerQueue () {
    queue.sort(function (a, b) { return a.id - b.id; });
    ...
    for (index = 0; index < queue.length; index++) {
        watcher = queue[index];
        if (watcher.before) {
            watcher.before();
        }
        /**
            before () {
                if (vm._isMounted && !vm._isDestroyed) {
                    callHook(vm, 'beforeUpdate');
                }
            }  
        **/
        id = watcher.id;
        has[id] = null;
        watcher.run();
    }
    ...
    // 下面就不介紹了。委刘。丧没。
    // 隊(duì)列的備份
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();
    // 隊(duì)列的初始化
    resetSchedulerState();
    // 觸發(fā)activated和updated的生命周期函數(shù)
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);
}

run的時(shí)候觸發(fā)get(),會(huì)和首次mount過(guò)程類(lèi)似锡移,多了patch的過(guò)程呕童,其中涉及著名的Diff算法,用于渲染頁(yè)面淆珊,從而更新頁(yè)面夺饲,并建立新的依賴(lài)關(guān)系

Watcher.prototype.run = function run () {
    if (this.active) {
        var value = this.get();
        if (
            value !== this.value ||
            // Deep watchers and watchers on Object/Arrays should fire even
            // when the value is the same, because the value may
            // have mutated.
            isObject(value) ||
            this.deep
        ) {
            // set new value
            var oldValue = this.value;
            this.value = value;
            if (this.user) {
                try {
                    this.cb.call(this.vm, value, oldValue);
                } catch (e) {
                    handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
                }
            } else {
                this.cb.call(this.vm, value, oldValue);
            }
        }
    }
};

完~

大致流程就是這樣了,似乎寫(xiě)的有點(diǎn)亂施符,如有問(wèn)題歡迎大佬們指正

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末往声,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子戳吝,更是在濱河造成了極大的恐慌浩销,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,639評(píng)論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件听哭,死亡現(xiàn)場(chǎng)離奇詭異慢洋,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)陆盘,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,277評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門(mén)普筹,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人隘马,你說(shuō)我怎么就攤上這事太防。” “怎么了酸员?”我有些...
    開(kāi)封第一講書(shū)人閱讀 157,221評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵蜒车,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我沸呐,道長(zhǎng)醇王,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,474評(píng)論 1 283
  • 正文 為了忘掉前任崭添,我火速辦了婚禮寓娩,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘呼渣。我一直安慰自己棘伴,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,570評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布屁置。 她就那樣靜靜地躺著焊夸,像睡著了一般。 火紅的嫁衣襯著肌膚如雪蓝角。 梳的紋絲不亂的頭發(fā)上阱穗,一...
    開(kāi)封第一講書(shū)人閱讀 49,816評(píng)論 1 290
  • 那天饭冬,我揣著相機(jī)與錄音,去河邊找鬼揪阶。 笑死昌抠,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的鲁僚。 我是一名探鬼主播炊苫,決...
    沈念sama閱讀 38,957評(píng)論 3 408
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼冰沙!你這毒婦竟也來(lái)了侨艾?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,718評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤拓挥,失蹤者是張志新(化名)和其女友劉穎唠梨,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體撞叽,經(jīng)...
    沈念sama閱讀 44,176評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡姻成,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,511評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了愿棋。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片科展。...
    茶點(diǎn)故事閱讀 38,646評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖糠雨,靈堂內(nèi)的尸體忽然破棺而出才睹,到底是詐尸還是另有隱情,我是刑警寧澤甘邀,帶...
    沈念sama閱讀 34,322評(píng)論 4 330
  • 正文 年R本政府宣布琅攘,位于F島的核電站,受9級(jí)特大地震影響松邪,放射性物質(zhì)發(fā)生泄漏坞琴。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,934評(píng)論 3 313
  • 文/蒙蒙 一逗抑、第九天 我趴在偏房一處隱蔽的房頂上張望剧辐。 院中可真熱鬧,春花似錦邮府、人聲如沸荧关。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,755評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)忍啤。三九已至,卻和暖如春仙辟,著一層夾襖步出監(jiān)牢的瞬間同波,已是汗流浹背鳄梅。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,987評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留参萄,地道東北人卫枝。 一個(gè)月前我還...
    沈念sama閱讀 46,358評(píng)論 2 360
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像讹挎,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子吆玖,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,514評(píng)論 2 348