本文将带大家疾速过一遍 Vue 数据响应式原理,解析源码,学习设计思路,循序渐进。
数据初始化
_init
在咱们执行 new Vue
创立实例时,会调用如下构造函数,在该函数外部调用this._init(options)
。
import {initMixin} from "./init.js";
// 先创立一个 Vue 类,Vue 就是一个构造函数(类)通过 new 关键字进行实例化
function Vue(options) {
// 这里开始进行 Vue 初始化工作
this._init(options);
}
// _init 办法是挂载在 Vue 原型的办法,每一个 new 实例能够调用,由 initMixin 办法挂载
// 将不同的操作拆分成不同的模块,导入后对 Vue 类做一些解决,此做法更利于保护
initMixin(Vue); // 定义原型办法_init
stateMixin(Vue) // 定义 $set $get $delete $watch 等
eventsMixin(Vue) // 定义事件 $on $once $off $emit
lifecycleMixin(Vue) // 定义 _update $forceUpdate $destroy
renderMixin(Vue) // 定义 _render 返回虚构 dom
export default Vue;
initMixin
函数外面定义了原型办法 _init
,_init
调用了 initState(vm)
等办法,_init
里做了很多初始化工作,咱们重点关注initState
import {initState} from "./state";
export function initMixin(Vue) {Vue.prototype._init = function (options) {const vm = this; // 这里的 this 指向调用_init 办法的对象(即 new 的实例)
// this.$options 就是用户 new Vue 的时候传入的属性
vm.$options = options;
...
initLifecycle(vm);
initEvents(vm);
initRender(vm);
callHook(vm, 'beforeCreate');
initInjections(vm); // resolve injections before data/props
// 初始化状态,在 beforeCreate 之前,created 之后
initState(vm);
initProvide(vm); // resolve provide after data/props
callHook(vm, 'created');
...
};
}
initState
initState 函数按程序初始化 $options
的数据,程序为 prop>methods>data>computed>watch
import {observe} from "./observer/index.js";
function initState (vm) {vm._watchers = [];
const opts = vm.$options;
// 按程序初始化 prop>methods>data>computed>watch
if (opts.props) {initProps(vm, opts.props); }
if (opts.methods) {initMethods(vm, opts.methods); }
if (opts.data) { // 初始化 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);
}
}
initData
initData 做了什么事?
-
将
vm.$options.data
赋值给vm._data
此处有个细节,vue 组件 data 举荐应用函数,避免数据在组件之间共享,起因是如果你定义的 data 是个对象的话,那所有的组件实例的 data 都会援用这个对象,一个组件更改了 data 别的组件也会发生变化,他们的 data 指向同一个内存地址。
- 判断办法和属性是否重名,以及是否有保留属性
- 没有问题就通过
proxy()
把 data 里的每一个属性都代理到以后实例上,就能够通过this.xx
拜访了 - 最初再调用
observe
监听整个 data,observe 办法用于创立监听器
import {observe} from "./observer/index.js";
function initState (vm) {
...
initData(vm);
}
function initData (vm: Component) {
// 获取以后实例的 data
let data = vm.$options.data
// 判断 data 的类型
data = vm._data = typeof data === 'function'
? getData(data, vm)
: data || {}
if (!isPlainObject(data)) {data = {}
process.env.NODE_ENV !== 'production' && warn(` 数据函数应该返回一个对象 `)
}
// 获取以后实例的 data 属性名汇合
const keys = Object.keys(data)
// 获取以后实例的 props
const props = vm.$options.props
// 获取以后实例的 methods 对象
const methods = vm.$options.methods
let i = keys.length
while (i--) {const key = keys[i]
// 非生产环境下判断 methods 里的办法是否存在于 props 中
if (process.env.NODE_ENV !== 'production') {if (methods && hasOwn(methods, key)) {warn(`Method 办法不能反复申明 `)
}
}
// 非生产环境下判断 data 里的属性是否存在于 props 中
if (props && hasOwn(props, key)) {process.env.NODE_ENV !== 'production' && warn(` 属性不能反复申明 `)
} else if (!isReserved(key)) {
// 都不重名的状况下,代理到 vm 上,能够让 vm._data.xx 通过 vm.xx 拜访
proxy(vm, `_data`, key)
}
}
// 监听 data
observe(data, true /* asRootData */)
}
proxy 数据代理
proxy 函数中调用了 Object.defineProperty
将_data
中的每个 property 代理到了 vm 身上,作用就是,能够 vm._data.xx 通过 vm.xx 拜访,当你拜访 vm.a 的时候实际上是拜访的 vm._data.a。
function proxy (target, sourceKey, key) {sharedPropertyDefinition.get = function proxyGetter () {return this[sourceKey][key]
};
sharedPropertyDefinition.set = function proxySetter (val) {this[sourceKey][key] = val;
};
Object.defineProperty(target, key, sharedPropertyDefinition);
}
observe 数据劫持
observe
该办法用于创立监听器实例
export function observe (value: any, asRootData: ?boolean): Observer | void {
// 如果不是 'object' 类型 或者是 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
}
Observer
监听器类,将数据转换为响应式数据
export class Observer {
value: any;
dep: Dep;
vmCount: number; // 根对象上的 vm 数量
constructor (value: any) {
this.value = value
this.dep = new Dep(); // 事后实例化一个 dep,用于保留数组的依赖
this.vmCount = 0
// 给 value 增加 __ob__ 属性,值为为以后 value 创立的 Observe 实例
// 示意曾经变成响应式了,目标是对象遍历时就间接跳过,防止反复监听
def(value, '__ob__', this)
// 类型判断
if (Array.isArray(value)) {
// 判断数组是否有__proto__
if (hasProto) {
// 如果有就把它的原型设置为 arrayMethods,arrayMethods 对象领有变异后的七个数组办法并且原型是原生数组 Array 的原型
protoAugment(value, arrayMethods); // 原型加强
} else {
// 没有就通过 def,也就是 Object.defineProperty 去定义属性值
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {this.walk(value)
}
}
// 如果是对象类型
walk (obj: Object) {const keys = Object.keys(obj)
// 遍历对象所有属性,转为响应式对象,也是动静增加 getter 和 setter,实现双向绑定
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])
}
}
}
参考 前端进阶面试题具体解答
对于数组和对象有不同的解决,咱们先来看解决对象响应式的办法,walk
。
walk
遍历对象所有属性,调用 defineReactive
办法转为响应式对象,
walk (obj: Object) {const keys = Object.keys(obj)
// 遍历对象所有属性,转为响应式对象,也是动静增加 getter 和 setter,实现双向绑定
for (let i = 0; i < keys.length; i++) {defineReactive(obj, keys[i])
}
}
defineReactive
定义响应式对象,getter 时收集依赖,setter 时触发依赖
export function defineReactive (
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
// 创立 dep 实例,保留属性的依赖,getter 时增加依赖,setter 时触发依赖
const dep = new Dep(); 这个是对象的依赖
// 拿到对象的属性描述符
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) {return}
// 获取自定义的 getter 和 setter
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {val = obj[key]
}
// 如果 val 是对象的话就递归监听
// 递归监听子属性,如果 value 还是一个对象会持续走一遍 defineReactive 层层遍历始终到 value 不是对象才进行,所以如果对象层级过深,对性能会有影响
let childOb = !shallow && observe(val) // data = {a: {b: 3}, c: [1, 2]} 属性值如果是对象或数组会返回 Observer 实例
// 截持对象属性的 getter 和 setter
Object.defineProperty(obj, key, { // 例如监听 data.a,那 val 就是{b: 3}
enumerable: true,
configurable: true,
// 拦挡 getter,当取值时会触发该函数
get: function reactiveGetter () {const value = getter ? getter.call(obj) : val
// 开始依赖收集(在 get 中会收集属性的依赖,以及其属性值的依赖)// 初始化渲染 watcher 时拜访到曾经被增加响应式的对象,从而触发 get 函数
if (Dep.target) { // 如果当初处于依赖收集阶段
dep.depend(); // 增加以后属性的依赖
if (childOb) { // 数组会在此收集依赖,在数组被 push 等操作时调用保留的 Observer 实例触发依赖;对象会收集两次依赖,然而对象的第二次收集不会被 setter 触发
// childOb.dep 就是 Observer 中 this.dep = new Dep()
childOb.dep.depend(); // 父属性蕴含子属性,即拜访了 this.a,实际上也拜访了 this.a.b,this.a.b 变了,this.a 就变了,所以子属性也要收集依赖
if (Array.isArray(value)) {dependArray(value)
}
}
}
return value
},
// 拦挡 setter,当值扭转时会触发该函数
set: function reactiveSetter (newVal) {const value = getter ? getter.call(obj) : val
// 判断是否发生变化
if (newVal === value || (newVal !== newVal && value !== value)) {return}
if (process.env.NODE_ENV !== 'production' && customSetter) {customSetter()
}
// 没有 setter 的拜访器属性
if (getter && !setter) return
if (setter) {setter.call(obj, newVal)
} else {val = newVal}
// 如果新值是对象的话递归监听
childOb = !shallow && observe(newVal)
// 遍历告诉贮存在 Dep 实例中的所有依赖
dep.notify()}
})
}
Object.defineProperty 定义响应式对象的毛病
- 监听嵌套层级过深的对象会影响性能
- 对象新增或者删除的属性无奈被
set
监听到 只有对象自身存在的属性批改才会被劫持,所以 Vue 设计了$set
和$delete
办法,更新数据的同时手动触发告诉依赖 - 如果用其来监听数组的话,无奈监听数组长度动态变化,并且只能监听通过对已有元素下标的拜访进行的批改,即
arr[已有元素下标] = val
咱们本人手写一个递归设置响应式的办法来试一下:
function defineProperty(obj, key, val){observer(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get() {
// 读取办法
console.log('读取', key, '胜利')
return val
},
set(newval) {
// 赋值监听办法
if (newval === val) return
observer(newval)
console.log('监听赋值胜利', newval)
val = newval
}
})
}
function observer(obj) {if (typeof obj !== 'object' || obj == null) {return}
for (const key of Object.keys(obj)) {
// 给对象中的每一个办法都设置响应式
defineProperty(obj, key, obj[key])
}
}
const arr = [{a:3}, 66, [4,5]];
const obj = {a:1, b: [2]};
arr.length = 33; // 无奈监听数组长度动态变化
arr[2].push(22) // 只能监听通过对已有元素下标的拜访进行的批改
arr[2][0] = 5; // 拜访已有元素的下标能够监听批改
obj.c = 6; // 无奈监听新增加的属性
delete obj.b // 无奈监听属性被删除
obj.b = 66; // 被删除后就失去响应式了
尽管 defineProperty
能够监听通过对已有元素下标拜访的批改,然而出于性能思考,vue 并没有应用这一性能来使数组实现响应式,因为数组元素太多时 消耗肯定性能 ,要挨个遍历监听一遍数组的每一个属性,属性可能还会蕴含本人的嵌套属性,所以vue
的做法是批改原生操作数组的办法,并且跟用户约定批改数组要用这些办法去操作。
尤大也做出了官网的解释:
数组的观测
数组元素增加或删除操作的观测通过 创立一个以原生 Array 的原型为原型的新对象,为新对象增加数组的变异办法,将察看的对象的原型设置为这个新对象,被察看的对象调用数组办法时就会应用被重写后的办法。
记得咱们在讲寄生式继承时说的么,寄生式继承的外围:应用原型式继承
Object.create(parent)
能够取得一份指标对象的浅拷贝,在这个浅拷贝对象上进行 加强 ,增加一些办法属性。
vue 对重写数组办法的设计与寄生式继承相似,都是 面向切面编程的思维 (AOP),即 不毁坏原有性能封装的前提下,动静的扩大性能
import {TriggerOpTypes} from '../../v3'
import {def} from '../util/index'
const arrayProto = Array.prototype // 用 Array 的原型创立一个新对象,arrayMethods.__proto__ === arrayProto,省得净化原生 Array
export const arrayMethods = Object.create(arrayProto);
// 须要重写的办法
const methodsToPatch = [
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
/** * Intercept mutating methods and emit events */
methodsToPatch.forEach(function (method) {
// cache original method
const original = arrayProto[method]
// 给 arrayMethods 对象定义上述办法,使该对象领有原生办法能力的同时增加响应式行为
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': // 能够监测数组长度变动
//splice 格局是 splice(下标,数量,插入的新项)
inserted = args.slice(2); // 获取插入的新项
break
}
if (inserted) ob.observeArray(inserted)
// notify change
if (__DEV__) {
ob.dep.notify({
type: TriggerOpTypes.ARRAY_MUTATION,
target: this,
key: method
})
} else {ob.dep.notify()
}
return result
})
})
因为出于性能思考,vue 没有应用
defineProperty
劫持数组,所以要通过索引批改数组,咱们须要应用$set
。
总结
以上就是 Vue2
的响应式数据原理,讲述了如何对数据进行响应式观测,外围就是通过 Object.defineProperty
对数据进行劫持,在 getter
中收集依赖,setter
中派发依赖,残缺的响应式原理,如批改数据后视图是如何更新视图的还须要联合 Dep 和 Watcher 来看,这段后续接着说,一点点地来消化。