Vue版本: vue@2.5.10,简单记录自己的学习过程

目录

实现双向绑定的方式区别

Vue实现数据劫持的方式

了解defineProperty

Observer/Dep/Watcher三者实现

1. Observer

2. Dep

3.Watcher


实现双向绑定的方式区别

脏值检测(angularjs)

这种模式下,使用之只有触发指定的事件进入脏值检测,才会进行双向数据绑定更新,一般是通过预先定义的事件行为触发,这也就是在angularjs中,使用普通的dom操作更改input的内容后,没有办法直接更新model的主要原因.

数据劫持(vue.js)

这种模式下,通过defineProperty()为每个属性在setter/getter添加拦截监听,不论以何种方式(vue预定义事件或者原生操作)变动或获取该属性,均可被监听到.必须注意的是,只有设置了setter/getter拦截的属性才可被监听,这也就是当向component的data根节点插入新的属性时,不会被监听的原因.因为新设置的属性没有设置相应的拦截.

Vue实现数据劫持的方式

了解defineProperty

首先了解Object.defineProperty()方法:defineProperty;

也就是说,object的每一个属性均可设置setter&getter,当数据更新和数据调用时,相应方法会被调用.实现数据劫持的核心思想,就是在可能会变化的Object的每个属性的setter&getter中设置订阅和更新,从而触发相应的watcher事件,实现双向绑定.

Observer/Dep/Watcher三者实现

1. Observer

先看一下源码中如何定义: observer/index.js

1.1. Observer中存在三个属性:

  • value:需要观察的数据
  • dep:数据订阅和通知watcher的事件处理方
  • vmCount:一个计数器

1.2. constructor

constructor (value: any) {
    this.value = value //设置属性value
    this.dep = new Dep() //设置属性dep
    this.vmCount = 0 //设置属性vmCount
    def(value, '__ob__', this) //为观察数据添加观察者引用
    if (Array.isArray(value)) {//观察数据为Array时的处理方式(observeArray),实现数组每项的观察
      const augment = hasProto ? protoAugment : copyAugment
      augment(value, arrayMethods, arrayKeys)
      this.observeArray(value)
    } else {//观察数据为Object时的处理方式(walk),实现每个属性的观察
      this.walk(value)
    }
  }

1.3. 观察数组时的处理方式:observeArray

对每项使用 observe方法为其创建observer.这里的处理方式经过递归,可以与处理object合并

export function observe (value: any, asRootData: ?boolean): Observer | void {...if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {ob = value.__ob__ // 如果value已经有对应的observer} else if (observerState.shouldConvert &&!isServerRendering() &&(Array.isArray(value) || isPlainObject(value)) &&Object.isExtensible(value) &&!value._isVue) {ob = new Observer(value) //用value创建新的observer}...return ob
}

1.4. 观察对象时的处理方式:walk

针对每个属性使用defineReactive,为其设置了setter/getter

export function defineReactive (obj: Object, key: string, val: any, customSetter?: ?Function, shallow?: boolean){const dep = new Dep()...let childOb = !shallow && observe(val)Object.defineProperty(obj, key, {...get: function reactiveGetter () {...if (Dep.target) {dep.depend()if (childOb) {childOb.dep.depend()//针对本属性的value值调用item.dep.depend()if (Array.isArray(value)) {dependArray(value) //本属性value值是array时,就对array每一项分别调用item.dep.depend()}}}return value},set: function reactiveSetter (newVal) {...dep.notify()//通知watcher更新}})
}

1.5. 总结

可以看出,针对对象的每一个属性,其getter/setter处理中附加的处理包含两部分:

  1. 在getter中,调用value相应obsever的dep.depend(),如果value是array,还会依次调用各项对应的obsever的dep.depend();
  2. 在setter陈宫执行之后,调用dep.notify().

以上是observer的主要实现内容,总的来说,针对一个value,建立对应的observer步骤是:

  1. 判断value是Object还是Array;
  2. 如果是Array,那么针对每一项建立对应的Observer,该问题回归到value是Object的情况;
  3. 如果是Object,那么针对每一个属性,在其setter中使用dep.notify()通知到watcher更新,在其getter 中使用dep.depend()添加订阅.

2. Dep

Dep是订阅者Watcher对应的数据依赖,Dep源码:observer/dep.js

2.1. Dep 内部属性:

  1. target: static watcher 当前正在计算的watcher
  2. id:number
  3. subs: Array<Watcher>;

2.2 constructor:

constructor () {this.id = uid++ //初始化id和subs,但是并未操作targetthis.subs = []
}

2.3. 在observer中被调用的两个方法: depend/notify:

depend () {if (Dep.target) {Dep.target.addDep(this)//说明在Dep.target存在时,dep.depend()相当于target.addDep(this);//也就是说,在observer对应getter触发时,会将observer的dep通过addDep(),添加到Dep.target中,具体实现可以查看Watcher源码}
}notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()//将当前dep事例中的subs各项全部更新//也就是说,当observer对应setter触发时,会向前期对应的dep中全部watcher发送更新消息,具体事项可以查看Watcher源码}
}

2.4 Dep.target:

(这部分我也还比较困惑,先简单记录一下自己的理解)

// 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
const targetStack = []export function pushTarget (_target: Watcher) {if (Dep.target) targetStack.push(Dep.target)Dep.target = _target
}export function popTarget () {Dep.target = targetStack.pop()
}

上面这部分代码主要实现两个方法:pushTarget/popTarget;

当push被调用的时候,会将要push的watcher实例赋值给Dep.target并且加入队列中;当pop被调用时,会将最近的一个Watcher出栈并赋值到Dep.target.

这两个方法均在Watcher.js中被调用,也可以看出,watcher在定义或执行过程会控制Dep中的target更新,具体还需要查看watcher代码来证实.

2.5. 总结

  1. Dep中存在id和subs两个实例属性,在构造函数中被初始化.通过addSub/removeSub更新subs,
  2. Dep还存在一个全局属性:target,代表当前处理的watcher,通过watcher中调用pushTarget/popTarget来进行更新,.
  3. 在observer触发属性上的getter时,会将对应的dep添加到当前处理的watcher(Dep.target)中;
  4. 在observer触发属性上的setter时,通知对应的dep的订阅者数据更新

3.Watcher

订阅者,通过Dep,在observer执行getter时添加订阅关系.源码:observer/watcher.js

这部分的理解参考了:https://juejin.im/post/5b5fc363f265da0fad0d1611

官方注释:watcher解析表达式,收集依赖项,并在表达式值更改时激发回调。被用于$watch()api和vue directives。

先简单了解一下watcher.js内容,再逐步了解其实现:

3.1. 属性:

vm: Component;
expression: string;
cb: Function;
id: number;
deep: boolean;
user: boolean;
lazy: boolean;
sync: boolean;
dirty: boolean;
active: boolean;
deps: Array<Dep>;//watcher需要订阅的一系列observer.dep
newDeps: Array<Dep>;//获取value值过程中临时变量,用于去重
depIds: SimpleSet;//watcher需要订阅的一系列observer.dep
newDepIds: SimpleSet; //获取value值过程中临时变量,用于去重
getter: Function; //获取value监听值方法
value: any; // watcher监听值

3.2. constructor

constructor (vm: Component,//相当于this.$watch中的thisexpOrFn: string | Function,//要监听的表达式cb: Function,//监听回调options?: ?Object,//监听配置(immediate等等)isRenderWatcher?: boolean
) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}vm._watchers.push(this)// optionsif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.sync} else {this.deep = this.user = this.lazy = this.sync = false}this.cb = cbthis.id = ++uid // uid for batchingthis.active = truethis.dirty = this.lazy // for lazy watchersthis.deps = []this.newDeps = []this.depIds = new Set()this.newDepIds = new Set()this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''// parse expression for getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = function () {}process.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}this.value = this.lazy? undefined: this.get()
}

在constructor中,有一系列的初始化过程,比较重要的代码是两部分:

1. this.getter赋值,可以看出,调用this.getter可以获取到要监听的值,举个例子:

export default {data () {return {msg: 'msg'}},computed: {upperMsg () {return this.msg.toUpperCase()}}
}

上面这样一个computed,根据constructor代码,其创建的watcher中getter=upperMsg.

2. this.value赋值,只有当非lazy的时候,才会执行this.get获取监听值value(也就是upperMsg的执行结果:'MSG')

总结来说,当非lazy时, constructor会调用this.get获取value值.下面会解释get实现了什么.

3.3.  addDep()

在了解get函数前,需要先了解几个会被用到的函数,先看一下源码:

 /*** Add a dependency to this directive.*/addDep (dep: Dep) {const id = dep.idif (!this.newDepIds.has(id)) {this.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {dep.addSub(this)}}}

可以看出,方法中只在操作this.newDepIds,this.newDeps,this.depIds三个属性,执行逻辑可以解释为:

  1. 将传入的dep实例添加到newDeps中并去重,newDepIds记录了newDeps的id列表;
  2. 为传入的dep实例添加订阅者this并去重

这里简单了解了这个函数的实现内容,置于为什么需要去重以及操作的newDeps等属性具体作用,稍后了解.

3.4. cleanupDeps()

这个函数会把 newDepIds 的值赋给 depIds,然后把 newDepIds 清空

/*** Clean up for dependency collection.*/cleanupDeps () {let i = this.deps.lengthwhile (i--) {const dep = this.deps[i]if (!this.newDepIds.has(dep.id)) {dep.removeSub(this)}}let tmp = this.depIdsthis.depIds = this.newDepIdsthis.newDepIds = tmpthis.newDepIds.clear()tmp = this.depsthis.deps = this.newDepsthis.newDeps = tmpthis.newDeps.length = 0}

这个方法执行逻辑可以被分割成两部分:

  1. 当deps和newDeps存在相同dep,说明本watcher被两次添加到了dep的订阅列表中,那么取消其中一次订阅;
  2. 把newDeps/newDepIds的值赋给deps/depIds,并将newDeps/newDepIds清空.

3.5. get()

了解上面两个方法后,再看一下get实现代码

/*** Evaluate the getter, and re-collect dependencies.*/
get () {pushTarget(this) // YT: 先将target设为自己let valueconst vm = this.vmtry {value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {// "touch" every property so they are all tracked as// dependencies for deep watchingif (this.deep) {traverse(value) // YT: 就是简单递归遍历收集一下所有的__ob__.dep.id,这就是"touch"的含义,但是实际并没有做更改的操作}popTarget() // YT: 将target设为原值this.cleanupDeps()}return value
}

上述代码在不考虑进入catch以及deep的情况,可以简化为几个主要步骤:

pushTarget(this)
value = this.getter.call(vm, vm)
popTarget()
this.cleanupDeps()
return value

通过之前对Dep的学习,可以知道,Dep存在一个static的属性target,代表着当前正在计算的watcher实例.通过在watcher中调用函数pushTarget/popTarget更改.那么这里执行的几步操作,翻译一下就是:

1. 将target设为自身,

2. 计算要监听的值,(this.getter)

3. 再将target还原,

4. 清空deps,(this.cleanupDeps)

5. 返回监听值

由于getter===upperMsg,2.中实际是在调用upperMsg,也就是 {return this.msg.toUpperCase()};

这意味着,此处会调用到observer为msg设置的get,这里在贴出get部分代码:

 get: function reactiveGetter () {...if (Dep.target) {dep.depend()...}...},

在执行这部分代码时,由于1.设置了Dep.target=this,所以相当于直接执行dep.depend(),这里再贴出depend代码:

 depend () {if (Dep.target) {Dep.target.addDep(this)}}

同理,执行这部分代码,等价于执行watcher.addDep(dep);

那么总结以上代码逻辑可以了解到,在执行2.时,不但获取到了this(当前watcher)要监听的value;而且还调用this.addDep方法,将value中对应的observer.dep添加到了this.newDeps中,并且为该dep添加订阅者this.

到此时,我们已经了解的有以下内容:

  1. watcher创建时,如果非lazy,那么会直接调用get获取value值并收集依赖,否则会在取值时第一次调用get获取value值和收集依赖;
  2. 调用getter获取value值过程中,会收集该value值的依赖dep,并为各dep添加本watcher到订阅列表

3.6. update()

上面的讨论过程解释了当获取一个observer值的时候,watcher是怎样收集依赖的,而要了解在改变observer值的时候,是怎样通知到watcher,就需要了解update函数.

回顾observer的设置set的代码:

set: function reactiveSetter (newVal) {...dep.notify()}

主要调用的notify函数如下:

notify () {// stabilize the subscriber list firstconst subs = this.subs.slice()for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}

所以在触发observer的setter时候,相当于触发了订阅其的watcher.update().

下面看一下update实现:

  /*** Subscriber interface.* Will be called when a dependency changes.*/update () {/* istanbul ignore else */if (this.lazy) {this.dirty = true} else if (this.sync) {this.run()} else {queueWatcher(this)}}

理解这段代码: 在不考虑lazy情况时,sync时,执行run(),async时,执行queueWatcher();前者会调用cb.call()通知回调函数value变化.后者会将watcher条能加到一个队列中,当nextTick时,在执行相关操作.

Vue 源码学习过程 - Observer/Dep/Watcher三者实现数据双向绑定相关推荐

  1. Vue源码之计算属性watcher

    在之前的文章<Vue源码分析基础之响应式原理>和<Vue源码实现之watcher拾遗>中,我们学习了watcher的实现原理.紧跟着这几天准备花点时间学习下watcher在vu ...

  2. Vue源码分析 - observer.js

    观察者模式一般包含发布者(Publisher)和订阅者(Subscriber)两种角色:顾名思义发布者负责发布消息,订阅者通过订阅消息响应动作了. 回到Vue中,在Vue源码core/oberver目 ...

  3. vue源码解析observer

    function Observer(data) {//在Observer实例上暂存datathis.data = data;this.walk(data); }Observer.prototype = ...

  4. Vue源码之渲染watcher

    1. 前文回顾 在上一篇文章<Vue源码之计算属性watcher>中,我们学习了计算属性watcher是如何与计算属性的computedGetter协作,在计算属性所依赖的数据发生变化时, ...

  5. vue源码:Watcher系列(一)

    少年驰骋,仗剑天涯 愿你眼眸有星辰,心中有大海 从此,以梦为马,不负韶华 在分析之前我们先来看看,vue中都有哪些Watcher种类呢?以及分别在什么时候创建呢?从vue源码里面看,Watcher是一 ...

  6. Vue源码实现之watcher拾遗

    目录 1. Watcher构造函数参数options和渲染watcher标志位 2. watcher收集的新老依赖deps和newDeps的作用 3. watcher中getter的目的就是去touc ...

  7. Vue源码学习之Computed与Watcher原理

    前言  computed与watch是我们在vue中常用的操作,computed是一个惰性求值观察者,具有缓存性,只有当依赖发生变化,第一次访问computed属性,才会计算新的值.而watch则是当 ...

  8. Vue源码之用户watcher

    通过上一篇文章<Vue源码之渲染watcher>我们学习到了每个组件实例初始化时都会创建一个渲染watcher来监控页面引用到的响应式数据的变动,一旦数据发生变化,就会通知渲染watche ...

  9. vue源码分析系列二:$mount()和new Watcher()的执行过程

    续vue源码分析系列一:new Vue的初始化过程 在initMixin()里面调用了$mount() if (vm.$options.el) {vm.$mount(vm.$options.el);/ ...

  10. Vue源码分析-手写Vue(简易版)

    1.Vue双向绑定/MVVM响应式原理/v-model的原理 vue.js通过数据劫持结合发布订阅者模式,通过Object.defineProperty来劫持各个属性的setter,getter,在数 ...

最新文章

  1. 算法导论 第二部分——排序和顺序统计量
  2. 计算体系结构的演进规律
  3. 软件测试--测试Demo
  4. “我太喜欢你了”——友情的表达方式?
  5. java添加时间,如何通过Java中的addHours()方法添加时间
  6. GIS集成技术之二:数据集成
  7. 代码覆盖度-NCover监控IIS和exe,结果分析
  8. 用Kubernetes搭建Etcd集群和WebUI
  9. eclipse构建及运行maven web项目
  10. 查oracle执行的sql,oracle查询正在执行的sql
  11. python基础教程电子版-Python基础教程(第2版 修订版) pdf
  12. 树莓派吃灰记——搭建本地git服务
  13. 在MySQL中使用正则表达式
  14. BSText - YY大神的富文本框架 YYText 的 Swift 版本
  15. 算法分析——算法的渐进效率分析 和 渐进符号大O、大Ω、大θ、小o、小ω
  16. 天啦噜!原来Android帧动画这么简单
  17. 初学JAVA项目(11、Ada银行-3)
  18. 【文章导读】什么是旁道攻击?Meltdown Redux英特尔漏洞(MDS攻击);KAISER:从用户空间隐藏内核(KAISER);Meltdown/Spectre分析
  19. 一款免费的屏幕录制软件
  20. JSON对象转java对象 JSON数组转LIST数组

热门文章

  1. 郭天祥的10天学会51单片机_第五节
  2. android定时器常用方法,Android计时器的三种实现方式
  3. CAD二次开发--自定义下拉菜单与工具栏踩坑总结(附带源码)
  4. 开放大学毕业生自我鉴定计算机,开放大学毕业生自我鉴定范文500字(精选7篇)...
  5. QQ坦白说使用Fiddler抓包获取json
  6. 【华为OD机试真题 JAVA】事件推送
  7. 射影几何----蝴蝶定理的证明
  8. Unity开发WebGL项目开发问题与解决方法汇总
  9. 【UnityWebGL】导出WebGL的问题汇总(持续更新)
  10. 微信缓存文件要这样处理,分分钟多出几个G!