Vue2响应式原理

129 阅读14分钟

🔥Vue2响应式原理

🏢核心:数据劫持

我们想实现响应式,我们就需要知道数据在什么时候发生了变化,变化后又需要更改哪些地方

js提供了Object.DefineProperty方法,我们通过它可以为数据定义satter和getter

通过setter,我们可以知道数据什么时候发生了变化

通过getter,我们可以知道数据什么时候被调用,以及被谁调用(后续会讲到方式)

调用方法为数据添加getter和setter:

let obj = {
    num:1
}

//为obj的num添加getter和setter
Object.defineProperty(obj, 'num', {
    	//定义属性是否可枚举、可配置
        enumerable: true,
        configurable: true,
    	//定义getter,数据被读取时就会执行此函数
        get() {
            console.log('有人读取num啦,快看看是谁!')
        },
    	//定义setter,数据被修改时就会执行此函数
        set(newVal) {
            console.log('有人设置num啦,快对调用num的地方做更新!')
        }
    })

let num = obj.num//输出:'有人读取num啦,快看看是谁!'

obj.num = 2//输出:'有人设置num啦,快对调用num的地方做更新!'

但是要注意,由于我们在getter中没有返回任何值,所以num中接收到的是undefined,并且由于我们没有在setter中对其进行赋值,所以obj.num也没有赋值为2,这就是数据劫持,如果getter和setter没有松手,数据是无法被拿到或修改的

那我们如何在getter中返回值,在setter中修改值呢?

如果我们直接在getter中返回obj.num,就会在返回其值时再次触发getter,造成死循环,同样,在setter中直接为obj.num进行赋值,也会无限触发setter,导致错误

这时候我们就需要一个中间变量来帮我们进行值传递

let obj = {
    num:1
}

let temp = obj.num //temp就成了obj.num的化身,任何对obj.num的操作都会转移到它身上

//为obj的num添加getter和setter
Object.defineProperty(obj, 'num', {
        enumerable: true,
        configurable: true,
    	//定义getter,数据被读取时就会执行此函数
        get() {
            return temp
        },
    	//定义setter,数据被修改时就会执行此函数
        set(newVal) {
            temp = newVal
        }
    })

let num = obj.num//得到的是temp的值

obj.num = 2//temp替obj.num修改为了2

这样我们就完成了数据的劫持,为属性添加了getter和setter,并使其可以正常工作

现在我们要进行一点改进

在vue中,我们需要为每个数据添加响应式,那么就要执行若干次Object.defineProperty方法,而每次执行都要为它指定一个中间人temp,会使代码显得凌乱又不好维护,所以我们要将其封装为defineReactive方法👇

function defineReactive(obj, key, value) {
	//value是传入的obj.num的值

    //为obj的num添加getter和setter
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        //定义getter,数据被读取时就会执行此函数
        get() {
            return value
        },
        //定义setter,数据被修改时就会执行此函数
        set(newVal) {
            value = newVal
        }
    })
}

我们以value的形式将要定义响应式的数据的值传了进来,利用非引用对象会在函数作用域中创建一个副本的特性,将其作为我们要用的中间人temp。

在这个方法中,get和set函数会被绑定到obj的属性描述符上,可以理解为保留了引用,所以不会被清除,这种形式有没有让你联想到闭包!

只要obj上的num属性还在,getter和setter作为num的属性描述符就在,这两个方法在,它们引用的value就不会被清除,也就是形成了闭包(关于闭包不理解的请看:小何的世界 (theluckyone.top)),我们每次调用get和set函数,进行操作的都是同一个value

🏢递归劫持

现在我们手握defineReactive,谁来了都要被我们劫持,但如果对象内又有对象呢?

let obj = {
    son: {
        name: '小明'
    }
}

这样我们调用defineReactive函数时,不仅要调用defineReactive(obj),还要调用defineReactive(son)才能将这个obj里里外外劫持完毕。

最简单的方法是我们直接调用两次,但在对象内部情况不确定的情况下,我们就无法直接写出所有情况了。

于是我们这一节要实现一个递归侦测,也就是传入一个obj,管他里面有多少子对象,统统被我们的defineReactive劫持

我们先用简单的递归来实现,在vue底层并不是这样做的,但为了理解,我们一步一步来👇

let obj = {
    son: {
        name: '小明'
    }
}

function defineReactive(obj, key, value) {
    //在这里判断obj[key]的值(即value)是不是对象
    //如果是对象,则遍历这个对象,为其每个子元素调用defineReactive为其添加响应式
    if(typeof value === 'object'&&value) {
        for(let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
    Object.defineProperty(obj, key, {
        //这里不变,此处省略
    })
}

defineReactive(obj, 'son', obj.son)

我们在defineReactive函数中对obj[key]的值进行了判断,如果其值是对象,且不为null,那我们就对他再执行defineReactive,如果他里面还有对象,那就再判断,再调用,直到把其中所有对象都进行属性劫持。

📕依赖容器与Watcher

我们现在对所有属性劫持完毕,就等着数据被调用啦

我们的想法是:当数据被调用时,我们记录下它在哪里被调用,以便在数据发生变化时,我们对调用的地方进行通知修改。

记录到哪里呢?记录到依赖容器里,记录谁呢?记录watcher

当一个地方调用了数据,就会生成一个叫watcher的东西,并且会将watcher实例放到这个数据对应的依赖容器中

当数据发生变动时,我们就从依赖容器中取出所有watcher,一个一个进行通知修改

🌙定义依赖容器

我们以类的形式定义,这样就可以在每个数据身上绑定一个专属于它的依赖容器,专门用来收集对它的依赖

class Dep {
	constructor() {
		this.subs = []//这里就是我们的容器,以后收集的watcher实例就放在这里面
	}

    depend() {
        //此处进行依赖收集
    }
    
    notify() {
		//属性发生改变时,在这里通知依赖容器中的watcher 
    }
}

🌙定义Watcher

当数据发生改变时,依赖容器中通过notify方法通知Watcher,Watcher收到通知后,就去执行对应的操作以完成响应式任务

我们同样以类的形式定义

class Watcher {
	constructor(obj, key) {}
    update() {
        console.log(`${obj}[${key}]的内容发生更新啦`)
    }
}

现在我们定义了它构造方法的参数,我们通过传入对应对象及其key来生成这个属性专有的watcher

我们还定义了一个update方法,用于在数据更新后调用,也是响应式的最后一步(更新数据或执行回调)

🌙为属性绑定容器

要为属性绑定专有容器,在为它定义响应式的时候进行再好不过了

所以我们在defineReactive中操作👇

function defineReactive(obj, key, value) {
	let dep = new Dep()//生成依赖容器
    //...
}

由于我们是在defineReactive作用域中定义的依赖容器,所以它的作用域只有这个函数,即每个属性只会有它专有的容器

🌙依赖收集

我们现在绑定了容器,有了Watcher,如何进行依赖收集呢?

先前说过,哪里调用响应式属性,哪里就会生成一个对应的watcher实例

所以我们要在依赖容器中存储调用者,就是要存储对应的watcher实例

所以我们现在的问题就是:如何在watcher生成后将它放到属性的Dep容器中呢?

我们想到了通过getter,当watcher实例化时必然会执行构造函数,那么在执行构造函数时,我们将watcher传入,getter里不就能获取到我们的watcher了吗

但是问题又来了,getter定义参数我们怎么传参呢?因为我们只需要读取对象就可以触发getter,完全没有传参的机会呀

我们另辟蹊径,想到js中不是还有个全局对象吗?浏览器中是window,node环境下是global,那我在生成watcher时,将它放到全局对象中,在getter中再去全局对象中读取watcher,不就获取到了吗

说干就干!

我们将语句封装到get方法中,注意这个和getter不是一个性质,这里的get只是一个普通函数

class Watcher {
	constructor(obj, key) {
		this.obj = obj
         this.key = key
		this.get()
	}
    get() {
        window.target = this //我们将this,也就是watcher实例,绑定到window.target属性上
		this.obj[this.key]//调用数据,触发getter
		window.target = null //别忘了调用完毕后释放window.target哦
    }
    update() {
        console.log(`${this.key}的内容发生更新啦`)
    }
}

Dep容器中封装收集依赖的流程:

Dep() {
	constructor() {
		this.subs = []
	}
	depend() {
		if(window.target && window.target instanceof Watcher) {
			//如果window.target上有watcher的话,就进行收集
			this.subs.push(window.target)
		}
	}
}

getter改造:

get() {
	dep.depend()//执行容器的depend方法进行依赖收集
    return value
}

这样一个流程走下来,watcher实例就成功加入了属性的依赖容器中

🌙更新通知

接下来就是响应式的最后一步,如果发生数据更新,我们就要进行通知,告诉所有的watcher:你观测的数据更新啦,快做出响应!

数据变化要触发setter,所以我们在setter中进行通知:

set(newVal) {
	if(value === newVal) {
		//如果新旧值相同,就不进行操作
		return
	}
	dep.notify() //执行notify进行通知
	value = newVal
}

那么notify函数中做了什么呢?

notify() {
	//遍历subs数组,挨个通知里面的watcher实例,触发其update方法
	this.subs.forEach(watcher => {
		watcher.update()
	})
}

这样我们就完成了一个简单的对象响应式实现,完整代码👇

let obj = {
    son: {
        name: '小明'
    }
}

function defineReactive(obj, key, value) {
    let dep = new Dep()//生成依赖容器
    
    //在这里判断obj[key]的值(即value)是不是对象
    //如果是对象,则遍历这个对象,为其每个子元素调用defineReactive为其添加响应式
    if(typeof value === 'object'&&value) {
        for(let key in value) {
            defineReactive(value, key, value[key]);
        }
    }
    Object.defineProperty(obj, key, {
        enumerable: true,
        configurable: true,
        //定义getter,数据被读取时就会执行此函数
        get() {
            dep.depend()//执行容器的depend方法进行依赖收集
            return value
        },
        //定义setter,数据被修改时就会执行此函数
        set(newVal) {
            if(value === newVal) {
                //如果新旧值相同,就不进行操作
                return
            }
            dep.notify() //执行notify进行通知
            value = newVal
        }
    })
}

class Dep {
	constructor() {
		this.subs = []//这里就是我们的容器,以后收集的watcher实例就放在这里面
	}

    depend() {
		if(window.target && window.target instanceof Watcher) {
			//如果window.target上有watcher的话,就进行收集
			this.subs.push(window.target)
		}
	}
    
    notify() {
        //遍历subs数组,挨个通知里面的watcher实例,触发其update方法
        this.subs.forEach(watcher => {
            watcher.update()
        })
    }
}

class Watcher {
	constructor(obj, key) {
		this.obj = obj
         this.key = key
		this.get()
	}
    get() {
        window.target = this //我们将this,也就是watcher实例,绑定到window.target属性上
		this.obj[this.key]//调用数据,触发getter
		window.target = null //别忘了调用完毕后释放window.target哦
    }
    update() {
        console.log(`${this.key}的内容发生更新啦`)
    }
}

defineReactive(obj, 'son', obj.son) //进行响应式转换
new Watcher(obj.son,'name') //数据调用

obj.son.name = '小王'//输出:name的内容发生更新啦

当然,这只是一个非常简陋的响应式实现,我们现在要对其进行改造,考虑更多的情况

📕进阶改造

以上的简陋实现还存在很多问题,如无法实现数组的响应式,无法侦测到对象新增或删除属性等

所以我们要进行深入改造,使其能应对更多情况

🌙Observer对象和observe函数

在之前我们是直接通过在defineReactive函数内递归进行内层响应式处理的

现在我们要引入Obeserver对象和observe函数来替代我们进行观测,完成其内所有的响应式转换

具体逻辑:

  1. defineReactive为对象创建响应式
  2. 在defineReactive内调用observe方法
  3. observe方法中判断传入的是值是对象还是基本数据类型
  4. 如果是对象,就为其创建Observer对象
  5. Observer实例化时,会作为__ob__属性绑定在响应式对象身上
  6. 同样也是在Observer实例化时,会遍历对象中的所有属性,执行defineReactive函数,这就回到了第一步,如此循环

既然是改造从defineReactive作为切入口,所以我们先对它进行改写:

/**
     * 为元素添加响应式(getter/setter)
     * @param {Object} obj 外层对象
     * @param {*} key 要添加响应式的元素的key
     * @param {*} value 元素对应的值
     */
    defineReactive(obj, key, value) {
        //获取元素的ob实例,如果没有则进行创建
        let childOb = this.observe(obj[key])
        Object.defineProperty(obj, key, {
            //...
        })
    }

我们这里先砍掉定义依赖容器的部分,因为后期会在其他地方对其进行初始化,这里先不展开说

我们在这里做的事情很简单,我们为传入的值调用observe进行观测

⭐observe函数

我们通过observe函数来查看元素是否已经具备响应式,若不具备响应式,就为其创建专有的Observer实例:

/**
     * 返回传入对象的Observer实例
     * @param {Object} obj 被检查对象
     * @returns 对象的Observer实例
     */
    observe(obj) {
        //判断是否为对象,不是对象则返回,不为其添加observer实例
        if(typeof obj !== 'object') {
            return
        }
        //如果obj上已经有了__ob__属性,说明obj已经是响应式对象了,直接返回它的__ob__属性
        if(obj.__ob__ && obj.__ob__ instanceof Observer) {
            return obj.__ob__
        }else {
            //如果obj不是响应式属性,那么就为其创建Observer实例
            let ob = new Observer(obj)
            return ob
        }
    }

⭐Observer类

在实例化Observer类时,我们将当前实例绑定到传入对象的__ob__属性上,然后再调用walk对obj进行遍历,为其下的每个属性进行defineReactive操作(因为我们定义的__ob__属性的enumerable为false,所以不会遍历它)

export const class Observer {
    constructor(obj) {
            this.value = obj
            //创建依赖容器
            let ob = this
            //每个对象上存放一个ob
            this.def(obj, '__ob__', ob)
            this.walk()
        }
    
    /**
     * 递归为Object类型上的每个元素添加响应式
     * @param {Object} obj 需要添加响应式的对象
     */
    walk() {
        //遍历所有key,执行defineReactive为其添加响应式
        Object.keys(this.value).forEach(key => {
            this.defineReactive(this.value, key, this.value[key])
        })
    }
    
    /**
     * 工具方法,封装Object.definProperty
     * @param {Object} obj 待添加属性对象
     * @param {String} key 待添加的key
     * @param {*} value 待添加属性的值
     */
    def(obj, key, value) {
        Object.defineProperty(obj, key, {
            value:value,
            enumerable:false,
            configurable:true,
            writable:true
        })
    }
}

现在我们完成了变换侦测的进阶改造,每一个响应式对象身上都会绑有一个__ob__实例,在这一小节中,这个属性只能用作证明这个属性已经被转换为响应式,之后我们再谈它还有其他哪些用处

🌙数组响应式的侦听方式

我们上面实现的响应式只是针对对象来实现,而没有完成数组的响应式,两者之间还是有一定差别的

在vue2中,数组的响应式实现还要从数组的方法入手

我们对数组进行操作时,要通过数组的各种方法来进行增删改,所以我们就可以在方法中动手脚,让它在执行数组方法时,进行响应通知

具体实现:

定义一个对象,在对象内实现数组的所有基本方法,我们将这个对象叫做拦截器👇


const original = Array.prototype
export const arrayMethods = Object.create(original)//基于数组原型创建一个新对象

;[
    'pop',
    'push',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(method => {
    //缓存原始方法
    const originalMethod = original[method]
    //方法替换增强
    Object.defineProperty(arrayMethods, method, {
        value:function mutator(...args) {
            //执行原始方法
            return originalMethod.call(this, ...args)
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
});

拦截器,顾名思义,我们利用它来拦截数组原型,将原本的Array.prototype设为我们自己创建的拦截器

⭐原型替换

我们将原型替换放在Observer中进行

改造后的Observer类:

//检查__proto__是否可用
const hasProto = '__proto__' in {}
//获取所有key(包括不可枚举的)
const arrayKeys = Object.getOwnPropertyNames(arrayMethods)

export default class Observer {
    //给构造方法传入要添加响应式的值
    constructor(value) {
        this.value = value
        this.dep = new Dep()
        let ob = this
        //将ob绑到数组上,没有搞懂这里为什么写在这里,这样不是普通对象也会存在ob吗?
        //经过查阅,普通对象上也会有ob,好像和后续$delete等方法有关
        Object.defineProperty(value, '__ob__', {
            enumerable:false,
            get() {
                return ob
            },
            set(newVal) {
                ob = newVal
            }
        })
               
        if(Array.isArray(value)) {
            //修改原型/直接添加方法
            const augment = hasProto?this.protoAugment : this.copyAugment
            augment(value, arrayMethods, arrayKeys)
            //遍历添加响应式
            for(let i = 0;i < value.length;i++) {
                observe(value[i])
            }
        }else {
            this.walk()
        }
    }

    walk() {
        Object.keys(this.value).forEach(key => {
            defineReactive(this.value, key, this.value[key])
        })    
    }

    protoAugment(target, src) {
        console.log('完成原型替换')
        target.__proto__ = src
    }

    copyAugment(target, src, keys) {
        keys.forEach(key => {
            Object.defineProperty(target, key, {
                value:src[key],
                enumerable:false,
                writable:true,
                configurable:true
            })
        });
    }
}

根据浏览器是否支持隐式原型,我们分别通过protoAugment和copyAugment进行数组函数的增强

在不支持隐式原型的情况下,我们直接将增强后的方法放到数组身上,而不是修改原型

而在上面也通过for循环为数组每个元素执行observe,实现递归观测

⭐为数组绑定容器

在绑定容器前,我们先考虑一下,我们要在哪里进行更新通知?

数组无法通过定义setter来更新内容,而是通过数组方法来进行修改,所以我们要在增强的方法中动手脚

那么我们的依赖容器该定义在哪里呢?

如果我们和之前一样,定义到defineReactive的闭包中,数组方法就拿不到容器咯

所以我们需要另想其他方法

这里__ob__属性就派上用场啦!

我们将__ob__属性绑定到了数组对象身上,那么数组方法是不是就能访问到数组对象身上的__ob__属性了呢?

在原型上是无法直接访问对象属性的,但我们可以通过在原型上的方法中获取this来访问实例

修改后的拦截器:


const original = Array.prototype
export const arrayMethods = Object.create(original)

;[
    'pop',
    'push',
    'shift',
    'unshift',
    'splice',
    'sort',
    'reverse'
].forEach(method => {
    //缓存原始方法
    const originalMethod = original[method]
    //方法替换增强
    Object.defineProperty(arrayMethods, method, {
        value:function mutator(...args) {
            this.__ob__.dep.notify()//通过__ob__获取dep容器
            //执行原始方法
            return originalMethod.call(this, ...args)
        },
        enumerable: false,
        writable: true,
        configurable: true
    })
});

这样我们就实现了数组的响应式

🌙Watcher补充

  1. 我们在Watcher中可以接收一个回调函数,在数据更新时进行调用
  2. 我们可以通过传入对象表达式来生成getter,就可以很方便地为其传入复杂表达式(通过下述的parsePath函数完成)
export default class Watcher {
    constructor(obj, expOrfn, cb) {
        this.data = obj
        this.callback = cb
        this.getter = this.parsePath(expOrfn)
        this.get()
    }

    get() {
        global.target = this
        this.value = this.getter(this.data)
        global.target = null
        return this.value
    }

    update() {
        let oldVal = this.value
        this.value = this.get()
        this.callback()
    }

    parsePath(expOrfn) {
        const bailRE = /[^\w.$]/
        if(bailRE.test() || typeof expOrfn === 'function') {
            return expOrfn
        }
        return (obj) => {
            expOrfn.split('.').forEach(key => {
                if(!obj) return
                obj = obj[key]
            });
            return obj
        }
    }
}

以上我们就简单完成了vue响应式的实现,当然源码考虑的情况更多会有出入,但大体思想都囊括了,还有部分内容没有完善,比如$delete方法等,后期我有空了再补,下面附上Vue的源码👇

🏢源码

🌙DefineReactive

export function defineReactive (
  obj: Object,
  key: string,
  val: any,
  customSetter?: ?Function,
  shallow?: boolean
) {
  // 声明 dep,每个声明为响应式的值都有一个自己的 dep
  const dep = new Dep()
  // 获取属性描述符
  const property = Object.getOwnPropertyDescriptor(obj, key)
  // configurable 为 false 则 return
  if (property && property.configurable === false) {
    return
  }

  // cater for pre-defined getter/setters
  // 获取 getter 和 setter
  const getter = property && property.get
  const setter = property && property.set
  
  if ((!getter || setter) && arguments.length === 2) {
    val = obj[key]
  }

  let childOb = !shallow && observe(val)
  // 使用 Object.defineProperty 设置 getter 和 setter
  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    // getter
    get: function reactiveGetter () {
      // 获取 value
      const value = getter ? getter.call(obj) : val
      // 在 pushTarget 函数中会赋值 Dep.target(初始值为 null,赋值为一个 watcher)
      // 可以使用 popTarget 函数移除当前 Dep.target
      // 保证只同时处理一个 watcher
      // 在声明组件的 watcher 时会调用 watcher.get,将当前 watcher push进 targetStack,且 Dep.target = watcher
      if (Dep.target) {
        // 收集依赖
        dep.depend()
        if (childOb) {
          // object 收集依赖
          childOb.dep.depend()
          if (Array.isArray(value)) {
            // 数组收集依赖
            dependArray(value)
          }
        }
      }
      return value
    },
    set: function reactiveSetter (newVal) {
      // 获取当前 value
      const value = getter ? getter.call(obj) : val
      /* eslint-disable no-self-compare */
      // 值并未变化,或都为 NaN
      if (newVal === value || (newVal !== newVal && value !== value)) {
        return
      }
      /* eslint-enable no-self-compare */
      if (process.env.NODE_ENV !== 'production' && customSetter) {
        customSetter()
      }
      // #7981: for accessor properties without setter
      if (getter && !setter) return
      if (setter) {
        setter.call(obj, newVal)
      } else {
        val = newVal
      }
      // 重新观察 newVal
      childOb = !shallow && observe(newVal)
      // 通知所有依赖更新
      dep.notify()
    }
  })
}


🌙Observe

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 不是 object 或 是VNode则 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 已经被观察过了,直接使用 __b__
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve && // 需要被观察
    !isServerRendering() && // 不是服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // value 是 array 或 纯object
    Object.isExtensible(value) && // 当前 value 是否可扩展
    !value._isVue // 不是 Vue 实例
  ) {
    // 创建新的观察者
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

🌙Observer

export function observe (value: any, asRootData: ?boolean): Observer | void {
  // 不是 object 或 是VNode则 return
  if (!isObject(value) || value instanceof VNode) {
    return
  }
  let ob: Observer | void
  // 已经被观察过了,直接使用 __b__
  if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
    ob = value.__ob__
  } else if (
    shouldObserve && // 需要被观察
    !isServerRendering() && // 不是服务端渲染
    (Array.isArray(value) || isPlainObject(value)) && // value 是 array 或 纯object
    Object.isExtensible(value) && // 当前 value 是否可扩展
    !value._isVue // 不是 Vue 实例
  ) {
    // 创建新的观察者
    ob = new Observer(value)
  }
  if (asRootData && ob) {
    ob.vmCount++
  }
  return ob
}

🌙Dap

export default class Dep {
  static target: ?Watcher;
  id: number;
  subs: Array<Watcher>;

  constructor () {
    this.id = uid++
    this.subs = []
  }

  addSub (sub: Watcher) {
    this.subs.push(sub)
  }

  removeSub (sub: Watcher) {
    remove(this.subs, sub)
  }

  depend () {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  notify () {
    // stabilize the subscriber list first
    const subs = this.subs.slice()
    if (process.env.NODE_ENV !== 'production' && !config.async) {
      // subs aren't sorted in scheduler if not running async
      // we need to sort them now to make sure they fire in correct
      // order
      subs.sort((a, b) => a.id - b.id)
    }
    // 更新
    for (let i = 0, l = subs.length; i < l; i++) {
      subs[i].update()
    }
  }
}

源码及注释来源:juejin.cn/post/700697…