经过对 Vue2.0 源码阅读,想写一写本身的理解,能力有限故从尤大佬2016.4.11第一次提交开始读,准备陆续写:javascript
其中包含本身的理解和源码的分析,尽可能通俗易懂!因为是2.0的最先提交,因此和最新版本有不少差别、bug,后续将陆续补充,敬请谅解!包含中文注释的Vue源码已上传...html
在说双向绑定以前,咱们先聊聊单向数据流的概念,引用一下Vuex官网的一张图:vue
这是单向数据流的极简示意,即状态(数据源)映射到视图,视图的变化(用户输入)触发行为,行为改变状态。但在实际的开发中,大部分的状况是多个视图依赖同一状态,多个行为影响同一状态,Vuex的处理是将共同状态提取出来,转化成单向数据流实现。另外,在Vue的父子组件中prop传值中,也有用到单向数据流的概念,即父级 prop 的更新会向下流动到子组件中,可是反过来则不行。java
不管是react仍是vue都提倡单向数据流管理状态,那咱们今天要谈的双向绑定是否和单向数据流理念有所违背?我以为不是,从上篇文章AST语法树转render函数了解到,Vue双向绑定,实质是 value 的单向绑定和 oninput/onchange 事件侦听的语法糖。这种机制在某些须要实时反馈用户输入的场合十分方便,这只是Vue内部对 action 进行了封装而造成的。react
因此咱们今天要说是,状态的变化怎么引发视图的变化?git
今天涉及到的代码全在observer文件夹下。流程大体以下:github
function Vue (options) { // ... var data = options.data; data = typeof data === 'function' ? data() : data || {}; observe(data, this); Watcher(this, this.render, this._update); // ... }
先对 data 进行数据劫持(observe),而后为当前实例建立一个订阅者(Watcher)。具体如何实现,下面将逐一阐述。segmentfault
数据劫持的实质就是使用 defineProperty
重写对象属性的 getter/setter
方法。但因为defineProperty
没法监测到对象和数组内部的变化,因此遇到子属性为对象时,会递归观察该属性直至简单数据类型;为数组时的处理是重写push
、pop
、shift
等方法,方法内部通知订阅中心:状态变化了!这样就能对全部类型数据进行监听了。数组
咱们先看看入口函数observe()
:app
function observe (value, vm) { // 若检测数据不是对象,则退出 if (typeof value !== 'object') return; var ob; if (value.__ob__ && value.__ob__ instanceof Observer) { ob = value.__ob__; } else { ob = new Observer(value); } return ob; }
observe()
方法尝试为 value 建立观察者实例,观察成功则返回新的观察者或已有的观察者。__ob__
属性下面将提到,即对象被观察事后会有__ob__
属性,用于存储观察者实例。再来看看Observer
类:
function Observer (value) { this.value = value; // 给value对象经过defineProperty追加__ob__属性 def(value, '__ob__', this); // 特殊处理数组 if (Array.isArray(value)) { value.__proto__ = arrayMethods; value.forEach(item => { observe(item); }) } else { this.walk(value); } }
很明显看到,Observer
类除开属性的定义,就是对数组的特殊处理了。处理的方法是经过原型链去修改数组的push
、pop
、shift
...等等方法,固然,还须要对数组的每一个元素进行observe(),由于数组元素也多是对象,咱们要继续劫持,直到基本类型!咱们先来看下arrayMethods
具体是怎么修改的这些方法:
const arrayProto = Array.prototype; export const arrayMethods = Object.create(arrayProto); ['push','pop','shift','unshift','splice','sort','reverse'] .forEach(method => { // 拿到对应的原生方法 var original = arrayProto[method]; def(arrayMethods, method, () => { // 参数处理 var i = arguments.length; var args = new Array(i); while (i--) { args[i] = arguments[i]; } // 运行原生方法 var result = original.apply(this, args); var ob = this.__ob__; // 特殊处理数组插入方法 var 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; }) })
能看出arrayMethods
的构造其实也很简单,首先是根据数组的prototype
建立一个新对象,而后对数组方法进行逐个重写。方法重写的重点在于:
dep.notify()
到这,defineProperty
没法监听数组内部变化的问题解决了,固然,你经过数组下标修改内部数据仍是察觉不到的!
咱们继续来看,walk()
函数:
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]]); } } Observer.prototype.convert = function (key, val) { defineReactive(this.value, key, val); }
walk()
意思就是遍历对象的每一个属性,并侵占(convert
)它们的getter/setter
,接下来就是整个数据劫持的重点函数defineReactive()
:
function defineReactive (obj, key, val) { var dep = new Dep(); // 获取对象的对象描述 var property = Object.getOwnPropertyDescriptor(obj, key); // 是否可配置 if (property && property.configurable === false) return; // 获取原来的get、set var getter = property && property.get; var setter = property && property.set; // 递归:继续监听该属性值(只有val为对象时才有childOb) var childOb = observe(val); Object.defineProperty(obj, key, { enumerable: true, // 可枚举 configurable: true, // 可配置 get: ..., set: ... }) }
以上为defineReactive()
函数的内部结构,先定义了依赖中心Dep
,再获取对象的原生get/set方法,而后递归监听该属性,由于当前属性可能也是对象,最后经过defineProperty
劫持getter/setter
函数,依次看一下get/set
:
get: function reactiveGetter () { // 计算value var value = getter ? getter.call(obj) : val if (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; }
你们看完这个函数,除开if
语句,其余的都是get
的基本逻辑。至于Dep.target
的含义,个人理解是它就像一个开关,当开关在打开的状态下访问该属性,则会被添加到订阅中心。至于何时开关打开、关闭,以及把谁添加到订阅中心,先留下疑问。继续看下set
:
set: function reactiveSetter (newVal) { // 计算value var value = getter ? getter.call(obj) : val; // 新旧值是否相等 if (newVal === value) return; // 不相等,设置新值 if (setter) { setter.call(obj, newVal); } else { val = newVal; } // 劫持新值 childOb = observe(newVal); // 发送变动通知 dep.notify(); }
set
也比较好理解,先是新旧值的比较,若不相等,则须要:设置新值,劫持新值,发布通知。
到这,数据劫持就完成了。总之,observe
对数据对象进行了递归遍历,递归包括数组和子对象,将每一个属性的getter/setter
进行了改造,使得在特殊状况下获取值(xxx.name
)会添加到订阅中心,在设置值(xxx.name = 'Tom'
)会触发订阅中心的通知事件。
订阅中心也就是前面提到的Dep
,它要作的事情很简单,维护一个容器(数组)存储订阅者,也就是说它有添加订阅者功能和发布通知功能。简单看一下:
let uid = 0; function Dep () { this.id = uid++; this.subs = []; } // 添加订阅者 Dep.prototype.addSub = function (sub) { this.subs.push(sub); } // 将本身做为依赖传给目标订阅者 Dep.prototype.depend = function () { Dep.target.addDep(this); } // 通知全部订阅者 Dep.prototype.notify = function () { var subs = this.subs.slice(); for (var i = 0, l = subs.length; i < l; i++) { subs[i].update(); } } Dep.target = null;
数据劫持中提到,当Dep.target
存在时调用get
,会触发dep.depend()
添加订阅者,那么这个Dep.target.addDep()
方法里确定含添加订阅者addSub()
方法。
注意Dep.target
的默认值为null
。
订阅者也就是前面提到的Watcher
,由于它也用于$watch()
接口,因此这边对其简化分析。
Watcher
接收3个参数,vm
:Vue实例对象,fn
:渲染函数,cb
:更新函数。先看看Watcher
对象:
function Watcher (vm, fn, cb) { this.vm = vm; this.fn = fn; this.cb = cb; this.depIds = new Set(); this.value = this.get(); } // 向当前watcher添加依赖项 Watcher.prototype.addDep = function (dep) { var id = dep.id; // 防止重复向订阅中心添加订阅者 if (!this.depIds.has(id)) { this.depIds.add(id); dep.addSub(this); } }
Watcher
的addDep()
方法内为了防止重复添加订阅者到订阅中心,故维护了一个Set
用于存储订阅中心(Dep
)的id,每次添加前看是否已存在。Watcher
在初始化时,执行了get()
函数,看看方法内部:
Watcher.prototype.get = function () { // 打开开关,指向自身(Watcher) Dep.target = this; // 指向渲染函数,会触发getter var value = this.fn.call(this.vm); // 关闭开关 Dep.target = null; return value; }
以前一直不理解这边为何会将订阅者推入各个订阅中心,后来才发现巧妙的地方:Dep.target
指向当前Watcher
(打开开关),而后执行渲染函数,渲染函数用到的数据都会触发其get
,这样就把当前Watcher
加入到这些数据的订阅中心了!而后Dep.target = null
(开关关闭)。
另外还有一个就是update
函数,也就是数据的set
被触发是,其订阅中心会发布通知(notify()
),而notify()
方法的本质就是依次执行订阅者的update()
方法。让咱们看一下:
Watcher.prototype.update = function () { var value = this.get(); if (value !== this.value) { var oldValue = this.value; this.value = value; this.cb.call(this.vm, value, oldValue); } }
update()
方法其实就是拿新值和旧值比较,若是不同就把它们做为参数,执行更新回调函数。
到这,关于订阅者部分的已经说完了。再回看到前面的调用Watcher(this, this.render, this._update);
,这边的渲染函数也就是前篇文章讲的render
函数,而_update
函数是用于比较vdom
并更新的函数,这是下一篇文章要说的内容。
最后再来理一遍,observe
递归遍历整个data
,给每一个属性建立一个订阅中心,并且重写他们的getter/setter
方法:在特殊状况(Dep.target
存在)下get
会添加订阅者到订阅中心,在set
时会通知订阅中心,继而通知每位订阅者;订阅者会特殊状况(Dep.target
存在)下,执行render
函数,get
每个涉及到的数据。这样,之后只要有数据发生变更,就会触发该订阅者的更新函数,就会引发dom
的变化!
最近工做比较忙,博客写的比较慢,可能也会有各类问题(┬_┬)...
溜了溜了