Skip to content

Latest commit

 

History

History
522 lines (427 loc) · 16.2 KB

双向数据绑定.md

File metadata and controls

522 lines (427 loc) · 16.2 KB

本篇文章,我们来说一说Vue里面的双向数据绑定。因为Vue处理的东西比较多,比较复杂,我们先用一个自己写的小demo演示一下它的大概思路是什么。

Vue里面的数据绑定是通过数据劫持的方式来实现的,其中最核心的便是Object.defineProperty(),不熟悉这个方法的,建议先去学习一下相关的内容,这里就不再赘述。

双向数据绑定,简单点来说分为三个部分:

  • 1、Observer。这里的主要工作是递归地监听对象上的所有属性,在属性值改变的时候,触发相应的watcher

  • 2、Watcher。观察者,当监听的数据值修改时,执行响应的回调函数(Vue里面的更新模板内容)。

  • 3、Dep。连接ObserverWatcher的桥梁,每一个Observer对应一个Dep,它内部维护一个数组,保存与该Observer相关的Watcher

数据绑定demo

接下来,先看我们极其简化的小demo。

第一部分是Observer

function Observer(obj, key, value){
	var dep = new Dep();
	if (Object.prototype.toString.call(value) == '[object Object]') {
		Object.keys(value).forEach(function(key){
			new Observer(value,key,value[key])
		})
	};

	Object.defineProperty(obj, key, {
		enumerable: true,
    	configurable: true,
    	get: function(){
    		if (Dep.target) {
    			dep.addSub(Dep.target);
    		};
    		return value;
    	},
    	set: function(newVal){
    		value = newVal;
    		dep.notify();
    	}
	})
}

我们先略过对数组的处理等。递归地为obj的每个属性添加gettersetter。在getter中,我们把watcher添加到dep中。setter中,触发watcher执行回调。

第二部分是Watcher

function Watcher(fn){
	this.update = function(){
		Dep.target = this;
		fn();
		Dep.target = null;
	}
	this.update();
}

极其简单的几行代码,fn是数据变化后要执行的回调函数,一般是获取数据渲染模板。默认执行一遍update方法是为了在渲染模板过程中,调用数据对象的getter时建立两者之间的关系。因为同一时刻只有一个watcher处于激活状态,把当前watcher绑定在Dep.target(方便在Observer内获取)。回调结束后,销毁Dep.target

最后是Dep

function Dep(){
	this.subs = [];

	this.addSub = function (watcher) {
		this.subs.push(watcher);
	}

	this.notify = function(){
		this.subs.forEach(function(watcher){
			watcher.update();
		});
	}
}

内部一个存放watcher的数组subsaddSub用于向数组中添加watcher(getter时)。notify用于触发watcher的更新(setter时)。

以上就是我们简易的双向数据绑定demo。我们看看用起来是什么样的。

<div id="test"></div>
<script type="text/javascript">
	var obj = {
		a: 1,
		b: 2,
		c: 3
	}
	Object.keys(obj).forEach(function(key){
		new Observer(obj, key, obj[key])
	});
	new Watcher(function(){
		document.querySelector("#test").innerHTML = obj.a;
	})
</script>

首先,我们给obj的每一个属性都添加gettersetter。创建一个Watcher对象,回调函数是使#test的内容为obj.a,这里是1

打开控制太,我们修改obj.a == 22。我们发现页面中显示的内容也变成了22。相关代码见这里

以上知识我们的开胃小菜,Vue中对数组做了处理,而且页面的更新是异步执行的,所以会有许许多多的处理,接下来我们慢慢分解。

Vue中的双向绑定

我们先看一张图,这是Vue文档中的一张图,解释的就是Vue的响应式原理。

vue数据绑定

整体上和我们之前的demo一样,我们之前说过,Vue的更新是生成render函数,然后生成虚拟dom,映射到页面上。左侧的部分其实就是我们watcher的回调,右下角的data就是通过我们上面说的Observer来添加gettersetterwatcher通过dependencydata联系在一起,并触发re-render

Vue中的Observer

接着,我们来看源码,打开src/core/observer/index.js

首先是我们的Observer对象。

export class Observer {
  value: any;
  dep: Dep;
  vmCount: number; // 把该对象作为root $data的vm个数

  constructor (value: any) {
    this.value = value
    this.dep = new Dep()
    this.vmCount = 0
    // 添加__ob__来标示value有对应的Observer
    def(value, '__ob__', this)
    // 对数组的处理
    if (Array.isArray(value)) {
      const augment = hasProto
        ? protoAugment
        : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    // 处理对象
    } else {
      this.walk(value)
    }
  }

  // 给每个属性添加getter/setters
  walk (obj: Object) {
    const keys = Object.keys(obj)
    for (let i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i], obj[keys[i]])
    }
  }

  // 观察数组的每一项
  observeArray (items: Array<any>) {
    for (let i = 0, l = items.length; i < l; i++) {
      observe(items[i])
    }
  }
}

整体上,value分为对象或数组两种情况。在这之前,我们先来说一下defineReactiveobserve这两个比较重要的函数。

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

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

  const getter = property && property.get
  const setter = property && property.set

  let childOb = 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
      }
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      childOb = observe(newVal)
      dep.notify()
    }
  })
}

这个方法,是具体为objkey值添加装饰器的地方。它会为每个值创建一个dep,如果用户为这个值传入gettersetter,则暂时保存。observe方法稍后介绍。之后通过Object.defineProperty,重新添加装饰器。在getter中,dep.depend其实做了两件事,一是向Dep.target内部的deps添加dep,二是将Dep.target添加到dep内部的subs,也就是建立它们之间的联系。在setter中,如果新旧值相同,直接返回,不同则调用dep.notify来更新与之相关的watchercustomSetter在开发过程中输出错误用。

export function observe (value: any, asRootData: ?boolean): Observer | void {
  if (!isObject(value)) {
    return
  }
  let ob: Observer | void
  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
  ) {
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

该方法用于观察一个对象,返回与对象相关的Observer对象,如果没有则为value创建一个对应的ObserverdefineReactive中调用该方法,其实就是为所有value为对象的值递归地观察。

我们再回到Observer,如果传入的是对象,我们就调用walk,该方法就是遍历对象,对每个值执行defineReactive

另一种情况是传入的对象是数组,因为数组本身只引用了一个地址,所以对数组进行pushsplicesort等操作,我们是无法监听的。所以,Vue中改写value__proto__(如果有),或在value上重新定义这些方法。augment在环境支持__proto__时是protoAugment,不支持时是copyAugment

function protoAugment (target, src: Object) {
  target.__proto__ = src
}

function copyAugment (target: Object, src: Object, keys: Array<string>) {
  for (let i = 0, l = keys.length; i < l; i++) {
    const key = keys[i]
    def(target, key, src[key])
  }
}

protoAugment很简单,其实就是执行了value.__proto__ = arrayMethods

copyAugment中循环把arrayMethods上的arrayKeys方法添加到value上。

arrayMethods其实是改写了数组方法的新对象。arrayKeysarrayMethods中的方法列表。我们一起来看看arrayMethods

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

;[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  const original = arrayProto[method]
  def(arrayMethods, method, function mutator () {

    let i = arguments.length
    const args = new Array(i)
    while (i--) {
      args[i] = arguments[i]
    }
    const result = original.apply(this, args)
    const ob = this.__ob__
    let inserted
    switch (method) {
      case 'push':
        inserted = args
        break
      case 'unshift':
        inserted = args
        break
      case 'splice':
        inserted = args.slice(2)
        break
    }
    if (inserted) ob.observeArray(inserted)

    ob.dep.notify()
    return result
  })
})

整体上其实还是调用数组相应的方法来操作value,只不过操作之后,添加了相关watcher的更新。这里解释一下为什么pushunshiftsplice参数大于2时,要重新调用ob.observeArray,因为这三种情况都是像数组中添加新的元素,所以需要重新观察每个子元素。

Vue中的Dep

let uid = 0

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 () {
    const subs = this.subs.slice()
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

Dep对象比较简单,内部有一个为一个id,用于作为Dep对象的唯一标识,还有一个保存watcher的数组subs。相比于我们之前的小demo,这里多了removeSubdependremoveSub是从数组中移除某一watcherdepend是调用了watcheraddDep,等会讲解。

Vue中的Watcher

let uid = 0

export default class Watcher {

  constructor (
    vm: Component,
    expOrFn: string | Function,
    cb: Function,
    options?: Object
  ) {
    this.vm = vm
    vm._watchers.push(this)
    ...
    this.cb = cb
    this.id = ++uid
    ...
    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 = function () {}
      }
    }
    this.value = this.get()
  }

  get () {
    pushTarget(this)
    let value
    const vm = this.vm
    ...
    value = this.getter.call(vm, vm)
    ...
    popTarget()
    this.cleanupDeps()
    return value
  }

   ...

  update () {
    ...
    queueWatcher(this)
  }

  run () {
    if (this.active) {
      const value = this.get()
      if (
        value !== this.value ||
        isObject(value) ||
        this.deep
      ) {
        // set new value
        const 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)
        }
      }
    }
  }
  ...
}

这里我们只说最普通的情况,deeplazy等就不多说了,大家有时间自己看一看就行。

创建Watcher对象时,有两个比较重要的参数,一个是expOrFn,一个是cb

Watcher创建时,会调用的this.get,里面会执行根据expOrFn解析出来的getter。在这个getter中,我们或渲染页面,或获取某个数据的值。总之,会调用相关datagetter,来建立数据的双向绑定。

当相关的数据改变时,会调用watcherupdate方法,进而调用run方法。我们看到,run中还会调用this.get来获取修改之后的value值。update中的queueWatcher方法稍后解释。

我们先来看看Watcher的两种主要用途:一种是更新模板,另一种就是监听某个值的变化。

** 模板更新 **

我们之前讲Vue声明周期时讲到挂载元素时,我们是通过创建Watcher对象,然后调用updateComponent来更新渲染模板的。

 vm._watcher = new Watcher(vm, updateComponent, noop)

这是因为创建Watcher会调用this.get,也就是这里的updateComponent。在render的过程中,会调用datagetter方法,以此来建立数据的双向绑定,当数据改变是,会重新触发updateComponent。在这里this.get的返回值是undefined,所以主要是用于渲染模板。

** 监听数据 **

另一个用途就是我们的computedwatch等,即监听数据的变化来执行响应的操作。

此时this.get返回的是要监听数据的值。初始化过程中,调用this.get会拿到初始值保存为this.value,监听的数据改变后,会再次调用this.get并拿到修改之后的值,将旧值和新值传给cb并执行响应的回调。

接下来我们谈谈queueWatcher,从函数名我们大致可以猜出,它是把当前的watcher添加到一个队列中。我们知道,Vue中页面的更新是异步的,所以一系列数据的变化,会在之后的某一时刻统一更新。我们来看看该方法的代码。

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i >= 0 && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(Math.max(i, index) + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

我们的watcher有从小到大的唯一id,在页面更新时,会按照一定的顺序依次更新,这里做了一个判断,如果watcher列表正在更新,则把新的watcher添加到对应的位置,并更新。否则,在下一个nextTick中执行flushSchedulerQueue

function flushSchedulerQueue () {
  flushing = true
  let watcher, id, vm

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

  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    id = watcher.id
    has[id] = null
    watcher.run()

  }

  const oldQueue = queue.slice()
  resetSchedulerState()  // 重置队列

  index = oldQueue.length
  while (index--) {
    watcher = oldQueue[index]
    vm = watcher.vm
    if (vm._watcher === watcher && vm._isMounted) {
      callHook(vm, 'updated')
    }
  }

}

该方法首先按照从小到大将watcher排序,并依次执行它的run方法。vm._watcher保存的是渲染模板时创建的watcher,所以如果队列中有该watcher,则说明模板有变化,随之调用'updated'钩子函数。

Watcher对象中还有一些销毁watcher等方法,这里就不再赘述,感兴趣的大家可以自己看一看。