## 响应式原理
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(),循环该数据订阅器中的观察者,并进行更新操作。
- 空白目录
- 双樾
- JS基础知识
- JS-WEB-API
- 开发环境
- 运行环境
- ES6
- 原型
- 异步
- 虚拟dom
- mvvm
- 组件化和React
- hybrid
- 其他
- 补充
- 技巧
- 快乐动起来呀
- css
- 掘金小册子
- js基础知识
- ES6知识点
- JS异步
- JS进阶知识
- 思考题
- DevTools Tips
- 浏览器基础知识
- 浏览器缓存机制0
- 浏览器渲染原理
- 安全防范知识点0
- 从V8中看JS性能优化0
- 性能优化琐碎事
- Webpack性能优化0
- 实现小型打包工具0
- React和Vue
- Vue生命周期
- vue基础知识点
- Vue响应式
- vue高级
- React基础
- Vue.js技术解密
- 准备工作
- 数据驱动
- new Vue()
- vue实例挂载
- 组件化
- 深入响应式原理
- 编译
- 扩展
- Vue Router
- Vuex