Vue响应式原理(Object.defineProperty)全过程解析

大致流程

  1. 发生在beforeCreate和created之间initState(vm)中的defineProperty
  2. 发生在beforeMount和mounted之间的Dep和Watcher的初始化
  3. 发生在beforeUpdate前到updated触发,这期间Watcher的相关变化

第一步:数据初始化

在new一个Vue实例时,其实只执行了一个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包含对data和props的处理过程;initData包含了对data创建观察者的observe函数

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' // 这行代码解释了平时为啥data为什么支持函数式的
        ? getData(data, vm)
        : data || {};
    ...
    proxy(vm, "_data", key);// 将data绑定到vue的this上
    ...
    observe(data, true);
}

这里observe(data)会return一个Observe类的实例

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类将传进来的参数进行递归调用,最终都会调用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]);
    }
};

终于看见和definePropoty长得差不多的defineReactive,其实defineReactive就是创建响应式对象,是对definePropoty的一层封装,到这里响应式数据的初始化就算完成了,完整代码如下:

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

第二步:建立依赖

建立依赖其实就是触发Object.defineProperty中定义get的一个过程,我们都知道get是在获取对象值的时候触发的函数,在vue运行过程中,get的触发是在beforeMount和mounted这两个声明周期之间,这里就不去罗列模板解析过程了,大致就是一个template => AST => render函数 => Vnode => DOM的过程,这里接着最上面created声明周期后的部分进行,执行了$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之间其实就定义了一个名为updateComponent(它是Watcher里的一个回调,发生在Watcher的get中),然后new了一个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的那个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();
};

上面的最后调用了this.get(),在get()函数里利用pushTarget把Watcher 作为 target 定义在了Dep上,并且执行了this.getter.call(vm, vm);这里的getter是Watcher构造函数的第二个参数expOrFn,内容为vm._update(vm._render(), hydrating),也就是触发了页面的渲染

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的渲染过程中,因为引用了data中的数据,所以会触发第一阶段中defineProperty为data内数据设置的get函数,代码如下:如果Dep.target存在会调用dep.depend()(Dep.target其实是一个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类,在defineReactive中会new一个Dep的实例,这里subs是一个装watcher的数组,一般在不自定义watch的前提下,这个数组里都只有一个Watcher

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

Dep.prototype.depend = function depend () {
    if (Dep.target) {
        Dep.target.addDep(this); // 根据上面的描述这里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);
};

忽视掉和响应式数据无关的部分,到这里基本就是mount结束的地方了,总结下都干了什么,触发beforeMount生命周期,new了一个Watcher对象,渲染模板,触发数据的get初始化,对每个响应式数据的Dep实例进行依赖收集,然后触发Mounted生命周期。

第三步:派发更新

当有响应式的数据被改变时,触发set函数,调用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就是一个装Watcher的数组(在没有绑定自定义Watcher的简单的Vue对象中,这个数组的长度是1),所以就等于是调用了当前vue对象对应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()会对根据Watcher初始化传入的option中sync字段进行一个判断,如果是true的直接触发run(),如果不是会进行一个队列的操作。因为我们在$mount过程中new Watcher时传的option只有before字段,所以其他lazy,sync等字段都是false,所以这里会产生一个队列,用于存放Watcher

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

这个队列会先判断之前是否添加过这个watcher,如果没有则添加,并会有一个针对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,首先会对队列中的Watcher进行排序,然后触发option中的before,也就是beforeUpdate的生命周期函数,然后执行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();
    }
    ...
    // 下面就不介绍了。。。
    // 队列的备份
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();
    // 队列的初始化
    resetSchedulerState();
    // 触发activated和updated的生命周期函数
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);
}

run的时候触发get(),会和首次mount过程类似,多了patch的过程,其中涉及著名的Diff算法,用于渲染页面,从而更新页面,并建立新的依赖关系

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

完~

大致流程就是这样了,似乎写的有点乱,如有问题欢迎大佬们指正

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352