【Vue2.x原理剖析一】响应式原理

前言
源码分析文章看了很多,也阅读了至少两遍源码。终归还是想自己写写,作为自己的一种记录和学习。重点看注释部分和总结,其余不用太关心,通过总结对照源码回看过程和注释收获更大
Vue核心
数据驱动视图变化
响应式基本原理
- 对象通过
Object.defineProperty
进行get、set
拦截 - 数组通过重写数组的七中修改方法完成对数组的监听
- 采用观察者模式实现依赖收集和派发更新
数据初始化
new Vue({
el: "#app",
router,
store,
render: h => h(App)
})
vue其实就是一个构造函数,在实例化过程中,对数据进行初始化,initMixin把_init
方法挂载到Vue原型上供Vue调用
// src/core/instance/index.js
...
function Vue (options) { //vue构造函数
this._init(options)
}
initMixin(Vue) // Vue.prototype._init
...
export default Vue
_init
方法会对一些声明周期等做初始化,其中就包括初始化状态
// /src/core/instance/init.js
export function initMixin (Vue) {
Vue.prototype._init = function (options) {
const vm = this
...
// 初始化状态
initState(vm)
...
}
}
在initState
方法中,就包括了一些对prop、methods、data
等的初始化,初始化的顺序为prop -> methods -> data -> computed -> watch
,在初始化data时,就会对data里的数据进行代理
// src/core/instance/state.js
export function initState (vm) {
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)
}
if (opts.computed) initComputed(vm, opts.computed)
if (opts.watch && opts.watch !== nativeWatch) {
initWatch(vm, opts.watch)
}
}
function initData (vm) {
let data = vm.$options.data
// 定义私有变量_data
data = vm._data = typeof data === 'function'? getData(data, vm): data || {}
// proxy data on instance
// 循环判断,props和methods定义的变量不能和data里定义的变量相同,因为他们最后都会挂载到vue实例vm上
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') {
if (methods && hasOwn(methods, key)) {
warn(
`Method "${key}" has already been defined as a data property.`,
vm
)
}
}
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挂载到vue实例vm上
proxy(vm, `_data`, key)
}
}
// 对data做劫持
observe(data, true /* asRootData */)
}
对对象的数据劫持
在初始化数据时,会对数据进行劫持,如果是对象类型才能被检测,同时在Observer类中,会声明一个__ob__
(通过修改属性描述符来标记数据不会被枚举)属性用来标记数据是否被监听过,如果监听过就不再进行监听。
// /src/core/observer/index.js
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep() //数据可能是数组或者对象,如果是数组此处会给数组添加一个dep
this.vmCount = 0
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods) // __proto__
} else {
copyAugment(value, arrayMethods, arrayKeys) //循环每一项赋值
}
this.observeArray(value) //观测数组每一项
} else {
this.walk(value)// 对对象的每一项进行劫持
}
}
walk (obj) {
const keys = Object.keys(obj)
for (let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i])
}
}
// 对数组的每一项检查如果是对象类型,则进行监听
observeArray (items) {
for (let i = 0, l = items.length; i < l; i++) {
observe(items[i])
}
}
}
...
// 如果是对象类型就递归判断劫持
export function observe (value, asRootData) {
if (!isObject(value) || value instanceof VNode) { //如果是虚拟节点也不用观测
return
}
let ob
if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
ob = value.__ob__
} else if (
shouldObserve &&
!isServerRendering() &&
(Array.isArray(value) || isPlainObject(value)) &&
Object.isExtensible(value) && //isExtensible表示可以被defineProperty
!value._isVue
) {
ob = new Observer(value)
}
if (asRootData && ob) {
ob.vmCount++
}
return ob
}
...
通过defineReactive
方法对对象不断的进行递归进行劫持,利用Object.defineProperty
中的getter
和setter
对数据进行拦截
// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
const dep = new Dep()
// 在data定义的对象中,可自定义getter和setter
const property = Object.getOwnPropertyDescriptor(obj, key)
if (property && property.configurable === false) { // 可配置才能加defineProperty
return
}
const getter = property && property.get
const setter = property && property.set
if ((!getter || setter) && arguments.length === 2) {
val = obj[key]
}
let childOb = !shallow && observe(val)
// 核心方法,进行get,set劫持
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
const value = getter ? getter.call(obj) : val
if (Dep.target) {
// 将dep添加到watcher里,同时将watcher添加到dep中
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
// 如果新值和旧值一样则返回
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
if (getter && !setter) return
if (setter) {
setter.call(obj, newVal)
} else {
val = newVal
}
//vm.a = 1 => vm.a = [1,2,3]
childOb = !shallow && observe(newVal) //当赋值一个新值时 需要重新监控 并且更新childOb
// 通知dep里存在的watcher去更新
dep.notify()
}
})
}
对数组的数据劫持
初始化Observer
类和对对象的递归劫持过程中会判断数据是否为数据,如果是数组会通过对数组方法的劫持来监听数组(切片编程),也会递归对数组中不是基本数据类型的数据进行观测
只有这七种方法会改变数组,所以Vue对这七种方法进行了重写,目的是对新增的内容进行观测,同时派发更新
// /src/core/observer/array.js
import { def } from '../util/index'
const arrayProto = Array.prototype
// 继承数组方法, 保留原有数组功能
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 inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果新添加的值是对象类型,回去再劫持
if (inserted) ob.observeArray(inserted)
// 通知相关watcher更新
ob.dep.notify()
return result
})
})
watcher和Dep
将watcher当做观察者,需要订阅数据的变动,当数据变动之后,通知watcher去执行某些方法
// /src/core/observer/watcher.js
import { queueWatcher } from './scheduler'
import Dep, { pushTarget, popTarget } from './dep'
export default class Watcher {
constructor (vm,expOrFn,cb,options,isRenderWatcher) {
this.vm = vm
...
vm._watchers.push(this) //为了能强制更新
// options
this.deps = []
this.depIds = new Set()
this.newDepIds = new Set()
this.expression = process.env.NODE_ENV !== 'production'? expOrFn.toString(): ''
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
}
}
// 默认初始化会执行this.get()
this.value = this.lazy? undefined: this.get()
}
get () {
// 在调用方法之前将当前watcher实例放在全局Dep.target上
pushTarget(this)
let value
...
// 如果watcher是渲染watcher,那么就相当于执行vm._update(vm._render())这个方法执行的时候会取值,实现依赖收集
value = this.getter.call(vm, vm)
...
// 在调用方法之后把当前watcher实例从全局Dep.target移除
popTarget()
return value
}
// 将dep放到deps里面,同时保证同一个dep只被保存到watcher一次,同一个watcher也只保存在dep一次
addDep (dep) {
const id = dep.id
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
// 将当前watcher添加到dep的subs容器里
dep.addSub(this)
}
}
}
update () {
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
// 异步执行
queueWatcher(this)
}
}
depend () {
let i = this.deps.length
while (i--) {
this.deps[i].depend()
}
}
}
Dep可以看做是观察者模式里的被观察者,在subs容器里收集watcher,当数据变动时同时自身subs所有的watcher去更新
// /src/core/observer/dep.js
let uid = 0
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
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() //调用watcher更新
}
}
}
// 全局watcher指向,初始状态是null
Dep.target = null
const targetStack = []
export function pushTarget (target) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
watcher
在初次数据渲染时为渲染watcher
依赖收集
对象依赖收集: 在defineReactive
函数里新建一个Dep
类(每一个属性都会有一个Dep
类用于存放当前watcher),在Object.defineProperty()
进行get
拦截时会调用dep
中的depend
方法,将dep
保存在watcher
中,同时,将watcher
也保存在dep
维护的栈中
// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
const dep = new Dep()
...
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) {
...
}
})
}
数组依赖收集: 与对象收集依赖同理,唯一不同的是会判断数组里是否嵌套了对象类型,如果嵌套了就再拦截
...
// 例obj为{a:[1,2,3]},key为a
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()
// 对嵌套数组里的对象进行拦截,例:{a:[{b:1},2]}
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
set: function reactiveSetter (newVal) {
...
}
})
...
派发更新
对象的更新: 在设置data
里的属性值时会触发set
拦截,如果赋值的是一个对象会对对象再进行劫持,**调用dep.notify()
**方法去让存在当前属性dep
里的所有watcher
进行更新操作
// /src/core/observer/index.js
export function defineReactive (obj,key,val,customSetter,shallow) {
const dep = new Dep()
...
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter () {
...
},
set: function reactiveSetter (newVal) {
const value = getter ? getter.call(obj) : val
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
...
//vm.a = 1 => vm.a = [1,2,3]
childOb = !shallow && observe(newVal) //当赋值一个新值时 需要重新监控 并且更新childOb
dep.notify()
}
})
}
// /src/core/observer/dep.js
// notify通知所有watcher更新
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
...
notify () {
const subs = this.subs.slice()
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update() //调用watcher更新
}
}
}
...
数组的更新: 通过重写的7种数组方法更改值时,就会被拦截到,同时去调用dep.notify()
去通知所有watcher
更新
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 inserted
switch (method) {
case 'push':
case 'unshift':
inserted = args
break
case 'splice':
inserted = args.slice(2)
break
}
// 如果添加的值时数组会再做劫持
if (inserted) ob.observeArray(inserted)
ob.dep.notify()
return result
})
})
注意点
- 由于利用
Object.defineProperty
递归对对象进行观测,所以对象的层级过深会导致性能受影响 - 此时的观测是基于
data
定义的对象属性(observe(data)初始化之后执行,之后不再执行
),如果后期给对象新增/删除
属性时,Vue是拦截不到的,也就是说不是响应式的,只有对象本身就存在的属性修改才会被劫持。 - 可以使用
$set
让对象自己去notify
,用户对对象属性赋值新对象,vue会对这个对象进行观测 - 通过数组索引修改数组或者改变数组长度,是不会触发更新的,通过7种重写的方法更新视图
- 数组重写的7种方法
push、shift、unshift、pop、reverse、sort、splice
- Vue支持在
data
属性中自定义setter
和getter
,在通过Object.defineProperty
进行劫持时会进行合并
总结
在new vue()
的过程中,vue会对传入的options
进行初始化,里边包括对data
定义的数据的初始化,同时对数据进行观测(observe(date)
)。对于对象,通过Object.defineProperty()
进行get
和set
劫持,在初始化渲染页面时,会触发get
劫持,此时在get
方法中会将当前的渲染watcher
存放在属性的dep
中,同时还会将dep
存放在watcher中
,当通过赋值等操作对data
里的属性值做更改是,会触发set
劫持,此时会触发属性里的dep.notify()
方法,让存在dep
中的所有watcher
进行更新操作。对于数组,get
拦截过程和对象拦截过程相似,Vue重写了数组的七种方法,所以,通过这七种方法更新数组值时,会调用属性的dep.notify()
方法通知所有watcher
进行更新操作。
系列链接
【Vue2.x原理剖析一】响应式原理 【Vue2.x原理剖析二】计算属性原理 【Vue2.x原理剖析三】侦听属性原理 【Vue2.x原理剖析四】模板编译原理 【Vue2.x原理剖析五】初始渲染及更新原理 【Vue2.x原理剖析六】diff算法原理 【Vue2.x原理剖析七】组件原理
转载自:https://juejin.cn/post/7126081397021736968