@hanting003
2016-10-31T02:37:57.000000Z
字数 6172
阅读 1064
Vue.js最显著的功能就是响应式系统,它是一个典型的MVVM框架,模型(Model)只是普通的JavaScript对象,修改它则视图(View)会自动更新。这种设计让状态管理变得非常简单而直观,不过理解它的原理也很重要,可以避免一些常见问题。下面让我们深挖Vue.js响应式系统的细节,来看一看Vue.js是如何把模型和视图建立起关联关系的。
我们先来看一个简单的例子。代码示例如下:
<div id="main"><h1>count: {{times}}</h1></div><script src="vue.js"></script><script>var vm = new Vue({el: '#main',data: function () {return {times: 1};},created: function () {var me = this;setInterval(function () {me.times++;}, 1000);}});</script>
运行后,我们可以从页面中看到,count后面的times每隔1s递增1,视图一直在更新。在代码中仅仅是通过setInterval方法每隔1s来修改vm.times的值,并没有任何DOM操作。那么Vue.js是如何实现这个过程的呢?我们可以通过一张图来看一下,如图20-1所示。
图20-1 模型和视图关联关系图
图中的模型(Model)就是data方法返回的{times:1},视图(View)是最终在浏览器中显示的DOM。模型通过Observer、Dep、Watcher、Directive等一系列对象的关联,最终和视图建立起关系。归纳起来,Vue.js在这里主要做了三件事:
接下来我们就结合Vue.js的源码来详细介绍这三个过程。
首先来看一下Vue.js是如何给data对象添加Observer的。我们知道,Vue实例创建的过程会有一个生命周期,其中有一个过程就是调用vm._initData方法处理data选项。_initData方法的源码定义如下:
<!--源码目录:src/instance/internal/state.js-->Vue.prototype._initData = function () {var dataFn = this.$options.datavar data = this._data = dataFn ? dataFn() : {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object.',this)}var props = this._props// proxy data on instancevar keys = Object.keys(data)var i, keyi = keys.lengthwhile (i--) {key = keys[i]// there are two scenarios where we can proxy a data key:// 1. it's not already defined as a prop// 2. it's provided via a instantiation option AND there are no// template prop presentif (!props || !hasOwn(props, key)) {this._proxy(key)} else if (process.env.NODE_ENV !== 'production') {warn('Data field "' + key + '" is already defined ' +'as a prop. To provide default value for a prop, use the "default" ' +'prop option; if you want to pass prop values to an instantiation ' +'call, use the "propsData" option.',this)}}// observe dataobserve(data, this)}
在_initData中我们要特别注意_proxy方法,它的功能就是遍历data的key,把data上的属性代理到vm实例上。_proxy方法的源码定义如下:
<!--源码目录:src/instance/internal/state.js-->Vue.prototype._proxy = function (key) {if (!isReserved(key)) {// need to store ref to self here// because these getter/setters might// be called by child scopes via// prototype inheritance.var self = thisObject.defineProperty(self, key, {configurable: true,enumerable: true,get: function proxyGetter () {return self._data[key]},set: function proxySetter (val) {self._data[key] = val}})}}
_proxy方法主要通过Object.defineProperty的getter和setter方法实现了代理。在前面的例子中,我们调用vm.times就相当于访问了vm._data.times。
在_initData方法的最后,我们调用了observe(data, this)方法来对data做监听。observe方法的源码定义如下:
<!--源码目录:src/observer/index.js-->export function observe (value, vm) {if (!value || typeof value !== 'object') {return}var obif (hasOwn(value, '__ob__') &&value.__ob__ instanceof Observer) {ob = value.__ob__} else if (shouldConvert &&(isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value)}if (ob && vm) {ob.addVm(vm)}return ob}
observe方法首先判断value是否已经添加了ob属性,它是一个Observer对象的实例。如果是就直接用,否则在value满足一些条件(数组或对象、可扩展、非vue组件等)的情况下创建一个Observer对象。接下来我们看一下Observer这个类,它的源码定义如下:
<!--源码目录:src/observer/index.js-->export function Observer (value) {this.value = valuethis.dep = new Dep()def(value, '__ob__', this)if (isArray(value)) {var augment = hasProto? protoAugment: copyAugmentaugment(value, arrayMethods, arrayKeys)this.observeArray(value)} else {this.walk(value)}}
Observer类的构造函数主要做了这么几件事:首先创建了一个Dep对象实例(关于Dep对象我们稍后作介绍);然后把自身this添加到value的ob属性上;最后对value的类型进行判断,如果是数组则观察数组,否则观察单个元素。其实observeArray方法就是对数组进行遍历,递归调用observe方法,最终都会调用walk方法观察单个元素。接下来我们看一下walk方法,它的源码定义如下:
<!--源码目录:src/observer/index.js-->Observer.prototype.walk = function (obj) {var keys = Object.keys(obj)for (var i = 0, l = keys.length; i < l; i++) {this.convert(keys[i], obj[keys[i]])}}
walk方法是对obj的key进行遍历,依次调用convert方法,对obj的每一个属性进行转换,让它们拥有getter、setter方法。只有当obj是一个对象时,这个方法才能被调用。接下来我们看一下convert方法,它的源码定义如下:
<!--源码目录:src/observer/index.js-->Observer.prototype.convert = function (key, val) {defineReactive(this.value, key, val)}
convert方法很简单,它调用了defineReactive方法。这里this.value就是要观察的data对象,key是data对象的某个属性,val则是这个属性的值。defineReactive的功能是把要观察的data对象的每个属性都赋予getter和setter方法。这样一旦属性被访问或者更新,我们就可以追踪到这些变化。接下来我们看一下defineReactive方法,它的源码定义如下:
<!--源码目录:src/observer/index.js-->export function defineReactive (obj, key, val) {var dep = new Dep()var property = Object.getOwnPropertyDescriptor(obj, key)if (property && property.configurable === false) {return}// cater for pre-defined getter/settersvar getter = property && property.getvar setter = property && property.setvar childOb = observe(val)Object.defineProperty(obj, key, {enumerable: true,configurable: true,get: function reactiveGetter () {var value = getter ? getter.call(obj) : valif (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()}if (isArray(value)) {for (var e, i = 0, l = value.length; i < l; i++) {e = value[i]e && e.__ob__ && e.__ob__.dep.depend()}}}return value},set: function reactiveSetter (newVal) {var value = getter ? getter.call(obj) : valif (newVal === value) {return}if (setter) {setter.call(obj, newVal)} else {val = newVal}childOb = observe(newVal)dep.notify()}})}
defineReactive方法最核心的部分就是通过调用Object.defineProperty给data的每个属性添加getter和setter方法。当data的某个属性被访问时,则会调用getter方法,判断当Dep.target不为空时调用dep.depend和childObj.dep.depend方法做依赖收集。如果访问的属性是一个数组,则会遍历这个数组收集数组元素的依赖。当改变data的属性时,则会调用setter方法,这时调用dep.notify方法进行通知。这里我们提到了dep,它是Dep对象的实例。接下来我们看一下Dep这个类,它的源码定义如下:
<!--源码目录:src/observer/dep.js-->export default function Dep () {this.id = uid++this.subs = []}// 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
Dep类是一个简单的观察者模式的实现。它的构造函数非常简单,初始化了id和subs。其中subs用来存储所有订阅它的Watcher,Watcher的实现稍后我们会介绍。Dep.target表示当前正在计算的Watcher,它是全局唯一的,因为在同一时间只能有一个Watcher被计算。
前面提到了在getter和setter方法调用时会分别调用dep.depend方法和dep.notify方法,接下来依次介绍这两个方法。depend方法的源码定义如下:
<!--源码目录:src/observer/dep.js-->Dep.prototype.depend = function () {Dep.target.addDep(this)}
depend方法很简单,它通过Dep.target.addDep(this)方法把当前Dep的实例添加到当前正在计算的Watcher的依赖中。接下来我们看一下notify方法,它的源码定义如下:
<!--源码目录:src/observer/dep.js-->Dep.prototype.notify = function () {// stablize the subscriber list firstvar subs = toArray(this.subs)for (var i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
notify方法也很简单,它遍历了所有的订阅Watcher,调用它们的update方法。
至此,vm实例中给data对象添加Observer的过程就结束了。接下来我们看一下Vue.js是如何进行指令解析的。