美文网首页
Vue响应式原理(Object.defineProperty)全

Vue响应式原理(Object.defineProperty)全

作者: zpkzpk | 来源:发表于2019-03-16 09:16 被阅读0次

大致流程

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

完~

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

相关文章

  • 深入 Vue3 源码,学习响应式原理

    Vue2 响应式原理 学过 Vue2 的话应该知道响应式原理是由 Object.defineProperty 对数...

  • Vue响应式基本原理

    Vue响应式系统基本原理 Object.defineProperty Object.defineProperty(...

  • Vue 数据双向绑定

    1.响应式原理 Vue的响应式原理依赖于Object.defineProperty,这也是Vue不支持IE8 以及...

  • 双向绑定

    数据响应式原理 vue实现数据响应式的原理就是利用了Object.defineProperty(),重新定义了对象...

  • Vue原理学习(二)

    响应式系统的基本原理 Vue基于Object.defineProperty来实现响应式,对于Object.defi...

  • js实用技巧

    vue相关 vue2.x的响应式 实现原理 。对象类型:通过Object.defineProperty()对属性的...

  • Vue原理学习(三)

    响应式依赖收集原理 在Vue原理学习 (二)中,介绍Object.defineProperty中的[[Getter...

  • Vue 2.0响应式原理

    Vue 2.0响应式原理:首先把data里的所有属性都用Object.defineProperty定义getter...

  • vue响应式原理:从defineReactive学习Object

    众所周知,vue响应式原理就是用的Object.defineProperty方法去定义setter和getter的...

  • Vue3新特性笔记

    vue3.0的主要变化 响应式基本原理:Object.defineProperty -> Proxy,提高性能 初...

网友评论

      本文标题:Vue响应式原理(Object.defineProperty)全

      本文链接:https://www.haomeiwen.com/subject/vzqdmqtx.html