文章目录
- 1.1 我首先想到的是,将c放在一个函数里面,当a或者b发生变化的时候,我们调用一下那个函数,让c回炉重造不就可以变化了吗?
- 1.2 我想到的办法是红宝书里面看到的`Object.defineProperty()`,利用`Object.defineProperty()`的`getter`以及`setter`的拦截特性, 让我i们来测试一下。
- `src\core\instance\index.js`
- `src\core\instance\init.js`
- `src\core\instance\state.js\initState`
- `src\core\instance\state.js\initData`
- ``src\core\instance\state.js\proxy`
- `src\core\observer\index.js\observe`
- `src\core\observer\index.js\Observer类`
- `src\core\observer\dep.js\Dep类`
- `src\core\observer\watcher.js\Watcher类`
- 1. 什么是响应式
- 2. `Vue`怎么知道数据更新?
- 3. 什么是`Watcher`?
- 4. 什么是`Dep`
- 5. 什么是`Observer`?
- 6.vue里面有多少种不同Watcher?
- 面试题: 请谈谈你对数据响应式原理的理解
学习目标
- 搞清楚什么是响应式
Vue
怎么知道我们数据更新了- 模拟数据响应式
- 通过阅读
Vue2
源码,理解Vue的双向数据绑定原理,可以跟面试官拉扯- 什么是
Watcher
- vue里面有多少种Watcher
- 什么是
Dep
学习过程
所谓数据响应式,不过是当依赖发生变化的时候,目标(视图)自动更新。
所以,要想理解数据响应式,我们先来尝试一下怎么让数据自动发生变化。
1. 让一个数据会变
模拟数据响应式,即当数据的依赖发生变化的时候,
target
也发生变化。如:
let c = a+b;
这里我们把
c
称为target
,a,b
是c
的依赖,因为c
是根据a
和b
得出来的。
那么我们怎么让c在a或者b发生变化得时候,c也跟着发生变化呢?
1.1 我首先想到的是,将c放在一个函数里面,当a或者b发生变化的时候,我们调用一下那个函数,让c回炉重造不就可以变化了吗?
让我们来测试一下我的想法。
"use strict"; let a = 1 let b = 2 let c = null; function fn(){ c =a+b; } console.log('1-',a,b,c) // 1- 1 2 null // 明显这里c 为null // 调用一下函数,让c得到初始化 fn() console.log('2-',a,b,c) // 2- 1 2 3 // 到了这里c=3 // 依赖发生变化 a = 9 console.log('3-',a,b,c) // 3- 9 2 3 // 这里a发生变化, 但是c并没有发生变化 fn() console.log('4-',a,b,c) // 4- 9 2 11 // 调用fn之后c=11,发生了变化
很明显到了这里,我们手动调用函数target确实会更新,但是老是手动的话,感觉怪怪的,那有没有什么办法,可以让他自动调用呢?
到了这里我们要解决的问题有两个:
1. 怎么知道自动数据发生变化 2. 怎么自动调用一个特定函数
1.2 我想到的办法是红宝书里面看到的Object.defineProperty()
,利用Object.defineProperty()
的getter
以及setter
的拦截特性, 让我i们来测试一下。
"use strict"; function say() { console.log('hello') } function defineReactive(obj, key, val) { return Object.defineProperty(obj, key, { get() { console.log('get->', key) return val }, set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // 数据发生变化,我们就调用函数 say() val = newVal } }) } let source = {} defineReactive(source, 'a', 1) console.log(source.a) source.a = 99 console.log(source.a)
结果:
可以看到我们对a的get以及set 都被识别到了,而且say函数也被成功调用了。
那我们怎么复现
c = a + b
这个例子呢?如下:
"use strict"; function defineReactive(obj, key, val) { return Object.defineProperty(obj, key, { get() { console.log('get->', key) return val }, set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // 数据发生变化,我们就调用函数 fn() val = newVal } }) } let source = {} defineReactive(source, 'a', 1) defineReactive(source, 'b', 2) let c; function fn(){ c = source.a + source.b; console.log('c自动变化成为',c) } // 初始化一下c fn() source.a = 99
我们发现fn虽然被自动调用了,但是c的值依然是3,那应该怎么解决呢?
经过查看发现是set里面的问题(先调用了fn导致val还没来得及发生变化。)
set(newVal) { if (newVal === val) return; console.log(`set ${key} from ${val} to ${newVal}`) // 数据发生变化,我们就调用函数 val = newVal fn() }
上面的问题就解决了。
2. 让一个对象会变
因为对象操作比较复杂,所以我们先实现对对象操作的拦截,比如对象的获取与设置我都知道。
2.1 拦截到对对象属性的获取与设置
新加observe对一个对象的属性遍历进行重新定义(类似于定义一个数据可变)
"use strict"; /** * 这里的defineReactive实际上是一个闭包, * 外面的对面引用着函数内的变量,导致这些临时变量一直存在 */ function defineReactive(obj, key, val){ // 2. observe 避免key的val是一个对象,对象里面的值没有响应式 observe(val) // 利用getter setter 去拦截数据 Object.defineProperty(obj,key, { get(){ console.log('get', key) return val }, set(newVal){ if( newVal !== val){ console.log(`set ${val} -> ${newVal}`) val = newVal } } }) } // 2. 观察一个对象,让这个对象的属性变成响应式 function observe(obj){ // 希望传入的是一个Object if( typeof obj !== 'object' || typeof(obj) == null){ return ; } Object.keys(obj).forEach(key=>{ defineReactive(obj, key, obj[key]) }) } let o = { a: 1, b: 'hello', c:{age:9}} observe(o) o.a o.a = 2 o.b o.b = 'world'
2.2 让对象属性变化
为了简化程序,我们只看一层的对象
"use strict"; const { log } = console; let target = null; let data = { a: 1, b:2 } let c, d; // 依赖收集,每个Object的key都有一个Dep实例 class Dep{ constructor(){ this.deps = [] } depend(){ target && !this.deps.includes(target) && this.deps.push(target) } notify() { this.deps.forEach(dep=>dep() ) } } Object.keys(data).forEach(key=>{ let v = data[key] const dep = new Dep() Object.defineProperty(data, key, { get(){ dep.depend() return v; }, set(nv){ v = nv dep.notify() } }) }) function watcher(fn) { target = fn target() target = null } watcher(()=>{ c = data.a + data.b }) watcher(()=>{ d = data.a - data.b }) log('c=',c) log('d=',d) data.a = 99 log('c=',c) log('d=',d) /** c= 3 d= -1 c= 101 d= 97 */
- 简述一下这个过程:
对data对象里面的每一个key利用defineProperty进行数据拦截,在get里面进行Dep依赖收集,在set里面通知数据更新。
依赖收集实则是将watcher实例加入deps队列,当接到通知更新的时候,对队列里面的函数遍历执行,达到数据自动更新的效果。
3. 源码阅读
在阅读源码的时候,为了我们方便寻找入口,我们先来看看官网对数据响应式的阐述。
看完官方给的图,我们可以明确知道,
Watcher
的粒度是组件,也就是说,每一个组件对应一个Watcher
。
那么Watcher
究竟是什么呢?Dep
又是什么? Observer
又是做什么用的?下面让我们到源码中去寻找答案吧。
测试代码
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <script src='../dist/vue.js'></script> </head> <body> <div id="app"> {{a}} </div> <script> const app = new Vue({ el:'#app', data: { a: 1 }, mounted(){ setInterval(()=>{ this.a ++ }, 3000) } }) </script> </body> </html>
跟踪调用栈
自己画的一个流程图
talk is cheap, show me the code
特别声明,为简化流程,所有的源码展示,均经过删减
src\core\instance\index.js
一个入口文件
function Vue (options) { /** * 初始化 */ this._init(options) } /** * 以下通过给Vue.prototype挂载的方法,混入其他方法 */ initMixin(Vue) /** * initMixin 通过该方法,给Vue提供__init方法, 初始化生命周期 initLifecycle -> initEvents -> initRender -> callHook(vm, 'beforeCreate') -> initInJections -> initState -> initProvide -> callHook(vm, 'created') -> if (vm.$options.el) { vm.$mount(vm.$options.el) } */ stateMixin(Vue) /** stateMixin $data -> $props -> $set -> $delete -> $watch */ eventsMixin(Vue) /** eventsMixin $on $once $off $emit */ lifecycleMixin(Vue) /** lifecycleMixin * _update(), $forceUpdate, $destroy * */ renderMixin(Vue) /** * $nextTick, _render, $vnode * * */ export default Vue
src\core\instance\init.js
/**
* initMixin
通过该方法,给Vue提供__init方法, 初始化生命周期
initLifecycle -> initEvents -> initRender
-> callHook(vm, ‘beforeCreate’) -> initInJections
-> initState -> initProvide
-> callHook(vm, ‘created’)
-> if (vm.$options.el) {
vm.m o u n t ( v m . mount(vm.mount(vm.options.el)
}
*/
export function initMixin (Vue: Class<Component>) { /** * @重要 数据初始化,响应式 * $set $delete $watch * 在reject之后,初始化数据,达到去重的效果 */ initState(vm) /** * 调用created钩子函数 */ callHook(vm, 'created') if (vm.$options.el) { vm.$mount(vm.$options.el) } } }
src\core\instance\state.js\initState
对data进行预处理
export function initState (vm: Component) { vm._watchers = [] const opts = vm.$options /** * state的初始化顺序 * props -> methods -> data -> computed -> watch */ if (opts.props) initProps(vm, opts.props) if (opts.methods) initMethods(vm, opts.methods) if (opts.data) { initData(vm) } if (opts.computed) initComputed(vm, opts.computed) }
src\core\instance\state.js\initData
对data进行observe,对数据的getter,setter拦截
function initData (vm: Component) { let data = vm.$options.data // proxy data on instance /** * 数据代理 */ const keys = Object.keys(data) let i = keys.length while (i--) { const key = keys[i] /** * @代理 */ proxy(vm, `_data`, key) } /** * @响应式操作 */ observe(data, true /* asRootData */) }
``src\core\instance\state.js\proxy`
const sharedPropertyDefinition = { enumerable: true, configurable: true, get: noop, set: noop } /** * proxy(vm, `_data`, key) 168行 */ export function proxy (target: Object, sourceKey: string, key: string) { sharedPropertyDefinition.get = function proxyGetter () { return this[sourceKey][key] } sharedPropertyDefinition.set = function proxySetter (val) { this[sourceKey][key] = val } Object.defineProperty(target, key, sharedPropertyDefinition) }
src\core\observer\index.js\observe
创建观察者实例
export function observe (value: any, asRootData: ? boolean): Observer | void { if (!isObject(value) || value instanceof VNode) { return } /** * @观察者 */ let ob: Observer | void ob = new Observer(value) } if (asRootData && ob) { ob.vmCount++ } return ob }
src\core\observer\index.js\Observer类
export class Observer { value: any; dep: Dep; vmCount: number; // number of vms that have this object as root $data constructor (value: any) { this.value = value /** * @思考为什么在Observer里面声明dep * 创建Dep实例 * Object 里面新增或者删除属性 * array 中有变更方法 */ this.dep = new Dep() this.vmCount = 0 /** * 设置一个—个 __ob__ 属性,引用当前Observer实例 */ /** * export function def (obj: Object, key: string, val: any, enumerable?: boolean) { Object.defineProperty(obj, key, { value: val, enumerable: !!enumerable, writable: true, configurable: true }) } */ def(value, '__ob__', this) /** * 类型判断 */ // 数组 if (Array.isArray(value)) { if (hasProto) { protoAugment(value, arrayMethods) } else { copyAugment(value, arrayMethods, arrayKeys) } /** * 如果数组里面的元素还是对象,还需要进行响应式处理 */ this.observeArray(value) } else { // 是一个对象 this.walk(value) } } /** * Walk through all properties and convert them into * getter/setters. This method should only be called when * value type is Object. */ walk (obj: Object) { const keys = Object.keys(obj) for (let i = 0; i < keys.length; i++) { defineReactive(obj, keys[i]) } } /** * Observe a list of Array items. */ observeArray (items: Array<any>) { for (let i = 0, l = items.length; i < l; i++) { observe(items[i]) } } }
src\core\observer\dep.js\Dep类
依赖收集
subs是一个Watcher队列
/** * A dep is an observable that can have multiple * directives subscribing to it. * dep是一个可观察对象,可以有多个指令订阅它。 */ export default class Dep { static target: ?Watcher; id: number; subs: Array<Watcher>; constructor () { this.id = uid++ this.subs = [] } addSub (sub: Watcher) { this.subs.push(sub) } removeSub (sub: Watcher) { remove(this.subs, sub) } depend () { Dep.target && Dep.target.addDep(this) } notify () { const subs = this.subs.slice() for (let i = 0, l = subs.length; i < l; i++) { subs[i].update() } } }
src\core\observer\watcher.js\Watcher类
/** *观察者解析表达式,收集依赖项, *并在表达式值改变时触发回调。 *这用于$watch() api和指令。 */ export default class Watcher { constructor () { this.vm = vm if (isRenderWatcher) { vm._watcher = this } vm._watchers.push(this) this.expression = process.env.NODE_ENV !== 'production' ? expOrFn.toString() : '' // parse expression for getter if (typeof expOrFn === 'function') { this.getter = expOrFn } else { this.getter = parsePath(expOrFn) if (!this.getter) { this.getter = noop 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() } /** * Evaluate the getter, and re-collect dependencies. */ get () { pushTarget(this) ... popTarget() this.cleanupDeps() return value } /** * Add a dependency to this directive. */ addDep (dep: Dep) { const id = dep.id if (!this.newDepIds.has(id)) { this.newDepIds.add(id) this.newDeps.push(dep) if (!this.depIds.has(id)) { dep.addSub(this) } } } /** * Clean up for dependency collection. */ cleanupDeps () { let i = this.deps.length while (i--) { const dep = this.deps[i] if (!this.newDepIds.has(dep.id)) { dep.removeSub(this) } } } /** * 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) } } /** * Scheduler job interface. * Will be called by the scheduler. */ run () { if (this.active) { const value = this.get() if ( value !== this.value || // Deep watchers and watchers on Object/Arrays should fire even // when the value is the same, because the value may // have mutated. isObject(value) || this.deep ) { // set new value const oldValue = this.value this.value = value if (this.user) { const info = `callback for watcher "${this.expression}"` invokeWithErrorHandling(this.cb, this.vm, [value, oldValue], this.vm, info) } else { this.cb.call(this.vm, value, oldValue) } } } } /** * Evaluate the value of the watcher. * This only gets called for lazy watchers. */ evaluate () { this.value = this.get() this.dirty = false } /** * Depend on all deps collected by this watcher. */ depend () { let i = this.deps.length while (i--) { this.deps[i].depend() } } /** * Remove self from all dependencies' subscriber list. */ teardown () { if (this.active) { if (!this.vm._isBeingDestroyed) { remove(this.vm._watchers, this) } let i = this.deps.length while (i--) { this.deps[i].removeSub(this) } this.active = false } } }
学习成果
声明,以下所说的data如果没有特别声明,都是指,定义组件时预定义的data对象。
1. 什么是响应式
拿
c=a+b
说明,当a或者b发生变化的时候,c也会跟着发生变化,这就是数据响应式的本质。
2. Vue
怎么知道数据更新?
Vue2.x对用户定义的
data
,利用Object.defineProperty
进行重定义然后才挂载到组件上,当组件获取或者更新数据,会触发getter或setter,也就让组件知道了用户对数据进行了“操作”。
3. 什么是Watcher
?
在Vue里面一个组件对应着一个
Watcher
,我们称之为“订阅者”。它的作用是:订阅数据的变化的并执行相应操作(例如更新视图
update
)。
4. 什么是Dep
组件data对象的每一个key都对应者一个Dep实例。
Dep是一个可观察类,当他被实例化之后,Watcher就可以订阅它。
Vue会在Dep里面进行依赖收集。
Dep有一个类属性,
Target
,用来存放Watcher订阅者,他是依赖收集的关键。
- 当data的属性被get之后,会调用dep.depend()。
- 而在dep.depend函数中,如果存在Dep.target,则会通知与之对应的Watcher添加依赖
- 当data的属性key被set(也就是更新的时候),调用与之对应dep.notify(),notify会调用所有订阅它的Watcher进行uodate更新
5. 什么是Observer
?
当组件调用observe(data)的时候,创建Observer实例,他会将data利用
Objce.property
重新定义然后挂载到组件上。我们称之为“观察者”,因为他通过观察data然后将data转化成为一个数据响应式的data,有人开玩笑的说,从此data经过了“社会主义的洗礼”,已经是一个成熟的社会主义接班人。值得注意的是每一个Observer实例,也有一个唯一的Dep实例与之对应。
6.vue里面有多少种不同Watcher?
因为Watcher是用来更新的,所以我们可以想一下Vue里面有多少个场景需要进行数据更新。
比如:
- 数据变 → 使用数据的视图变
- 数据变 → 使用数据的计算属性变 → 使用计算属性的视图变
- 数据变 → 开发者主动注册的watch回调函数执行
三个场景,对应三种watcher:
- 组件的Watcher,即render-watcher
- 用户定义的计算属性对应的computed-watcher
- 用户定义的监听属性对应的Watcher(watch-api或watch属性)
面试题: 请谈谈你对数据响应式原理的理解
首先我们可以看一下
c=a+b
,在这条等式里面,c是我们的目标数据,a和b都是c的依赖,当a或者b发生变化的时候,c会自动进行更新,就是我所理解的数据响应式。而在Vue里面跟数据响应式有关的主要有Dep, Watcher,以及Observer三个类。
在我们创建组件的时候,vue会对用户预定义的data进行observe创建一个Observer实例,将data对象利用
Object.defineProperty()
的getter以及setter进行重定义,使后面对data所做的所有操作,均可被组件察觉。然后我们获取Watcher实例的时候,会先调用Watcher里面的get函数,这个get函数,会将当前触发的依赖push到targetStack里面,然后触发这个依赖也就是我们前面定义的
obj.key
的getter,在这个getter里面,如果Dep.target不为空则调用depend进行依赖收集。当我们再次更次数据的时候,触发的obj.key的setter会调用与之对应Dep实例的notify函数, notify遍历订阅者队列,调用所有订阅了这个key依赖的Watcher的update函数进行数据更新。从而达到数据响应的目的。
请问面试官我的理解是否有错?
the end!!!!