🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
## 响应式原理 Vue内部使用了**Object.defineProperty()**来实现数据响应式,通过这个函数可以监听到set和get的事件 ~~~ var data = { name: 'yck'}; observe(data); let name = data.name; //let data.name = "yyy" //set function observe(obj){ //判断类型 if(!obj || typeof obj !== 'object') { return; } Object.keys(obj).forEach(key =>{ defineReactive(obj, key, obj[key]) }) } function defineReactive(obj, key, val){ //递归子属性 observe(val) Object.defineProperty(obj, key, { //可枚举 enumerable: true, //可配置 configurable: true, //自定义函数 get: function reactiveGetter(){ console.log('get value'); return val; }, set: function reactiveSetter(newVal){ console.log('change value'); val = newVal; } }) } ~~~ 以上代码简单的实现了如何监听数据的set和get,但是仅仅如此是不够的,因为自定义的函数一开始是不会执行的。只有先执行了依赖收集,才能在属性更新的时候派发更新,所以接下来我们需要先**触发依赖收集**。 ~~~ <div>{{name}}</div> ~~~ 在解析如上模板代码时,遇到{{name}}就会进行依赖收集。 接下来我们先来实现一个Dep类,用于**解耦属性的依赖收集**和**派发更新操作**。 ~~~ //通过Dep解耦属性的依赖和更新操作 class Dep{ constructor(){ this.subs = [] } //添加依赖 addSub(sub){ this.subs.push(sub) } //更新 notify(){ this.subs.forEatch(sub =>{ sub.update() }) } } //全局属性,通过该属性配置Watcher Dep.target = null ~~~ 当需要依赖收集的时候调用addSub,当需要派发更新的时候调用notify。 Vue组件挂载时添加响应式的过程:在**组件挂载**时,会先对所有**需要的属性**调用**Object.defineProperty()**,然后实例化**watcher**,传入**组件更新的回调**。在实例化的过程中,会对模板中的**属性进行求值,触发依赖收集**。(取值时,把所以取到的属性放入依赖中) 触发依赖收集时的操作: ~~~ class Watcher{ constructor(obj, key, cb){ // 将Dep.target指向自己 //然后触发属性的getter添加监听 //最后将Dep.target置空 Dep.target = this; this.cb = cb; this.obj = obj; this.key =key; this.value = obj[key]; Dep.target = null; } update(){ //获得新值 this.value = this.obj[this.key] //调用update方法更新dom this.cb(this.value); } } ~~~ 以上就是Watcher的简单实现,在执行构造函数的时候,将**Dep.target指向自身,从而使得收集到了对应的Watcher**,在**派发更新的时候取出对应的Watcher然后执行update函数**。 ## Vue数据绑定源码 数据的双向绑定:数据变化了自动更新视图,视图变化了自动更新数据,实际上视图变化更新数据只需要通过事件监听,并不是数据双向绑定的关键点。关键还是数据变化了驱动视图自动更新。 原理是Object.defineProperty()对属性设置一个set/get,get/set只是可以做到**对数据的读取进行劫持**,就可以让我们知道数据更新了。 ![](https://box.kancloud.cn/e0a25bf9129ec5e161abe15979cc3a85_1207x742.png) * Observe类**劫持监听所有属性**,主要给响应式对象的属性添加getter/setter用于**依赖收集与派发更新**。Observer是用来给数据添加Dep依赖(目前我的理解:data中的每个属性,会被很多其他属性依赖(data中属性变化,这些依赖随之改变)) * Dep类用于**收集当前响应式对象的依赖关系**。Dep是data每个对象包括子对象都拥有一个该对象,当所绑定的数据有变更时,通过dep.notify()通知watcher。 * Watcher类是观察者,实例分为**渲染watcher**,**计算属性watcher**,**侦听器watcher**三种 ### defineReactive 它给对象的键值添加get/set方法,也就是对属性的取值和赋值都加了拦截,同时用闭包**给每个属性都保存了一个Dep对象**。 当读取该值的时候,就把当前这个watcher(Dep.target)添加进它的dep里的观察者列表,这个watcher也会把这个dep添加进它的依赖列表。 对于Observe,watcher,dep不是很理解,就多翻阅了资料进行学习: Vue最独特的特性之一,是其非侵入性的响应式系统。数据模型仅仅是普通的JavaScript对象,而当你修改它们时,视图会进行更新,这使得状态管理非常简单直接,我们可以只关注数据本身,而不用手动处理数据到视图的渲染,避免了繁琐的DOM操作,提供了开发效率 三个重要的类:Dep类,Watcher类,Observer类,然后使用**发布订阅模式**的思想将它们柔和在一起。 **观察者模式**:定义了对象间一种一对多的依赖关系,当一个对象的状态发生改变时,所有依赖于它的对象都将得到通知,并且自动更新。观察者模式有一个别名叫“发布-订阅模式”,或者“订阅-发布模式”,订阅者和订阅目标是联系在一起的,当订阅目标发生改变时,逐个通知订阅者。 **发布订阅模式(Pub-Sub Pattern)**:只是观察者模式的一个别称,但经过时间的沉淀,已经强大起来,独立于观察者,成为另一种不同的设计模式。 在现在的发布订阅模式中,称为**发布者的消息发送者不会将消息直接发送给订阅者**,意味着,发布者和订阅者不知道彼此的存在,在发布者和订阅者之间存在第三个组件,成为**消息代理或调度中心,或中间件**,它维持着发布者和订阅者之间的联系,**过滤所有发布者传入的消息并相应的分发他们给订阅者**。 举例:微博关注了A,同时其他很多人也关注了A,那么A发布动态的时候,微博就会为你们推送这条动态,A就是发布者,你是订阅者,微博就是调度中心,你和A是没有直接消息往来的。 ![](https://box.kancloud.cn/e55d72940f30a1a993623d67d741175b_846x551.png) 观察者模式:观察者(Observer)直接订阅(Subscribe)主题(Subject),而当主题被激活的时候,会触发观察者里的事件。 发布订阅模式:订阅者(Subscriber)把自己想订阅的事件注册(Subscribe)到调度中心(Topic),当发布者(Publisher)发布该事件(Publish topic)到调度中心,也就是该事件触发时,由调度中心统一调度(Fire Event)订阅者注册到调度中心的处理代码。 观察者模式和发布订阅者模式最大的区别就是**发布订阅模式有个事件调度中心**。 ![](https://box.kancloud.cn/2f4a5649a8287b271ce56d31fd2e51eb_894x197.png) **Observer**: Observe扮演的角色是发布者,主要作用是调用defineReactive函数,在defineReactive函数中使用Object.defineProperty方法对对象的每一个子属性进行数据劫持/监听。 defineReactive函数,Observe的核心,劫持数据,**在setter中向Dep(调度中心)添加观察者,在getter中图纸观察者更新**。 ~~~ function defineReactive(obj, key, val, customSetter, shallow){ //监听属性key //关键点:在闭包中声明一个Dep实例,用于保存watcher实例 var dep = new Dep(); var getter = property && property.get; var setter = property && property.set; if( !getter && arguments.length ===2){ val = obj[key] } //执行observe,监听属性key所代表的值val的子属性 var childOb = observer(val); Object.defineProperty(obj, key, { enumerable: true, configurable: true, get :function reactiveGetter(){ //获取值 var value = getter?getter.call(obj):val; //依赖收集,有个当前有活动的Dep.target(观察者--watcher实例) if(Dep.target){ //将dep放进当前观察者的deps中,同时,将该观察者放入dep中,等待变更通知。 dep.depend(); if(childOb){ //为子属性进行依赖收集 //其实就是将同一个watcher观察者实例放进了两个dep中,一个是正在本身闭包中的dep,另一个是子属性的dep childOb.dep.depned() } } return value; } set: function reactiveSetter(newVal){ //获取value var value=getter?getter.call(obj):val; if(newVal === value || (newVal!== newVal && value !==value)){ return; } if(setter){ setter.call(obj,newVal); }else{value = newVal;} //新的值需要重新进行observe,保证数据响应式 childOb = observe(newVal); // 关键点: 遍历dep.subs,通知所有的观察者 dep.notify(); } }) } ~~~ **Dep** Dep扮演的角色是调度中心/订阅器,**主要作用是收集观察者Watcher和通知观察者目标更新**。 每个属性用于自己的消息订阅器dep,用于**存放所有订阅了该属性的观察者对象**。当数据发生改变时,会遍历**观察者列表(dep.subs)**,通知所有的watch,让订阅者执行自己的update逻辑。 Dep的设计比较简单,**就是收集依赖,通知观察者**。 ~~~ var Dep = function Dep(){ this.id = uid++; this.subs=[]; } //向dep的观察者列表subs添加观察者 Dep.prototype.addSub = function addSub(sub){ this.subs.push(sub); } //从dep的观察者列表subs移除观察者 Dep.protptype.removeSub = function(sub){ remove(this.subs,sub); } Dep.prototype.depend = function(){ //依赖收集,如果当前有观察者,将该dep放进当前观察者的deps中, //同时,将当前观察者放入观察者列表subs中。 if(Dep.target){ Dep.target.addDeps(this); } } Dep.prototype.notify = function(){ //循环处理,运行每个观察者的update接口 var subs = this.subs.slice(); for(var i=0,l = subs.length; i<l;i++){ subs[i].update(); } } Dep.target是观察者,这个是全局唯一的,因为在任何时候只有一个观察者被处理 Dep.target = null; //待处理的观察者队列 var targetStack =[]; function pushTarget(_target){ if(Dep.target){ trgetStack.push(Dep.target); } //将Dep.target指向需要处理的观察者 Dep.target = _target; } function popTarget(){ //将Dep.target指向栈顶的观察者,并将他移除队列 Dep.target = targetStack.pop(); } ~~~ **Watcher**: Watcher扮演的角色是订阅者/观察者,他的主要作用是为观察属性提供回调函数以及收集依赖(如计算属性computed,vue会把该属性所依赖的数据的dep添加到自身的deps中),当被观察的值发生变化时,会接收到来自dep的通知,从而触发回调函数。 Watcher类的实现比较复杂,因为他的实例分**为渲染watcher(render-watcher),计算属性watcher(computed-watcher),侦听器watcher(normal-watcher)**三种,这三个实例分别在三个函数中构建:**mountComponent,initComputed和Vue.protptype.$watch** normal-watcher:我们在组件钩子函数watch中定义,都属于这种类型,即只要监听的属性发生改变,都会触发定义好的回调函数 computed-watcher:我们在组件钩子函数computed中定义的,都属于这种类型,每一个computed属性,最后都会生成一个对应的watcher对象,特点:**当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备lazy(懒计算)特性**。 render-watcher:每一个组件都会有一个render-watcher,当data/coumputed中属性改变的时候,会调用该render-watcher来更新组件的视图。 ~~~ function(){vm._update(vm.render(),hydrating)} ~~~ 这三种watcher也有固定的执行顺序,分别是computed-render-->normal watcher -->render-watcher 原因:尽可能保证,在更新组件视图的时候,computed属性已经是最新值了,否则可能导致页面更新的时候computed值为旧数据。 ~~~ function Watcher(vm, expOrFn, cb,options, isRenderWatcher){ this.vm = vm; if(isRenderWatcher){ vm._watcher = this; } vm._watchers.push(this); if(options){ //是否启用深度遍历 this.deep = !! options.deep; // 主要用于错误处理,侦听器watcher的user为true,其他均为false this.user = !! options.user; //惰性求值,当属于计算属性watcher时为true // 标记为同步计算,三大类型暂无 this.sync =!! options.sync; }else{ this.deep = this.user = this.lazy = this.sync = false; //初始化各种属性和option //观察者的回调 //除了侦听器watcher外,其他大多为空函数 this.cb = cb; this.id = ++uid$1;//uid for batching this.active = tue; this.dirty = this.lazy;//for lazy watcher; this.deps = []; this.newDeps =[]; this.depIds = new _Set(); this.newDepIds = new _Set(); this.expression = expOrFn.toString(); //解析expOrFn,赋值给this.getter //当时渲染watcher时,expOrFn时updateComponent,即重新渲染执行render(_update) //当是计算watcher时,expOrFn时计算属性的计算方法 //当是侦听器watcher时,expOrFn是watch属性的名字,this.cb就是watch的handler属性 //对于渲染watcher和计算watcher来说,expOrFn的值是一个函数,可以直接设置setter //对于侦听器watcher来说,expOrFn是watch属性的名字,会使用parsePath函数解析路径,获取组件上该属性的值(运行getter) //依赖(订阅目标)更新,执行update,会进行取值操作,运行watcher.getter,也就是expOrFn函数 if(typeof expOrFn === 'function'){ this.getter = expOrFn; }else{ this.getter = parsePatch(expOrFn); } this.value = this.lazy? undefined:this.get(); } } //取值操作 Watcher.prototype.get = function(){ //Dep.target设置为观察者 pushTarget(this); var vm = this.vm; //取值 var value = this.getter.call(vm,vm) //移除该观察者 popTarget() return value; } Watcher.protyotype.addDep = function(dep){ var id = dep.id; if(!this.newDepsIds.has(id)){ //为观察者的deps添加依赖dep this.newDepIds.add(id); this.newDeps.push(dep); if(!this.depIds.has(id)){ //为dep添加该观察者 dep.addSub(this); } } } //当一个依赖改变的时候,通知它update Watcher.prototype.update = function(){ //三种watcher,只有计算属性watcher的lazy设置了true,表示启用惰性求值 if(this.lazy){ this.dirty = true; }else if(this.sync){ //标记为同步计算的直接运行run,三大类型暂无 this.run(); }else{ //将watcher推入观察者队列中,下一个tick时调用 //也就是数据变化不是立即就去更新的,而是异步批量去更新的。 queueWatcher(this); } } //update执行后,运行回调cb Watcher.prototype.run =funtion(){ if(this.active){ var value = this.get(); if( value != this.value || isObject(value) || this.deep ){ var oldValue = this.value; this.value = value; //运行cb函数,这个函数就是之前传入的watch中的handle回调函数 if(this.user){ try{ this.cb.call(this.vm, value, oldValue) }catche(e){ handleError(e, this.vm,('callback for watcher' +(this.expression))) } }else{ this.cb.call(this.vm, value, oldValue); } } } } //对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty时true //说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。 Watcher.prototype.evalute = function evaluate(){ this.value = this.get(); this.dirty = false; } //收集依赖 Watcher.prototype.depend = function(){ var this$1 = this; var i = this.deps.length; while(i--){ this$1.deps[i].depend(); } } ~~~ **也就是数据变化不是立即就去更新的,而是异步批量去更新的**。 **//对于计算属性,当取值计算属性时,发现计算属性的watcher的dirty时true //说明数据不是最新的了,需要重新计算,这里就是重新计算计算属性的值。** 总结: Observe是对数据进行监听,Dep是一个订阅器,每一个被监听的数据都有一个Dep实例,Dep实例里面存放了N多个订阅者(观察者)对象watcher 被监听的数据进行取值操作时(getter),如果存在Dep.target(某一个观察者),则说明这个观察者是依赖该数据的(如计算属性中,计算某一个属性会用到其他已经被监听的数据,就说该属性依赖于其他属性,会对其他属性进行取值),就会把这个观察者添加到该数据的订阅器subs里面,留待后面数据变更时通知(会先通过观察者id判断订阅器中是否已经存在该观察者),同时该观察者也会把该数据的订阅器dep添加到自身deps中,方便其他地方使用。 被监听的数据进行赋值操作时(setter),就会触发dep.notify(),循环该数据订阅器中的观察者,并进行更新操作。