变化侦测
侦测状态变化,重新渲染页面。
拉(通知状态改变,然后暴力比对哪些节点需要重新渲染): Angular脏检查、React虚拟dom
推(明确知道哪些状态改变,细粒度,通知绑定这个状态的依赖节点更新): Vue
但,粒度越细,每个状态绑定的依赖越多,追踪开销就越大。从Vue2.0开始引入虚拟dom,绑定依赖到组件层面,而不是节点层面。状态改变,通知到组件,组件内部再使用虚拟dom进行比对。
Object变化侦测
追踪变化 Object.defineProperty 和 Proxy
收集依赖
当数据发生变化的时候,需要通知使用了该数据的地方。所以在gettter中收集依赖,在setter中触发依赖。
function defineReactive(data, key, val) {
let dep = []; // 用于存储被收集的依赖
Obejct.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.push(window.target)
return val
},
set: function(newVal) {
if (val === newVal) return
for(let i=0; i<dep.length; i++) { // 遍历所有收集的依赖
dep[i](newVal, val)
}
val = newVal
},
})
}
为了减少耦合,封装Dep类,专门管理依赖
export default class Dep {
constructor () {
this.subs = []
}
addSub (sub) {
this.subs.push(sub)
}
removeSub (sub) {
remove(this.subs, sub)
}
depend () { // 收集依赖
if (window.target) {
this.addSub(window.target)
}
}
notify () { // 遍历依赖数组通知更新
const subs = this.subs.slice()
for ( let i = 0, l = subs.length; i < l; i++) {
subs[i].update()
}
}
}
function remove (arr, item) {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)
}
}
}
// 此时,defineReactive只需要调用depend收集,notify通知更新
function defineReactive(data, key, val) {
let dep = new Dep()
Obejct.defineProperty(data, key, {
enumerable: true,
configurable: true,
get: function() {
dep.depend()
return val
},
set: function(newVal) {
if (val === newVal) return
val = newVal
dep.notify()
},
})
}
收集的依赖window.target,到底是啥?依赖是用到数据的地方,可能是模板,可能是用户写的一个watch,需要抽象出一个类集中处理多种情况,收集依赖阶段只收集这个类的实例,通知也只通知它,它再负责通知其他地方 -- Watcher。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.getter = parsePath(expOrFn) // 读取“data.a.b.c”的值,用.分割成数组,再递归一层层查找
this.cb = cb
this.value = this.get()
}
get() {
window.taget = this
let value = this.getter.call(this.vm, this.vm)
window.target = undefined
return value
}
update() {
const oldValue = this.value
this.value = this.get()
this.cb.call(this.vm, this.value, oldValue)
}
}
递归侦测所有key
封装一个Observer类用于将data中的所有属性(包括子属性)都转化成getter/setter的形式。
export class Observer {
constructor (value) {
this.value = value
if (!Array.isArray(value)) { // object类型时,调用walk
this.walk(value)
}
}
walk () { // 遍历,将每一个属性都变成getter/setter形式
const keys = Object.keys(obj)
for ( let i = 0; i < keys.length; i++) {
defineReactive(obj, keys[i], obj[keys[i]])
}
}
}
function defineReactive(data, key, val) {
if (typeof val === 'object') { // 如果值是object,则递归把子属性也变成getter/setter
new Observer(val)
}
// ... 其余代码同上
}
getter/setter只能追踪一个属性是否被修改,但无法追踪新增和删除属性,所以另外提供了vm.delete两个api。ES6之前。
图
Array变化侦测
侦测Object变化是通过getter/setter实现的,但是如果用Array原型上的方法改变数组,就无法侦测了。同setter追踪,如果可以在用户使用Array原型上的方法改变数组时,得到通知,就可以侦测变化。
我们可以用一个拦截器arrayMethods去覆盖Array.prototype,在拦截器中发送变化通知, 再执行原本的功能。改变数组自身内容的7个方法: ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse']
const arrayProto = Array.prototype
export const arrayMethods = Object.create(arrayProto)
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
const original = arrayProto[method] // 缓存原有方法
Object.defineProperty(arrayMethods, method, {
value: function mutator (...args) {
// 发送变化通知
return original.apply(this, args)
},
enumerable: false,
writable: true,
configuration: true,
})
})
拦截器arrayMethods不能直接覆盖Array.prototype,会污染全局的Array。我们的拦截操作只需要针对那些被侦测了变化的数据生效,也就是说拦截器只覆盖那些响应式数组的原型。将一个数据转化成响应式,需要用到Observer。
import { arrayMethods } from './array'
const hasProto = '_proto_' in {}
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)
export class Observer {
constructor (value) {
this.value = value
if (Array.isArray(value)) {
// value._proto_ = arrayMethods // 覆盖原型上方法
// 浏览器是否支持_proto_,支持则覆盖原型,不支持则直接复制挂载在value上
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
} else {
this.walk(value)
}
}
}
function protoAugment (target, src, keys) {
target._proto_ = src
}
function copyAugment (target, src, keys) {
for (let i = 0; i < keys.length; i++) {
const key = keys[i]
def(target, key, src[key])
}
}
ES6用Object.getPropertyOf和Object.setPropertyOf替代了proto。
每次访问数组的值,就会触发getter。所以Array在getter里收集依赖,在拦截器中触发依赖。
依赖列表dep存储在Observer中,因为getter和拦截器中都可以访问到Observer实例。
getter中访问:
function defineReactive (data, key, val) {
let childOb = observe(val)
let dep = new Dep()
Object.defineProperty(data, key {
enumerable: true,
configurable: true,
get: function () {
dep.depend()
if (childOb) {
childOb.dep.depend() // 收集依赖到Observer实例(childOb)的dep中
}
return val
}
})
}
export function observe (value, asRootData) { // 创建响应式实例Observer
if (!isObject(value)) { // 如果是object,直接返回,childOb = null
return
}
let ob
if (hasOwn(value, '_ob_') && value._ob_ instanceof Observer) { // 如果已经是响应式,直接返回Observer实例
ob = value._ob_
} else { // 否则,创建
ob = new Observer(value)
}
return ob
}
拦截器中访问:
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value, '_ob_', this) // value._ob_ = Observer实例
}
// ...
}
// 工具函数 def
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
这样,就可以通过数组值的ob属性访问到Observer实例上的dep,调用改变数组内容的方法时,通知依赖。同时,收集依赖中的observe函数中通过ob来判断,数据是否已经被Observer转换成了响应式。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].forEach(function (method) {
const original = arrayProto[method]
def(arrayMethods, method, function mutator (...args) {
const result = original.apply(this, args)
const ob = this._ob_
ob.dep.notify // 通知依赖队列中的Watcher
return result
})
})
侦测数组中元素变化
export class Observer {
constructor (value) {
this.value = value
this.dep = new Dep()
def(value, '_ob_', this) // value._ob_ = Observer实例
if (Array.isArray(value)) {
const augment = hasProto ? protoAugment : copyAugment
augment(value, arrayMethods, arrayKeys)
this.observeArray(value)
} else {
this.walk(value)
}
}
observeArray (items) { // 递归侦测数组中每一项
for (let i = 0; i < items.length; i++) {
observe(items[i])
}
}
}
侦测新增元素变化
可以新增数组元素的方法为:push、unshift 和splice,可以取出新增元素,使用observeArray方法使其变成响应式的。
['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'].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
})
})
Array的变化侦测是通过拦截原型上方法实现的,所以对直接给数组某一项赋值,或者通过设置length改变数组,是侦测不到的。所以可以用api或方法代替。
// 代替vm.items[1] = 'x'
// Vue.set
vm.$set(vm.items, indexOfItem, newValue)
// Array.prototype.splice
vm.items.splice(indexOfItem,1, newValue)
// 代替vm.items.length = 2
vm.items.splice(newLength)
变化侦测相关api实现原理
vm.$watch(expOrFn, callback, [options])
expOrFn: a.b.c or 函数
options: { deep, immediate }
用于观察一个表达式或computed函数在Vue实例上的变化。回调函数调用时,会从参数得到newValue和oldValue。返回一个取消观察函数,用来停止触发回调。
var unwatch = vm.$watch('a', (newVal, oldVal) => {})
unwatch() // 不再watch
deep: watch对象内部值的变化,都会触发回调
immediate: 立即以表达式的当前值触发回调
所有vm.$开头的属性,都是写在Vue.prototype上的。
原理
Vue.prototype.$watch = function(expOrFn, cb, options) {
const vm = this
options = options || {}
const watcher = new Watcher(vm, expOrFn, cb, options)
if (options.immediate) {
cb.call(vm, watcher, value)
}
return function unwatchFn() {
watcher.teardown() // 本质是把watcher实例从dep的依赖列表中移除
}
}
teardown 首先需要先在Watcher中记录自己被收录进了哪些Dep中,当unwatch时,遍历自己的记录列表,从dep依赖列表中把自己删除。
export default class Watcher {
constructor (vm, expOrFn, cb) {
this.vm = vm
this.deps = [] // dep列表
this.depIds = new Set() // 避免重复记录
this.getter = parsePath(expOrFn)
this.cb = cb
this.value = this.get()
}
// ...
addDep (dep) {
const id = dep.id
if (!this.depIds.has(id)) {
this.depIds.add(id)
this.deps.push(dep)
dep.addSub(this) // 把自己订阅到dep中
}
}
// 遍历dep列表,让其把自己从subs列表中删除,以后数据更新,则不再通知到
teardown () {
let i = this.deps.length
while(i--) {
this.deps[i].removeSub(this)
}
}
}
let uid = 0
export default class Dep {
constructor () {
this.id = uid++
this.subs = []
}
// ...
depend () {
if (window.target) {
window.target.addDep(this)
}
}
removeSub (sub) {
const index = this.subs.indexOf(sub)
if (index > -1) {
return this.subs.splice(index, 1)
}
}
}
deep实现原理:除了要触发当前这个被监听数据的收集依赖之外,需要把其所有子值都触发一遍收集依赖。当子数据发生变化时,可以通知当前Watcher。
export default class Watcher {
constructor (vm, expOrFn, cb, options) {
this.vm = vm
if (options) {
this.deep = !!options.deep
} else {
this.deep = false
}
// ...
}
get () {
window.target = this
let value = this.getter.call(vm, vm)
if (this.deep) { // 一定要在window.target = undefined之前,保证子集收集的是当前watcher
traverse(value)
}
window.target = undefined
return value
}
}
const seenObjects = new Set()
export function traverse (val) {
_traverse(val, seenObjects)
seenObjects.clear()
}
function _traverse(val, seen) {
let i, keys
const isA = Array.isArray(val)
// 如果不是Array和Object,或者已经被冻结
if ((!isA && !isObject(val)) || Object.isFrozen(val)) {
return
}
// 收集依赖,用id避免重复收集
if (val._ob_) {
const depId = val._ob_.dep.id
if (seen.has(depId)) {
return
}
seen.add(depId)
}
if (isA) {
// 数组,遍历每一项递归调用_traverse
i = val.length
while(i--) _traverse(val[i], seen)
} else {
// 对象,遍历每一项的值递归调用_traverse
keys = Object.keys(val)
i = keys.length
while (i--) _traverse(val[keys[i]], seen)
// val[keys[i]]会触发getter,收集依赖,所以此时window.target不能被清空
}
}
vm.$set(target, key, value)
在taget上设置一个属性,如果target是响应式的,被创建的属性也是响应式的,并触发视图更新。主要用来避免vue侦测不到新增加属性的限制。
import {set} from '../observer/index' // observer中抛出set方法
Vue.prototype.$set = set
export function set (target, key, val) {
// target为array的处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
target.length = Math.max(target.length, key)
// splice方法将触发数组拦截器,通知侦测到的变化,从而val变成响应式的
target.splice(key, 1, val)
return val
}
// key已经存在于target中,已经为响应式,直接修改值就好
if (key in target && !(key in Object.prototype)) {
target[key] = val
return val
}
// 新增属性
const ob = target._ob_
if (target._isVue || (ob && ob.vmCount)) { // target不能为vue实例或实例上根数据
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data' + 'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) { // 没有_ob_,说明并不是响应式的,直接设置
target[key] = val
return val
}
defineReactive(ob.value, key, val) // 追踪新增属性setter getter
ob.dep.notify() // 通知
return val
}
vm.$delete(target, key)
用于删除target对象上的key属性。如果对象是响应式的,需要确保删除能触发更新试图。主要为了避免直接使用delete无法被侦测到变化的限制。
import {del} from '../observer/index' // observer中抛出set方法
Vue.prototype.$delete = del
export function del (target, key) {
// target为array的处理
if (Array.isArray(target) && isValidArrayIndex(key)) {
// splice方法将触发数组拦截器,通知侦测到的变化,从而val变成响应式的
target.splice(key, 1)
return
}
const ob = target._ob_
if (target._isVue || (ob && ob.vmCount)) { // target不能为vue实例或实例上根数据
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data' + 'at runtime - declare it upfront in the data option.'
)
return
}
// 如果key不是target自身属性, 直接返回
if(!hasOwn(target, key)) {
return
}
delete target[key]
// 如果不是响应式的,则不需要通知,直接返回
if (!ob) {
return
}
ob.dep.notify() // 通知
}