前边我们已经实现数据的响应式和模板解析,并在模板解析的时候读取data的数据触发了get,本篇的目的是实现基本数据的依赖收集(老样子先不写数组),并在数据更新后自动更新视图。
Dep类和Watcher类
说过好多次在读取数据的时候会触发get方法,我们只要在get的时候把哪个方法读取了这个数据給记录下来然后在触发set的时候把记录的方法循环调用即可。
这个记录是给每个属性绑定一个dep,每个应用数据或者说将数据展示到页面上的操作都是通过watcher实现的,也就是dep去记住wahtcher。同时为了后续computed的实现,也需要watcher记住dep,也就是dep和watcher是多对多的互相记住的。
新建src/observe/dep.js和 src/observe/watcher.js 分别写Dep类和Watcher类
get的时候调用dep的depend方法,更新的时候调用notify方法。这里可以理解为一种闭包,每个属性都实例化一次Dep,set的时候调用的相同的一个Dep
js
export function defineReactive(target, key, value) {
// 递归判断
observe(value)
let dep = new Dep()
Object.defineProperty(target, key, {
get() {
// 可以进行调试
console.log('被读取了')
dep.depend() // 依赖收集
return value
},
set(newValue) {
if(newValue === value) return
console.log('被修改了')
//设置的新值也要进行劫持
observe(newValue)
value = newValue
dep.notify() // 数据更新
}
})
}
我们具体来实现Dep
js
// 每个dep有自己的id 以此来作为唯一标识
let id = 0
class Dep {
constructor() {
this.id = id ++
// 数组记录用到的watcher
this.subs = [] // 对应所有的watcher
}
depend() {
// 不希望放重复的watcher
// this.subs.push(Dep.target)
Dep.target.addDep(this) // 让watcher记住dep
}
// addSub是在watcher里调用的 目的是去重watcher和dep互相都不重复记录
addSub(watcher) {
this.subs.push(watcher )
}
// 通知更新
notify() {
this.subs.forEach(watcher => watcher.update())
}
}
// 静态属性这里在读取到get的时候将当前watcher存放到Dep的静态属性上 这样Dep可以调用 watcher的方法
Dep.target = null
export default Dep
实现watcher之前,这里还要说明一下我们从模板解析到读取数据生成真实dom的操作,实际调用的是vm._update(vm._render())但是之前说的是用的watcher,所以在初次渲染的时候我们需要小做修改mountComponent
js
export function mountComponent(vm, el) {
vm.$el = el
// 1.调用render产生虚拟节点 虚拟dom
//vm.$options.render() 虚拟节点
const updateComponent = () => {
vm._update(vm._render())
}
// 将初次渲染的的方法传给watcher,在watcher里调用
new Watcher(vm, updateComponent,true) // true表示渲染watcher 第三个参数作为一个渲染watcher的标识
// 2.根据虚拟dom产生真实dom
// 3.插入到el元素中
}
watcher.js 先简单实现 不考虑watch和computed
js
import Dep, { popTarget, pushTarget } from './dep'
let id = 0
// 1) 创建渲染watcher的时候 把当前watcher放到Dep.target
// 2) 调用_render() 的取值会走到 get上
class Watcher {
constructor(vm, fn, options) {
this.id = id++
this.renderWatcher = options
this.getter = fn
this.deps = [] // 后续实现计算属性 和清理工作
this.depsId = new Set()
this.vm = vm
if(options) this.get()
}
get() {
// 打个log看效果
console.log('更新了')
Dep.target = this // 调用的时候把当前watcher赋值给Dep.target
this.getter.call(this.vm) // 会去vm上取值
Dep.target = null // 渲染完成置空
}
addDep(dep) { //一个组件对应多个属性 ,重复属性不用记录
let id = dep.id
if(!this.depsId.has(id)) {
this.deps.push(dep)
this.depsId.add(id)
dep.addSub(this) // watcher已经记住dep了而且去重 此时让dep也记住watcher
}
}
update() {
this.get()
}
}
export default Watcher
检验成果
优化
最开始的目标已经达到,2秒后视图上的数据自动更新了
但是问题其实还是很明显的,当出现了多个变量都进行修改操作,我们调用了多次相同的更新方法,其原因是两个变量是在一个watcher上使用的,我们读两个变量的时候都进行了depend,更新的时候循两个变量都执行了notify,所以会调用两次相同的watcher,接下来我们来优化这里,同一次更新相同的watcher只执行一次。
js
class Watcher{
...
update() {
queueWatcher(this)
}
run() {
this.get()
}
}
// 这块也很好理解,就是传进来的watcher通过id去重,传进来一个如果不重复就放进队列里
// 然后就是用一个变量锁pending,一进来就关闭,然后中间是异步代码,期间多次执行update都只会往队列里放不重复的watcher,而不会执行中间的异步代码,执行完异步代码放开锁
let has = {}
let queue = []
let pending = false
function flushSchedulerQueue () {
let flushQueue = queue.slice(0)
queue = []
has = {}
pending = false
flushQueue.forEach(q => q.run())
}
function queueWatcher(watcher) {
const id = watcher.id
if(!has[id]) {
queue.push(watcher)
has[id] = true
// 不管watcher执行多少次 但是只执行一次刷新操作
if(!pending) {
setTimeout(flushSchedulerQueue,0)
pending = true
}
}
}
再看效果
ok,目的达成,效果实现。
遗留问题
但是还有个问题,就是这个异步更新用的setTimeout实现的,当我们更新了数据同时去获取dom,由于异步原因我们获取dom的操作是先于更新数据的,所以拿不到最新的dom,这就要我们再开一个异步去获取dom,如果我们用setTimeout还好两个宏任务按顺序执行也能拿到最新的dom,但是一旦用promise.then()去获取dom,这个微任务是早于宏任务执行的,这又会出现问题,所以下一篇写统一异步更新方法即 nextTick