如何把data里面的数据做成响应式的,能监听的到数据的变化,能通知到数据变化后相应模块的变化
1. Observe意思,作用
响应式对象,意思是把这个对象做成响应式的对象,不是针对某一个值,而是这个对象里面的所有。所以一个对象/数组就会创建一个Observe实例
作用(干什么的):
把一个对象本身以及对象里面所有的值做成响应式的对象
2. 代码执行流程
1) function Vue
2) this._init(options)
3) initState(vm)
状态初始化,props,data,methods,conputed等里面的状态初始化。我们主要看针对data的initData
4) initData(vm)
获取data,设置代理,判重,启动响应式
5) observe(data, true /* asRootData */)
判断不是对象或者是vnode则直接return,否则new一个Observe观察者
6) ob = new Observer(value)
带上自己的dep:this.dep = new Dep()
给对象加上响应式标志__ob__,标记为已经做过响应式的对象
改变数组原型链指向,使数组的push等方法重写,能被响应式
数组需要额外判断一遍数组内部是否有对象需要响应式
执行this.walk(value)
7) this.walk(value)
循环把对象里面的值做成响应式
8) defineReactive(obj, keys[i])
Object.defineProperty里面定义getter和setter函数
getter:依赖收集dep.depend(),Dep.target指向的是当前的watcher,depend就是收集这个watcher的,收集完以后tager立刻销毁
setter:派发更新dep.notify(),执行watcher里面的需要更新的函数
9) Dep通知的相关逻辑
接下去就是dep的事情,Observer观察者的任务到此结束
3. 具体详细代码逻辑步骤
1. initState(vm)函数
状态初始化,props,methods,conputed等里面的状态初始化
scss
// 初始化props,data等
export function initState (vm: Component) {
vm._watchers = []
const opts = vm.$options
if (opts.props) initProps(vm, opts.props)
if (opts.methods) initMethods(vm, opts.methods)
if (opts.data) {
initData(vm) // 重点
} else {
observe(vm._data = {}, true /* asRootData */)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
2. initData函数
initData
主要作用:获取data,设置代理,判重,启动响应式
kotlin
// 把data赋值给vm
function initData (vm: Component) {
// 为什么在data里面定义了属性,可以通过this拿到,就是在这里做的
let data = vm.$options.data
// 因为对象是一个引用数据类型,如果data是一个对象的情况下会造成所有组件共用一个data。
// 而当data是一个函数的情况下,每次函数执行完毕后都会返回一个新的对象,这样的话每个组件都会维护一份独立的对象(data)
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {
// todo..开发环境不重要逻辑
}
// proxy data on instance 代理数据到实例上
const keys = Object.keys(data)
const props = vm.$options.props
const methods = vm.$options.methods
let i = keys.length
while (i--) {
const key = keys[i]
if (process.env.NODE_ENV !== 'production') {
// todo 去重methods
}
if (props && hasOwn(props, key)) {
process.env.NODE_ENV !== 'production' && warn(
// todo 去重props
} else if (!isReserved(key)) {
// 把data上的东西代理到vm实例上,这一行解释了为什么可以通过this访问data上属性
proxy(vm, `_data`, key)
}
}
// observe data
// 响应式操作的地方
observe(data, true /* asRootData */)
}
proxy(vm, _data
, key)
把data上的东西代理到vm实例上,解释了为什么可以通过this访问data上属性
vbnet
// 代理函数,this.xx访问为this._data.xx
export function proxy (target: Object, sourceKey: string, key: string) {
// 通过这个函数,最终实际上访问的是vm._data.key
sharedPropertyDefinition.get = function proxyGetter () {
// this[sourceKey][key]其实是vm.$options.data[key]
return this[sourceKey][key]
}
sharedPropertyDefinition.set = function proxySetter (val) {
this[sourceKey][key] = val
}
Object.defineProperty(target, key, sharedPropertyDefinition)
}
3. observe(data, true /* asRootData */)
文件地址:src\core\observer\index.js
observe文件夹是MVVM框架最重要的文件夹,因为mvvm框架最重要的就是响应式
observe:观察者,把数据做成响应式的,我们看到的__ob__这个属性,就是Observer的实例
里面执行ob = new Observer(value)
typescript
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 必须是对象/数组,且不能是vnode,vnode不需要观察
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
// 有__ob__那就直接返回,说明已经加入响应式了
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 创建新的实例
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
4. Observer类
每一个响应式对象,都会有一个Observer类的实例__ob__
必须是对象或者数组才有,普通的原始值不会有
observe的作用是把这个对象里面的所有数据都变成响应式的,里面执行this.walk(value)
scss
export class Observer {
value: any;
dep: Dep;
vmCount: number; // number of vms that has this object as root $data
constructor (value: any) {
this.value = value
// 为什么给每一个Observer设置一个dep?
// object有新增或者删除属性,array中便跟方法
// 那么都必须通过dep去通知
this.dep = new Dep()
this.vmCount = 0
// 设置一个__ob__的属性,def的意思:Define a property
def(value, '__ob__', this)
if (Array.isArray(value)) {
if (hasProto) {
// 改写数组原型的指向,把__proto__指向了一个自己创建的对象,这个对象里面重新实现了push,pop等7个方法,剩余属性和数组的原型合并,这里就是为什么调用数组方法也能实现响应式的原因
protoAugment(value, arrayMethods)
} else {
// 兼容性,没有原型则直接强硬覆盖(比如垃圾ie)
copyAugment(value, arrayMethods, arrayKeys)
}
// 如果数组里面的值还是对象,则需要再次响应式处理
this.observeArray(value)
} else {
this.walk(value)
}
}
/**
* Walk through each property 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])
}
}
}
5. defineReactive(obj, keys[i])
定义getter和setter做的事情
vbnet
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 和key一一对应
const dep = new Dep()
// 如果数据是不可改变的则直接return,节约性能(vue性能优化点)
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {
return
}
// cater for pre-defined getter/setters
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val) // val仍然为对象时候才有值,数组也算
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 这个dep是一个类,主要作用是建立起数据和watch之间的一个桥梁
// target就是watcher,同一时间只有一个watch能被计算
if (Dep.target) {
// 依赖收集
dep.depend()
// 如果有子对象,则子对象也收集这个依赖(watcher)
if (childOb) {
childOb.dep.depend()
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
/* eslint-disable no-self-compare */
// 如果值是相同的则什么都不做
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// #7981: for accessor properties without setter
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
// 如果新值是一个对象,那么重新把它变成响应式对象
childOb = !shallow && observe(newVal)
// 这个函数就是派发更新的过程
dep.notify()
}
})
}
4. 后续步骤(数据何时收集到watcher)
-
以上所有的步骤都是在initState(vm)函数里面执行,执行完后,此时知识初始化完数据,把数据变成响应式的,created的都还没有执行。创建了一大堆dep(每个组件里面的data都是一个数据对应一个dep),但是dep里面全都是空的,没有管理任何watcher。
-
watcher在挂在的时候才会创建,是在vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( v m . mount(vm. </math>mount(vm.options.el)里面执行,在执行挂在的时候,会创建watcher,创建后target有值,然后会立即真正渲染一次(updateComponent),此时会访问当data里面的值,触发响应式getter,watcher被收集到了dep里面,然后target置为null,此为依赖收集过程。
-
全程没有触发过setter,setter是在数据二次变成时候才触发,初始化不触发,后续值二次更新才触发
挂载在初始化数据之后执行
scss
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
const vm: Component = this
// a uid
vm._uid = uid++
let startTag, endTag
/* istanbul ignore if */
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
// optimize internal component instantiation
// since dynamic options merging is pretty slow, and none of the
// internal component options needs special treatment.
initInternalComponent(vm, options)
} else { // new Vue时候触发
// 把传入的options merge到$options上
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {}, // 这个option是new Vue传入的参数
vm
)
}
/* istanbul ignore else */
if (process.env.NODE_ENV !== 'production') {
initProxy(vm)
} else {
vm._renderProxy = vm
}
// expose real self
vm._self = vm
initLifecycle(vm)
initEvents(vm)
initRender(vm)
callHook(vm, 'beforeCreate')
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化props,data等
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created')
/* istanbul ignore if */
if (vm.$options.el) { // 判断有没有传el,然后挂载
// 没传就不进行下一步操作,子组件也都会执行这些初始化,所以子组件就走不到这个逻辑,子组件内部会手动调用$mount
vm.$mount(vm.$options.el)
}
}
}
挂载简易步骤
vm. <math xmlns="http://www.w3.org/1998/Math/MathML"> m o u n t ( v m . mount(vm. </math>mount(vm.options.el) = 》
return mountComponent(this, el, hydrating) =》
updateComponent =》
vm._update(vm._render(), hydrating)
patch等渲染步骤...
5. 问题知识点问答
1) 问:Observer类里为什么给每一个对象都设置一个dep
this.dep = new Dep()
答:
- 对象本身变化需要通知
dep的作用是通知更新,dep和key是一一对应的,这个对象本身也可以被重新赋值的(this.obj = 33),变化后也需要通知
2) 对象里面的数据的增加和删除需要通知,给$.set()使用。
对象的新增和删除obj.a = 11,以及数组的新增和删除是监听不到的,(Object.defineProperty监听不到删除事件,而新增监听不到是因为还没有被监听,修改某一个值能监听得到,因为修改的那个值已经被监听了)
所以要新增和删除属性要用 <math xmlns="http://www.w3.org/1998/Math/MathML"> . s e t ( ) 方法,可是 .set()方法,可是 </math>.set()方法,可是.set()方法怎么知道具体通知哪些个组件去跟新,所以在这个对象/数组上挂了dep,增加和删除属性仍然表示这个对象本身有变化,而不是对象里面的某个值有变化,所以触发这个对象上dep通知更新(具体更新啥由diff去计算,不会整个组件重新渲染一遍)
3) 问:Vue 中 Observer 的用处是什么?
详细:在 state.js 的 initData() 函数中,使用的是 observe() 方法来为数据对象绑定一个Observer对象,Observer对象执行 defineReactive() 方法为数据对象设置 setter 和 getter。
而在 initProps() 函数中,通过遍历 props 选项直接对数据执行了 defineReactive() 方法来设置 setter 和 getter。
那么,同样是为数据设置 setter 和 getter,为什么 initData() 比 initProps() 多一个 Observer 类,这个 Observer 类的功能到底是什么?
答案:
站在component的角度,props是immutable的,而data是mutable的
所以相对于props,data需要对对象的子对象以及数组内的元素都设置setter 和 getter
而props不用
所以Observer的功能,应该就是对数组元素的遍历执行defineReactive(),以及深度遍历Object为每一个子对象都执行defineReactive()
对应的就是Observer.observeArray和Observer.walk这两个方法
4) 问:dep和watcher的关系
总:Dep时管理watcher的。
Dep管理一组watcher,Dep关联的值更新时通知其管理watcher更新。Dep和watcher是多堆垛的关系,一个dep可能管理多个watch,因为在一个组件中,除了渲染watcher以外,还可能有多个用户watch,比如watch选项,或者是调用了$watch方法,或者是computed,此时就是一个key关联了多个watcher。watcher对应多个dep,因为一个组件当然可以绑定多个data中的key,
5) 问:依赖收集什么时候发生的(代码展示)
每一个组件都会有唯一对应的watcher。当watcher中的_render函数执行的时候,render函数会对界面中所有涉及到的数值进行访问,此时触发在defineReactive中定义的getter,于是依赖收集就发生了
6) vue2 中响应式的缺点
递归遍历,性能受影响(vue3的proxy可以解决)
api不统一,$set的数组和对象api不一致(vue3proxy可以解决)