zhangzheng-zz / blog

1 stars 0 forks source link

Vue3源码 #15

Open zhangzheng-zz opened 3 years ago

zhangzheng-zz commented 3 years ago

响应式系统:

https://vue3js.cn/reactivity/reactive.html

createApp 到 mounted

process

zhangzheng-zz commented 3 years ago

nextTick 原理 与 Vue3 的任务调度

定义: 在下次 DOM 更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的 DOM

问题:

1、DOM 的异步更新怎么做到?如何组件防止重复渲染? 2、nextTick 的延迟执行? 3、watch watchEffect 的区别和原理? 4、遗留 queuePostRenderEffect 在各个地方的调用?生命周期钩子的调用?是否也是异步?

源码:scheduler.ts 重点在 currentFlushPromise

// packages\runtime-core\src\scheduler.ts 第51行
export function nextTick(fn?: () => void): Promise<void> {
  const p = currentFlushPromise || resolvedPromise
  return fn ? p.then(fn) : p
}

DOM更新也是异步行为,怎么保证在 DOM更新之后执行?

queueJob 维护job列队,去重,保证任务的唯一性,每次调用去执行 queueFlush queueCb 维护cb列队,去重,保证任务的唯一性,每次调用去执行 queueFlush

// queueFlush
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

queueFlush 注意这里就给currentFlushPromise 赋值了,nextTick 之后的回调会慢于 currentFlushPromise 的调用,接下来只要找到哪里调用了 queueJob queueCb 就行。 接下来我们看 flushJobs 的内容。 先明确两个队列: queueJob 保存了任务队列 queue queueCb 是通过 queuePreFlushCb queuePostFlushCb 调用,分别保存任务队列 pendingPreFlushCbs pendingPostFlushCbs

现在的问题就变成了:queueCb、queuePreFlushCb、queuePostFlushCb 在什么地方调用?

function flushJobs(seen?: CountMap) {

  // 递归调用排序好的 pendingPreFlushCbs
  flushPreFlushCbs(seen)

  // 排序
  queue.sort((a, b) => getId(a!) - getId(b!))

 //调用排序好的 queue
  try {
    for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
      const job = queue[flushIndex]
      if (job) {
        callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
      }
    }
  } finally {
    flushIndex = 0
    queue.length = 0

    // 递归调用排序好的 pendingPostFlushCbs
    flushPostFlushCbs(seen)

    // 递归 queue
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}

queueCb 的调用:

这里的 update 其实就是 组件的更新方法,同时也是一个 effect,componentEffect。在组件响应式数据更新的时候,componentEffect 成为数据依赖的 effect 副作用函数调用 ,从而更新视图。

// effect.ts // queueJob 在这里 将 effect push 到 queue if (effect.options.scheduler) { effect.options.scheduler(effect) } else { effect() }

#### 小结:经过以上过程,组件会在`render`阶段将`componentEffect`方法添加进`queue`中,这就保证了`nextTick`可以在数据更新之后被调用。

### queuePreFlushCb 的调用
- **watch 的回调里面**
```js
// apiWatch.ts
if (!instance || instance.isMounted) {
    queuePreFlushCb(job)
} else {
     // with 'pre' option, the first call must happen before
     // the component is mounted so it is called synchronously.
     job()
}

queuePostFlushCb 的调用 在 renderer.ts 变成 queuePostRenderEffect 的调用


#### 总结:nextTick 将回调函数放到微任务队列里面(currentFlushPromise  的 resolve), 在当前工作 currentFlushPromise  执行完之后调用。而当前工作 currentFlushPromise 由 queueFlush 调用,queueFlush 会按队列里面任务的顺序循环执行:pendingPreFlushCbs、 queue、pendingPostFlushCbs 三个队列。
#### 在不同的时机,vue会通过 queuePreFlushCb、queueCb、queuePostFlushCb 三个函数将不同的任务压入队列,其中
#### queuePreFlushCb 会将 watch 的回调压入 pendingPreFlushCbs 队列;
#### queueCb 会在 render 阶段将 componentEffect 即组件渲染的副作用函数压入 queue 队列;
#### queuePostFlushCb 在很多个地方调用,将必要的回调任务压入 queuePostFlushCb 队列以保证在 nextTick 之前调用。

```js
    // nextTick 原理

    // 定义当前任务
    let c
    // 定义任务队列
    let a = []
    // 设置调度标志位
    let isFlushPending = false
    // 简易的 nextTick 
    const nT = (fn) => {
      c.then(fn)
    }

    // 1、c 首先是一个微任务才能保证利用 js 的调度机制
    // 2、判断是否在入队阶段,是的话只要给 c 赋值一次
    const queueFlush = () => {
      if (!isFlushPending) {
        isFlushPending = true
        c = Promise.resolve().then(callFn)
      }
    }

    // 按顺序执行队列中的任务
    const callFn = () => {
      isFlushPending = false
      // 任务去重
      const jobs = [...new Set(a)]
      jobs.forEach(cb => {
        cb()
      })
    }

    // 将任务入队的函数 入队后的任务会在 nextTick 之前执行
    const queueJob = (job) => {
      a.push(job)
      queueFlush()
    }

    // 测试 nextTick 的作用
    queueJob(() => {
      console.log('job_1')
    })

    nT(() => {
      console.log('nextTick')
    })

    // 假设这里数据改变 nT 永远保证在 queue 之后执行 
    queueJob(() => {
      console.log('job_2---data change')
    })

    // 测试 DOM 更新是异步的 假设 effect 是渲染组件的副作用函数
    const effect = () => {
        console.log('effect')
    }

    // 多次改变数据
    for (let index = 0; index < 10000; index++) {
      queueJob(effect)
    }

任务调度相关:https://juejin.cn/post/6947296766433705998

总结:Vue 的 DOM 更新是异步的并且多次改变响应式数据是不会造成重复渲染也是可以理解的,DOM 的渲染 ComponentEffect 是经过 effect 包装的,在 trigger 里面将 ComponentEffect 放入 queue 中并且进行了去重,queue 最终的执行 queueflush 也是放到微任务里头的。

watch watchEffect 原理:

创建effect,以实现响应式 并将scheduler传入。当响应式数据发生变化,就会调用scheduler函数

zhangzheng-zz commented 3 years ago

微任务里面继续执行入队操作:

一开始:
let isFlushing = false 清空队列的控制
let isFlushPending = false 入队过程中的控制 

只要有一次入队就开启微任务 并关闭开关
function queueFlush() {
  if (!isFlushing && !isFlushPending) {
    // 关闭开关
    isFlushPending = true
    currentFlushPromise = resolvedPromise.then(flushJobs)
  }
}

清空队列的时候也是关闭开关 isFlushing = true
flushJobs(seen?: CountMap) {
  isFlushPending = false
  isFlushing = true

  // 清空 pre
  flushPreFlushCbs(seen)

  try {
     // 清空 queue componentEffect 里面执行了 queuePostFlushCb 
     // queuePostFlushCb 通过 queueCb 入队到 post 里面,此时 post 里面的任务还未成为微任务,等待下次微任务开启
  } finally {

    // 清空 post
    flushPostFlushCbs(seen)

    // 开启开关 下一次重新开启新的微任务
    isFlushing = false

    // 递归 重新开启新的微任务 所以这里还会判断 post 是不是空的
    if (queue.length || pendingPostFlushCbs.length) {
      flushJobs(seen)
    }
  }
}