vue原理探索--响应式系统分分快三计划

作者:分分快三计划
class Vue {
    /* Vue构造类 */
    constructor(options) {
        this._data = options.data;
        observer(this._data);
    }
}
function observer (value) {
    if (!value || (typeof value !== 'object')) {
        return;
    }

    Object.keys(value).forEach((key) => {
        defineReactive(value, key, value[key]);
    });
}
function defineReactive (obj, key, val) {
    Object.defineProperty(obj, key, {
        enumerable: true,       /* 属性可枚举 */
        configurable: true,     /* 属性可被修改或删除 */
        get: function reactiveGetter () {
            return val;         
        },
        set: function reactiveSetter (newVal) {
            if (newVal === val) return;
            cb(newVal);
        }
    });
}
scripts:{ "dev": "rollup -w -c build/config.js --environment TARGET:web-full-dev",}

Observer

下图是Observer类的结构

分分快三计划 1

我们先来看看Observer的实现(大家看代码的时候可以注意下引文的注释,英文的注释是官方的,说的很棒。中文注释是我加的)

/**
 * Attempt to create an observer instance for a value,
 * returns the new observer if successfully observed,
 * or the existing observer if the value already has one.
(observer实例的生成函数,如果数据没有被observe过,那么新建一个observer类并返回,否则直接返回observer类)
 */
function observe (value, asRootData) {
  if (!isObject(value)) {
    return
  }
  var ob;
  //如果存在__ob__属性,说明该对象没被observe过,不是observer类
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__;
  } else if (
    observerState.shouldConvert &&
    !isServerRendering() &&
    (Array.isArray(value) || isPlainObject(value)) &&
    Object.isExtensible(value) &&
    !value._isVue
  ) {
    //如果数据没有被observe过,且数据是array或object类型,那么将数据转化为observer类型,所以observer类接收的是对象和数组。
    ob = new Observer(value);
  }
  //如果是RootData,即咱们在新建Vue实例时,传到data里的值,只有RootData在每次observe的时候,会进行计数。
  vmCount是用来记录此Vue实例被使用的次数的,
  比如,我们有一个组件logo,页面头部和尾部都需要展示logo,都用了这个组件,那么这个时候vmCount就会计数,值为2
  if (asRootData && ob) {
    ob.vmCount  ;
  }
  return ob
}
//下面是oberver类
/**
 * Observer class that are attached to each observed
 * object. Once attached, the observer converts target
 * object's property keys into getter/setters that
 * collect dependencies and dispatches updates.
 */
var Observer = function Observer (value) {
  this.value = value;
  this.dep = new Dep();
  this.vmCount = 0;
  //def是定义的函数,使用Object.defineProperty()给value添加不可枚举的属性,__ob__是一个对象被observe的标志。
    我们在开发的过程中,有时会遇到,数据改变但视图没有更新的问题。
    这个时候,你可以log一下,看看该对象是否有__ob__属性来判断该对象是不是被observe了,如果没有,那么数据改变后视图是不可能更新的。
  def(value, '__ob__', this);
  //数组特殊处理,下面详细讲解
  if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } else {
    //对于对象,遍历对象,并用Object.defineProperty转化为getter/setter,便于监控数据的get和set
    this.walk(value);
  }
};

//遍历对象,调用defineReactive将每个属性转化为getter/setter
/**
 * Walk through each property and convert them into
 * getter/setters. This method should only be called when
 * value type is Object.
 */
Observer.prototype.walk = function walk (obj) {
  var keys = Object.keys(obj);
  for (var i = 0; i < keys.length; i  ) {
    defineReactive$$1(obj, keys[i], obj[keys[i]]);
  }
};

/**
 * Observe a list of Array items.
 */
//observe每个数组元素(observe会生成Observer类)
Observer.prototype.observeArray = function observeArray (items) {
  for (var i = 0, l = items.length; i < l; i  ) {
    observe(items[i]);
  }
};
/**
 * Define a reactive property on an Object.
 */
function defineReactive$$1 (
  obj,
  key,
  val,
  customSetter
) {
  //实例化一个Dep,这个Dep存在在下面的get和set函数的作用域中,用于收集订阅数据更新的Watcher。这里一个Dep与一个属性(即参数里的key)相对应,一个Dep可以有多个订阅者。
  var dep = new Dep();
  var property = Object.getOwnPropertyDescriptor(obj, key);
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  var getter = property && property.get;
  var setter = property && property.set;
  //注意下面这行代码,设置getter/setter之前,会observe该属性对应的值(val)。
  比如此时参数传入的obj是{ objKey: { objValueKey1:{ objValueKey2: objValueValue2 } } },
  key是objKey,
  val是{ objValueKey1:{ objValueKey2: objValueValue2 } },
  那么这个时候{ objValueKey1:{ objValueKey2: objValueValue2 } }对象也会被observe到,在observe该对象的时候,{ objValueKey2: objValueValue2 }也会被observe到。
  以此类推,不管对象的结构有多深都会被observe到。
  var childOb = observe(val);
  //设置getter/setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      //获取属性的值,如果这个属性在转化之前定义过getter,那么调用该getter得到value的值,否则直接返回val。
      var value = getter ? getter.call(obj) : val;
      注意这里,这里是Dep收集订阅者的过程,只有在Dep.target存在的情况下才进行这个操作,在Watcher收集依赖的时候才会设置Dep.target,所以Watcher收集依赖的时机就是Dep收集订阅者的时机。
      调用get的情况有两种,一是Watcher收集依赖的时候(此时Dep收集订阅者),二是模板或js代码里用到这个值,这个时候是不需要收集依赖的,只要返回值就可以了。
      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;
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if ("development" !== 'production' && customSetter) {
        customSetter();
      }
      if (setter) {
        setter.call(obj, newVal);
      } else {
        val = newVal;
      }
      //当为属性设置了新的值,是需要observe的
      childOb = observe(newVal);
      //set的时候数据变化了,通知更新数据
      dep.notify();
    }
  });
}

/**
 * Collect dependencies on array elements when the array is touched, since
 * we cannot intercept array element access like property getters.
 */
function dependArray (value) {
  for (var e = (void 0), i = 0, l = value.length; i < l; i  ) {
    e = value[i];
    //在调用这个函数的时候,数组已经被observe过了,且会递归observe。(看上面defineReactive函数里的这行代码:var childOb = observe(val);)
    所以正常情况下都会存在__ob__属性,这个时候就可以调用dep添加依赖了。
    e && e.__ob__ && e.__ob__.dep.depend();
    if (Array.isArray(e)) {
      dependArray(e);
    }
  }
}

 

global-api/index.js

Watcher

Watcher里面的属性很多,我们下面只注释本文关心的内容

/**
 * A watcher parses an expression, collects dependencies,
 * and fires callback when the expression value changes.
 * This is used for both the $watch() api and directives.
 */
var Watcher = function Watcher (
  vm,
  expOrFn,
  cb,
  options
) {
  this.vm = vm;
  vm._watchers.push(this);
  // options
  if (options) {
    //对应$watch参数的deep,具体的可以参考官网文档:https://cn.vuejs.org/v2/api/#vm-watch
    this.deep = !!options.deep;
    this.user = !!options.user;
    //跟computed相关,这里不具体讲解
    this.lazy = !!options.lazy;
    this.sync = !!options.sync;
  } 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
  //注意这里,关于deps和newDeps下面详细讲解
  this.deps = [];
  this.newDeps = [];
  this.depIds = new _Set();
  this.newDepIds = new _Set();
  this.expression = expOrFn.toString();
  // parse expression for getter
  //这里的getter会有两种情况:
   一、一个函数,比如在生命周期mount的时候,需要watch模板中的值,这个时候传过来的是一个函数,后面在get函数里调用时这个函数时,这个函数会调用数据的getter函数。
   二、一个表达式,比如我们在Vue实例的watch中写的表达式,后面在get函数里获取表达式的值的时候会调用数据的getter函数。
   expOrFn参数是一个字符串,比如testObj.testObjFirstVal,此时testObj仅仅是一个字符串,而不是对象,我们无法直接获取testObjFirstVal属性的值。
   所以我们在获取值得时候不能直接拿到值,parsePath函数就是用来解决这个问题的,这个函数具体的操作,在后面的代码里。
  if (typeof expOrFn === 'function') {
    this.getter = expOrFn;
  } else {
    //这里是针对表达式
    this.getter = parsePath(expOrFn);
    if (!this.getter) {
      this.getter = function () {};
      "development" !== 'production' && warn(
        "Failed watching path: ""   expOrFn   "" "  
        'Watcher only accepts simple dot-delimited paths. '  
        'For full control, use a function instead.',
        vm
      );
    }
  }
  //注意这个地方,在非computed调用Watch函数外,都会调用get函数(computed有自己的逻辑)
  this.value = this.lazy
    ? undefined
    : this.get();
};

/**
 * Evaluate the getter, and re-collect dependencies.
 */
//get函数,用来收集依赖和获取数据的值
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 {
    // "touch" every property so they are all tracked as
    // dependencies for deep watching
    if (this.deep) {
      traverse(value);
    }
    popTarget();
    //注意这里下面详细讲解
    this.cleanupDeps();
  }
  return value
};

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
Dep.target = null;
var targetStack = [];

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

function popTarget () {
  Dep.target = targetStack.pop();
}

/**
 * Add a dependency to this directive.
 */
//添加一个依赖
Watcher.prototype.addDep = function addDep (dep) {
  var id = dep.id;
  if (!this.newDepIds.has(id)) {
    this.newDepIds.add(id);
    this.newDeps.push(dep);
    //这里做了一个去重,如果depIds里包含这个id,说明在之前给depIds添加这个id的时候,已经调用过 dep.addSub(this),即添加过订阅,不需要重复添加。
    if (!this.depIds.has(id)) {
      dep.addSub(this);
    }
  }
};

/**
 * Clean up for dependency collection.
 */
//下面详细讲
Watcher.prototype.cleanupDeps = function cleanupDeps () {
    var this$1 = this;

  var i = this.deps.length;
  //去除多余的订阅者
  while (i--) {
    var dep = this$1.deps[i];
    //如果Watcher不依赖于某个数据,即某个Dep,那么不需要再订阅这个数据的消息。
    if (!this$1.newDepIds.has(dep.id)) {
      dep.removeSub(this$1);
    }
  }
  var tmp = this.depIds;
  //更新depIds
  this.depIds = this.newDepIds;
  this.newDepIds = tmp;
  //清空newDepIds
  this.newDepIds.clear();
  tmp = this.deps;
  //更新deps
  this.deps = this.newDeps;
  this.newDeps = tmp;
  //清空newDeps
  this.newDeps.length = 0;
};

/**
 * Subscriber interface.
 * Will be called when a dependency changes.
 */
//更新模板或表达式:调用run方法
Watcher.prototype.update = function update () {
  /* istanbul ignore else */
  //下面三种情况均会调用run方法
  if (this.lazy) {
    this.dirty = true;
  } else if (this.sync) {
    this.run();
  } else {
    //queueWatcher这个函数最终会调用run方法。
    queueWatcher(this);
  }
};

/**
 * Scheduler job interface.
 * Will be called by the scheduler.
 */
//注意这里调用了get方法,会更新模板,且重新收集依赖
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;
      //注意下面 this.cb.call,调用回调函数来更新模板或表达式的值($watch表达式的时候,会更新表达式的值)
      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);
      }
    }
  }
};

/**
 * Parse simple path.
 */
var bailRE = /[^w.$]/;
function parsePath (path) {
  //如果字符串中没有符号".",直接返回即可,比如:testVal
  if (bailRE.test(path)) {
    return
  }
  //用符号"."分割字符串,遍历数组,依次获取obj对象上相应的值。
  比如:testObj.testObjFirstVal,先分割成数组[testObj,testObjFirstVal],其次遍历数组,获取obj[testObj]的值,最后获取obj[testObj][testObjFirstVal]的值。
  var segments = path.split('.');
  //这个地方将函数作为返回值,感兴趣的话,可以看一下函数式编程
  return function (obj) {
    for (var i = 0; i < segments.length; i  ) {
      if (!obj) { return }
      obj = obj[segments[i]];
    }
    return obj
  }
}

我们先来理一理watch函数做的事情:初始化变量——>获取getter函数,这里的getter函数是用来获取数据的值,函数执行过程中会调用数据的getter函数,会收集依赖——>调用watcher的get方法,收集依赖,获取值,并将这些东西记录下来。
这个过程就完成了收集依赖的过程,而update函数是用来接收数据发布更新的消息并更新模板或表达式的。
下面我们重点来关注这几点,这些是我刚接触Vue时想不清楚的地方
1.收集依赖指的是谁收集依赖,依赖又是指的什么?这是我一直很迷惑的问题。看英文注释:Watcher的作用是分割表达式,收集依赖并且在值变化的时候调用回调函数。那么我们很明确知道是Watcher在收集依赖,依赖到底指什么呢?
我们上面说过一个Dep对应着一个数据(这个数据可能是:对象的属性、一个对象、一个数组);一个Watcher对应可以是一个模板也可以是一个$watch对应的表达式、函数等,无论那种情况,他们都依赖于data里面的数据,所以这里说的依赖其实就是模板或表达式所依赖的数据,对应着相关数据的Dep。
举个例子:下面这个$watch对应的函数依赖的数据就是testWatcher和testVal。所以这个$watch对应的Watcher收集的依赖就是testWatcher和testVal对应的Dep。

app.$watch(function(){
     return this.testWatcher   this.testVal;
},function(newVal){
    console.log(newVal
 })

2.Watcher有四个使用的场景,只有在这四种场景中,Watcher才会收集依赖,更新模板或表达式,否则,数据改变后,无法通知依赖这个数据的模板或表达式
*第一种:观察模板中的数据
*第二种:观察创建Vue实例时watch选项里的数据
*第三种:观察创建Vue实例时computed选项里的数据所依赖的数据
*第四种:调用$watch api观察的数据或表达式
所以在解决数据改变,模板或表达式没有改变的问题时,可以这么做:
首先仔细看一看数据是否在上述四种应用场景中,以便确认数据已经收集依赖;其次查看改变数据的方式,确定这种方式会使数据的改变被拦截(关于这一点,上面Obsever相关内容中说的比较多)

3.Dep.target的作用:我们前面说过收集依赖的时机是在调用数据的getter函数的时候,但是在这个时候数据的getter函数不知道当前的Watcher是哪一个,所以这里使用了一个全局变量来记录当前的Watcher,方便添加依赖到正在执行的Watcher。关于这点官方的英文注释写的挺清楚的。
4.targetStack的作用(Watcher函数的get方法中pushTarget和popTarget方法中用到):Vue2 中(本文源码为Vue2),视图被抽象为一个 render 函数,一个 render 函数只会生成一个 watcher。比如我们有如下一个模板,模板中使用了Header组件。Vue2 中组件数的结构在视图渲染时就映射为 render 函数的嵌套调用,有嵌套调用就会有调用栈。当 render模板时,遇到Header组件会调用Header组件的render函数,两个render函数依次入栈,执行完函数,依次出栈。

<div id="app">
  <Header></Header>
</div>

5.Watcher函数的get方法中调用this.getter.call(vm, vm)收集完依赖后,又调用this.cleanupDeps()清除依赖。excus me ???第一次看这个地方的时候,我很困扰,为什么添加完依赖后要清楚。后面仔细看了代码发现是这个样子的:
Watcher里面有两个属性:deps和newDeps。他们是用来记录上一次Watcher收集的依赖和新一轮Watcher收集的依赖,每一次有数据的更新都需要重新收集依赖(数据发布更新后,会调用Watcher的notify方法,notify方法会调用run方法,run方法会调用get方法,重新获取值,并重新收集依赖)。举个简单的例子:我们点击一个按钮,用$set给data添加了一个新属性newVal。上一轮收集的依赖中并没有newVal的依赖,所以需要重新收集依赖。
this.cleanupDeps()这个函数的作用就是将新收集的依赖newDeps赋值给deps,并将newDeps清空,准备在下一次数据更新时收集依赖。所以这个函数不是真正的清空Watcher的依赖,而是清除临时保存依赖的newDeps。

看完上面的这些后,再看官方给出的图,就更明白了,不过官方的图中,并没有标提到Dep和Observer。

分分快三计划 2

总之,调用的作用就是管理Watcher。

以上都是简单实现,并且有好多细节没有体现出来,例如:添加watcheer实例时会有id记录这个对象有没有存在于数组中,如果有就不会在添加了;在给对象赋值时VUE中也考虑到了如果当前对象存在自己的setget属性的情况,但是本例中没有考虑这种情况只是简单的赋值 等;详细细节可以去VUE源码中查看

1.双向数据绑定

M ,即 model,指的是模型,也就是数据;V 即view,指的是视图,也就是页面展现的部分。
双向数据绑定大概概括为:每当数据有变更时,会进行渲染,从而更新视图,使得视图与数据保持一致(model到view层);而另一方面,页面也会通过用户的交互,产生状态、数据的变化,这个时候,这时需要将视图对数据的更新同步到数据(view到model层)。

不同的前端 MV* 框架对于这种 Model 和 View 间的数据同步有不同的处理,如:
脏值检查(angular.js)
数据劫持 发布者-订阅者模式(Vue)

我们上面说的Vue的数据响应式原理其实就是实现数据到视图更新原理,而视图到数据的更新,其实就是此基础上给可表单元素(input等)添加了change等事件监听,来动态修改model和 view。

class Watcher{
    constructor(vm,expOrFn,cb,options){
        //传进来的对象 例如Vue
        this.vm = vm
        //在Vue中cb是更新视图的核心,调用diff并更新视图的过程
        this.cb = cb
        //收集Deps,用于移除监听
        this.newDeps = []
        this.getter = expOrFn
        //设置Dep.target的值,依赖收集时的watcher对象
        this.value =this.get()
    }

    get(){
        //设置Dep.target值,用以依赖收集
        pushTarget(this)
        const vm = this.vm
        let value = this.getter.call(vm, vm)
        return value
    }

    //添加依赖
      addDep (dep) {
          // 这里简单处理,在Vue中做了重复筛选,即依赖只收集一次,不重复收集依赖
        this.newDeps.push(dep)
        dep.addSub(this)
      }

      //更新
      update () {
        this.run()
    }

    //更新视图
    run(){
        //这里只做简单的console.log 处理,在Vue中会调用diff过程从而更新视图
        console.log(`这里会去执行Vue的diff相关方法,进而更新数据`)
    }
}

2.发布-订阅者模型

订阅发布模式定义了一种一对多的依赖关系,让多个订阅者对象同时监听某一个主题对象。这个主题对象在自身状态变化时,会通知所有订阅者对象,使它们能够自动更新自己的状态。
Vue中的Dep和Watcher共同实现了这个模型

const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)

const methodsToPatch = [
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]

/**
 * Intercept mutating methods and emit events
 */
methodsToPatch.forEach(function (method) {
  // cache original method
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator (...args) {
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)
    // notify change
    ob.dep.notify()
    return result
  })
})
<input type="text" />function trigger{ const e = document.createEvent('HTMLEvents'); e.initEvent(type,true,true); el.dispatchEvent;}var dom = document.getElementById;dom.addEventListener('compositionstart', onCompositionStart)dom.addEventListener('compositionend', onCompositionEnd)function onCompositionStart{ e.target.composing = true console.log('==========================='); console.log; console.log('===========================');}function onCompositionEnd{ e.target.composing = false console.log('==========================='); console.log; console.log('==========================='); trigger(dom,'ceshi');}dom.addEventListener('ceshi',function{ console.log

二、 代码实现

2、排序,按解析渲染的先后顺序执行,即Watcher小的先执行。Watcher里面的id是自增的,先创键的比后创建的id小。所以会有如下规律:

entries/web-runtime-with-compiler.js

1.Vue数据响应式的设计思想
2.了解Observer,Dep,Watcher的源码实现原理
3.getter/setter 拦截数据方式的不足及解决方案

实际应用中,各种系统复杂无比。假设我们现在有一个全局的对象,我们可能会在多个 Vue 对象中用到它进行展示。又或者写在data中的数据并没有应用到视图中呢,这个时候去更新视图就是多余的了。这就需要依赖收集的过程。

export function initGlobalAPI (Vue: GlobalAPI) {}<!--添加工具函数-->initUseinitMixininitExtendinitAssetRegisters

三、相关概念

defineReactive:

  1. compositionstart compositionend

我们关注以下几点:

1.Observer类中的属性和方法都比较好理解,我在这里只说一下vmCount属性:
vmCount属性是用来记录该实例被创建的次数,我们看下面的代码(戳我查看demo源码及效果),调用了两次my-component组件,这个时候vmCount为2.

<div >
        <div id="example">
          <my-component></my-component>
          <my-component></my-component>
        </div>
    </div>
        <script src="./vue.js"></script>
        <script>
        var data = { counter: 1 }
        Vue.component('my-component', {
          template: '<div>{{ counter }}</div>',
          data: function () {
            return data
          }
        })
        // 创建根实例
        var app2 = new Vue({
          el: '#example'
        })
</script>

效果:

分分快三计划 3

  • enumerable,属性是否可枚举,默认 false。
  • configurable,属性是否可以被修改或者删除,默认 false。
  • get,获取属性的方法。
  • set,设置属性的方法。
  1. compositionstart 事件触发于一段文字的输入之前(类似于 keydown 事件,但是该事件仅在若干可见字符的输入之前,而这些可见字符的输入可能需要一连串的键盘操作、语音识别或者点击输入法的备选词)。
  2. compositionend 事件触发一段文字输入完成或者删除文字时

一、设计模式

Vue 采用数据劫持结合发布者-订阅者模式的方式来实现数据的响应式,通过Object.defineProperty(点我查看该属性)来劫持数据的setter,getter,在数据变动时发布消息给订阅者,订阅者收到消息后进行相应的处理。

现在我们来看一下下面的图,共涉及5个概念,data和view的意义很明显,主要讲解Observer,Dep和Watcher。
Observer:数据的观察者,让数据对象的读写操作都处于自己的监管之下。当初始化实例的时候,会递归遍历data,用Object.defineProperty来拦截数据(包含数组里的每个数据)。
Dep:数据更新的发布者,get数据的时候,收集订阅者,触发Watcher的依赖收集;set数据时发布更新,通知Watcher 。
Watcher:数据更新的订阅者,订阅的数据改变时执行相应的回调函数(更新视图或表达式的值)。
一个Watcher可以更新视图,如html模板中用到的{{test}},也可以执行一个$watch监督的表达式的回调函数(Vue实例中的watch项底层是调用的$watch实现的),还可以更新一个计算属性(即Vue实例中的computed项)。

分分快三计划 4

图中红色的箭头表示的是收集依赖时获取数据的流程。Watcher会收集依赖的时候(这个时机可能是实例创建时,解析模板、初始化watch、初始化computed,也可能是数据改变后,Watcher执行回调函数前),会获取数据的值,此时Observer会拦截数据(即调用get函数),然后通知Dep可以收集订阅者啦。Dep将订阅数据的Watcher保存下来,便于后面通知更新。

图中绿色的箭头表示的是数据改变时,发布更新的流程。当数据改变时,即设置数据时,此时Observer会拦截数据(即调用set函数),然后通知Dep,数据改变了,此时Dep通知Watcher,可以更新视图啦。

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  const dep = new Dep()

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

  // cater for pre-defined getter/setters
  const getter = property && property.get
  const setter = property && property.set
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter () {
      const 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) {
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = !shallow && observe(newVal)
      dep.notify()
    }
  })
}

二. 源码分析

这方面的文章很多,但是我感觉很多写的比较抽象,本文会通过举例更详细的解释。(此文面向的Vue新手们,如果你是个大牛,看到这篇文章就可以点个赞,关掉页面了。)通过阅读这篇文章,你将了解到:

在vue中,共有4种情况会产生Watcher:

import Vue from 'core/index'

getter/setter方法拦截数据的不足:

  1. 当对象增删的时候,是监控不到的。比如:data={a:"a"},这个时候如果我们设置data.test="test",这个时候是监控不到的。因为在observe data的时候,会遍历已有的每个属性(比如a),添加getter/setter,而后面设置的test属性并没有机会设置getter/setter,所以检测不到变化。同样的,删除对象属性的时候,getter/setter会跟着属性一起被删除掉,拦截不到变化。
  2. getter/setter是针对对象的,那么对于数组的修改,怎样监控变化呢
  3. 每次给数据设置值得时候,都会调用setter函数,这个时候就会发布属性更新消息,即使数据的值没有变。从性能方便考虑我们肯定希望值没有变化的时候,不更新模板。

对于第一个问题,Vue官方给出了vm.$set/Vue.set和vm.$delete/Vue.delete这样的api来解决这个问题。我们来看下$set的代码:

/**
 * Set a property on an object. Adds the new property and
 * triggers change notification if the property doesn't
 * already exist.
 */
function set (target, key, val) {
   //对于数组的处理,调用变异方法splice,这个时候数组的Dep会发布更新消息
  if (Array.isArray(target) && isValidArrayIndex(key)) {
    target.length = Math.max(target.length, key);
    target.splice(key, 1, val);
    return val
  }
  //如果set的是对象已经有的属性,那么该属性已经有getter/setter函数了,此时直接修改即可
  if (hasOwn(target, key)) {
    target[key] = val;
    return val
  }
  var ob = (target).__ob__;
  if (target._isVue || (ob && ob.vmCount)) {
    "development" !== 'production' && warn(
      'Avoid adding reactive properties to a Vue instance or its root $data '  
      'at runtime - declare it upfront in the data option.'
    );
    return val
  }
  if (!ob) {
    target[key] = val;
    return val
  }
  //如果是对象没有的属性,则添加getter/setter
  defineReactive$$1(ob.value, key, val);
  //注意此处,对象的Dep会发布更新
  ob.dep.notify();
  return val
}

上面的代码比较简单,看过注释应该就能明白,我不做过多解释。我们着重注意下这句代码:ob.dep.notify(),对象的Dep发布更新。可是这个dep是在什么地方收集的订阅者呢?

还记得defineReactive函数里让大家注意的这句代码吗:childOb.dep.depend(),这句代码就是在收集订阅者
仔细阅读Observer相关的代码,我们会发现,dep实例化的地方有两处
一处是在defineReactive函数里,每次调用这个函数的时候都会创建一个新的Dep实例,存在在getter/setter闭包函数的作用域链上,是为对象属性服务的。在Watcher获取属性的值的时候收集订阅者,在设置属性值的时候发布更新。
另一处是在observe函数中,此时的dep挂在被observe的数据的__ obj__属性上,他是为对象或数组服务的,在Watcher获取属性的值的时候,如果值被observe后返回observer对象(对象和数组才会返回observer),那么就会在此时收集订阅者,在对象或数组增删元素时调用$set等api时发布更新的;
defineReactive函数的getter函数里这段代码就是在收集订阅者:

get: function reactiveGetter () {
      var value = getter ? getter.call(obj) : val;
      if (Dep.target) {
        dep.depend();
        if (childOb) {
          //注意这里,此处的dep就是在执行var childOb = observe(val)时产生的,是用来收集childOb的订阅者的
          childOb.dep.depend();
        }
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }

我们来看个例子,data={testkey:{testValueKey:testValueValue}},这个时候模板里有{{testkey}},模板的Watcher在执行getter函数的时候,testkey属性的getter对应的dep会将此Watcher收集为订阅者;同时{testValueKey:testValueValue}对象也会将此Watcher收集为订阅者(我们在给testkey属性设置getter/setter函数时,会执行var childOb = observe(val)和childOb.dep.depend(),而此时的val就是{testValueKey:testValueValue}对象)。
在这个时候我们设置this.$set(this.testkey, "addKey", "addValue"),就会触发this.testkey对应的值:{testValueKey:testValueValue}对象的dep发布更新,而此时dep的订阅者中包含模板{{testkey}}的watcher,此时模板更新视图。同理每个数组也是有相应的dep来发布更新的,比如data={arr:[1,2,2]}},此时[1,2,2]这个数组的__ obj__属性下也会有dep的。

其实只有对象和数组才会有这种删除和增加的操作,而其他的字符串等都是直接赋值修改的,getter/setter都是能检测到的,所以observe对象和数组的时候会创建一个dep,用来收集订阅和发布更新

对于第二个问题,我们先来考虑下数组修改有哪几种情况
1.当你利用索引直接设置一个项时
比如:data={arr:[1,2,3]},这个时候我设置this.arr[0] = 4,会发现数据改变了,但是视图没有更新,Vue根本没有检测到变化。
这个时候可能你会说,observeArray的时候不是会遍历数组,observe每个元素吗?可是Observe数据的时候是会判断数据类型的,只会处理数组和对象,而this.arr里面的元素是字符串,所以无法转化成observer类,也就不会有getter/setter。另一方面,即便arr里面是对象,比如{arr:[{testobj: true}]},数组元素{testobj: true}会被observe到,那也只是在{testobj: true}对象里面的属性改变的时候响应,而{testobj: true}对象被替换是无法感知的。
2.调用数组的变异方法(push(),pop(),shift(),unshift(),splice(),sort(),reverse()),这些方法是会让数组的值发生改变的,比如:arr=[0,1];arr.puah(3);此时arr=[1,2,3],arr发生了改变,此时是需要更新视图的,但是arr的getter/setter拦截不到变化(只有在赋值的时候才会调用setter,比如:arr=[6,7,8])。
3.当你修改数组的长度时,例如:vm.items.length = newLength

对于第一种情况,和对象的增减一样,可以使用vm.$set/Vue.set和vm.$delete/Vue.delete这几个api.
对于第二种情况,可以通过改写这些变异方法完成,在调用这些方法的时候发布更新消息。下面我们来看代码

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */

var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);
//遍历变异方法

[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  //重写arrayMethods里的变异方法
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];

    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    //inserted存储的是新加到数组里的元素,需要被observe
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    //发布更新
    ob.dep.notify();
    return result
  });
});

回过头再看Observer类中对于数组的处理,先覆盖变异数组,再observe每个数组元素。所以每当调用数组的变异方法的时候,都会更新视图。

if (Array.isArray(value)) {
    var augment = hasProto
      ? protoAugment
      : copyAugment;
    //用改变变异数组后的arrayMethods的方法覆盖被observe的数组的方法
    augment(value, arrayMethods, arrayKeys);
    this.observeArray(value);
  } 

对于第三种情况,可以使用splice来完成,splice是变异方法,会发布更新。
戳这,查看官网对此的解决方案。

对于上面提到的第三个问题,Watcher在run方法里解决了这个问题,他会检测value !== this.value,只更新值变化的情况。

Observer相关代码就看到这里,下面来看Dep

3、删除Watcher,如果一个组件的Watcher在队列中,而他的父组件被删除了,这个时候也要删掉这个Watcher。

import Vue from './web-runtime'

关于Dep主要注意以下几点:

1.Dep是发布订阅者模型中的发布者,Watcher是观察者,一个Dep实例对应一个对象属性或一个被观察的对象,用来收集订阅者和在数据改变时,发布更新。
2.Dep实例有两种实例:
*第一种:在observe方法里生成的,用来给被观察的对象收集订阅者和发布更新,挂在对象的__ ob__对象上,通常在defineReactive函数里的getter函数里调用childOb.dep.depend()来收集依赖,在vm.$set/Vue.set和vm.$delete/Vue.delete这些api中调用来发布更新。
*第二种:在defineReactive函数里,是用来set/get数据时收集订阅者和发布更新的,保存在getter/setter闭包函数的作用域上。set数据时收集依赖,get数据时发布更新。

比如我们有一个data:data={testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}};
这个时候,这个Vue实例上会有四个Dep实例:
第一个是调用data的observe方法时生成的挂在{testVal: "testVal", testObj: {testObjFirstEle: "testObjFirstEle"}}对象的__ ob__方法上的,属于上面说的第一种实例;
第二个是调用defineReactive函数给属性testVal添加getter,setter函数时生成的。保存在getter/setter闭包函数的作用域上,属于第二种实例。
第三个是调用defineReactive函数给属性testObj添加getter,setter函数时生成的。保存在getter/setter闭包函数的作用域上,属于第二种实例。
第四种是Watcher收集依赖时,调用testObj属性的set函数添加依赖时observe属性的值(即{testObjFirstEle: "testObjFirstEle"}对象)生成的。

现在我们来验证一下:
*源码:http://runjs.cn/code/9p5ydg84
*效果:http://sandbox.runjs.cn/show/9p5ydg84

分分快三计划 5

dep的id从0开始,到3结束一共四个。这个时候你可能会问,dep的id为2和3的实例去哪里了?注意上面我们说的第二种Dep实例对存在在getter/setter闭包函数的作用域中的,我们获取不到,你可以在源码里debugger来看。

3.当我们想要给{testObjFirstEle: "testObjFirstEle"}对象添加属性并更新视图时有两种方式:
一、利用getter/setter,重新设置testObj属性的值,testObj属性的setter执行的过程中会调用dep.notify()发布更新。比如:this.testObj = {testObjFirstEle: "testObjFirstEle", "newEle": "newEle};
二、利用$set函数:this.$set(this.testObj, "newEle", "newEle")。此时是{testObjFirstEle: "testObjFirstEle"}对象的__obj __对象上的dep发布的更新。

4.Dep.target:Dep.target为什么是全局唯一的呢?这是我以前一直不理解的地方。这一点我想,在讲完Watcher时会更清晰,所以请大家耐心读完下面的内容,就明白了。

主要涉及属性:

function dedupe (latest, sealed) { // compare latest and sealed to ensure lifecycle hooks won't be duplicated // between merges if (Array.isArray { const res = [] sealed = Array.isArray ? sealed : [sealed] for (let i = 0; i < latest.length; i  ) { if (sealed.indexOf(latest[i]) < 0) { res.push(latest[i]) } } return res } else { return latest }}

Dep

下图是Dep的结构图

分分快三计划 6

下面是源码:

//全局变量,每个实例中的dep实例的id都是从0开始累加的
var uid$1 = 0;

/**
 * A dep is an observable that can have multiple
 * directives subscribing to it.
 */
var Dep = function Dep () {
  this.id = uid$1  ;
  //subscribe的简写,存放订阅者
  this.subs = [];
};
//添加一个订阅者
Dep.prototype.addSub = function addSub (sub) {
  this.subs.push(sub);
};
//删除一个订阅者
Dep.prototype.removeSub = function removeSub (sub) {
  remove$1(this.subs, sub);
};
//让Watcher收集依赖并添加订阅者。
Dep.target是一个Watcher, 可以查看Watcher的addDep方法。
这个方法做的事情是:收集依赖后,调用了Dep的addSub方法,给Dep添加了一个订阅者
Dep.prototype.depend = function depend () {
  if (Dep.target) {
    Dep.target.addDep(this);
  }
};
//发布数据更新:通过调用subs里面的每个Watcher的update发布更新
Dep.prototype.notify = function notify () {
  // stablize the subscriber list first
  var subs = this.subs.slice();
  for (var i = 0, l = subs.length; i < l; i  ) {
    subs[i].update();
  }
};

// the current target watcher being evaluated.
// this is globally unique because there could be only one
// watcher being evaluated at any time.
//注意这里,target是全局唯一的。下面详细讲解。
Dep.target = null;

  2.2、用户创建的Watcher会比render时候创建的先解析。所以用户创建的Watcher的id比render时候创建的小。

core/index.js

答案是数组的响应式是不够完全的,VUE只重写了有限的方法。重写逻辑如下:

initMixin()

依赖收集

package.json

首先看一下 Object.defineProperty,Vue.js就是基于它实现「响应式系统」的。

instance/index

所以响应式原理就是,我们通过递归遍历,把vue实例中data里面定义的数据,用defineReactive(Object.defineProperty)重新定义。每个数据内新建一个Dep实例,闭包中包含了这个 Dep 类的实例,用来收集 Watcher 对象。在对象被「读」的时候,会触发 reactiveGetter 函数把当前的 Watcher 对象(存放在 Dep.target 中)收集到 Dep 类中去。之后如果当该对象被「写」的时候,则会触发 reactiveSetter 方法,通知 Dep 类调用 notify 来触发所有 Watcher 对象的 update 方法更新对应视图。

import Vue from './instance/index'import { initGlobalAPI } from './global-api/index'initGlobalAPI

Vue.js 是一款 MVVM 框架,数据模型仅仅是普通的 JavaScript 对象,但是对这些对象进行操作时,却能影响对应视图,它的核心实现就是「响应式系统」。

initLifecycleinitEventsinitRendercallHook(vm, 'beforeCreate')initInjections // resolve injections before data/propsinitStateinitProvide // resolve provide after data/propscallHook(vm, 'created')

  2.1、组件是允许嵌套的,而且解析必然是先解析了父组件再到子组件。所以父组件的id比子组件小。

function Vue  { if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue)) { warn('Vue is a constructor and should be called with the `new` keyword') } this._init}initMixin // 添加_init方法// 初始化一些数据,其中 beforeCreate钩子在initState()数据之前,导致在beforeCreate钩子中拿不到data或者props的值,create钩子中能拿到data或者props中的值 stateMixin // 初始化 props,data,computed,watch 全部通过defineReactive变成响应式数据 添加$watch方法eventsMixin // 添加$on $off $emit方法lifecycleMixin // 添加_update方法,$forceUpdate方法,$destroyrenderMixin // 添加 _render方法,添加$nextTick方法

 所谓依赖收集,就是把一个数据用到的地方收集起来,在这个数据发生改变的时候,统一去通知各个地方做对应的操作。“订阅者”在VUE中基本模式如下:

web-runtime.js

1、Vue实例对象上的watcher,观测根数据,发生变化时重新渲染组件
updateComponent = () => {   vm._update(vm._render(), hydrating) }
vm._watcher = new Watcher(vm, updateComponent, noop)
2、用户在vue对象内用watch属性创建的watcher
3、用户在vue对象内创建的计算属性,本质上也是watcher
4、用户使用vm.$watch创建的watcher

  1. 其中响应式数据核心:Observe,Dep,Watcher,defineReactive

    1. Obejct.defineProperty

    方法会直接在一个对象上定义一个新属性,或者修改一个对象的现有属性, 并返回这个对象。

    语法:Object.defineProperty(obj, prop, descriptor)

    参数

     obj 要在其上定义属性的对象。 prop 要定义或修改的属性的名称。 descriptor 将被定义或修改的属性描述符。
    

    Object.defineProperty详细描述

    注: configurable 为false时,属性不能被删除,为true时能被删除,默认值为false

    1. defineReactive

    主要作用就是在get时将watcher对象放入Dep.subs数组中,set时去更新视图

    简易实现(详细参考Vue源码)

    function defineReactive(obj,key,val){ const dep = new Dep(); Object.defineProperty(obj,key,{ enumerable:true, configurable:true, get: function reactiveGetter(){ dep.addSub(Dep.target); return val; }, set: function reactiveSetter{ if(val == newVal){ return; } dep.nodify(); val = newVal; // cb; } });}
    
    1. Observer 将现有数据变成可视化数据

    简易实现

    function observer{ if(!value || typeof value !='object'){ return ; } // 递归循环 for(let key in value){ if(Obejct.prototype.toString.call( value[key]) == '[Object object]'){ dgObj(value[key],key,value); }else{ defineReactive(value, key , value[key]); } }}
    
    1. Dep 主要作用收集watcher对象

    简易实现

    function Dep(){// this.subs = [];}Dep.prototype ={ subs:[], addSub:function{ this.subs.push; }, nodify:function(){ this.subs.forEach(sub =>{ sub.update; }};Dep.target = null;
    
    1. Watcher 将Dep.target赋值,更新视图
    function Watcher(){ Dep.target = this;}Watcher.prototype ={ update:function(){ console.log('视图更新了!!!!~~~'); }}
    
    1. Vue构造函数

    简易实现

     function Vue { this._data = options.data; new Watcher(); observer(this._data);}
    

调度的作用:

一、vue

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid  
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }
 //依赖收集,有需要才添加订阅
  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i  ) {
      subs[i].update()
    }
  }
}

build/config.js

响应式基本原理就是,在 Vue 的构造函数中,对 options 的 data 进行处理。即在初始化vue实例的时候,对data、props等对象的每一个属性都通过Object.defineProperty定义一次,在数据被set的时候,做一些操作,改变相应的视图。

1.入口文件

1、去重,每个Watcher有一个唯一的id。首先,如果id已经在队列里了,跳过,没必要重复执行,如果id不在队列里,要看队列是否正在执行中。如果不在执行中,则在下一个时间片执行队列,因此队列永远是异步执行的。

TARGET: web-full-dev// Runtime compiler development build  'web-full-dev': { entry: path.resolve(__dirname, '../src/entries/web-runtime-with-compiler.js'), dest: path.resolve(__dirname, '../dist/vue.js'), format: 'umd', env: 'development', alias: { he: './entity-decoder' }, banner },if (process.env.TARGET) { module.exports = genConfig(builds[process.env.TARGET])} 

有了订阅者,再来看看Watcher的实现。源码Watcher比较多逻辑,简化后的模型如下

  1. provide, inject

    provide选项应该是一个对象或返回一个对象的函数。该对象包含可注入其子孙的属性。

    inject选项应该是:

    • 一个字符串,或
    • 一个对象,对象的key是本地的绑定名,value是:
      • 在可用的注入内容中搜索用的key(字符串或Symbol)或
      • 一个对象,该对象的:
        • from 属性是在可用的注入内容中搜索用的key
        • default 属性时降级情况下使用的value
    // 父组件provide:{ }// 子组件inject:{ foo:{ from:'foo', default:'test' }},mounted(){ console.log; // test}<!--另外一种写法-->// 子组件inject:['foo'];mounted(){ console.log;// test}
    
  2. el

    提供一个在页面上已经存在的DOM元素作为Vue实例的挂载目标。可以是CSS选择器,也可以是一个HTMLElement。

    在实例挂载之后,元素可以用vm.分分快三计划 7mount()手动开启编译

    源代码:

    if (vm.$options.el) { vm.$mount(vm.$options.el)} 
    
     var app = new Vue({ data:{ message:'hello Vue!' }, el:'#bpp', directives:{ }, provide(){ return { getData(){ console.log } } }, beforeCreate(){ console.log(this.message) }, mounted(){ console.log(this._data, this._provided) } });// 在 id= bpp的元素上加载Vue实例
    
  3. 数组去重

补充:

 queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index  ) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0)   1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop '   (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

4、队列执行过程中,存一个对象circular,里面有每个watcher的执行次数,如果哪个watcher执行超过MAX_UPDATE_COUNT定义的次数就认为是死循环,不再执行,默认是100次。

Wathcer会增减,也可能在render的时候新增。所以,必须有一个Schedule来进行Watcher的调度。部分主要代码如下:

 VUE中是如何用Object.defineProperty给数组对象重新定义的呢,为什么我们直接修改数据中某项(arr[3] = 4)的时候,视图并没有响应式地变化呢。

本文由分分快三计划发布,转载请注明来源

关键词: 分分快三计划 前端 vue-que... Vue 中不