实现 nextTick 功能
- 在实现 nextTick 功能前,我们先实现 render 函数的异步渲染,为什么 render 函数的视图渲染是异步渲染呢?
- 我们先初始化文件
js
复制代码
import { h, ref } from '../../lib/guide-mini-vue.esm.js'
export const App = {
name: 'App',
setup() {
const count = ref(1)
function onClick() {
for (let i = 0; i < 100; i++) {
console.log('update');
count.value = i
}
}
return {
count,
onClick
}
},
render() {
const button = h("button", { onClick: this.onClick }, "update")
const p = h('p', {}, "count" + this.count)
return h("div", {}, [
button,
p
])
}
}
- 我们看到点击button,循环赋值100次,如果是同步渲染,就要依次渲染100次,这是相当消耗性能的,做成异步渲染,再使用变量控制执行时机,能够做到同步执行完以后再一次性进行异步渲染,简单方便,我们实现一下
js
复制代码
// 设计程序
// 变量的响应式赋值触发视图更新,每次赋值触发一次更新job,将视图更新放入队列中,正好 effect 有一个 scheduler 的方法,之前我们封装过,配置这个方法后,变量响应式更新就开始走这里 scheduler 配置的方法,在这里可以将 effect 的返回的视图更新,放入异步队列中
// renderer.ts
function setupRenderEffect(instance, vnode, container, anchor) {
instance.update = effect(() => {
let { proxy } = instance
if (!instance.isMounted) {
const subTree = instance.subTree = instance.render.call(proxy)
patch(null, subTree, container, instance, anchor)
// 在所有 element 都已经挂载完毕后,才能够拿到 虚拟节点
vnode.el = subTree.el
instance.isMounted = true
} else {
// 需要一个更新完成之后的 vnode
const { next, vnode } = instance // 这里的 vnode 是我们更新之前的虚拟节点,next是我们下次要更新的虚拟节点
if (next) {
next.el = vnode.el
updateComponentPreRender(instance, next)
}
const { proxy } = instance
const subTree = instance.render.call(proxy)
const prevSubTree = instance.subTree
instance.subTree = subTree
patch(prevSubTree, subTree, container, instance, anchor)
}
},{
scheduler() { // ✅
queueJobs(instance.update)
}
})
}
// scheduler.ts ✅
let queue: any[] = []
export function queueJobs(job) {
if (!queue.includes(job)) {
queue.push(job)
}
queryFlush()
}
function queryFlush() {
Promise.resolve().then(() => {
let job
while (job = queue.shift()) {
job && job()
}
})
}
- 上面我们把更新的逻辑转为异步队列,不过点击以后这个异步队列还是会执行 100 次,我们需要一个锁来控制该队列仅执行一次
js
复制代码
let queue: any[] = []
let isFlushPending = false // ✅
export function queueJobs(job) {
if (!queue.includes(job)) {
queue.push(job)
}
queryFlush()
}
function queryFlush() {
if(isFlushPending) return // ✅
isFlushPending = true // ✅
Promise.resolve().then(() => {
isFlushPending = false // ✅
let job
while (job = queue.shift()) {
job && job()
}
})
}
// 这里的 queryFlush 是函数调用,属于同步任务,第一次执行会一直往下走,走到 Promise.resolve 微任务,这个微任务放到执行栈中,同时用锁将后面逻辑锁住,等到同样的 queryFlush 函数调用时,不走微任务,最终同步任务执行完毕,仅执行第一次调用时挂在执行栈的微任务
- 我们已经实现了异步渲染,我们想要在循环后面直接获取dom的内容,就需要实现 nextTick 功能
js
复制代码
import { getCurrentInstance, h, ref } from '../../lib/guide-mini-vue.esm.js'
export const App = {
name: 'App',
setup() {
const count = ref(1)
const instance = getCurrentInstance()
function onClick() {
for (let i = 0; i < 100; i++) {
count.value = i
}
// 这里数据已经变为99,但我们拿到页面上的数据还是 1
console.log(instance);
// 所以我们要实现 nextTick
console.log(instance);
nextTick(()=> {
console.log(instance);
})
await nextTick()
console.log(instance);
}
return {
count,
onClick
}
},
render() {
const button = h("button", { onClick: this.onClick }, "update")
const p = h('p', {}, "count" + this.count)
return h("div", {}, [
button,
p
])
}
}
- 我们对 nextTick 进行实现
js
复制代码
let queue: any[] = []
let isFlushPending = false
export function nextTick(fn) { // ✅
return fn ? Promise.resolve(fn) : Promise.resolve()
}
export function queueJobs(job) {
if (!queue.includes(job)) {
queue.push(job)
}
queryFlush()
}
function queryFlush() {
if (isFlushPending) return
isFlushPending = true
nextTick(flushJobs) // ✅
}
function flushJobs() { // ✅ 将 Promise.resolve 抽离出来实现 nextTick
isFlushPending = false
let job
while (job = queue.shift()) {
job && job()
}
}
- 效果实现我们进一步优化
js
复制代码
const p = Promise.resolve()
export function nextTick(fn) { // ✅
return fn ? p.then(fn) : p
}