本文主要是介绍vue2源码浅读(三):数据响应式的原理与实现,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!
vue2源码浅读(三):数据响应式
- 众所周知,vue2响应式核心原理是数据劫持,即采用的是
Object.defineProperty
,关于Object.defineProperty
,请移步这里数据劫持(一)由浅入深理解Object.defineProperty
数据响应式入口
- 紧接着上一篇,在
initState
中初始化data,调用了initData()
方法: - src\core\instance\state.js
function initData (vm) {let data = vm.$options.data// 获取到用户传入的options 取到data属性, data的写法如果是函数形式 那么就使用.call指向传入的vm 拿到返回值 // 如果不是函数形式就是对象了不处理, 将数据绑定到data 和 vm._data(绑定到_data统一管理)上data = vm._data = typeof data === 'function'? getData(data, vm): data || {}if (!isPlainObject(data)) {data = {}process.env.NODE_ENV !== 'production' && warn('data functions should return an object:\n' +'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',vm)}const keys = Object.keys(data)const props = vm.$options.propsconst methods = vm.$options.methodslet i = keys.lengthwhile (i--) {const key = keys[i]// 检查data里的数据是否与methods里的方法名冲突if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method "${key}" has already been defined as a data property.`,vm)}}// 检查data里的数据是否与props里的数据冲突if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(`The data property "${key}" is already declared as a prop. ` +`Use prop default value instead.`,vm)} else if (!isReserved(key)) {// 如果不是保留字段,将data里的属性代理到vm实例上,使得可以通过this获取proxy(vm, `_data`, key)}}// 响应式处理第一步observe(data, true /* asRootData */)
}
- vue 设计, 不希望访问 _ 开头的数据,_ 开头的数据是私有数据
- 那么,经过proxy后, data挂载vm的实例上,访问app.xxx就相当于访问app._data.xxx
const sharedPropertyDefinition = {enumerable: true,configurable: true,get: noop,set: noop
}
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)
}
- 找到observe方法,这里才是响应式处理入口
- src\core\observer\index.js
function observe (value, asRootData) {if (!isObject(value) || value instanceof VNode) {return}let ob;//已经被做过的响应式处理,数据会有__ob__属性,且__ob__为Observer实例,直接返回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) {// 创建一个Observer(观察者)实例ob = new Observer(value)}if (asRootData && ob) {ob.vmCount++}return ob
}
响应式处理
- Vue的响应式数据分为两类:对象和数组
- 遍历对象的所有属性,并为每个属性设置getter和setter,以便将来的获取和设置,如果属性的值也是对象,则递归为属性值上的每个key设置getter和setter
- 接下来进入正题, Observer 类的具体实现
- src\core\observer\index.js
class Observer {value;dep;vmCount: number; // number of vms that have this object as root $dataconstructor (value) {this.value = value// 实例化一个dep,即Observer都会有一个Dep依赖this.dep = new Dep()this.vmCount = 0//添加'__ob__'def(value, '__ob__', this)// 数组需要单独处理if (Array.isArray(value)) {if (hasProto) {// 如果是现代的浏览器,复杂类型数据有原型,调用arrayMethods,重写相关方法,具体见下边protoAugment(value, arrayMethods)} else {// 如果是老旧浏览器,没有原型,直接给数组上的方法给重写替换掉copyAugment(value, arrayMethods, arrayKeys)}// 调用数组观察方法this.observeArray(value)} else {// 对象的响应式处理this.walk(value)}}// 对象响应式处理:遍历所有属性,并为每个属性添加geeter/setter。walk (obj: Object) {const keys = Object.keys(obj)for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])}}// 数组观察方法observeArray (items: Array<any>) {for (let i = 0, l = items.length; i < l; i++) {observe(items[i])}}
}
数组响应式处理
- 众所周知,
Object.defineProperty
不能监听数组的变化,具体为使用push、unshift、pop、shift、splice, sort, revers
,是触发不了set的。 - 通过重写数组操作方法实现数组监听
- src\core\observer\array.js
// 获取数组的原型Array.prototype
const arrayProto = Array.prototype// 创建一个空对象arrayMethods,并将arrayMethods的原型指向arrayProto
export const arrayMethods = Object.create(arrayProto)// 这些方法需要重写覆盖
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse'
]methodsToPatch.forEach(function (method) {const original = arrayProto[method]def(arrayMethods, method, function mutator (...args) {// 执行原始行为的逻辑const result = original.apply(this, args)const ob = this.__ob__let insertedswitch (method) {case 'push':case 'unshift':inserted = argsbreakcase 'splice':inserted = args.slice(2)break}// 新加入的元素做响应式处理if (inserted) ob.observeArray(inserted)// 内部的dep去通知更新ob.dep.notify()return result})
})
对象响应式处理
- 在Observer类里边,walk方法里调用了defineReactive方法
- 获取数据时:在dep中添加相关的watcher
- 设置数据时:再由dep通知相关的watcher去更新
- src\core\observer\index.js
function defineReactive (obj,key,valcustomSetter,shallow) {const dep = new Dep(); // 每个key都会实例化一个Dep// 获取属性相关描述const property = Object.getOwnPropertyDescriptor(obj, key)// configurable为false时,该属性相关的配置不能再被更改,也不能被删除if (property && property.configurable === false) {return}const getter = property && property.getconst setter = property && property.setif ((!getter || setter) && arguments.length === 2) {val = obj[key]}let childOb = !shallow && observe(val)// 拦截对obj[key]的获取和设置Object.defineProperty(obj, key, {enumerable: true,configurable: true,// 拦截对obj[key]的获取操作get: function reactiveGetter () {// 获取obj[key]的值const value = getter ? getter.call(obj) : val// 依赖收集// 如果存在,则说明此次调用触发者是一个Watcher实例if (Dep.target) {// 依赖关系的创建,建立dep和Dep.target之间的依赖关系(把dep添加到watcher中,也将watcher添加到dep中)dep.depend()if (childOb) {// 建立是ob内部的dep和Dep.target之间的依赖关系,也就是嵌套对象的依赖收集childOb.dep.depend()if (Array.isArray(value)) {// 如果是数组,数组内部的所有项都需要做依赖收集处理dependArray(value)}}}return value},// 拦截对obj[key]的设置操作set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val// 如果新值和老值相等则不做处理 直接返回if (newVal === value || (newVal !== newVal && value !== value)) {return}// 如果setter不存在,说明只能获取不能设置,也直接返回if (getter && !setter) return// 设置为新的值if (setter) {setter.call(obj, newVal)} else {val = newVal}// 对新值也做响应式处理childOb = !shallow && observe(newVal)// 通知更新dep.notify()}})
}
- 收集对数组元素的依赖项
function dependArray (value: Array<any>) {for (let e, i = 0, l = value.length; i < l; i++) {e = value[i]e && e.__ob__ && e.__ob__.dep.depend()if (Array.isArray(e)) {dependArray(e)}}
}
依赖管理Dep类
- 接着找到依赖管理Dep类,主要实现提供依赖收集 ( depend ) 和 派发更新 ( notify ) 的功能。
- 每个在页面上使用了的数据都会有一个Dep 类,主要访问属性的时候 get 方法会收集对应的 watcher
- 在获取数据的时候知道自己(Dep)依赖的watcher都有谁,同时在数据变更的时候通知自己(Dep)依赖的这些watcher去执行对应update,以在页面多组件情况下实现局部渲染。
- src\core\observer\dep.js
class Dep {static target: ?Watcher;id: number;subs: Array<Watcher>;constructor () {this.id = uid++// subs用于存放依赖this.subs = []}// 在dep中添加watcheraddSub (sub: Watcher) {this.subs.push(sub)}// 删除dep中的watcherremoveSub (sub: Watcher) {remove(this.subs, sub)}// 在watcher中添加depdepend () {if (Dep.target) {Dep.target.addDep(this)}}// 遍历dep的所有watcher 然后执行他们的update notify () {const subs = this.subs.slice()if (process.env.NODE_ENV !== 'production' && !config.async) {subs.sort((a, b) => a.id - b.id)}for (let i = 0, l = subs.length; i < l; i++) {subs[i].update()}}
}Dep.target = null
const targetStack = []
// 渲染阶段,访问页面上的属性变量时,给对应的 Dep 添加 watcher
export function pushTarget (target: ?Watcher) {targetStack.push(target)Dep.target = target
}
// 访问结束后删除
export function popTarget () {targetStack.pop()Dep.target = targetStack[targetStack.length - 1]
}
观察者watcher
- 页面访问的属性,将该属性的watcher 关联到该属性的 dep 中
- 同时, 将 dep 也存储 watcher 中. ( 互相引用的关系 )
- 在 Watcher 调用 get 方法的时候, 将当前 Watcher 放到全局, 在 get 之前结束的时候(之后), 将这个 全局的 watcher 移除
- src\core\observer\watcher.js
class Watcher {vm: Component;expression: string;cb: Function;id: number;deep: boolean;user: boolean;lazy: boolean;sync: boolean;dirty: boolean;active: boolean;deps: Array<Dep>;newDeps: Array<Dep>;depIds: SimpleSet;newDepIds: SimpleSet;before: ?Function;getter: Function;value: any;constructor (vm: Component,expOrFn: string | Function,cb: Function,options?: ?Object,isRenderWatcher?: boolean) {this.vm = vmif (isRenderWatcher) {vm._watcher = this}// // 当前Watcher添加到vue实例上vm._watchers.push(this)// 参数配置,options默认falseif (options) {this.deep = !!options.deepthis.user = !!options.userthis.lazy = !!options.lazythis.sync = !!options.syncthis.before = options.before} 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(): ''// 如果exporfn是函数的话,就会把这个函数赋值给getterif (typeof expOrFn === 'function') {this.getter = expOrFn} else {// 如果不是函数是字符串的话,会调用parsePath方法,// parsePath方法会把我们传入的path节分为数组,通过patch来访问到我们的对象。this.getter = parsePath(expOrFn)if (!this.getter) {this.getter = noopprocess.env.NODE_ENV !== 'production' && warn(`Failed watching path: "${expOrFn}" ` +'Watcher only accepts simple dot-delimited paths. ' +'For full control, use a function instead.',vm)}}// Watcher不止会监听Observer,还会直接把值计算出来放在this.value上this.value = this.lazy? undefined: this.get()}get () {// 将Dep的target添加到targetStack,同时Dep的target赋值为当前watcher对象pushTarget(this)let valueconst vm = this.vmtry {// 去访问我们给属性重写的 get 方法,添加 watcher 依赖value = this.getter.call(vm, vm)} catch (e) {if (this.user) {handleError(e, vm, `getter for watcher "${this.expression}"`)} else {throw e}} finally {if (this.deep) {traverse(value)}// update执行完成后,弹出target,防止data上每个属性都产生依赖,只有页面上使用的变量需要依赖popTarget()this.cleanupDeps()}return value}// 添加依赖addDep (dep: Dep) {const id = dep.idif (!this.newDepIds.has(id)) {// watcher添加它和dep的关系this.newDepIds.add(id)this.newDeps.push(dep)if (!this.depIds.has(id)) {// dep添加它和watcher的关系dep.addSub(this)}}}// 清理依赖项收集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}// 更新update () {if (this.lazy) {// 懒执行,computedthis.dirty = true} else if (this.sync) {// 同步执行this.run()} else {// 将watcher放到watcher队列中queueWatcher(this)}}
// 更新视图run () {if (this.active) {const value = this.get()if (value !== this.value ||isObject(value) ||this.deep) {const oldValue = this.valuethis.value = valueif (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)}}}}// 懒执行的watcher会调用该方法evaluate () {this.value = this.get()this.dirty = false}
// 依赖这个观察者收集的所有depsdepend () {let i = this.deps.lengthwhile (i--) {this.deps[i].depend()}}// // 从所有依赖项的订阅者列表中把自己删除teardown () {if (this.active) {if (!this.vm._isBeingDestroyed) {remove(this.vm._watchers, this)}let i = this.deps.lengthwhile (i--) {this.deps[i].removeSub(this)}this.active = false}}
}
梳理整个过程
- 在initState方法中,首先是用户传入参数有没有data,没有则
observe(vm._data = {}, true /* asRootData */)
,将一个空的data代理到vm实例上,有则调用initData方法。 - 在initData方法中,主要遍历data中的属性,通过
proxy(vm,
_data, key)
,并将其代理到vm实例上,然后对data进行观察observe(data, true /* asRootData */)
; - 在observe方法中,主要判断传入的value有没有
__ob__
属性,有则说明已经被观察过了,没有则进行new Observer(value)
。 - 在Observer类中,主要实现了对数组和对象的响应式处理。
- 首先是调用
def(value, '__ob__', this)
给value添加__ob__
属性; - 判断value如果是数组,再判断当前环境
- 如果是现代的浏览器,复杂类型数据有原型,调用arrayMethods,重写相关方法
- 将数组原型指向空对象上,对
push、unshift、pop、shift、splice, sort, revers
方法重写覆盖。具体还是执行原来的行为逻辑,主要是拦截对其做响应式处理:调用 ob.dep.notify()通知更新,如果是新插入的元素调用ob.observeArray(inserted)。
- 将数组原型指向空对象上,对
- 如果是老旧浏览器,没有原型,直接给数组上的方法给重写替换掉
- 最后调用observeArray方法,对每个数组元素进行observe
- 如果是现代的浏览器,复杂类型数据有原型,调用arrayMethods,重写相关方法
- 判断value如果是对象
- 遍历对象,对每个键调用
defineReactive(obj, keys[i])
方法,做响应式处理
- 首先是调用
- 在defineReactive(obj, keys[i])方法中,首先对key都会实例化一个Dep。
- 在get方法中,收集依赖,把dep添加到watcher中,也将watcher添加到dep中
dep.depend()
。- 如果有子对象,则进行嵌套对象的依赖收集
childOb.dep.depend()
。 - 如果是数组,数组内部的所有项都需要做依赖收集
dependArray(value)
。
- 如果有子对象,则进行嵌套对象的依赖收集
- 在set方法中,如果新值和老值相等则不做处理直接返回,否则赋新值,且对新值做响应式处理,
dep.notify()
通知更新
- 在get方法中,收集依赖,把dep添加到watcher中,也将watcher添加到dep中
- 在Dep类中,访问属性的时候 get 方法会收集对应的 watcher,在数据变更的时候通知自己(Dep)依赖的这些watcher去执行对应update,实现局部渲染。
- Watcher类中,将Dep也存储 watcher 中,形成相互引用的关系,提供了update方法
- Watcher中 不止会监听Observer,还会直接把值计算出来放在this.value上。其中get方法主要应用于computed计算和watch监听。
这篇关于vue2源码浅读(三):数据响应式的原理与实现的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!