响应式系统
前言
渲染:vue通过状态生成DOM,并在页面上显示出来的过程就是渲染。
在运行时,应用内部状态会不断发生变化,需要不停地渲染,而变化侦测就是来解决这个问题的。
变化侦测
vue、react、angular变化侦测的区别?
Angular和React的变化侦测属于”拉”,就是说框架不清楚是哪个状态发生变化,框架知道状态变了,然后进行“暴力对比”寻找需要重新渲染的DOM节点(Angular是脏检查的流程,React中使用的是虚拟DOM,待展开…..)
Vue的变化侦测属于”推”,当状态发生变化时,Vue可以知道说是哪些状态发生了变化,然后它可以向这个状态的所有依赖发送通知。
粒度
Vue.js2.0以前,是一个状态绑定好多个依赖,每个依赖表示一个具体的DOM节点,当状态发生变化时,向这个状态的所有依赖发送通知,让他们进行DOM更新操作。(粒度比较细,但是绑定的依赖多,而且依赖追踪在内存上的开销大)
Vue.js2.0之后,引入了虚拟DOM,一个状态绑定的依赖不再是DOM节点,而是一个组件,粒度就调整为中等粒度了。(比之前有了提升,但是它的粒度还是比”拉”的粒度小的)
侦测方式
Object.defineProperty,proxy(下面Vue3再讲)
补充个人的大白话
其实没那么复杂,就是就是比如说在代码的各个地方都用到了某个数据,当这个数据变化时,这些地方就都要更新。更新的方法就是之前在用这个数据的时候就把它使用的地方都收集起来(这些地方就叫依赖),后面这个数据变化了,就通过遍历之前收集的那些地方,一一去更新它。
Object
如何收集依赖?
在getter中收集依赖,在setter中触发依赖
function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
){
let dep=new Dep();
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
//getter
get:function(){
dep.depend();//收集依赖
return val;
},
//setter
set:function(newVal){
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
val=newVal;
dep.notify();//触发依赖
}
})
}
依赖收集在哪里?
用Dep类收集,可以用这个类来收集依赖、删除依赖、向依赖发送通知等等
Dep其实主要就两个主要的功能,一是用一个数组收集依赖Watcher,二就是遍历数组对每个Watcher进行update
Dep.target = null
//Dep其实是一个依赖收集器,一个响应式数据的依赖收集器可以被许多指令订阅
export default class Dep{
//静态属性target
static target: ?Watcher;
constructor(){
//收集所有的订阅者
this.subs=[];
}
//添加订阅者
addSub (sub: Watcher) {
this.subs.push(sub)
}
//取消订阅
removeSub (sub: Watcher) {
remove(this.subs, sub)
}
//触发getter时收集依赖
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()
}
}
}
export function remove (arr: Array<any>, item: any): Array<any> | void {
if (arr.length) {
const index = arr.indexOf(item)
if (index > -1) {
return arr.splice(index, 1)//删除某一个
}
}
}
//Dep.target是指向一个具体的Watcher,每个Watcher自己也维护了一个依赖收集器
//表示自己依赖了多少个数据
Dep.target = null
//targetStack用来存放依赖的
const targetStack = []
export function pushTarget (target: ?Watcher) {
targetStack.push(target)
Dep.target = target
}
export function popTarget () {
targetStack.pop()
Dep.target = targetStack[targetStack.length - 1]
}
依赖究竟是什么?
由于使用这个数据的地方有很多,有多种类型,有可能是模板,也有可能是用户写的一个watch,所以抽象成一个类来处理这些情况,这个类叫Watcher,所以收集的就是Watcher实例
class Watcher{
constructor(
vm: Component,
expOrFn: string | Function,//表达式
cb: Function,//回调函数
options?: ?Object,
isRenderWatcher?: boolean
...
){
//只截取关键部分代码
//这里主要是跟vm.$watch的deep选项有关
if (options) {
this.deep = !!options.deep
}else{
this.deep=false;
}
this.newDeps = []
this.newDepIds = new Set()//用set,不会重复订阅
/*
调用this.getter()就可以读取到属性的值
注意这里expOrFn可以是函数,如果它是函数,Watcher会同时观察函数里读取的所有Vue.js示例上的
响应式数据,而如果是字符串类型的则会直接读取字符串指向的数据
*/
if (typeof expOrFn === 'function') {
this.getter = expOrFn
} else {
this.getter = parsePath(expOrFn)
if (!this.getter) {
this.getter = noop
process.env.NODE_ENV !== 'production' && warn(
`Failed watching path: "${expOrFn}" ` +
'Watcher only accepts simple dot-delimited paths. ' +
'For full control, use a function instead.',
vm
)
}
}
//这个是更新时要用的方法
this.cb=cb;
//在非惰性检测情况下,触发对象属性的getter方法,收集依赖
this.value=this.value = this.lazy? undefined: this.get()
}
//触发getter,重新收集依赖
get () {
pushTarget(this)//把当前Watcher的实例赋值加入targetStack
let value
const vm = this.vm
try {
//获取属性的值,这样就会触发属性的getter,然后将watcher实例push进Dep实例里
value = this.getter.call(vm, vm)
} catch (e) {
if (this.user) {
handleError(e, vm, `getter for watcher "${this.expression}"`)
} else {
throw e
}
} finally {
//实现vm.$watch的deep选项,递归value的所有子值来触发它们的收集依赖的功能
if (this.deep) {
traverse(value)
}
popTarget()//把当前Watcher的实例赋值弹出targetStack
this.cleanupDeps()
}
return value
}
addDep (dep: Dep) {
const id = dep.id
//如果没有判断,每次数据发生了变化,watcher的get方法都会读取最新数据,收集依赖,会导致Dep有
//依赖重复
if (!this.newDepIds.has(id)) {
this.newDepIds.add(id)
//把dep加入当前watcher,在Watcher中记录自己都订阅过哪些Dep
this.newDeps.push(dep)
if (!this.depIds.has(id)) {
//把当前watcher加入dep
dep.addSub(this)
}
}
}
update(){
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
//同步更新
this.run()
} else {
//异步更新
queueWatcher(this)
}
}
//从所有依赖项的Dep列表中将自己移除
teardown () {
if (this.active) {
// remove self from vm's watcher list
// this is a somewhat expensive operation so we skip it
// if the vm is being destroyed.
if (!this.vm._isBeingDestroyed) {
remove(this.vm._watchers, this)
}
let i = this.deps.length
while (i--) {
this.deps[i].removeSub(this)
}
this.active = false
}
}
}
触发属性的getter,就会调用Watcher实例的get()方法收集该依赖
触发属性的setter,就会通知Dep的各个依赖,然后调用依赖的update()方法更新
上面的addDep中,Dep收集了Watcher,而Watcher中同样也记录了自己会被哪些Dep通知,Dep和Watcher之间是多对多的关系。为什么Watcher被多个Dep通知?
因为如果expOrFn参数是一个表达式,那么肯定只收集一个Dep,但是如果expOrFn是一个函数,而且此函数使用了多个数据,那么此时的Watcher就要收集多个Dep了
this.$watch(function(){
return this.age+this.name
},function(newVal,oldVal){
})
上例用vm.$watch观察一个函数,函数里用到了age和name两个响应式数据,这种情况下Watcher内部会收集两个Dep(name的Dep和age的Dep),同时这两个Dep中也会收集Watcher,这样age和name中任意一个数据发生变化,Watcher都会收到通知
递归侦测所有属性
上面已经可以侦测一个属性了,但是要实现对象所有属性的侦测,因为存在对象嵌套,所以要递归,需要封装一个Observer类。
这个类的作用是将一个数据内的所有属性都转换成getter、setter形式,然后可以去追踪他们的变化。
class Observer{
value: any;
dep: Dep;
vmCount: number; // number of vms that have this object as root $data
//目前分析这里传入的value是一个对象
constructor (value: any) {
this.value = value
this.dep = new Dep()
this.vmCount = 0
def(value, '__ob__', this)
if (Array.isArray(value)) {
...
} else {
// 遍历对象
this.walk(value)
}
}
walk(obj){
const keys=Object.keys(obj);//这里的keys是obj第一层的key,不包括嵌套的
for(let i=0;i<keys.length;i++){
defineReactive(obj,keys[i],obj[keys[i]]);
}
}
}
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]
}
// 若属性值也是对象,深度遍历递归执行observe实例化
let childOb = !shallow && observe(val)
Object.defineProperty(data,key,{
enumerable:true,
configurable:true,
// getter,触发getter时,收集依赖。
get: function reactiveGetter () {
// 获得当前值
const value = getter ? getter.call(obj) : val
// 如果当前有目标依赖这个数据,则添加依赖Watcher
if (Dep.target) {
dep.depend()
// 子对象也要增加依赖收集
if (childOb) {
childOb.dep.depend()
// 数组特殊处理
if (Array.isArray(value)) {
dependArray(value)
}
}
}
return value
},
// setter,触发setter时,派发更新。newVal待设置的值
set: function reactiveSetter (newVal) {
// 获得当前值
const value = getter ? getter.call(obj) : val
// 如果值没有变化,则不触发更新通知
if (newVal === value || (newVal !== newVal && value !== value)) {
return
}
// 自定义setter方法
if (process.env.NODE_ENV !== 'production' && customSetter) {
customSetter()
}
// 如果属性不支持setter,则直接跳过
if (getter && !setter) return
if (setter) {
// 有自己的setter就调用自身setter
setter.call(obj, newVal)
} else {
// 更新赋值
val = newVal
}
// 子对象也要重新observe实例化
childOb = !shallow && observe(newVal)
// 通知更新
dep.notify()
}
})
}
存在问题?
1.Vue.js通过Object.defineProperty来将对象的key转换成getter/setter的形式来追踪变化,但getter/setter只能追踪一个属性是否被修改(就是这个属性要存在),无法追踪新增属性和删除属性。(vm.$set和vm.$delete可以解决)
2.数组调用原型上的方法(push、pop、shift、unshift、splice、sort、reverse),还有直接修改数组长度,直接修改某个元素的值,并不会触发setter。前一个问题可以用拦截器覆盖原型上的方法,后两个问题未解决。(或许es6的poxy就可以解决了。。)
Array
拦截器
拦截器可以覆盖原型上的方法,这样每次调用的就是拦截器中提供的方法
//src/core/observer/array.js
const arrayProto = Array.prototype
//arrayProto作为arrayMethods的原型
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]
//覆盖原方法
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
})
})
// src/core/util/lang.js
export function def (obj: Object, key: string, val: any, enumerable?: boolean) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
为什么不直接覆盖Array.prototype?
因为我们希望拦截操作只覆盖那些响应式数组的原型,不希望污染全局的Array。而把一个数据转换成响应式的,需要通过Observer,所以需要在Observer中使用拦截器覆盖数组即可。
有些浏览器不支持 **__proto__**
?
Vue的做法是,直接将arrayMethods身上的这些方法设置到被侦测的数组上
import {arrayMethods} from './array';
const hasProto = '__proto__' in {};//是否支持__proto__
const arrayKeys = Object.getOwnPropertyNames(arrayMethods);
export class Observer{
constructor(value){
this.value = value;
if (Array.isArray(value)) {
if (hasProto) {
protoAugment(value, arrayMethods)
} else {
copyAugment(value, arrayMethods, arrayKeys)
}
this.observeArray(value)
} else {
this.walk(value)
}
}
}
/**
* Augment a target Object or Array by intercepting
* the prototype chain using __proto__
*/
function protoAugment (target, src: Object) {
/* eslint-disable no-proto */
target.__proto__ = src
/* eslint-enable no-proto */
}
/**
* Augment a target Object or Array by defining
* hidden properties.
*/
/* istanbul ignore next */
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])
}
}
如何收集依赖
数组收集依赖的方式和object一样,都是在getter收集,但是要注意的是数组是在拦截器中触发依赖
依赖收集在哪里?
数组的依赖保存在Observer里,因为getter需要访问到依赖,拦截器也需要,所以要保存在两者都能访问到的地方——Observer.
export class Observer{
constructor(value: any){
this.value = value;
this.dep=new Dep();
...
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();//收集依赖
}
return val;
},
//setter
set:function(newVal){
if(val===newVal){
return;
}
val=newVal;
dep.notify();
}
})
}
//判断value是否存在Observer实例,有则返回无则新建
export function observe(value,asRootData){
if(!isObject(value)){return;}
let ob;
if(hasOwn(value,'__ob__')&&value.__ob__ instanceof Observer){
ob=value.__ob__;
}else{
ob=new Observer(value);
}
return ob;
}
拦截器访问依赖并发送通知
// 工具函数
function def (obj, key, val, enumerable) {
Object.defineProperty(obj, key, {
value: val,
enumerable: !!enumerable,
writable: true,
configurable: true
})
}
export class Observer{
constructor(value){
this.value = value;
this.dep=new Dep();
def(value,'__ob__',this);//这样就可以拿到observer实例,从而拿到dep了
...}
}
['push','pop','shift','unshift','splice','sort','reverse']
.forEach(function(method){
const original = arrayProto[method]
Object.defineProperty(arrayMethods, method ,{
value:function mutator(...args){
const result=original.apply(this,args);
const ob=this.__ob__;
ob.dep.notify();//发送通知
return original.apply(this,args);
}
...
}
侦测所有数据子集
前面的只是侦测到一个数组自身的变化,比如增加一个元素,删除一个元素等等,但是数组中保存的元素也需要侦测
export class Observer{
constructor(value){
this.value = value;
def(value,'__ob__',this);
if(Array.isArray(value)){
this.observeArray(value);
}else{
this.walk(value);
}
}
....
//侦测Array的每一项
observeArray(items){
for(let i=0,l=items.length;i<l;i++){
observe(items[i]);
}
}
}
侦测新增元素的变化
;[
'push',
'pop',
'shift',
'unshift',
'splice',
'sort',
'reverse'
]
.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
})
})