众所周知,vue 2.x是通过Object.defineProperty来实现数据响应式的。那vue中具体是如何实现的呢?数据初始化的流程又是什么样的呢?还有数组又是如何实现响应式的呢?让我们从vue的初始化一步步分析,看看这其中到底经历了什么。
vue的初始化
构造函数
源码路径:\src\core\instance\index.js
function Vue (options) {
if (process.env.NODE_ENV !== 'production' &&
!(this instanceof Vue)
) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
this._init(options)
}
initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)
export default Vue
上述代码为vue的构造函数,我们可以看到当我们new Vue()的时候会执行_init方法,该方法是通过initMixin方法混入进去的,我们进入到initMixin方法。
initMixin
源码路径:\src\core\instance\init.js
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 */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
startTag = `vue-perf-start:${vm._uid}`
endTag = `vue-perf-end:${vm._uid}`
mark(startTag)
}
// a flag to avoid this being observed
vm._isVue = true
// merge options
if (options && options._isComponent) {
initInternalComponent(vm, options)
} else {
vm.$options = mergeOptions(
resolveConstructorOptions(vm.constructor),
options || {},
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') // 调用生命周期beforeCreate钩子
initInjections(vm) // resolve injections before data/props
initState(vm) // 初始化状态
initProvide(vm) // resolve provide after data/props
callHook(vm, 'created') // 调用生命周期created钩子
/* istanbul ignore if */
if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${vm._name} init`, startTag, endTag)
}
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
我们可以看到这个方法只在vue构造函数的原型上增加了_init方法,而_init方法中调用了各种初始化方法,并且也能看到在生命周期钩子beforeCreate和created之间初始化provide、inject和初始化状态,我们今天要探讨的是数据响应式,我们只看数据相关的initState()就行了。
initState
源码路径:\src\core\instance\state.js
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)
}
}
initState方法从实例上获取用户传入的options,然后细分对options中的每一项进行初始化,到这里我们也终于看见了今天的目标initData()。
initData
源码路径:src\core\instance\state.js
避免篇幅过长,这里我就不贴源码了,实际initData中只是判断了用户传入data的类型,如果类型是function则执行该函数得到object类型的data,否则直接返回data,然后调用observe函数传入data。这里就是响应式的部分了,我们接着往下看.
vue数据的响应式
observe
源码路径:src\core\observer\index.js
export function observe (value: any, asRootData: ?boolean): Observer | void {
if (!isObject(value) || value instanceof VNode) {
return
}
let ob: Observer | void
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
}
可以看到observe方法首先判断传入的value是否有__ob__属性(是否是响应式数据),是则取__ob__属性值,否则new Observer()并且传入了用户定义的data。vue中已经是响应式的数据会被添加上__ob__属性,此判断可避免重复监听。我们来看Observer类。
源码路径:src\core\observer\index.js
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
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 (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])
}
}
}
可以看到Observer类给传入的value定义了一个__ob__的属性,值为当前实例。并且对传入的value做了判断如果是Array调用this.observerArray进行处理,是Object的话则调用this.walk进行处理,我们先来看Object的情况。
Object 的变化侦测
上面的walk方法获取了object的所有key进行遍历,并且调用defineReactive传入objec和key进行变化侦测,而且通过函数名我们也知道这是定义响应式的方法。
源码路径:src\core\observer\index.js
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep()
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) // 递归遍历
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
// 收集依赖
if (Dep.target) {
dep.depend()
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()
}
})
}
Tips:如果还有同学不知道Object.defineProperty方法怎么用的建议先去这里看一下。
defineReactive方法利用了Object.defineProperty重新定义了get、set方法,并且在get中收集依赖、在set中触发依赖。通过get方法中的收集依赖逻辑可以看出:Dep.target存在则调用dep.depend方法收集依赖。而set方法中则调用dep.notify方法通知依赖更新。这样只能监听数据中的属性并不能监听子属性,因为data某个属性的值也可能是对象或数组,所以需要调用observe方法递归监听。这样我们就实现了对object的监听。我们再回过头来看数组是如何处理的。
Array的变化侦测
vue中数组并不是通过监听索引去实现变化侦测的,因为这样做当我们用list.push(1)向list数组中push一个1时,并不会触发get/set。而我们日常编程中会频繁使用Array原型上的方法去操作数组,所以object那种方法就不灵了。
也正因为如此,vue中重写了数组原型上的方法,当我们使用数组原型方法改变数组时,向依赖发送通知即可。
我们回过头来看Observe类的observerArray方法,只是遍历了传进来的数组,并且递归调用observe监听子项。而再调用observerArray方法之前则将value的原型指向了arrayMethods,我们来看下arrayMethods具体是怎么实现的。
源码路径:src\core\observer\array.js
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
const methodsToPatch = ['push','pop','shift','unshift','splice','sort','reverse']
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
let inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
if (inserted) ob.observeArray(inserted)
// notify change
ob.dep.notify()
return result
})
})
首先保存了Array的原型,然后创建了arrayMethods的原型指向Array的原型,因为js中只有push、pop、shift、unshift、splice、sort、reverse这七个方法会改变原数组,所以只需要重写这七个方法就可以了。然后给arrayMethods添加这七个方法。在重写的方法里调用原来的方法对数组进行操作,然后手动通知依赖进行更新即可。因为push、unshift、splice可以给数组新增项,而新增项也可能是对象或数组所以调用observeArray方法对新增项进行变化侦测。
上面我们频繁提到依赖、依赖收集、通知依赖更新,那什么是依赖呢?依赖又收集到哪里去呢?我们接着往下看。
依赖收集到哪里?
vue中封装了一个Dep类,用来管理依赖,使用这个类我们可以添加依赖、删除依赖、向依赖发送通知等,也被称为依赖收集器。
源码路径:src\core\observer\dep.js
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub: Watcher) {
this.subs.push(sub)
}
removeSub (sub: Watcher) { // 删除依赖
remove(this.subs, sub)
}
depend () { // 添加依赖
if (Dep.target) {
Dep.target.addDep(this)
}
}
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()
}
}
}
看到这里你也就明白了,其实就是data的每一个key都有一个dep依赖收集器,依赖则收集在该dep的subs数组中。上面提到的收集依赖就是调用depend方法,但是我们通过上面代码发现貌似addSub方法才是将依赖添加到数组中的,这是怎么回事呢?因为dep和依赖是相互订阅的。depend方法中调用依赖的addDep方法将该dep保存到依赖中,而依赖的addDep方法中调用了dep.addSub将依赖保存到dep里。
那什么是依赖呢?这里的Dep.target其实就是依赖也是watcher,watcher分为渲染watcher和用户watcher,vue是组件级别的更新,就是因为一个组件对应一个渲染watcher,它控制着当前组件的更新,但是并不会控制子组件的更新。而用户watcher就是watch选项和$watch、计算属性也是通过watcher实现的,这些我们以后再说,先来看渲染相关的watcher。
依赖Watcher
为了突出重点,这里代码精简过。
源码路径:src\core\observer\watcher.js
export default class Watcher {
constructor (
vm: Component,
expOrFn: string | Function,
cb: Function,
options?: ?Object,
isRenderWatcher?: boolean
) {
this.vm = vm
if (isRenderWatcher) {
vm._watcher = this
}
vm._watchers.push(this)
// options
if (options) {
this.deep = !!options.deep
this.user = !!options.user
this.lazy = !!options.lazy
this.sync = !!options.sync
this.before = options.before
} else {
this.deep = this.user = this.lazy = this.sync = false
}
this.cb = cb
this.id = ++uid // uid for batching
this.active = true
this.dirty = this.lazy // for lazy watchers
this.deps = []
this.newDeps = []
this.depIds = new Set()
this.newDepIds = new Set()
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)
}
this.value = this.lazy
? undefined
: this.get()
}
get () {
pushTarget(this)
let value
const vm = this.vm
try {
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)
}
popTarget()
this.cleanupDeps()
}
return value
}
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)
}
}
}
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
}
当组件渲染时会执行new Watcher(),此时会调用Watcher类中的get方法,我们重点关注一下这里,首先调用了pushTarget将this传过去,这个方法就是设置Dep.target = this(watcher实例),然后执行this.getter方法,其实这里就会获取数据,从而触发data的get方法此时Dep.target存在值,依赖就会被收集。然后调用popTarget方法,就是设置Dep.target = undefined。这里还是很有意思的,在组件渲染时初始化一个Watcher实例,也就完成了对应的收集依赖。
当数据改变时会触发set方法,而set方法中会调用该key对应的dep.notify通知watcher进行更新,通过notify方法,我们能看出就是循环subs取出其中的每一个watcher并调用其update方法。update方法将watcher推入一个队列进行异步更新。
写在最后
因为vue实现数据响应式的特点,也导致object、array有些操作是无法侦测到变化的。
object的问题
例如:我们使用obj.name='pdd'或者obj['name'] = 'pdd'向obj中新增一个属性,而vue.js无法侦测到变化,使用delete操作符删除obj一个属性时,vue.js同样无法侦测到变化。
array的问题
例如: 我们使用arr[0] = 2,即使用数组索引去改变数组某一项时,vue.js无法侦测到变化,使用arr.length = 0,去清空数组时也不能侦测到变化。
VUE3采用了ES6 Proxy重写了响应式部分,不存在以上问题。但是VUE2.X为了解决这些问题提供了两个api:Vue.set 、Vue.delete。但是对于数组使用以上两个方法只能通过索引去操作,通过length去清空数组还是无法侦测到变化的。后面会单独写一篇关于常用的全局api的实现原理。
以上就是VUE 2.X数据响应式的内容,希望小伙伴们能从本文中有所收获,三连等于学会(滑稽)。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 模板不会安装或需要功能定制以及二次开发?
发表评论
还没有评论,快来抢沙发吧!