Heim > Web-Frontend > js-Tutorial > Hauptteil

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

青灯夜游
Freigeben: 2021-09-18 10:02:23
nach vorne
52059 Leute haben es durchsucht

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

In diesem Artikel werden 12 hochfrequente Vue-Prinzipien-Interviewfragen behandelt, die die Kernimplementierungsprinzipien von Vue abdecken. Tatsächlich ist es unmöglich, die Implementierungsprinzipien eines Frameworks in einem Artikel zu erklären. Ich hoffe, dass diese Frage den Lesern durch diese 12 ein gewisses Verständnis ihrer eigenen Vue-Beherrschung (Nummer B) ermöglicht, um ihre eigenen Mängel auszugleichen und Vue besser zu beherrschen.

[Verwandte Empfehlungen: Vue-Interviewfragen(2021)]

1. Vue-Reaktionsfähigkeitsprinzip

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

Kernimplementierungsklasse:

Beobachter: seine Funktion dient dazu, Getter und Setter zu den Eigenschaften des Objekts hinzuzufügen, um Abhängigkeiten zu sammeln und Aktualisierungen zu versenden.

Dep: wird zum Sammeln von Abhängigkeiten des aktuellen reaktionsfähigen Objekts verwendet. Jedes reaktionsfähige Objekt, einschließlich Unterobjekten, verfügt über eine Dep-Instanz. (subs darin ist ein Array von Watcher-Instanzen). Wenn sich die Daten ändern, wird jeder Watcher über dep.notify() benachrichtigt.

Watcher: Beobachterobjekt, die Instanz ist in drei Typen unterteilt: Rendering-Watcher (Render-Watcher), berechneter Attribut-Watcher (berechneter Watcher), Listener-Watcher (Benutzer-Watcher)

Watcher und Dep Dep wird in der Beziehung

watcher instanziiert und fügt Abonnenten zu dep.subs hinzu. dep durchläuft dep.subs durch notify, um jedes Watcher-Update zu benachrichtigen.

Abhängigkeitssammlung

  1. InitState, wenn das berechnete Attribut initialisiert wird, wird der berechnete Watcher ausgelöst.
  2. InitState, wenn das Listenattribut initialisiert wird Der Benutzer-Watcher wird ausgelöst. Der Prozess der Abhängigkeitssammlung
  3. render() löst den Render-Watcher
  4. erneut aus. Wenn die Abhängigkeitssammlung
neu gerendert wird, wird vm.render() erneut ausgeführt Watcher-Abonnements in Subs und erneutes Rendern.

    Updates verteilen
  1. Die Antwortdaten werden in der Komponente geändert und die Logik zum Auslösen des Setters
  2. ruft dep.notify() auf
Durchlaufen Sie alle Subs (Watcher-Instanzen) und rufen Sie die Aktualisierungsmethode jedes Watchers auf.

Prinzip

Beim Erstellen einer Vue-Instanz durchläuft Vue die Eigenschaften der Datenoption und verwendet Object.defineProperty, um den Eigenschaften Getter und Setter hinzuzufügen, um deren Lesen zu kapern Daten (Getter werden verwendet (um Abhängigkeiten zu sammeln, Setter werden zum Versenden von Aktualisierungen verwendet) und Abhängigkeiten intern verfolgen und Änderungen benachrichtigen, wenn auf Eigenschaften zugegriffen und diese geändert werden.

Jede Komponenteninstanz verfügt über eine entsprechende Watcher-Instanz, die alle Datenattribute von Abhängigkeiten während des Komponenten-Rendering-Prozesses aufzeichnet (Abhängigkeitssammlung, berechneter Watcher, Benutzer-Watcher-Instanzen), und dann werden die Abhängigkeiten geändert Da die Watcher-Instanz von diesen Daten abhängig ist, benachrichtigt die Setter-Methode die Watcher-Instanz zur Neuberechnung (Versand von Aktualisierungen) und rendert dadurch die zugehörige Komponente neu.

Zusammenfassung in einem Satz:

vue.js verwendet Daten-Hijacking in Kombination mit dem Publish-Subscribe-Modus, kapert die Setter und Getter jeder Eigenschaft über Object.defineproperty und veröffentlicht Nachrichten an Abonnenten, wenn die Datenänderungen. , Auslösen des Antwort-Listening-Callbacks 2. Das Implementierungsprinzip von Computed

Computed ist im Wesentlichen ein Lazy-Evaluation-Beobachter.

computed implementiert intern einen Lazy Watcher, d. h. der Computed Watcher wertet nicht sofort aus und enthält eine Dep-Instanz.

Intern wird das Attribut this.dirty verwendet, um zu markieren, ob die berechnete Eigenschaft neu bewertet werden muss.

Wenn sich der Abhängigkeitsstatus der berechneten Änderungen ändert, wird dieser Lazy Watcher benachrichtigt.

Der berechnete Watcher bestimmt, ob über this.dep.subs.length Abonnenten vorhanden sind. Wenn Ja, es wird neu berechnet und dann werden die neuen und alten Werte verglichen. Wenn es sich ändert, wird es neu gerendert. (

Vue möchte sicherstellen, dass sich nicht nur der Wert ändert, von dem die berechnete Eigenschaft abhängt, sondern auch, dass der Rendering-Beobachter zum erneuten Rendern veranlasst wird, wenn sich der endgültig berechnete Wert der berechneten Eigenschaft ändert, was im Wesentlichen eine Optimierung darstellt.

)Wenn nicht, setzen Sie einfach this.dirty = true. (

Wenn ein berechnetes Attribut von anderen Daten abhängt, wird das Attribut nicht sofort neu berechnet. Es wird nur dann tatsächlich berechnet, wenn andere Stellen das Attribut später lesen müssen, d. h. es weist verzögerte (verzögerte Berechnungs-)Eigenschaften auf.

)3. Was ist der Unterschied zwischen berechnet und beobachtet und ihre Anwendungsszenarien?

Unterschied

berechnet Berechnetes Attribut: hängt von anderen Attributwerten ab und der berechnete Wert wird nur dann zwischengespeichert, wenn sich der Attributwert ändert, von dem er abhängt, und der berechnete Wert wird beim nächsten Abrufen des berechneten Werts neu berechnet. Zuhörer beobachten: eher eine „Beobachtungs“-Rolle,

kein Caching

, ähnlich dem Überwachungsrückruf bestimmter Daten, der Rückruf wird immer dann ausgeführt, wenn sich die überwachten Daten ändern. Verfolgen.

Anwendungsszenarien

Anwendungsszenarien:

Wenn wir numerische Berechnungen durchführen müssen und auf andere Daten angewiesen sind, sollte „Computed“ verwendet werden, da die Cache-Funktion von „Computed“ genutzt werden kann vermeiden Jedes Mal, wenn ein Wert erhalten wird, wird er neu berechnet.

Watch sollte verwendet werden, wenn wir asynchrone oder teure Vorgänge ausführen müssen, wenn sich die Daten ändern. Mithilfe der Watch-Option können wir einen asynchronen Vorgang ausführen (auf eine API zugreifen) und die Häufigkeit und den Zeitpunkt der Ausführung des Vorgangs begrenzen Wir bekommen Bevor wir das Endergebnis erhalten, legen wir den Zwischenzustand fest. Dies sind Dinge, die berechnete Eigenschaften nicht leisten können.

4. Warum wird Proxy in Vue3.0 übernommen und Object.defineProperty aufgegeben?

Object.defineProperty selbst verfügt über eine gewisse Fähigkeit, Änderungen in Array-Indizes zu überwachen, aber in Vue wurde diese Funktion unter Berücksichtigung der Kosteneffizienz von Leistung/Erfahrung aufgegeben ( Warum kann Vue keine Array-Änderungen erkennen ). Um dieses Problem zu lösen, können Sie nach der internen Vue-Verarbeitung die folgenden Methoden verwenden, um das Array zu überwachen:
push();
pop();
shift();
unshift();
splice();
sort();
reverse();
Nach dem Login kopieren

Da nur die oben genannten 7 Methoden gehackt werden, können die Attribute anderer Arrays immer noch nicht erkannt werden gewisse Einschränkungen.

Object.defineProperty kann nur die Eigenschaften eines Objekts kapern, daher müssen wir jede Eigenschaft jedes Objekts durchlaufen. In Vue 2.x wird die Datenüberwachung durch Rekursion + Durchquerung des Datenobjekts erreicht. Wenn der Attributwert auch ein Objekt ist, ist eine tiefe Durchquerung erforderlich, wenn ein vollständiges Objekt gekapert werden kann.

Proxy kann das gesamte Objekt kapern und ein neues Objekt zurückgeben. Proxy kann nicht nur Proxy-Objekte, sondern auch Proxy-Arrays vertreten. Sie können auch dynamisch hinzugefügte Attribute als Proxy verwenden.

5. Wozu dienen Schlüssel in Vue?

Schlüssel ist die eindeutige ID, die jedem V-Knoten zugewiesen wird. Abhängig vom Schlüssel kann unsere Diff-Operation genauer und schneller sein (Diff-Knoten sind für die einfache Darstellung von Listenseiten auch schneller, aber dort Es wird einige versteckte Nebenwirkungen geben, z. B. dass der Übergangseffekt möglicherweise nicht erzeugt wird oder eine Zustandsfehlausrichtung auftritt, wenn einige Knoten gebundene Daten-(Formular-)Zustände haben.)

Der Diff-Algorithmus wird zuerst durchgeführt Wenn es keine Übereinstimmung gibt, wird der Schlüssel des neuen Knotens mit dem alten Knoten verglichen, um den entsprechenden alten Knoten zu finden.

ist genauer: Da der Schlüssel nicht wiederverwendet wird An Ort und Stelle kann die direkte Wiederverwendung im Vergleich der sameNode-Funktion a.key === b.key vermieden werden. Daher ist es genauer, wenn der Schlüssel nicht hinzugefügt wird, der Status des vorherigen Knotens beibehalten wird, was zu einer Reihe von Fehlern führt.

Schneller: Die Eindeutigkeit des Schlüssels kann von der Kartendatenstruktur vollständig genutzt werden. Im Vergleich zur Zeitkomplexität der Durchlaufsuche beträgt die Zeitkomplexität von Map nur O(1). ist wie folgt:

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;
}
Nach dem Login kopieren

6. Sprechen wir über das Prinzip von nextTick

JS-Betriebsmechanismus

JS-Ausführung erfolgt Single-Threaded und basiert auf einer Ereignisschleife. Die Ereignisschleife ist grob in die folgenden Schritte unterteilt:

  1. Alle Synchronisierungsaufgaben werden im Hauptthread ausgeführt und bilden einen Ausführungskontextstapel.
  2. Neben dem Hauptthread gibt es auch eine „Aufgabenwarteschlange“. Solange die asynchrone Aufgabe laufende Ergebnisse hat, wird ein Ereignis in die „Aufgabenwarteschlange“ gestellt.
  3. Sobald alle Synchronisierungsaufgaben im „Ausführungsstapel“ ausgeführt wurden, liest das System die „Aufgabenwarteschlange“, um zu sehen, welche Ereignisse darin enthalten sind. Diese entsprechenden asynchronen Aufgaben beenden den Wartezustand, betreten den Ausführungsstapel und beginnen mit der Ausführung.
  4. Der Hauptthread wiederholt den dritten Schritt oben immer wieder.

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

Der Ausführungsprozess des Hauptthreads dauert einen Tick, und alle asynchronen Ergebnisse werden über die „Aufgabenwarteschlange“ geplant. Die Nachrichtenwarteschlange speichert Aufgaben einzeln. Die Spezifikation legt fest, dass Aufgaben in zwei Kategorien unterteilt werden, nämlich Makroaufgaben und Mikroaufgaben, und dass nach dem Ende jeder Makroaufgabe alle Mikroaufgaben gelöscht werden müssen.

for (macroTask of macroTaskQueue) {
  // 1. Handle current MACRO-TASK
  handleMacroTask();

  // 2. Handle all MICRO-TASK
  for (microTask of microTaskQueue) {
    handleMicroTask(microTask);
  }
}
Nach dem Login kopieren

In der Browserumgebung:

Zu den allgemeinen Makroaufgaben gehören setTimeout, MessageChannel, postMessage, setImmediate

Zu den häufigen Mikroaufgaben gehören MutationObsever und Promise.then

Asynchrone Update-Warteschlange

Vielleicht haben Sie es noch nicht bemerkt, wenn Vue das DOM aktualisiert, ist esAsynchron ausgeführt. Solange Vue auf Datenänderungen wartet, öffnet es eine Warteschlange und puffert alle Datenänderungen, die in derselben Ereignisschleife auftreten.

Wenn derselbe Watcher mehrmals ausgelöst wird, wird er nur einmal in die Warteschlange verschoben. Diese Deduplizierung während der Pufferung ist wichtig, um unnötige Berechnungen und DOM-Operationen zu vermeiden.

Dann leert Vue beim nächsten „Tick“ der Ereignisschleife die Warteschlange und führt die eigentliche (deduplizierte) Arbeit aus.

Vue versucht intern, natives Promise.then, MutationObserver und setImmediate für asynchrone Warteschlangen zu verwenden. Wenn die Ausführungsumgebung dies nicht unterstützt, wird stattdessen setTimeout(fn, 0) verwendet.

Im Quellcode von vue2.5 lauten die Makrotask-Downgrade-Lösungen: setImmediate, MessageChannel, setTimeout

vue 的 nextTick 方法的实现原理:

  1. vue 用异步队列的方式来控制 DOM 更新和 nextTick 回调先后执行
  2. microtask 因为其高优先级特性,能确保队列中的微任务在一次事件循环前被执行完毕
  3. 考虑兼容问题,vue 做了 microtask 向 macrotask 的降级方案

7. vue 是如何对数组方法进行变异的 ?

我们先来看看源码

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]);
  }
};
Nach dem Login kopieren

简单来说,Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的ob,也就是它的 Observer 对象,如果有新的值,就调用 observeArray 对新的值进行监听,然后手动调用 notify,通知 render watcher,执行 update

8. Vue 组件 data 为什么必须是函数 ?

new Vue()实例中,data 可以直接是一个对象,为什么在 vue 组件中,data 必须是一个函数呢?

因为组件是可以复用的,JS 里对象是引用关系,如果组件 data 是一个对象,那么子组件中的 data 属性值会互相污染,产生副作用。

所以一个组件的 data 选项必须是一个函数,因此每个实例可以维护一份被返回对象的独立的拷贝。new Vue 的实例是不会被复用的,因此不存在以上问题。

9. 谈谈 Vue 事件机制,手写$on,$off,$emit,$once

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;
  }
}
Nach dem Login kopieren

10. 说说 Vue 的渲染过程

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 函数解析 template,生成 ast(抽象语法树)
  • optimize 函数优化静态节点 (标记不需要每次都更新的内容,diff 算法会直接跳过静态节点,从而减少比较的过程,优化了 patch 的性能)
  • generate 函数生成 render 函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

11. 聊聊 keep-alive 的实现原理和缓存策略

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]);
  }
};
Nach dem Login kopieren

原理

  1. 获取 keep-alive 包裹着的第一个子组件对象及其组件名
  2. 根据设定的 include/exclude(如果有)进行条件匹配,决定是否缓存。不匹配,直接返回组件实例
  3. 根据组件 ID 和 tag 生成缓存 Key,并在缓存对象中查找是否已缓存过该组件实例。如果存在,直接取出缓存值并更新该 key 在 this.keys 中的位置(更新 key 的位置是实现 LRU 置换策略的关键)
  4. 在 this.cache 对象中存储该组件实例并保存 key 值,之后检查缓存的实例数量是否超过 max 的设置值,超过则根据 LRU 置换策略删除最近最久未使用的实例(即是下标为 0 的那个 key)
  5. 最后组件实例的 keepAlive 属性设置为 true,这个在渲染和执行被包裹组件的钩子函数会用到,这里不细说

LRU 缓存淘汰算法

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。

12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse)

keep-alive 的实现正是用到了 LRU 策略,将最近访问的组件 push 到 this.keys 最后面,this.keys[0]也就是最久没被访问的组件,当缓存实例超过 max 设置值,删除 this.keys[0]

12. vm.$set()实现原理是什么?

受现代 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;
}
Nach dem Login kopieren
  1. 如果目标是数组,使用 vue 实现的变异方法 splice 实现响应式
  2. 如果目标是对象,判断属性存在,即为响应式,直接赋值
  3. 如果 target 本身就不是响应式,直接赋值
  4. 如果属性不是响应式,则调用 defineReactive 方法进行响应式处理

本文转载自:https://segmentfault.com/a/1190000021407782

推荐教程:《JavaScript视频教程

Das obige ist der detaillierte Inhalt von12 Vue-Interviewfragen zum Hochfrequenzprinzip (mit Analyse). Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!

Verwandte Etiketten:
Quelle:segmentfault.com
Erklärung dieser Website
Der Inhalt dieses Artikels wird freiwillig von Internetnutzern beigesteuert und das Urheberrecht liegt beim ursprünglichen Autor. Diese Website übernimmt keine entsprechende rechtliche Verantwortung. Wenn Sie Inhalte finden, bei denen der Verdacht eines Plagiats oder einer Rechtsverletzung besteht, wenden Sie sich bitte an admin@php.cn
Beliebte Tutorials
Mehr>
Neueste Downloads
Mehr>
Web-Effekte
Quellcode der Website
Website-Materialien
Frontend-Vorlage