mybatis-plus对datetime返回去掉.0_Vue3.0 数据响应机制解析

c56ae01dce81703dcdba7ebcd1204d38.png

简单画了一张图片(请叫我灵魂画手,哈哈哈)

从上面的图片其实大家就能看出,数据渲染解析分为两条路,

  1. 通过节点的方式,然后根据指令,然后实例effect然后调用effect

  2. 通过reactive,通过baseHandle然后进行依赖收集,广播触发

因为篇幅有限今天主要分享的是data => reactive => effect这条路径

代码解析

reactive

  1. 可以跟大家说一下我一般情况是如何进行源代码阅读的,首先你先要对阅读的源代码实现的功能有所了解,然后对于这个功能的实现你的思路是什么,然后带着你的思路去源码中寻找作者的实现思路,在阅读的过程中对比自己的想法,弥补自己的不足。

  2. 最开始阅读的时候,我们要先了解我们要阅读的源码的整体结构,工程配置,然后通过项目的根文件(一般是index.js)进行阅读,寻找到自己最熟悉的或者最想了解的功能模块,进行代码阅读。

  3. 另外一个比较重要的点可能是,源码在实现过程中代码写的比较抽象,所以我们最开始阅读的时候大可不必全部了解细节,知道函数作用、变量用途就可以了,对整体实现流程捋通顺后在进行细节的阅读。

  4. 为什么阅读源码对程序员有帮助,在我看来,我们在阅读的过程中一方面是了解作者的实现思路,另外一方面是学习代码的抽象能力,提升自己的代码质量。

  1. 首先我们看reactive.ts中的reactive方法做了什么

export function reactive(target: object) {
// if trying to observe a readonly proxy, return the readonly version.
if (readonlyToRaw.has(target)) {
return target
}
// target is explicitly marked as readonly by user
if (readonlyValues.has(target)) {
return readonly(target)
}
return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)
}

去掉上面的两个if判断其实这个方法就做了一件事情就是调用了createReactiveObject这个方法

function createReactiveObject(
target: unknown,
toProxy: WeakMap,
toRaw: WeakMap,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler) {
// 如果target 不是对象
if (!isObject(target)) {
if (__DEV__) {
console.warn(`value cannot be made reactive: ${String(target)}`)
}
return target
}
// target already has corresponding Proxy
// 如果target 已经存在在相应的代理中则返回
let observed = toProxy.get(target)
// 不为undefined 时返回observed
if (observed !== void 0) {
return observed
}
// target is already a Proxy
// 如果target已经在代理中同样返回
if (toRaw.has(target)) {
return target
}
// only a whitelist of value types can be observed.
// 如果target并不能被observe 则返回
if (!canObserve(target)) {
return target
}

// 如果collectionTypes继承于target,handlers为collectionHandlers
// 不存在的话handlers为baseHandlers
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers

// 创建一个Proxy对象
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}

上面的代码我进行了一些简单的注释,同样我们把if判断中带有return的部分忽略掉不看的话,其实这个方法的代码实现的功能就非常明了了

function createReactiveObject(
target: unknown,
toProxy: WeakMap,
toRaw: WeakMap,
baseHandlers: ProxyHandler,
collectionHandlers: ProxyHandler) {
// target already has corresponding Proxy
// 如果target 已经存在在相应的代理中则返回
let observed = toProxy.get(target)

// 如果collectionTypes继承于target,handlers为collectionHandlers
// 不存在的话handlers为baseHandlers
const handlers = collectionTypes.has(target.constructor)
? collectionHandlers
: baseHandlers

// 创建一个Proxy对象
observed = new Proxy(target, handlers)
toProxy.set(target, observed)
toRaw.set(observed, target)
if (!targetMap.has(target)) {
targetMap.set(target, new Map())
}
return observed
}

我们在看上面简化的这段代码,实际上就是使用ES6中的Proxy,相信大家对于Vue2.0的响应式是通过Object.defineProperty来实现的,兼容主流浏览器和IE9以上的IE浏览器,能够监听数据对象的变化,但是监听不到对象属性的增删、数组元素和长度的变化,同时会在vue初始化的时候把所有的Observer都建立好,才能观察到数据对象属性的变化。这次3.0使用的ES6的Proxy可以做到监听对象属性的增删和数组元素和长度的修改,还可以监听Map、Set、WeakSet、WeakMap,同时还实现了惰性的监听,不会在初始化的时候创建所有的Observer,而是会在用到的时候才去监听。因此性能方面肯定是极大的提升!当然因为Proxy存在兼容性问题,所以3.0会对IE系列做兼容,对外api保持一致,底层通过Object.defineProperty实现。

baseHandle

通过上面的代码我们再来看Proxy中的handler是什么,const handlers = collectionTypes.has(target.constructor) ? collectionHandlers : baseHandler,这里做了一下判断如果当前传入的target被初始化过,那么使用传入collectionHandlers,如果没有被初始化过那么使用传入的baseHandler。

那么我们回顾一下最开始我们提供的第一个代码块中createReactiveObject的入参分别都是什么:

return createReactiveObject(
target,
rawToReactive,
reactiveToRaw,
mutableHandlers,
mutableCollectionHandlers
)

我们首先去看baseHandler(mutableHandlers)做了哪些操作:

export const mutableHandlers: ProxyHandler<object> = {
// 数据收集
get: createGetter(false),
// 数据变化 ==> 广播
set,
// 删除
deleteProperty,
has,
ownKeys
}
createGetter
function createGetter(isReadonly: boolean, unwrap = true) {
return function get(target: object, key: string | symbol, receiver: object) {
let res = Reflect.get(target, key, receiver)
if (isSymbol(key) && builtInSymbols.has(key)) {
return res
}
if (unwrap && isRef(res)) {
res = res.value
} else {
track(target, OperationTypes.GET, key)
}
return isObject(res)
? isReadonly
? // need to lazy access readonly and reactive here to avoid
// circular dependency
readonly(res)
: reactive(res)
: res
}
}

首先从代码上看,这个createGetter返回的是一个get方法,因为数据监听是使用Proxy做的,所以我们在对target进行获取内部信息时,使用了Reflect这个新的API,Reflect与Proxy是相辅相成的,在Proxy上有的方法,在Reflect就一定有,Reflect可以确保对象的属性能正确赋值,广义上讲,即确保对象的原生行为能够正常进行,这就是Reflect的作用,然后对res进行了判断,if (unwrap && isRef(res)) res = res.value在ref中同样做了track操作,看到这里大家肯定也能想到或者察觉到这个track是在做什么事情。没错,track这个函数就是做了依赖收集。

set
function set(
target: object,
key: string | symbol,value: unknown,
receiver: object): boolean {
value = toRaw(value)
const oldValue = (target as any)[key]
if (isRef(oldValue) && !isRef(value)) {
oldValue.value = value
return true
}
const hadKey = hasOwn(target, key)
const result = Reflect.set(target, key, value, receiver)
// don't trigger if target is something up in the prototype chain of original
// 在reactive维护了一个reactiveToRaw队列,存储了[proxy]:[target]这样的队列,这里检测下是否是使用createReactiveObject新建的proxy
if (target === toRaw(receiver)) {
/* istanbul ignore else */
// 判断是否值改变,才触发更新
if (__DEV__) {
const extraInfo = { oldValue, newValue: value }
if (!hadKey) {
trigger(target, OperationTypes.ADD, key, extraInfo)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key, extraInfo)
}
} else {
if (!hadKey) {
trigger(target, OperationTypes.ADD, key)
} else if (hasChanged(value, oldValue)) {
trigger(target, OperationTypes.SET, key)
}
}
}
return result
}

大家通过上面的我代码中的注释就能看出set在做什么事情,没错就是做更新广播!后面的deleteProperty、has、ownKeys实际上做的事情和上面get,set做的事情是一样的,这里就不做过多的赘述了。后面我们单独去看track和tigger。我们来总结一下baseHandler的作用

  • get获取值,其次依赖收集

  • set设置值,其次更新广播、触发任务调度

那么到底什么是依赖收集,什么是任务调度,dom和data的关系又是什么呢!!??

effect

effect在vue3.0中到底做了什么呢,我们带着疑问去代码中寻找答案:

我们先来看下面这段代码:

// 初始化effect
export function effect<T = any>(
fn: () => T,options: ReactiveEffectOptions = EMPTY_OBJ
): ReactiveEffect<T> {
if (isEffect(fn)) {
fn = fn.raw
}
const effect = createReactiveEffect(fn, options)
if (!options.lazy) {
effect()
}
return effect
}

// 创建一个effect
function createReactiveEffect<T = any>(
fn: () => T,options: ReactiveEffectOptions
): ReactiveEffect<T> {
const effect = function reactiveEffect(...args: unknown[]): unknown {
return run(effect, fn, args)
} as ReactiveEffect
effect._isEffect = true
effect.active = true
effect.raw = fn
effect.deps = []
effect.options = options
return effect
}

function run(effect: ReactiveEffect, fn: Function, args: unknown[]): unknown {
if (!effect.active) {
return fn(...args)
}
if (!effectStack.includes(effect)) {
cleanup(effect)
try {
effectStack.push(effect)
return fn(...args)
} finally {
effectStack.pop()
}
}
}

大家从上面的代码中能看出什么,实不相瞒,我看不出什么??? 但是,我从测试用例上面知道了这个effect的作用???

effect测试用例

  it('should observe basic properties', () => {
let dummy
const counter = reactive({ num: 0 })
effect(() => (dummy = counter.num))

expect(dummy).toBe(0)
counter.num = 7
expect(dummy).toBe(7)
})

computed测试用例

  it('should return updated value', () => {
const value = reactive({})
const cValue = computed(() => value.foo)
expect(cValue.value).toBe(undefined)
value.foo = 1
expect(cValue.value).toBe(1)
})

看完上面的代码是不是有了思路,哈哈哈哈哈哈 effect从字面上来看就是影响的意思,因此effect实际上做的就是在数据响应的过程中,处理产生的副作用,因此在每次触发get的时候收集effect,在set的时候触发effects!

下面来看一下大佬用effect实现的computed

function computed (fn) {
let value = undefined
const runner = effect(fn, {
// 如果lazy不置为true的话,每次创建effect的时候都会立即执行一次
// 而我们要实现computed显然是不需要的
lazy: true
})
// 为什么要使用对象的形式,是因为我们最后需要得到computed的值
// 如果不用对象的 get 方法的话我们就需要手动再调用一次 computed()
return {
get value() {
return runner()
}
}
}

// 使用起来是这样的

const value = reactive({})
const cValue = computed(() => value.foo)
value.foo = 1

console.log(cValue.value) // 1

作者:前端技匠
链接:https://juejin.im/post/5d9f5090e51d4578502c24b1
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

下面我们再来看track和trigger的实现

// 数据依赖收集
export function track(target: object, type: OperationTypes, key?: unknown) {
if (!shouldTrack || effectStack.length === 0) {
return
}
const effect = effectStack[effectStack.length - 1]
if (type === OperationTypes.ITERATE) {
key = ITERATE_KEY
}
let depsMap = targetMap.get(target)
if (depsMap === void 0) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key!)
if (dep === void 0) {
depsMap.set(key!, (dep = new Set()))
}
if (!dep.has(effect)) {
dep.add(effect)
effect.deps.push(dep)
if (__DEV__ && effect.options.onTrack) {
effect.options.onTrack({
effect,
target,
type,
key
})
}
}
}

// 数据变化时 ==> 更新广播
export function trigger(
target: object,
type: OperationTypes,
key?: unknown,
extraInfo?: DebuggerEventExtraInfo) {
const depsMap = targetMap.get(target)
if (depsMap === void 0) {
// never been tracked
return
}
const effects = new Set()const computedRunners = new Set()if (type === OperationTypes.CLEAR) {// collection being cleared, trigger all effects for target
depsMap.forEach(dep => {
addRunners(effects, computedRunners, dep)
})
} else {// schedule runs for SET | ADD | DELETEif (key !== void 0) {
addRunners(effects, computedRunners, depsMap.get(key))
}// also run for iteration key on ADD | DELETEif (type === OperationTypes.ADD || type === OperationTypes.DELETE) {const iterationKey = isArray(target) ? 'length' : ITERATE_KEY
addRunners(effects, computedRunners, depsMap.get(iterationKey))
}
}const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}// Important: computed effects must be run first so that computed getters// can be invalidated before any normal effects that depend on them are run.
computedRunners.forEach(run)
effects.forEach(run)
}

所以effect做的事情就可以总结为:

  1. 当响应数据触发get时, 将所有get的target、key与effect建立对应关系,因为每个target可能会有多个副作用effect,所以建立一个deps依赖收集器,

  2. 因为deps是target的副作用集合,因此放在target上面并不合适,因此将这个对应关系存储在targetMap中。

  3. 当trigger触发时,则拿到target下对应所有的effect,遍历执行。

到这里我们就把vue3.0中的数据响应介绍的差不多了,但是好像我们说的这个跟dom没啥关系啊,dom上的数据最后是怎么变更的???

相信细心的同学发现了在trigger函数中定义了一个run函数:

const run = (effect: ReactiveEffect) => {
scheduleRun(effect, target, type, key, extraInfo)
}

我们在来看scheduleRun这个函数做了什么:

function scheduleRun(
effect: ReactiveEffect,
target: object,
type: OperationTypes,
key: unknown,
extraInfo?: DebuggerEventExtraInfo) {
if (__DEV__ && effect.options.onTrigger) {
const event: DebuggerEvent = {
effect,
target,
key,
type
}
effect.options.onTrigger(extraInfo ? extend(event, extraInfo) : event)
}
if (effect.options.scheduler !== void 0) {
effect.options.scheduler(effect)
} else {
effect()
}
}

effect.options.scheduler(effect)没错这句代码会触发watch中的queuePostRenderEffect,而这个函数的作用是什么呢?

我们可以在renderer.ts中找到答案:

export const queuePostRenderEffect = __FEATURE_SUSPENSE__
? queueEffectWithSuspense
: queuePostFlushCb

我们看看 queuePostRenderEffect 函数,本质是调用的 queuePostFlushCb

export function queuePostFlushCb(cb: Function | Function[]) {
if (!isArray(cb)) {
postFlushCbs.push(cb)
} else {
postFlushCbs.push(...cb)
}
queueFlush()
}

function queueFlush() {
if (!isFlushing && !isFlushPending) {
isFlushPending = true
nextTick(flushJobs)
}
}

queuePostFlushCb 函数也比较简单,收集回调函数然后在nextTick,最后执行flushJobs。

我们在scheduler中发现存在2个队列:

  1. queue

  2. postFlushCbs

对应的添加函数为

  1. queueJob

  2. queuePostFlushCb

很显然,这是对应的两种更新时机的回调,而触发回调都是由flushJobs 完成:

function flushJobs(seen?: CountMap) {
isFlushPending = false
isFlushing = true
let job
if (__DEV__) {
seen = seen || new Map()
}
while ((job = queue.shift())) {
if (__DEV__) {
checkRecursiveUpdates(seen!, job)
}
callWithErrorHandling(job, null, ErrorCodes.SCHEDULER)
}
flushPostFlushCbs(seen)
isFlushing = false
// some postFlushCb queued jobs!
// keep flushing until it drains.
if (queue.length || postFlushCbs.length) {
flushJobs(seen)
}
}

最后我们回头在看回调函数applyCb

const applyCb = cb
? () => {
if (instance && instance.isUnmounted) {
return
}
const newValue = runner()
if (deep || hasChanged(newValue, oldValue)) {
// cleanup before running cb again
if (cleanup) {
cleanup()
}
callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [
newValue,
oldValue,
registerCleanup
])
oldValue = newValue
}
}
: void 0

综上我们可以得到以下几点:

  1. reactivity 的作用在于处理对象的 proxy,在每个取值操作的地方 track。

  2. 当对象属性值发生变化的时候,触发trigger

  3. watch的巧妙之处在于取值时添加依赖传入回调函数、创建effect的同时,对回调进行scheduler处理,最后scheduler根据flush时机来进行区分。

至此,我今天要跟大家分享的内容基本上就差不多了,最后我在阅读的过程中对源码添加了注释GitHub - Jhaidi/vue-next

如果你觉得这篇内容对你挺有启发,我想邀请你帮我两个小忙:

  • 点个【在看】,或者分享转发,让更多的人也能看到这篇内容

  • 关注公众号【思享说】,不定期分享原创&精品技术文章。

892a66dfb59444b5c534d443c4a32f33.png

欢迎 评论区留下你的精彩评论~

觉得文章不错可以分享到朋友圈让更多的小伙伴看到哦~

客官!在看一下呗
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值