이 기사에서는 Vue의 핵심 구현 원칙을 다루는 12개의 고빈도 Vue 원칙 인터뷰 질문을 공유합니다. 실제로 프레임워크의 구현 원칙을 하나의 기사로 마무리하는 것은 불가능합니다. 독자는 자신의 단점을 보완하고 Vue를 더 잘 마스터할 수 있도록 Vue 숙달(B 번호)에 대해 어느 정도 이해해야 합니다.
[관련 권장사항: vue 인터뷰 질문(2021)]
관찰자: 그 기능 종속성 수집 및 업데이트 전달을 위해 개체의 속성에 getter 및 setter를 추가하는 것입니다.
Dep: 현재 반응형 개체의 종속성을 수집하는 데 사용됩니다. 하위 개체를 포함한 각 반응형 개체에는 Dep 인스턴스가 있습니다(내부 하위는 Watcher 인스턴스입니다). array), 데이터가 변경되면 dep.notify()를 통해 각 watcher에게 알림이 전달됩니다.
Watcher: Observer 객체, 인스턴스는 렌더링 감시자(render watcher), 계산된 속성 감시자(computed watcher), 청취자 감시자(user watcher)의 세 가지 유형으로 나뉩니다.
watcher는 dep를 인스턴스화합니다. dep.subs에 구독자를 추가했고, dep는 노티파이를 통해 dep.subs를 순회하여 각 감시자 업데이트를 알렸습니다.
Vue 인스턴스를 생성할 때 Vue는 데이터 옵션의 속성을 순회하고 Object.defineProperty를 사용하여 속성에 getter 및 setter를 추가하여 데이터 읽기를 하이재킹합니다(getter는 컬렉션에 의존하는 데 사용됩니다. 업데이트를 전달하는 데 사용되며 내부적으로 종속성을 추적하여 속성에 액세스하고 수정될 때 변경 사항을 알립니다.
각 구성 요소 인스턴스에는 구성 요소 렌더링 프로세스 중 종속성의 모든 데이터 속성(종속성 수집, 계산된 감시자 및 사용자 감시자 인스턴스)을 기록하는 해당 감시자 인스턴스가 있습니다. setter 메소드 이 데이터에 의존하는 감시자 인스턴스는 다시 계산(업데이트 디스패치)하라는 알림을 받고 관련 구성 요소를 다시 렌더링합니다.
한 문장으로 요약:
vue.js는 게시-구독 모드와 결합된 데이터 하이재킹을 사용하고, Object.defineproperty를 통해 각 속성의 setter 및 getter를 하이재킹하고, 데이터가 변경되면 구독자에게 메시지를 게시하고, 응답 모니터링 콜백을 트리거합니다
computed의 구현 원리는 본질적으로 게으른 평가 관찰자입니다.
computed는 게으른 감시자, 즉 계산된 감시자를 내부적으로 구현합니다. 계산된 감시자는 즉시 평가하지 않으며 dep 인스턴스도 보유합니다.
내부적으로 this.dirty 속성을 사용하여 계산된 속성을 재평가해야 하는지 여부를 표시합니다.
계산된 종속성 상태가 변경되면 이 게으른 감시자가 알림을 받게 됩니다.
계산된 감시자는 this.dep.subs.length를 통해 구독자가 있는지 여부를 판단합니다.
있는 경우 새 항목과 구독자를 다시 계산하여 비교합니다. 값이 변경되면 다시 렌더링됩니다. (Vue는 계산된 속성이 변경에 따라 달라지는 값뿐만 아니라 계산된 속성의 최종 계산된 값이 변경되면 렌더링 감시자가 다시 렌더링되도록 트리거되는 것을 보장하고 싶어하는데, 이는 본질적으로 최적화입니다. )
그렇지 않다면 그냥 this.dirty = true라고 입력하세요. (계산된 속성이 다른 데이터에 의존하는 경우 해당 속성은 즉시 다시 계산되지 않습니다. 나중에 다른 곳에서 해당 속성을 읽어야 하는 경우에만 실제로 계산됩니다. 즉, 게으른(lazy Calculation) 특성이 있습니다. )
애플리케이션 시나리오
애플리케이션 시나리오: 수치 계산을 수행해야 하고 다른 데이터에 의존해야 하는 경우 계산을 사용해야 합니다. 왜냐하면 계산의 캐시 기능을 사용하면 값을 얻을 때마다 재계산을 피할 수 있기 때문입니다.
Watch는 데이터가 변경될 때 비동기식 또는 비용이 많이 드는 작업을 수행해야 할 때 사용해야 합니다. watch 옵션을 사용하면 비동기 작업(API 액세스)을 수행하고 작업 수행 빈도를 제한하며 최종 결과를 얻을 때까지 기다릴 수 있습니다. 결과 이전에 중간 상태를 설정합니다. 이는 계산된 속성이 수행할 수 없는 작업입니다.
Object.defineProperty 자체에는 배열 첨자 변경 사항을 모니터링하는 특정 기능이 있지만 Vue에서는 성능/경험 비용 효율성 측면에서 Youda가 이 기능을 포기했습니다. (Vue에서 배열 변경 사항을 감지할 수 없는 이유 ) . 이 문제를 해결하기 위해 Vue 내부 처리 후 다음 방법을 사용하여 배열을 모니터링할 수 있습니다
push(); pop(); shift(); unshift(); splice(); sort(); reverse();
위 7가지 방법만 해킹되었기 때문에 다른 배열의 속성을 감지할 수 없으며 여전히 일정한 제한이 있습니다. .
Object.defineProperty는 객체의 속성만 하이재킹할 수 있으므로 각 객체의 각 속성을 순회해야 합니다. Vue 2.x에서는 데이터 객체의 재귀 + 순회를 통해 데이터 모니터링이 이루어집니다. 속성 값도 객체라면 심층 탐색이 필요합니다. 물론 완전한 객체를 탈취할 수 있다면 더 나은 선택입니다.프록시는 전체 개체를 하이재킹하고 새 개체를 반환할 수 있습니다. 프록시는 객체뿐만 아니라 프록시 배열도 프록시할 수 있습니다. 동적으로 추가된 속성을 프록시할 수도 있습니다.
key는 각 vnode에 부여되는 고유 ID입니다. 키에 따라 diff 작업이 더 정확하고 빨라질 수 있습니다. diff 노드는 간단한 목록 페이지 렌더링의 경우에도 더 빠르지만 몇 가지 숨겨진 부작용이 발생합니다. 등 전환 효과가 없거나 일부 노드에 바인딩된 데이터(형태) 상태가 있을 수 있으며 상태 불일치가 발생할 수 있습니다.)
diff 알고리즘 과정에서 이전과의 정면 교차 비교가 수행됩니다. 새 노드가 먼저 수행되고 일치하는 항목이 없으면 새 노드의 키를 이전 노드와 비교하여 해당 이전 노드를 찾습니다.
더 정확합니다. 키가 제자리에서 재사용되지 않기 때문입니다. sameNode 함수에서 비교할 수 있습니다. a.key === b.key 내부 재사용을 피하세요. 따라서 키를 추가하지 않으면 이전 노드의 상태가 유지되므로 일련의 버그가 발생합니다.
빠름: Map 데이터 구조를 통해 키의 고유성을 최대한 활용할 수 있습니다. 순회 검색의 시간 복잡도 O(n)에 비해 Map의 시간 복잡도는 O(1)에 불과합니다.
function createKeyToOldIdx(children, beginIdx, endIdx) { let i, key; const map = {}; for (i = beginIdx; i <= endIdx; ++i) { key = children[i].key; if (isDef(key)) map[key] = i; } return map; }
JS 실행 메커니즘
JS 실행은 단일 스레드이며 이벤트 루프를 기반으로 합니다. 이벤트 루프는 대략 다음 단계로 나뉩니다.
메인 스레드의 실행 프로세스는 1틱이며 모든 비동기 결과는 "작업 대기열"을 통해 예약됩니다. 메시지 큐는 작업을 하나씩 저장합니다. 사양에서는 작업을 매크로 작업과 마이크로 작업이라는 두 가지 범주로 나누고 각 매크로 작업이 끝나면 모든 마이크로 작업을 지워야 한다고 규정합니다.
for (macroTask of macroTaskQueue) { // 1. Handle current MACRO-TASK handleMacroTask(); // 2. Handle all MICRO-TASK for (microTask of microTaskQueue) { handleMicroTask(microTask); } }
브라우저 환경:
공통 매크로 작업에는 setTimeout, MessageChannel, postMessage, setImmediate
공통 마이크로 작업에는 MutationObsever 및 Promise.then
비동기 업데이트 대기열
이 포함됩니다.어쩌면 당신은 천국일지도 모릅니다 DOM을 업데이트할 때 Vue가 비동기적으로 실행된다는 사실을 알지 못했습니다. 데이터 변경 사항을 수신하는 한 Vue는 대기열을 열고 동일한 이벤트 루프에서 발생하는 모든 데이터 변경 사항을 버퍼링합니다.
동일한 감시자가 여러 번 실행되면 대기열에 한 번만 푸시됩니다. 버퍼링 중 이러한 중복 제거는 불필요한 계산 및 DOM 작업을 방지하는 데 중요합니다.
그런 다음 다음 이벤트 루프 "tick"에서 Vue는 대기열을 플러시하고 실제(중복 제거된) 작업을 수행합니다.
Vue는 내부적으로 비동기 대기열에 대해 기본 Promise.then, MutationObserver 및 setImmediate를 사용하려고 시도합니다. 실행 환경이 이를 지원하지 않으면 setTimeout(fn, 0)이 대신 사용됩니다.
vue2.5의 소스 코드에서 매크로태스크 다운그레이드 계획은 다음과 같습니다: setImmediate, MessageChannel, setTimeout
vue 的 nextTick 方法的实现原理:
我们先来看看源码
const arrayProto = Array.prototype; export const arrayMethods = Object.create(arrayProto); const methodsToPatch = [ "push", "pop", "shift", "unshift", "splice", "sort", "reverse" ]; /** * Intercept mutating methods and emit events */ methodsToPatch.forEach(function(method) { // cache original method const original = arrayProto[method]; def(arrayMethods, method, function mutator(...args) { const result = original.apply(this, args); const ob = this.__ob__; let inserted; switch (method) { case "push": case "unshift": inserted = args; break; case "splice": inserted = args.slice(2); break; } if (inserted) ob.observeArray(inserted); // notify change ob.dep.notify(); return result; }); }); /** * Observe a list of Array items. */ Observer.prototype.observeArray = function observeArray(items) { for (var i = 0, l = items.length; i < l; i++) { observe(items[i]); } };
简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update
new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?
因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。
所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。
Vue 事件机制 本质上就是 一个 发布-订阅 模式的实现。
class Vue { constructor() { // 事件通道调度中心 this._events = Object.create(null); } $on(event, fn) { if (Array.isArray(event)) { event.map(item => { this.$on(item, fn); }); } else { (this._events[event] || (this._events[event] = [])).push(fn); } return this; } $once(event, fn) { function on() { this.$off(event, on); fn.apply(this, arguments); } on.fn = fn; this.$on(event, on); return this; } $off(event, fn) { if (!arguments.length) { this._events = Object.create(null); return this; } if (Array.isArray(event)) { event.map(item => { this.$off(item, fn); }); return this; } const cbs = this._events[event]; if (!cbs) { return this; } if (!fn) { this._events[event] = null; return this; } let cb; let i = cbs.length; while (i--) { cb = cbs[i]; if (cb === fn || cb.fn === fn) { cbs.splice(i, 1); break; } } return this; } $emit(event) { let cbs = this._events[event]; if (cbs) { const args = [].slice.call(arguments, 1); cbs.map(item => { args ? item.apply(this, args) : item.call(this); }); } return this; } }
export default { name: "keep-alive", abstract: true, // 抽象组件属性 ,它在组件实例建立父子关系的时候会被忽略,发生在 initLifecycle 的过程中 props: { include: patternTypes, // 被缓存组件 exclude: patternTypes, // 不被缓存组件 max: [String, Number] // 指定缓存大小 }, created() { this.cache = Object.create(null); // 缓存 this.keys = []; // 缓存的VNode的键 }, destroyed() { for (const key in this.cache) { // 删除所有缓存 pruneCacheEntry(this.cache, key, this.keys); } }, mounted() { // 监听缓存/不缓存组件 this.$watch("include", val => { pruneCache(this, name => matches(val, name)); }); this.$watch("exclude", val => { pruneCache(this, name => !matches(val, name)); }); }, render() { // 获取第一个子元素的 vnode const slot = this.$slots.default; const vnode: VNode = getFirstComponentChild(slot); const componentOptions: ?VNodeComponentOptions = vnode && vnode.componentOptions; if (componentOptions) { // name不在inlcude中或者在exlude中 直接返回vnode // check pattern const name: ?string = getComponentName(componentOptions); const { include, exclude } = this; if ( // not included (include && (!name || !matches(include, name))) || // excluded (exclude && name && matches(exclude, name)) ) { return vnode; } const { cache, keys } = this; // 获取键,优先获取组件的name字段,否则是组件的tag const key: ?string = vnode.key == null ? // same constructor may get registered as different local components // so cid alone is not enough (#3269) componentOptions.Ctor.cid + (componentOptions.tag ? `::${componentOptions.tag}` : "") : vnode.key; // 命中缓存,直接从缓存拿vnode 的组件实例,并且重新调整了 key 的顺序放在了最后一个 if (cache[key]) { vnode.componentInstance = cache[key].componentInstance; // make current key freshest remove(keys, key); keys.push(key); } // 不命中缓存,把 vnode 设置进缓存 else { cache[key] = vnode; keys.push(key); // prune oldest entry // 如果配置了 max 并且缓存的长度超过了 this.max,还要从缓存中删除第一个 if (this.max && keys.length > parseInt(this.max)) { pruneCacheEntry(cache, keys[0], keys, this._vnode); } } // keepAlive标记位 vnode.data.keepAlive = true; } return vnode || (slot && slot[0]); } };
原理
LRU 缓存淘汰算法
LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。
keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]
受现代 JavaScript 的限制 (而且 Object.observe 也已经被废弃),Vue 无法检测到对象属性的添加或删除。
由于 Vue 会在初始化实例时对属性执行 getter/setter 转化,所以属性必须在 data 对象上存在才能让 Vue 将它转换为响应式的。
对于已经创建的实例,Vue 不允许动态添加根级别的响应式属性。但是,可以使用 Vue.set(object, propertyName, value) 方法向嵌套对象添加响应式属性。
那么 Vue 内部是如何解决对象新增属性不能响应的问题的呢?
export function set(target: Array<any> | Object, key: any, val: any): any { // target 为数组 if (Array.isArray(target) && isValidArrayIndex(key)) { // 修改数组的长度, 避免索引>数组长度导致splice()执行有误 target.length = Math.max(target.length, key); // 利用数组的splice变异方法触发响应式 target.splice(key, 1, val); return val; } // target为对象, key在target或者target.prototype上 且必须不能在 Object.prototype 上,直接赋值 if (key in target && !(key in Object.prototype)) { target[key] = val; return val; } // 以上都不成立, 即开始给target创建一个全新的属性 // 获取Observer实例 const ob = (target: any).__ob__; // target 本身就不是响应式数据, 直接赋值 if (!ob) { target[key] = val; return val; } // 进行响应式处理 defineReactive(ob.value, key, val); ob.dep.notify(); return val; }
本文转载自:https://segmentfault.com/a/1190000021407782
推荐教程:《JavaScript视频教程》
위 내용은 12 Vue 고주파 원리 면접 질문(분석 포함)의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!