开始之前先对上一篇有个小bug进行说明修复,就是在触发数据的get的时候不能简单粗暴的直接进行dep.depend(),触发这个方法会去调用Dep.target.addDep(),如果触发get只是在逻辑里读取数据并没有在视图上读取,就会导致Dep.target为空报错。
其实就是只有在视图上读取的数据才应该进行依赖收集,所以要通过判断Dep.target是否有值来决定是否进行depend,具体改正如下
export function defineReactive(target, key, value) {
// 递归判断
observe(value)
let dep = new Dep()
Object.defineProperty(target, key, {
get() {
// 可以进行调试
if(Dep.target) { // 只有视图上使用的数据才进行依赖收集
dep.depend()
}
return value
},
set(newValue) {
if(newValue === value) return
//设置的新值也要进行劫持
observe(newValue)
value = newValue
dep.notify()
}
})
}
ok开始本篇正文
先看我们要解决的问题
可见更新数据之后直接获取dom和通过promise.then获取的dom都是滞后的,
原因是当前我们通过setTimeout实现的数据异步更新,同样是异步的微任务会优先于它执行,这就导致我们必须通过setTimeout获取新dom,但延时器对前端开发者来说都是不小的心智负担,所以我们需要规定一个统一的方法去异步更新。
回到watcher.js
function queueWatcher(watcher) {
const id = watcher.id
if(!has[id]) {
queue.push(watcher)
has[id] = true
// 不管watcher执行多少次 但是只执行一次刷新操作
if(!pending) {
// setTimeout(flushSchedulerQueue,0) // 统一更新方法避免promise.then微任务
// 改动点 引出nextTick
nextTick(flushSchedulerQueue)
pending = true
}
}
}
let callbacks = []
let waiting = false
function flushCallbacks() {
waiting = false
let cbs = callbacks.slice(0)
callbacks = []
cbs.forEach(cb => cb())
}
export function nextTick(cb) {
callbacks.push(cb)
if(!waiting) {
setTimeout(() => {
flushCallbacks()
},0)
waiting = true
}
}
会发现跟之前的watcher队列去重执行是一个思路,通过变量锁和一个异步代码,只要中间执行nextTick就会往队列里加等待执行的回调函数,也就是外部我们书写的业务逻辑只要用到nextTick,并且此时也涉及到数据更新的watcher正在等待执行,就会把他们按顺序放进callbacks队列里等待异步执行,开始执行放开锁。
但是看到我们开异步的方法仍然是延时器,跟我们外部自己用延时器没有本质的区别,所以就要优化这个异步的方法了,Promise.resolve.then()肯定是最优解,但是vue2还是对一些低版本的浏览器保证兼容,所以这里采用了一系列的降级策略。
// nextTick 没有直接使用某个api 而是优雅降级
// 先采用promise(ie不兼容) MutationObserve 可以考虑ie专享setImmediate setTimeout
let timerFunc;
if(Promise){
timerFunc = () => {
Promise.resolve().then(flushCallbacks)
}
} else if(MutationObserver) {
// 这个api就是实例化的时候传入回调函数,然后他可以监听一个文本节点,当文本节点改变就会执行传入的回调函数
let observer = new MutationObserver(flushCallbacks)
let textNode = document.createTextNode(1)
observer.observe(textNode)
timerFunc = () => {
textNode.textContent = 2
}
}else if(setImmediate) {
timerFunc = () => {
setImmediate(flushCallbacks)
}
}else {
// 最后考虑延时器
setTimeout(() => {
flushCallbacks()
},0)
}
最后,我们还要把nextTick暴露出去供业务逻辑使用
ok我们验证下效果
完美实现。
这块还有个数组的依赖收集和更新还没实现,这个比较复杂先放一放,下一篇先实现mixin和生命周期