[TOC]
- 搞清楚什么是响应式
Vue
怎么知道我们数据更新了- 模拟数据响应式
- 通过阅读
Vue2
源码,理解Vue的双向数据绑定原理,可以跟面试官拉扯- 什么是
Watcher
- 什么是
Dep
所谓数据响应式,不过是当依赖发生变化的时候,目标(视图)自动更新。
所以,要想理解数据响应式,我们先来尝试一下怎么让数据自动发生变化。
模拟数据响应式,即当数据的依赖发生变化的时候,
target
也发生变化。如:let c = a+b;这里我们把
c
称为target
,a,b
是c
的依赖,因为c
是根据a
和b
得出来的。
那么我们怎么让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() }上面的问题就解决了。
因为对象操作比较复杂,所以我们先实现对对象操作的拦截,比如对象的获取与设置我都知道。
新加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'
为了简化程序,我们只看一层的对象
"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队列,当接到通知更新的时候,对队列里面的函数遍历执行,达到数据自动更新的效果。
在阅读源码的时候,为了我们方便寻找入口,我们先来看看官网对数据响应式的阐述。
看完官方给的图,我们可以明确知道,
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>
特别声明,为简化流程,所有的源码展示,均经过删减
一个入口文件
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
/**
* initMixin
通过该方法,给Vue提供__init方法, 初始化生命周期
initLifecycle -> initEvents -> initRender
-> callHook(vm, 'beforeCreate') -> initInJections
-> initState -> initProvide
-> callHook(vm, 'created')
-> if (vm.$options.el) {
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)
}
}
}
对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)
}
对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 */)
}
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)
}
创建观察者实例
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
}
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])
}
}
}
依赖收集
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()
}
}
}
/**
*观察者解析表达式,收集依赖项,
*并在表达式值改变时触发回调。
*这用于$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
}
}
}
RenderWatcher 及异步更新
核心代码就在这里了。这个 watcher 就是 Vue 实例对象唯一的 RenderWatcher,在 watcher 构造函数中,会记录到 vm._watcher 上(普通 watcher 只会记录到 vm._watchers 数组中)。
这个 watcher 也会在创建的最后执行 watcher.get(),也就是执行 getter 收集依赖的过程。而在这里,getter 就是 updateComponent,也就是说,执行了渲染+更新 DOM!并且,这个过程中使用到的数据也被收集了依赖关系。
那么,理所当然地,在 render() 中使用到数据,发生改变,自然会通知到 RenderWatcher,从而最终更新视图!
不过,这里会有个疑问:如果进行多次数据修改,那么岂不是要频繁执行 DOM 更新?
这里就涉及到 RenderWatcher 的特殊功能了:异步更新。
// Vue.prototype.$mount() -> mountComponent()
let updateComponent = () => {
vm._update(vm._render(), hydrating)
}
new Watcher(vm, updateComponent, noop, null, true /* isRenderWatcher */)
声明,以下所说的data如果没有特别声明,都是指,定义组件时预定义的data对象。
拿
c=a+b
说明,当a或者b发生变化的时候,c也会跟着发生变化,这就是数据响应式的本质。
Vue2.x对用户定义的
data
,利用Object.defineProperty
进行重定义然后才挂载到组件上,当组件获取或者更新数据,会触发getter或setter,也就让组件知道了用户对数据进行了“操作”。
在Vue里面一个组件对应着一个
Watcher
,我们称之为“订阅者”。它的作用是:订阅数据的变化的并执行相应操作(例如更新视图
update
)。
组件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进行update更新
当组件调用observe(data)的时候,创建Observer实例,他会将data利用
Objce.property
重新定义然后挂载到组件上。我们称之为“观察者”,因为他通过观察data然后将data转化成为一个数据响应式的data,有人开玩笑的说,从此data经过了“社会主义的洗礼”,已经是一个成熟的社会主义接班人。值得注意的是每一个Observer实例,也有一个唯一的Dep实例与之对应。
因为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函数进行数据更新。从而达到数据响应的目的。
请问面试官我的理解是否有错?