Skip to content

Commit

Permalink
feat: computed && watch
Browse files Browse the repository at this point in the history
  • Loading branch information
xs-web-lhdd committed Mar 20, 2022
1 parent 890893c commit 5e26a7c
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 2 deletions.
1 change: 1 addition & 0 deletions packages/reactivity/src/computed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export class ComputedRefImpl<T> {
public readonly [ReactiveFlags.IS_READONLY]: boolean

// 数据是否脏:用来判断 computed 里面的数据需不需要重新计算,默认是 true,也就是第一次需要初始化计算,所以为 true
// 数据“脏了 ->>> 依赖的数据发生了变化”就要重新计算啦
public _dirty = true
public _cacheable: boolean

Expand Down
40 changes: 39 additions & 1 deletion packages/runtime-core/src/apiWatch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import {
callWithErrorHandling,
callWithAsyncErrorHandling
} from './errorHandling'
// 在不涉及 suspense 情况下, queuePostRenderEffect 就是 queuePostFlushCb(位于 scheduler.ts 文件里面)
import { queuePostRenderEffect } from './renderer'
import { warn } from './warning'
import { DeprecationTypes } from './compat/compatConfig'
Expand Down Expand Up @@ -76,6 +77,10 @@ export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase {

export type WatchStopHandle = () => void

// watchEffect
// 1 侦听的是一个普通的 function ,内部访问了响应式对象即可,并不需要返回响应对象
// 2 没有回调函数,副作用函数内部响应式对象发生变化后,会再次执行这个副作用函数
// 3 立即执行, 在创建好 watcher 后,立刻执行它的副作用函数,而 watch 需要配置 immediate 为 true,才会立即执行回调函数
// Simple effect.
export function watchEffect(
effect: WatchEffect,
Expand Down Expand Up @@ -160,22 +165,29 @@ export function watch<T = any, Immediate extends Readonly<boolean> = false>(
cb: any,
options?: WatchOptions<Immediate>
): WatchStopHandle {
// 现在生产环境下判断第二个参数是不是一个函数,如果不是一个函数(包括没有传递),那么就会报警告。
if (__DEV__ && !isFunction(cb)) {
warn(
`\`watch(fn, options?)\` signature has been moved to a separate API. ` +
`Use \`watchEffect(fn, options?)\` instead. \`watch\` now only ` +
`supports \`watch(source, cb, options?) signature.`
)
}
// 核心是调用 doWatch 函数
return doWatch(source as any, cb, options)
}

// watch 的核心逻辑:
function doWatch(
// 传入的第一个监听参数,应该是 一个响应式数据, 或者 一个函数(返回值是一个响应式数据), 再或者 多个响应式数据组成的数组
source: WatchSource | WatchSource[] | WatchEffect | object,
cb: WatchCallback | null,
// options 中不同的配置,决定了 cb 执行时不同的行为
// flush onTrack onTrigger 见 Vue3 官网 https://v3.cn.vuejs.org/guide/reactivity-computed-watchers.html#%E6%B8%85%E9%99%A4%E5%89%AF%E4%BD%9C%E7%94%A8
{ immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ
): WatchStopHandle {
if (__DEV__ && !cb) {
// 在生产环境下并且没有传入第二个侦听函数,那么就根据传入的 options 进行警告
if (immediate !== undefined) {
warn(
`watch() "immediate" option is only respected when using the ` +
Expand All @@ -190,6 +202,7 @@ function doWatch(
}
}

// 当前 source 不合法时就会报警告:
const warnInvalidSource = (s: unknown) => {
warn(
`Invalid watch source: `,
Expand All @@ -199,18 +212,23 @@ function doWatch(
)
}

// 当前组件实例:
const instance = currentInstance
let getter: () => any
let forceTrigger = false
let isMultiSource = false

// 通过 source 标准化 getter 函数:
if (isRef(source)) {
// 传入的 source 是一个 ref 类型响应式数据时: 创建一个返回 source.value 的 getter 函数
getter = () => source.value
forceTrigger = isShallow(source)
} else if (isReactive(source)) {
// 传入的 source 是一个 reactive 类型的响应式数据: 创建一个返回值是 source 的 getter 函数,并且设置 deep 为 true,目的是为了深层监听
getter = () => source
deep = true
} else if (isArray(source)) {
// 传入的是一个包含多个响应式数据的数组的情况:
isMultiSource = true
forceTrigger = source.some(isReactive)
getter = () =>
Expand All @@ -226,16 +244,20 @@ function doWatch(
}
})
} else if (isFunction(source)) {
// 传入的是一个 getter 函数的情况: 会先判断 cb 函数是否存在, 在这种情况下, getter 就是简单的对 source 进行一次封装
if (cb) {
// getter with cb
getter = () =>
callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER)
} else {
// no cb -> simple effect
// no cb -> simple effect 这种情况是为了 watchEffect API 设计的
getter = () => {
// 当组件销毁时直接返回:
// console.log('这不是 watchEffect 的情况嘛!!!');
if (instance && instance.isUnmounted) {
return
}
// 执行清理:
if (cleanup) {
cleanup()
}
Expand All @@ -248,6 +270,7 @@ function doWatch(
}
}
} else {
// 老规矩,当 source 什么都不是的时候,默认标准化 getter 为一个空函数, 并且在控制台报一个警告
getter = NOOP
__DEV__ && warnInvalidSource(source)
}
Expand All @@ -267,7 +290,10 @@ function doWatch(
}
}

console.log('xzxx');

if (cb && deep) {
// 如果 deep 为 true 并且 cb 存在,那么就相当于把 getter 函数用 traverse 包装一层
const baseGetter = getter
getter = () => traverse(baseGetter())
}
Expand Down Expand Up @@ -338,19 +364,25 @@ function doWatch(
// it is allowed to self-trigger (#1727)
job.allowRecurse = !!cb

// scheduler 主要是把 watcher 以某种调度方式执行:不同的 flush 决定了不同的调度,也就是 watcher 的执行时机:
let scheduler: EffectScheduler
if (flush === 'sync') {
// flush 表示这是一个 同步 的 watcher ,即当数据发生变化时同步执行 回调函数
scheduler = job as any // the scheduler function gets called directly
} else if (flush === 'post') {
// 回调通过 queuePostRenderEffect 的方式在组件更新之后执行
scheduler = () => queuePostRenderEffect(job, instance && instance.suspense)
} else {
// default: 'pre'
// 默认 flush 为 pre,
scheduler = () => {
if (!instance || instance.isMounted) {
// 如果组件挂载,则通过 queueJob 的方式在组件更新之前执行
queuePreFlushCb(job)
} else {
// with 'pre' option, the first call must happen before
// the component is mounted so it is called synchronously.
// 如果组件还没挂载,则同步执行确保回调函数在组件挂载之前执行
job()
}
}
Expand All @@ -376,12 +408,16 @@ function doWatch(
instance && instance.suspense
)
} else {
// 没有 cb 的情况:
effect.run()
// console.log(effect.run().value);
}

// 返回一个取消侦听的函数: 当该函数执行时会取消对相关依赖的侦听
return () => {
effect.stop()
if (instance && instance.scope) {
// 如果是组件也会移除组件的引用:
remove(instance.scope.effects!, effect)
}
}
Expand Down Expand Up @@ -418,6 +454,7 @@ export function instanceWatch(
return res
}

// 解析 getter 里面监听数据的响应数据的路径:
export function createPathGetter(ctx: any, path: string) {
const segments = path.split('.')
return () => {
Expand All @@ -429,6 +466,7 @@ export function createPathGetter(ctx: any, path: string) {
}
}

// traverse 函数作用就是递归访问 value 的每一个子属性,以实现深层的侦听
export function traverse(value: unknown, seen?: Set<unknown>) {
if (!isObject(value) || (value as any)[ReactiveFlags.SKIP]) {
return value
Expand Down
32 changes: 31 additions & 1 deletion packages/runtime-core/src/scheduler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,16 +33,20 @@ export interface SchedulerJob extends Function {

export type SchedulerJobs = SchedulerJob | SchedulerJob[]

// 任务队列是否正在执行: 判断是否正在执行任务队列
let isFlushing = false
// 任务队列是否等待执行: 判断是否在等待 nextTick 执行 flushJobs
let isFlushPending = false

// 用于异步任务队列:
const queue: SchedulerJob[] = []
let flushIndex = 0

const pendingPreFlushCbs: SchedulerJob[] = []
let activePreFlushCbs: SchedulerJob[] | null = null
let preFlushIndex = 0

//
const pendingPostFlushCbs: SchedulerJob[] = []
let activePostFlushCbs: SchedulerJob[] | null = null
let postFlushIndex = 0
Expand All @@ -55,6 +59,7 @@ let currentPreFlushParentJob: SchedulerJob | null = null
const RECURSION_LIMIT = 100
type CountMap = Map<SchedulerJob, number>

// Vue3 中 nextTick 是通过 Promise.resolve().then 去异步执行 flushJobs
export function nextTick<T = void>(
this: T,
fn?: (this: T) => void
Expand All @@ -81,6 +86,7 @@ function findInsertionIndex(id: number) {
return start
}

// 往队列里面放组件更新函数:
export function queueJob(job: SchedulerJob) {
// the dedupe search uses the startIndex argument of Array.includes()
// by default the search index includes the current job that is being run
Expand All @@ -90,7 +96,7 @@ export function queueJob(job: SchedulerJob) {
// ensure it doesn't end up in an infinite loop.
if (
(!queue.length ||
!queue.includes(
!queue.includes( // 去重!一个组件的更新函数只能进去一次
job,
isFlushing && job.allowRecurse ? flushIndex + 1 : flushIndex
)) &&
Expand All @@ -101,13 +107,17 @@ export function queueJob(job: SchedulerJob) {
} else {
queue.splice(findInsertionIndex(job.id), 0, job)
}
// 刷新队列:
queueFlush()
}
}

function queueFlush() {
// 首次执行时 isFlushing 和 isFlushPending 都是 false,开始执行后 isFlushPending 设置为true, isFlushPending 的作用是即使多次调用 queueFlush, 也不会多次去执行 flushJobs
if (!isFlushing && !isFlushPending) {
isFlushPending = true
// 利用 Promise.then 启动一个微任务(resolvedPromise 是 Pormise 实例):
// 所以 flushJobs 就是一个异步任务:
currentFlushPromise = resolvedPromise.then(flushJobs)
}
}
Expand Down Expand Up @@ -183,6 +193,7 @@ export function flushPreFlushCbs(

export function flushPostFlushCbs(seen?: CountMap) {
if (pendingPostFlushCbs.length) {
// 先拷贝一个副本,因为在回调函数执行时可能会改变 pendingPostFlushCbs
const deduped = [...new Set(pendingPostFlushCbs)]
pendingPostFlushCbs.length = 0

Expand Down Expand Up @@ -220,7 +231,9 @@ export function flushPostFlushCbs(seen?: CountMap) {
const getId = (job: SchedulerJob): number =>
job.id == null ? Infinity : job.id

// 在微任务中把队列中每一个任务拿出来执行一遍:
function flushJobs(seen?: CountMap) {
// 把 isFlushPending 重置为 false , isFlushing 重置为 true,表示该队列正在执行而不是处于等待状态
isFlushPending = false
isFlushing = true
if (__DEV__) {
Expand All @@ -236,18 +249,26 @@ function flushJobs(seen?: CountMap) {
// priority number)
// 2. If a component is unmounted during a parent component's update,
// its update can be skipped.
// 为啥排序,上面那段英语已经解释很清楚了:
// 组件的更新是先父后子的,
// 如果一个组件在父组件更新过程中卸载,它自身的更新应该被跳过
queue.sort((a, b) => getId(a) - getId(b))
// 创建组件的过程是由父到子,创建组件副作用渲染函数先父后子
// 父组件的副作用渲染函数的 effect id 是小于子组件的
// 每次更新组件也是通过 queueJob 把 effect 推入异步任务队列 queue 中

// conditional usage of checkRecursiveUpdate must be determined out of
// try ... catch block since Rollup by default de-optimizes treeshaking
// inside try-catch. This can leave all warning code unshaked. Although
// they would get eventually shaken by a minifier like terser, some minifiers
// would fail to do that (e.g. https://github.com/evanw/esbuild/issues/1610)
// 用来在非生产环境下检测是否有循环更新的: 例子见 01-checkRecursiveUpdateds.html
const check = __DEV__
? (job: SchedulerJob) => checkRecursiveUpdates(seen!, job)
: NOOP

try {
// 拿到队列中的每一个任务,调用这个任务:
for (flushIndex = 0; flushIndex < queue.length; flushIndex++) {
const job = queue[flushIndex]
if (job && job.active !== false) {
Expand All @@ -264,10 +285,13 @@ function flushJobs(seen?: CountMap) {

flushPostFlushCbs(seen)

// 重置 isFlushing 为 false 代表着执行完毕
isFlushing = false
currentFlushPromise = null
// some postFlushCb queued jobs!
// keep flushing until it drains.
// 因为执行过程中可能会再次添加异步任务
// 所以需要继续判断如果 queue 或者 pendingPreFlushCbs 或者 pendingPostFlushCbs 队列中还存在任务,则递归执行 flushJobs
if (
queue.length ||
pendingPreFlushCbs.length ||
Expand All @@ -278,6 +302,7 @@ function flushJobs(seen?: CountMap) {
}
}

// checkRecursiveUpdates 的逻辑是用来在非生产环境下检测是否有循环更新的: 例子见 01-checkRecursiveUpdateds.html
function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
if (!seen.has(fn)) {
seen.set(fn, 1)
Expand All @@ -301,3 +326,8 @@ function checkRecursiveUpdates(seen: CountMap, fn: SchedulerJob) {
}
}
}


// 异步任务队列的创建引发的思考???
// JavaScript 是单线程的,异步设计在一个 Tick 内可以多次执行 queueJob 或者
// queueCb 去添加任务,可以保证宏任务执行完毕后的微任务阶段执行一次 flushJobs
26 changes: 26 additions & 0 deletions packages/vue/examples/01-checkRecursiveUpdateds.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div id="app">
{{state.count}}
</div>

<script src="../dist/vue.global.js"></script>

<script>
Vue.createApp({
setup() {
const state = Vue.reactive({
count: 0
})

Vue.watch(() => state.count, (nowValue, preValue) => {
state.count ++
console.log('===>之前的值:', preValue, "====>现在的值:", nowValue);
}, { flush: 'post' })

state.count ++

return {
state
}
}
}).mount('#app')
</script>
26 changes: 26 additions & 0 deletions packages/vue/examples/01-queue.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<div id="app">
{{count}}
</div>

<script src="../dist/vue.global.js"></script>

<script>
Vue.createApp({
setup() {
const count = Vue.ref(1)
// Vue.watch(() => count, () => {
// return 1
// })
Vue.watch(() => count, null, {
deep: true,
immediate: true
})
setTimeout(() => {
count.value++
})
return {
count
}
}
}).mount('#app')
</script>
Loading

0 comments on commit 5e26a7c

Please sign in to comment.