Vue3 Listener Watch의 구현 원리는 무엇입니까
watch의 본질
소위 시계의 본질은 반응하는 데이터를 관찰하고, 데이터가 변경되면 이를 알리고 해당 콜백 함수를 실행하는 것입니다. 실제로 watch의 구현 본질은 effect 및 options.scheduler 옵션을 사용하는 것입니다. 다음 예와 같이
// watch 函数接收两个参数,source 是响应式数据,cb 是回调函数 function watch(source, cb){ effect( // 触发读取操作,从而建立联系 () => source.foo, { scheduler(){ // 当数据变化时,调用回调函数 cb cb() } } ) }
코드에서 보듯이 source는 반응형 데이터이고 cb는 콜백 함수입니다. Side Effect 함수에 스케줄러 옵션이 있는 경우, 반응형 데이터가 변경되면 Side Effect 기능을 직접 실행하는 것이 아니라 스케줄러 기능을 실행하게 됩니다. 이러한 관점에서 스케줄러 스케줄링 기능은 콜백 기능과 동일하며 watch 구현은 이를 활용합니다.
watch
의 함수 서명은 여러 소스를 수신합니다
청취할 데이터 소스는 아래 함수 서명에 표시된 것처럼 배열일 수 있습니다.
// packages/runtime-core/src/apiWatch.ts // 数据源是一个数组 // overload: array of multiple sources + cb export function watch< T extends MultiWatchSources, Immediate extends Readonly<boolean> = false >( sources: [...T], cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate> ): WatchStopHandle
배열을 사용하여 여러 소스를 수신할 수도 있습니다.
// packages/runtime-core/src/apiWatch.ts // 使用数组同时侦听多个源 // overload: multiple sources w/ `as const` // watch([foo, bar] as const, () => {}) // somehow [...T] breaks when the type is readonly export function watch< T extends Readonly<MultiWatchSources>, Immediate extends Readonly<boolean> = false >( source: T, cb: WatchCallback<MapSources<T, false>, MapSources<T, Immediate>>, options?: WatchOptions<Immediate> ): WatchStopHandle
단일 소스 청취
청취할 데이터 소스는 다음과 같이 ref 유형 데이터 또는 반환 값이 있는 getter 함수입니다. 함수 서명:
// packages/runtime-core/src/apiWatch.ts // 数据源是一个 ref 类型的数据 或者是一个具有返回值的 getter 函数 // overload: single source + cb export function watch<T, Immediate extends Readonly<boolean> = false>( source: WatchSource<T>, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle export type WatchSource<T = any> = Ref<T> | ComputedRef<T> | (() => T)
청취할 데이터 소스는 다음 함수 서명에 표시된 것처럼 반응형 obj 개체입니다.
// packages/runtime-core/src/apiWatch.ts // 数据源是一个响应式的 obj 对象 // overload: watching reactive object w/ cb export function watch< T extends object, Immediate extends Readonly<boolean> = false >( source: T, cb: WatchCallback<T, Immediate extends true ? T | undefined : T>, options?: WatchOptions<Immediate> ): WatchStopHandle
watch 구현
watch 함수
// packages/runtime-core/src/apiWatch.ts // implementation export function watch<T = any, Immediate extends Readonly<boolean> = false>( source: T | WatchSource<T>, 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.` ) } return doWatch(source as any, cb, options) }
보시다시피 watch 함수는 3개의 매개변수: source 수신할 데이터 소스, cb 콜백 함수, 옵션 수신 옵션.
source 매개변수
watch의 함수 오버로드를 통해 단일 소스를 들을 때 소스가 ref 유형 데이터이거나 반환 값이 있는 getter 함수일 수도 있고, 반응형 obj 객체일 수도 있다는 것을 알 수 있습니다. 여러 소스를 청취할 때 소스는 배열이 될 수 있습니다.
cb 매개변수
cb 콜백 함수에서는 개발자에게 최신 값, 이전 값 및 부작용 제거를 위한 onCleanup 함수를 제공합니다. 다음 유형 정의에서 볼 수 있듯이:
export type WatchCallback<V = any, OV = any> = ( value: V, oldValue: OV, onCleanup: OnCleanup ) => any
options 매개변수
options 옵션은 시계의 동작을 제어할 수 있습니다. 예를 들어 옵션의 즉시 옵션 매개변수를 사용하여 시계의 콜백이 즉시 실행되는지 여부를 제어할 수 있습니다. 옵션의 옵션 매개변수를 사용하여 시계의 콜백 기능을 동기식으로 실행할지 비동기식으로 실행할지 제어할 수 있습니다. 옵션 매개변수의 유형 정의는 다음과 같습니다.
export interface WatchOptionsBase extends DebuggerOptions { flush?: 'pre' | 'post' | 'sync' } export interface WatchOptions<Immediate = boolean> extends WatchOptionsBase { immediate?: Immediate deep?: boolean }
WatchOptions가 WatchOptionsBase를 상속하는 옵션의 유형 정의를 볼 수 있습니다. 즉, watch 옵션의 즉각적 및 심층적이라는 두 가지 고유 매개변수 외에도 WatchOptionsBase의 모든 매개변수를 전달하여 부작용 실행 동작을 제어할 수도 있습니다.
doWatch 함수는 watch의 함수 본문에서 호출됩니다. 구현을 살펴보겠습니다.
doWatch 함수
사실 watch 함수든 watchEffect 함수든 결국 doWatch 함수는 실행 중에 호출됩니다.
doWatch 함수 시그니처
function doWatch( source: WatchSource | WatchSource[] | WatchEffect | object, cb: WatchCallback | null, { immediate, deep, flush, onTrack, onTrigger }: WatchOptions = EMPTY_OBJ ): WatchStopHandle
doWatch의 함수 시그니처는 기본적으로 watch의 함수 시그니처와 동일하며, 3개의 매개변수도 받습니다. options 옵션의 사용을 용이하게 하기 위해 doWatch 함수는 옵션을 분해합니다.
변수 초기화
먼저, 구성 요소를 통해 현재 구성 요소 인스턴스를 가져온 다음 세 가지 다른 변수를 선언합니다. 함수 중 하나는 getter라고 하며 부작용 함수에 인수로 전달됩니다. forceTrigger 변수는 부작용 기능을 강제로 적용해야 하는지 여부를 나타내는 부울 값입니다. isMultiSource 변수는 수신 데이터 소스가 단일 소스인지 또는 배열 형식으로 전달된 여러 소스인지를 표시하는 데 사용되는 부울 값이기도 합니다. 이는 수신 데이터 소스가 단일 소스임을 나타냅니다. 아래 코드와 같이
const instance = currentInstance let getter: () => any // 是否需要强制触发副作用函数执行 let forceTrigger = false // 侦听的是否是多个源 let isMultiSource = false
다음으로 청취한 데이터 소스에 따라 이 세 가지 변수를 초기화합니다.
청취 데이터 소스가 ref 유형 데이터입니다
청취 데이터 소스가 ref 유형 데이터인 경우 source.value를 반환하여 getter가 초기화됩니다. 즉, getter 함수가 트리거되면 실제 청취 데이터가 됩니다. source.value를 통해 얻을 수 있습니다. 그런 다음 isShallow 함수를 사용하여 수신된 데이터 소스가 얕은 응답인지 확인하고 결과를 forceTrigger에 할당하여 forceTrigger 변수의 초기화를 완료합니다. 다음 코드와 같이
if (isRef(source)) { // 侦听的数据源是 ref getter = () => source.value // 判断数据源是否是浅响应 forceTrigger = isShallow(source) }
청취 데이터 소스가 반응형 데이터
청취 데이터 소스가 반응형 데이터인 경우 소스를 직접 반환하여 getter를 초기화합니다. 즉, getter 함수가 다음과 같은 경우입니다. Triggered 수신 데이터 소스를 직접 반환합니다. 반응형 데이터가 객체일 수 있으므로 deep을 true로 설정하면 getter 함수가 트리거될 때 객체의 속성 값을 재귀적으로 읽을 수 있습니다. 아래 코드와 같이
else if (isReactive(source)) { // 侦听的数据源是响应式数据 getter = () => source deep = true }
청취 데이터 소스가 배열입니다
청취 데이터 소스가 배열인 경우, 즉 여러 소스를 동시에 청취합니다. 이때 isMultiSource 변수를 직접 true로 설정하여 여러 소스가 청취되고 있음을 나타냅니다. 그런 다음 배열의 some 메서드를 사용하여 여러 수신 소스에 응답 개체가 있는지 감지하고 결과를 forceTrigger에 할당합니다. 배열을 탐색하고 각 소스의 유형에 따라 getter 함수의 초기화를 완료합니다. 아래 코드와 같이
else if (isArray(source)) { // 侦听的数据源是一个数组,即同时侦听多个源 isMultiSource = true forceTrigger = source.some(isReactive) getter = () => // 遍历数组,判断每个源的类型 source.map(s => { if (isRef(s)) { // 侦听的数据源是 ref return s.value } else if (isReactive(s)) { // 侦听的数据源是响应式数据 return traverse(s) } else if (isFunction(s)) { // 侦听的数据源是一个具有返回值的 getter 函数 return callWithErrorHandling(s, instance, ErrorCodes.WATCH_GETTER) } else { __DEV__ && warnInvalidSource(s) } }) }
청취 데이터 소스는 함수
입니다.当侦听的数据源是一个具有返回值的 getter 函数时,判断 doWatch 函数的第二个参数 cb 是否有传入。如果有传入,则处理的是 watch 函数的场景,此时执行 source 函数,将执行结果赋值给 getter 。该情况仅适用于 watchEffect 函数未接收到参数的情况。如果组件实例已被卸载,则直接返回而不执行 source 函数,根据该场景进行处理。如果未能执行成功,则执行清除依赖的代码并调用source函数,将返回结果赋值给getter。如下面的代码所示:
else if (isFunction(source)) { // 处理 watch 和 watchEffect 的场景 // watch 的第二个参数可以是一个具有返回值的 getter 参数,第二个参数是一个回调函数 // watchEffect 的参数是一个 函数 // 侦听的数据源是一个具有返回值的 getter 函数 if (cb) { // getter with cb // 处理的是 watch 的场景 // 执行 source 函数,将执行结果赋值给 getter getter = () => callWithErrorHandling(source, instance, ErrorCodes.WATCH_GETTER) } else { // no cb -> simple effect // 没有回调,即为 watchEffect 的场景 getter = () => { // 件实例已经卸载,则不执行,直接返回 if (instance && instance.isUnmounted) { return } // 清除依赖 if (cleanup) { cleanup() } // 执行 source 函数 return callWithAsyncErrorHandling( source, instance, ErrorCodes.WATCH_CALLBACK, [onCleanup] ) } } }
递归读取响应式数据
如果侦听的数据源是一个响应式数据,需要递归读取响应式数据中的属性值。如下面的代码所示:
// 处理的是 watch 的场景 // 递归读取对象的属性值 if (cb && deep) { const baseGetter = getter getter = () => traverse(baseGetter()) }
在上面的代码中,doWatch 函数的第二个参数 cb 有传入,说明处理的是 watch 中的场景。deep 变量为 true ,说明此时侦听的数据源是一个响应式数据,因此需要调用 traverse 函数来递归读取数据源中的每个属性,对其进行监听,从而当任意属性发生变化时都能够触发回调函数执行。
定义清除副作用函数
声明 cleanup 和 onCleanup 函数,并在 onCleanup 函数的执行过程中给 cleanup 函数赋值,当副作用函数执行一些异步的副作用时,这些响应需要在其失效是清除。如下面的代码所示:
// 清除副作用函数 let cleanup: () => void let onCleanup: OnCleanup = (fn: () => void) => { cleanup = effect.onStop = () => { callWithErrorHandling(fn, instance, ErrorCodes.WATCH_CLEANUP) } }
封装 scheduler 调度函数
为了便于控制 watch 的回调函数 cb 的执行时机,需要将 scheduler 调度函数封装为一个独立的 job 函数,如下面的代码所示:
// 将 scheduler 调度函数封装为一个独立的 job 函数,便于在初始化和变更时执行它 const job: SchedulerJob = () => { if (!effect.active) { return } if (cb) { // 处理 watch 的场景 // watch(source, cb) // 执行副作用函数获取新值 const newValue = effect.run() // 如果数据源是响应式数据或者需要强制触发副作用函数执行或者新旧值发生了变化 // 则执行回调函数,并更新旧值 if ( deep || forceTrigger || (isMultiSource ? (newValue as any[]).some((v, i) => hasChanged(v, (oldValue as any[])[i]) ) : hasChanged(newValue, oldValue)) || (__COMPAT__ && isArray(newValue) && isCompatEnabled(DeprecationTypes.WATCH_ARRAY, instance)) ) { // 当回调再次执行前先清除副作用 // cleanup before running cb again if (cleanup) { cleanup() } // 执行watch 函数的回调函数 cb,将旧值和新值作为回调函数的参数 callWithAsyncErrorHandling(cb, instance, ErrorCodes.WATCH_CALLBACK, [ newValue, // 首次调用时,将 oldValue 的值设置为 undefined // pass undefined as the old value when it's changed for the first time oldValue === INITIAL_WATCHER_VALUE ? undefined : oldValue, onCleanup ]) // 更新旧值,不然下一次会得到错误的旧值 oldValue = newValue } } else { // watchEffect // 处理 watchEffect 的场景 effect.run() } }
在 job 函数中,判断回调函数 cb 是否传入,如果有传入,那么是 watch 函数被调用的场景,否则就是 watchEffect 函数被调用的场景。
如果是 watch 函数被调用的场景,首先执行副作用函数,将执行结果赋值给 newValue 变量,作为最新的值。然后判断需要执行回调函数 cb 的情况:
如果侦听的数据源是响应式数据,需要深度侦听,即 deep 为 true
如果需要强制触发副作用函数执行,即 forceTrigger 为 true
如果新旧值发生了变化
如果存在上述三种情况之一,就必须执行 watch 函数的回调函数 cb。如果回调函数 cb 是再次执行,在执行之前需要先清除副作用。然后调用 callWithAsyncErrorHandling 函数执行回调函数cb,并将新值newValue 和旧值 oldValue 传入回调函数cb中。在回调函数cb执行后,更新旧值oldValue,避免在下一次执行回调函数cb时获取到错误的旧值。
如果是 watchEffect 函数被调用的场景,则直接执行副作用函数即可。
设置 job 的 allowRecurse 属性
设置 job 函数的 allowRecurse 属性根据是否传递回调函数 cb 来进行。这个设置非常关键,因为它可以使作业充当监听器的回调,这样调度程序就能够知道它是否允许调用自身。
// important: mark the job as a watcher callback so that scheduler knows // it is allowed to self-trigger (#1727) // 重要:让调度器任务作为侦听器的回调以至于调度器能知道它可以被允许自己派发更新 job.allowRecurse = !!cb
flush 选项指定回调函数的执行时机
在调用 watch 函数时,可以通过 options 的 flush 选项来指定回调函数的执行时机:
当 flush 的值为 sync 时,代表调度器函数是同步执行,此时直接将 job 赋值给 scheduler,这样调度器函数就会直接执行。
当 flush 的值为 post 时,代表调度函数需要将副作用函数放到一个微任务队列中,并等待 DOM 更新结束后再执行。
当 flush 的值为 pre 时,即调度器函数默认的执行方式,这时调度器会区分组件是否已经挂载。如果组件未挂载,则先执行一次调度函数,即执行回调函数cb。在组件挂载之后,将调度函数推入一个优先执行时机的队列中。
// 这里处理的是回调函数的执行时机
let scheduler: EffectScheduler if (flush === 'sync') { // 同步执行,将 job 直接赋值给调度器 scheduler = job as any // the scheduler function gets called directly } else if (flush === 'post') { // 将调度函数 job 添加到微任务队列中执行 scheduler = () => queuePostRenderEffect(job, instance && instance.suspense) } else { // default: 'pre' // 调度器函数默认的执行模式 scheduler = () => { if (!instance || instance.isMounted) { // 组件挂载后将 job 推入一个优先执行时机的队列中 queuePreFlushCb(job) } else { // with 'pre' option, the first call must happen before // the component is mounted so it is called synchronously. // 在 pre 选型中,第一次调用必须发生在组件挂载之前 // 所以这次调用是同步的 job() } } }
创建副作用函数
初始化完 getter 函数和调度器函数 scheduler 后,调用 ReactiveEffect 类来创建一个副作用函数
// 创建一个副作用函数 const effect = new ReactiveEffect(getter, scheduler)
执行副作用函数
在执行副作用函数之前,首先判断是否传入了回调函数cb,如果有传入,则根据 options 的 immediate 选项来判断是否需要立即执行回调函数cb,如果指定了immediate 选项,则立即执行 job 函数,即 watch 的回调函数会在 watch 创建时立即执行一次。如果不这样做,就需要手动调用副作用函数,将其返回值赋值给oldValue作为旧值。如下面的代码所示:
if (cb) { // 选项参数 immediate 来指定回调是否需要立即执行 if (immediate) { // 回调函数会在 watch 创建时立即执行一次 job() } else { // 手动调用副作用函数,拿到的就是旧值 oldValue = effect.run() } }
如果 options 的 flush 选项的值为 post ,需要将副作用函数放入到微任务队列中,等待组件挂载完成后再执行副作用函数。如下面的代码所示:
else if (flush === 'post') { // 在调度器函数中判断 flush 是否为 'post',如果是,将其放到微任务队列中执行 queuePostRenderEffect( effect.run.bind(effect), instance && instance.suspense ) }
其余情况都是立即执行副作用函数。如下面的代码所示:
else { // 其余情况立即首次执行副作用 effect.run() }
返回匿名函数,停止侦听
最终,doWatch函数返回了一个匿名函数,该函数用于取消对数据源的监听。因此在调用 watch 或者 watchEffect 时,可以调用其返回值类结束侦听。
return () => { effect.stop() if (instance && instance.scope) { // 返回一个函数,用以显式的结束侦听 remove(instance.scope.effects!, effect) } }
위 내용은 Vue3 Listener Watch의 구현 원리는 무엇입니까의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

핫 AI 도구

Undresser.AI Undress
사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover
사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool
무료로 이미지를 벗다

Clothoff.io
AI 옷 제거제

Video Face Swap
완전히 무료인 AI 얼굴 교환 도구를 사용하여 모든 비디오의 얼굴을 쉽게 바꾸세요!

인기 기사

뜨거운 도구

메모장++7.3.1
사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전
중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기
강력한 PHP 통합 개발 환경

드림위버 CS6
시각적 웹 개발 도구

SublimeText3 Mac 버전
신 수준의 코드 편집 소프트웨어(SublimeText3)

뜨거운 주제











스마트폰 화면에 녹색 줄이 나타나는 문제를 겪어보셨을 텐데요. 한 번도 본 적이 없더라도 인터넷에서 관련 사진을 본 적이 있을 것입니다. 그렇다면 스마트워치 화면이 하얗게 변하는 상황을 겪어보신 적 있으신가요? CNMO는 지난 4월 2일 외신을 통해 한 Reddit 사용자가 소셜 플랫폼에 삼성 워치 시리즈 스마트워치 화면이 하얗게 변하는 사진을 공유했다는 사실을 접했습니다. 해당 이용자는 "떠날 때 충전 중이었는데, 돌아올 때 이랬다. 재시작을 하려고 했는데, 삼성워치 스마트워치 화면이 하얗게 변했다"고 적었다. Reddit 사용자가 특정 모델을 지정하지 않았습니다. 하지만 사진으로 보면 삼성 워치5가 될 것 같습니다. 이전에 다른 Reddit 사용자도 보고했습니다.

tinymce는 완전한 기능을 갖춘 리치 텍스트 편집기 플러그인이지만,tinymce를 vue에 도입하는 것은 다른 Vue 리치 텍스트 플러그인만큼 원활하지 않습니다.tinymce 자체는 Vue에 적합하지 않으며 @tinymce/tinymce-vue를 도입해야 합니다. 외국 서식 있는 텍스트 플러그인이며 중국어 버전을 통과하지 못했습니다. 공식 웹사이트에서 번역 패키지를 다운로드해야 합니다(방화벽을 우회해야 할 수도 있음). 1. 관련 종속성을 설치합니다. npminstalltinymce-Snpminstall@tinymce/tinymce-vue-S2. 중국어 패키지를 다운로드합니다. 3. 프로젝트 공용 폴더에 스킨과 중국어 패키지를 새로 만들고 다운로드합니다.

vue3+vite:src는 require를 사용하여 이미지를 동적으로 가져오고 vue3+vite는 여러 이미지를 동적으로 가져옵니다. vue3을 사용하는 경우 require는 이미지를 사용할 수 없습니다. imgUrl:require(' .../assets/test.png') 와 같은 vue2는 typescript가 require를 지원하지 않기 때문에 가져오므로 이를 해결하는 방법은 다음과 같습니다. waitimport를 사용합니다.

페이지를 부분적으로 새로 고치려면 로컬 구성 요소(dom)의 다시 렌더링만 구현하면 됩니다. Vue에서 이 효과를 얻는 가장 쉬운 방법은 v-if 지시어를 사용하는 것입니다. Vue2에서는 v-if 명령을 사용하여 로컬 DOM을 다시 렌더링하는 것 외에도 새 빈 구성 요소를 만들 수도 있습니다. 로컬 페이지를 새로 고쳐야 할 경우 이 빈 구성 요소 페이지로 점프한 다음 다시 돌아올 수 있습니다. 빈 원본 페이지의 beforeRouteEnter 가드. 아래 그림과 같이 Vue3.X에서 새로 고침 버튼을 클릭하여 빨간색 상자 안에 DOM을 다시 로드하고 해당 로딩 상태를 표시하는 방법입니다. Vue3.X의 scriptsetup 구문에 있는 구성 요소의 가드에는

Vue로 블로그 프론트엔드를 구현하려면 마크다운 파싱을 구현해야 합니다. 코드가 있는 경우 코드 하이라이팅을 구현해야 합니다. markdown-it, vue-markdown-loader,marked,vue-markdown 등과 같은 Vue용 마크다운 구문 분석 라이브러리가 많이 있습니다. 이 라이브러리는 모두 매우 유사합니다. 여기서는 Marked가 사용되었고, 코드 하이라이팅 라이브러리로 하이라이트.js가 사용되었습니다. 구체적인 구현 단계는 다음과 같습니다. 1. 종속 라이브러리를 설치합니다. vue 프로젝트에서 명령 창을 열고 다음 명령 npminstallmarked-save//marked를 입력하여 markdown을 htmlnpmins로 변환합니다.

vue3+ts+axios+pinia는 무의미한 새로 고침을 실현합니다. 1. 먼저 프로젝트에서 aiXos 및 pinianpmipinia를 다운로드합니다--savenpminstallaxios--save2. AxiosResponse}from"axios";importaxiosfrom'axios';import{ElMess

머리말 Vue든 React든, 여러 개의 반복되는 코드를 접하게 되면, 파일을 중복된 코드 덩어리로 채우는 대신, 이러한 코드를 어떻게 재사용할 수 있을지 고민해 보겠습니다. 실제로 vue와 React 모두 컴포넌트를 추출하여 재사용할 수 있지만, 작은 코드 조각이 발견되어 다른 파일을 추출하고 싶지 않은 경우, 이에 비해 React는 동일한에서 사용할 수 있습니다. 파일에서 해당 위젯을 선언합니다. 또는 다음과 같은 renderfunction을 통해 구현합니다. constDemo:FC=({msg})=>{returndemomsgis{msg}}constApp:FC=()=>{return(

최종 효과는 VueCropper 컴포넌트 Yarnaddvue-cropper@next를 설치하는 것입니다. 위의 설치 값은 Vue2이거나 다른 방법을 사용하여 참조하려는 경우 공식 npm 주소: 공식 튜토리얼을 방문하세요. 컴포넌트에서 참조하고 사용하는 것도 매우 간단합니다. 여기서는 해당 컴포넌트와 해당 스타일 파일을 소개하기만 하면 됩니다. 여기서는 import{userInfoByRequest}from'../js/api만 소개하면 됩니다. 내 구성 요소 파일에서 import{VueCropper}from'vue-cropper&
