我的开源库:
- fly-barrage 前端弹幕库,项目官网:https://fly-barrage.netlify.app/,可实现类似于 B 站的弹幕效果,并提供了完整的 DEMO,Gitee 推荐项目;
- fly-gesture-unlock 手势解锁库,项目官网:https://fly-gesture-unlock.netlify.app/,在线体验:https://fly-gesture-unlock-online.netlify.app/,可高度自定义锚点的数量、样式以及尺寸;
这篇博客主要讲解 Vue 是如何将数据转换成响应式的,将数据转换成响应式是进行依赖收集和变化侦测的基础。
1,对数据响应式处理的入口
第一小节是为了让大家了解响应式处理的代码在整体源码中的位置。
1-1,执行 new Vue(core/instance/index.js)
function Vue (options) {
// 执行 vm 原型上的 _init 方法,该方法在 initMixin 方法中定义
this._init(options)
}
// 写入 vm._init
initMixin(Vue)
vue 里面执行 _init 方法,该方法定义在 initMixin 方法中。
1-2,_init 方法(core/instance/init.js)
export function initMixin (Vue: Class<Component>) {
Vue.prototype._init = function (options?: Object) {
// 初始化 state,包括 props、methods、data、computed、watch
initState(vm)
// 如果配置中有 el 的话,则自动执行挂载操作
if (vm.$options.el) {
vm.$mount(vm.$options.el)
}
}
}
初始化数据的方法是 initState。
1-3,initState 方法(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)
// 初始化监听属性
// nativeWatch的作用:Firefox has a "watch" function on Object.prototype...
if (opts.watch && opts.watch !== nativeWatch) {
// 进行侦听属性的初始化过程
initWatch(vm, opts.watch)
}
}
initState 方法内进行了一系列状态的初始化,我们以 initData 为例。
1-4,initData 方法(core/instance/state.js)
// 初始化我们配置中写的 data 对象,传递的参数(vm)是当前 Vue 的实例
function initData (vm: Component) {
// observe data
observe(data, true /* asRootData */)
}
1-5,observe 方法(core/observer/index.js)
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果这个值不是一个对象、或者这个值是一个虚拟节点实例的话,在这里直接 return
if (!isObject(value) || value instanceof VNode) {
return
}
// 声明要返回的 ob 变量
let ob: Observer | void
// 如果当前 value 有 __ob__ 属性,且这个属性是 Observer 类的实例的话
// 直接将 value.__ob__ 赋值给 ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
observerState.shouldConvert &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) &&
!value._isVue
) {
// 传递进来的 value 并不是响应式的,在这里。通过 new Observer(value) 将其转换成响应式的
// 并且返回 new 出来的实例
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
在这里通过执行 ob = new Observer(value) 将数据转换成响应式的,所谓响应式的数据是指 Vue 能够侦测到该数据的使用和变更。
2,借助 Observer 类将数据转换成响应式的
响应式的数据是指 Vue 能够侦测到该数据的使用和变更。
侦测数据的使用是为了依赖收集。
侦测数据的变更是为了触发依赖更新
2-1,class Observer(core/observer/index.js)
export class Observer {
// 被处理成响应式的数据,可以是对象类型或者是数组类型
value: any;
dep: Dep;
// number of vms that has this object as root $data
// 将此对象作为根 $data 的 vms 数量
vmCount: number;
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
// 对象类型值和数组类型值有不同的处理,在这里进行 if else 判断。
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
// 用于将对象中的属性都转换成响应式的
this.walk(value)
}
}
/**
* 该方法用于将 obj 中所有的 key 都转换成响应式的
* 具体的做法是遍历 keys,每个 key 都执行 defineReactive 方法
* defineReactive 方法用于将对象中具体的 key 转换成响应式的
*/
walk (obj: Object) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
/**
* 用于将数组中的元素都转换成响应式的
*/
observeArray (items: Array<any>) {
for (let i = 0, l = items.length; i < l; i++) {
// 对数组中的每个元素都执行 observe 方法
observe(items[i])
}
}
}
Observer 类的构造方法中会判断处理的数据是不是数组类型,对象类型和数组类型的处理方式是不一样的,对象类型会执行 walk 方法。
walk 方法会遍历对象的 key,执行 defineReactive 方法,在 defineReactive 方法中将对象中的 key 都转换成 Object.defineProperty 的形式,这样 Vue 就能监控到对象属性的使用和变更了。
2-2,defineReactive 方法(core/observer/index.js)
export function defineReactive (
// 对象
obj: Object,
// key
key: string,
// 值
val: any,
customSetter?: ?Function,
// 浅的
shallow?: boolean
) {
// 如果 val 是一个对象类型的话,那么这个 dep 将用于保存 val 的依赖列表
// 数组类型值的依赖列表保存在 observer.dep 中
const dep = new Dep()
const getter = property && property.get
const setter = property && property.set
// 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 在此进行依赖收集
get: function reactiveGetter () {
// 触发执行上面拿到的 getter
const value = getter ? getter.call(obj) : val
/ 下面是依赖收集的操作 /
// 如果 Dep 上的静态属性 target 存在的话
if (Dep.target) {
// 向 dep 中添加依赖,依赖是 Watcher 的实例
dep.depend()
if (childOb) {
// childOb.dep 用来存储数组类型值的依赖
childOb.dep.depend()
// 如果值是数组类型的话
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// getter 返回值
return value
},
// 在此进行派发更新
set: function reactiveSetter (newVal) {
// 触发依赖的更新
dep.notify()
}
})
}
在 defineReactive 方法的开头创建了一个 Dep 类的实例,这个 dep 实例是用于存储对象类型值依赖的,我们可以看到在下面的 get 中,执行 dep.depend() 进行依赖的收集,依赖收集的细节,下面再说。
如果数据是数组类型的话,还需要执行数组类型值对应 dep 实例的 depend 方法,数组类型的 dep 实例存储在 Observer 实例上面。关于数组类型值为什么要多这一步处理?以及 dep 为什么存储在 Observer 实例上面,我们在下一小节细讲一下,要不然容易引起困惑。
2-3,数组类型值额外处理的原因
假设上面 defineReactive 方法的 obj 参数为:
let obj = {
names: ['tom', 'jack', 'rose']
}
key 参数为 'names',value 参数为 ['tom', 'jack', 'rose']。
那么上面的这个 names 属性的变更有两种方式:
(1)obj.names = ['小明', '小红', '小山']
(2)obj.names.push('alice')
第一种方式是直接对 obj.names 重新设值,这种变更数据的方式 Object.defineProperty set 是能够侦测到变化的,这在 set 中执行 dep.notify() 触发依赖更新就可以了。
但是第二种方式使用数组原型上的方法变更数据,这种变更方法 Object.defineProperty set 是侦测不到的。所以 Vue 另辟新径,重写了数组的原型方法,如果用户执行数组的原型方法变更数组的话,Vue 就能够在重写的原型方法中执行依赖更新的操作。
Vue 在重写的原型方法中执行依赖更新操作的前提是:在重写的原型方法中能够拿到这个数组对应的 dep 实例,而在 defineReactive 方法中创建的 dep 实例,在重写的原型方法中是获取不到的。Vue 的解决方案是在 Observer 实例上创建一个专门服务于数组收集依赖的 dep 实例,因为Observer 的实例会被定义在值的 '__ob__' 属性上,所以在数组的原型方法中,可以通过 this.__ob__.dep 获取到数组对应的 Dep 实例,这就解决了问题。相关代码如下:
(1)将为数组类型值的 dep 设值到 Observer 实例上
export class Observer {
value: any;
dep: Dep;
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
}
}
(2)在 get 中为数组类型值收集依赖,依赖存储到 childOb.dep 中
// 这个 childOb(Observer类的实例)中的 dep 是用来保存数组类型值的依赖的
let childOb = !shallow && observe(val)
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
// 在此进行依赖收集
get: function reactiveGetter () {
// 触发执行上面拿到的 getter
const value = getter ? getter.call(obj) : val
/ 下面是依赖收集的操作 /
// 如果 Dep 上的静态属性 target 存在的话
if (Dep.target) {
// 向 dep 中添加依赖,依赖是 Watcher 的实例
dep.depend()
if (childOb) {
// childOb.dep 用来存储数组类型值的依赖
childOb.dep.depend()
// 如果值是数组类型的话
if (Array.isArray(value)) {
dependArray(value)
}
}
}
// getter 返回值
return value
},
// 在此进行派发更新
set: function reactiveSetter (newVal) {
......
}
})
(3)在重写的原型方法中,获取 dep,并且触发依赖更新
[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.forEach(function (method) {
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this.__ob__
// 通知 ob.dep 中的依赖
ob.dep.notify()
// 在最后,返回 Array 方法执行的结果
return result
})
})
2-4,数组类型数据原型方法的重写
export class Observer {
constructor (value: any) {
this.value = value
this.dep = new Dep()
def(value, '__ob__', this)
if (Array.isArray(value)) {
const augment = hasProto
? protoAugment
: copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
// 用于将对象中的属性都转换成响应式的
this.walk(value)
}
}
}
hasProto 方法用于判断当前的浏览器环境支不支持 __proto__。
如果支持原型的话,就将重写的数组原型方法赋值到数组的 __proto__ 属性上。
function protoAugment (target, src: Object, keys: any) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
如果不支持 __proto__ 的话,直接将重写的原型方法赋值到数组值上面。
function copyAugment (target: Object, src: Object, keys: Array<string>) {
for (let i = 0, l = keys.length; i < l; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
加下来看看 Vue 是如何重写原型方法的,上面的 arrayMethods 就是包含重写原型方法的对象。
// 拿到 Array 的 prototype 原型对象
const arrayProto = Array.prototype
// 利用 Object.create() 创建一个新的对象,并且这个新的对象的原型链(__proto__)指向 arrayProto。
// 这样的话,我们只需要将一些需要改写的方法定义到 arrayMethods 对象中即可。
// 这样的话,我们既可以访问到 arrayMethods 对象中已经改写了的方法,也能访问到 arrayProto 对象中未改写的方法
// ^o^ 完美!
export const arrayMethods = Object.create(arrayProto)
// 能够改变数组内容方法的数组
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
// 进行遍历处理
.forEach(function (method) {
// 缓存原生的相应方法
const original = arrayProto[method]
// 定义该 method 对应的自定义方法
def(arrayMethods, method, function mutator (...args) {
// 执行原生方法拿到执行结果值,在最后将这个结果值返回
const result = original.apply(this, args)
// 这里的 this 是执行当前方法的数组的实例。在 Vue 中,每个数据都会有 __ob__ 属性,这个属性
// 是 Observer 的实例,该实例有一个 dep 属性(Dep 的实例),该属性能够收集数组的依赖
const ob = this.__ob__
// 数组有三种新增数据的方法。分别是:'push','unshift','splice'
// 这些新增的数据也需要变成响应式的,在这里,使用 inserted 变量记录新增的数据
let inserted
switch (method) {
// 如果当前的方法是 push 或者 unshift 的话,新增的数据就是 args,将 args 设值给 inserted 即可
case 'push':
case 'unshift':
inserted = args
break
// 如果当前的方法是 splice 的话,那么插入的数据就是 args.slice(2)
case 'splice':
inserted = args.slice(2)
break
}
// 如果的确新增了数据的话,将 inserted 作为参数执行 observer.observeArray() 方法,把新增的每个元素都变成响应式的
if (inserted) ob.observeArray(inserted)
// 通知 ob.dep 中的依赖
ob.dep.notify()
// 在最后,返回 Array 方法执行的结果
return result
})
})
(1)首先使用变量 arrayProto 保存数组原先的原型对象。
(2)然后创建对象 arrayMethods,原型链指向 arrayProto。
(3)最后在 arrayMethods 中重写那些能够改变数组本身的原型方法,在重写的原型函数中会执行 ob.dep.notify()
这样做的好处是:只重写了那些能够改变数组内容的原型方法,其余的原型方法借助于原型链还使用其原本实现。