Некоторое время назад я опубликовал похожую статью про React, где с помощью пары строк кода мы создали крошечный клон React.js с нуля. Но React — далеко не единственный инструмент в современном фронтенд-мире, Vue.js стремительно набирает популярность. Давайте разберемся, как работает этот фреймворк, и создадим примитивный клон, похожий на Vue.js, в образовательных целях.
Как и React.js, Vue является реактивным, то есть все изменения в состоянии приложения автоматически отражаются в DOM. Но в отличие от React, Vue отслеживает зависимости во время рендеринга и обновляет только связанные части без каких-либо «сравнений».
Ключ к реактивности Vue.js — это метод
С помощью этого мы можем определять, когда происходит доступ к определенному свойству, или когда оно изменяется, а затем повторно оценивать все зависимые выражения после изменения свойства.
Vue.js позволяет привязать выражение JavaScript к атрибуту узла DOM с помощью директивы. Например,
Самый простой подход к оценке строк, таких как
Это немного безопаснее, чем нативный
Теперь мы можем использовать
Теперь мы можем оценивать произвольные выражения и отслеживать, какие выражения оценивать при изменении одной конкретной переменной данных. Осталось только назначить выражения определенным свойствам узла DOM и фактически изменить их при изменении данных.
Как и в Vue.js, мы будем использовать специальные атрибуты, такие как
Вот неполный список возможных поддерживаемых директив:
Каждая директива — это функция, которая принимает узел DOM, имя необязательного параметра для таких случаев, как
Теперь у нас есть все строительные блоки, пора всё склеить вместе!
Реактивный, подобный Vue.js фреймворк во всей красе. Насколько он полезен? Вот пример:
Нажатие на одну кнопку увеличивает счетчик и автоматически обновляет содержимое
Как видите, Vue.js, на первый взгляд, кажется магией, но внутри он очень прост, а основные функции могут быть реализованы всего в нескольких строках кода.
Если вам интересно узнать о Vue.js больше — попробуйте реализовать «q-if» для переключения видимости элементов на основе выражения или «q-each» для привязки списков повторяющихся дочерних элементов (это будет хорошим упражнением).
Полные исходники нанофреймворка Q находятся на Github. Не стесняйтесь контрибьютить, если вы заметили проблему или хотите предложить улучшение!
В заключение я должен упомянуть, что
Я оставляю оба Proxy / Reflect в качестве упражнения для читателя, так что, кто бы ни сделал пулл-реквест для правильного использования их в Q — я буду рад совместить это. Удачи!
Надеюсь, вам понравилась статья. Вы можете следить за новостями и делиться предложениями в Github, Twitter или подписываться через rss.
Реактивность
Как и React.js, Vue является реактивным, то есть все изменения в состоянии приложения автоматически отражаются в DOM. Но в отличие от React, Vue отслеживает зависимости во время рендеринга и обновляет только связанные части без каких-либо «сравнений».
Ключ к реактивности Vue.js — это метод
Object.defineProperty. Он позволяет указывать настраиваемый метод getter / setter для поля объекта и перехватывать каждый доступ к нему:const obj = {a: 1}; Object.defineProperty(obj, 'a', { get() { return 42; }, set(val) { console.log('you want to set "a" to', val); } }); console.log(obj.a); // prints '42' obj.a = 100; // prints 'you want to set "a" to 100'
С помощью этого мы можем определять, когда происходит доступ к определенному свойству, или когда оно изменяется, а затем повторно оценивать все зависимые выражения после изменения свойства.
Выражения
Vue.js позволяет привязать выражение JavaScript к атрибуту узла DOM с помощью директивы. Например,
<div v-text="s.toUpperCase()"></div> установит текст внутри div’а в значение переменной s в верхнем регистре.Самый простой подход к оценке строк, таких как
s.toUpperCase(), использовать eval(). Хотя eval никогда не считался безопасным решением, мы можем попробовать сделать его немного лучше, обернув в функцию и передав специальный глобальный контекст:const call = (expr, ctx) => new Function(`with(this){${`return ${expr}`}}`).bind(ctx)(); call('2+3', null); // returns 5 call('a+1', {a:42}); // returns 43 call('s.toUpperCase()', {s:'hello'}); // returns "HELLO"
Это немного безопаснее, чем нативный
eval, и его достаточно для простого фреймворка, который мы создаем. Прокси
Теперь мы можем использовать
Object.defineProperty, чтобы обернуть каждое свойство объекта данных; можно использовать call() для оценки произвольных выражений и для того, чтобы сказать, к каким свойствам прямо или косвенно обращалось выражение. Мы также должны суметь определить, когда выражение должно быть повторно вычислено, потому что одна из его переменных изменилась:const data = {a: 1, b: 2, c: 3, d: 'foo'}; // Data model const vars = {}; // List of variables used by expression // Wrap data fields into a proxy that monitors all access for (const name in data) { let prop = data[name]; Object.defineProperty(data, name, { get() { vars[name] = true; // variable has been accessed return prop; }, set(val) { prop = val; if (vars[name]) { console.log('Re-evaluate:', name, 'changed'); } } }); } // Call our expression call('(a+c)*2', data); console.log(vars); // {"a": true, "c": true} -- these two variables have been accessed data.a = 5; // Prints "Re-evaluate: a changed" data.b = 7; // Prints nothing, this variable does not affect the expression data.c = 11; // Prints "Re-evaluate: c changed" data.d = 13; // Prints nothing.
Директивы
Теперь мы можем оценивать произвольные выражения и отслеживать, какие выражения оценивать при изменении одной конкретной переменной данных. Осталось только назначить выражения определенным свойствам узла DOM и фактически изменить их при изменении данных.
Как и в Vue.js, мы будем использовать специальные атрибуты, такие как
q-on:click для связывания обработчиков событий, q-text для привязки textContent, q-bind:style для привязки стиля CSS и так далее. Я использую здесь префикс «q-», потому что «q» похоже на «vue».Вот неполный список возможных поддерживаемых директив:
const directives = { // Bind innerText to an expression value text: (el, _, val, ctx) => (el.innerText = call(val, ctx)), // Bind event listener on: (el, name, val, ctx) => (el[`on${name}`] = () => call(val, ctx)), // Bind node attribute to an expression value bind: (el, name, value, ctx) => el.setAttribute(name, call(value, ctx)), };
Каждая директива — это функция, которая принимает узел DOM, имя необязательного параметра для таких случаев, как
q-on:click (имя будет «click»). Также требуется строка выражения (value) и объект данных для использования в качестве контекста выражения.Теперь у нас есть все строительные блоки, пора всё склеить вместе!
Конечный результат
const call = .... // Our "safe" expression evaluator const directives = .... // Our supported directives // Currently evaluated directive, proxy uses it as a dependency // of the individual variables accessed during directive evaluation let $dep; // A function to iterate over DOM node and its child nodes, scanning all // attributes and binding them as directives if needed const walk = (node, q) => { // Iterate node attributes for (const {name, value} of node.attributes) { if (name.startsWith('q-')) { const [directive, event] = name.substring(2).split(':'); const d = directives[directive]; // Set $dep to re-evaluate this directive $dep = () => d(node, event, value, q); // Evaluate directive for the first time $dep(); // And clear $dep after we are done $dep = undefined; } } // Walk through child nodes for (const child of node.children) { walk(child, q); } }; // Proxy uses Object.defineProperty to intercept access to // all `q` data object properties. const proxy = q => { const deps = {}; // Dependent directives of the given data object for (const name in q) { deps[name] = []; // Dependent directives of the given property let prop = q[name]; Object.defineProperty(q, name, { get() { if ($dep) { // Property has been accessed. // Add current directive to the dependency list. deps[name].push($dep); } return prop; }, set(value) { prop = value; }, }); } return q; }; // Main entry point: apply data object "q" to the DOM tree at root "el". const Q = (el, q) => walk(el, proxy(q));
Реактивный, подобный Vue.js фреймворк во всей красе. Насколько он полезен? Вот пример:
<div id="counter"> <button q-on:click="clicks++">Click me</button> <button q-on:click="clicks=0">Reset</button> <p q-text="`Clicked ${clicks} times`"></p> </div> Q(counter, {clicks: 0});
Нажатие на одну кнопку увеличивает счетчик и автоматически обновляет содержимое
<p>. Щелчок по другому устанавливает счетчик на ноль, а также обновляет текст.Как видите, Vue.js, на первый взгляд, кажется магией, но внутри он очень прост, а основные функции могут быть реализованы всего в нескольких строках кода.
Дальнейшие шаги
Если вам интересно узнать о Vue.js больше — попробуйте реализовать «q-if» для переключения видимости элементов на основе выражения или «q-each» для привязки списков повторяющихся дочерних элементов (это будет хорошим упражнением).
Полные исходники нанофреймворка Q находятся на Github. Не стесняйтесь контрибьютить, если вы заметили проблему или хотите предложить улучшение!
В заключение я должен упомянуть, что
Object.defineProperty использовался в Vue 2, а создатели Vue 3 переключились на другой механизм, предоставляемый ES6, а именно Proxy и Reflect. Прокси позволяет передать обработчик для перехвата доступа к свойствам объекта, как и в нашем примере, в то время как Reflect позволяет получить доступ к свойствам объекта изнутри прокси и сохранить this объект нетронутым (в отличие от нашего примера с defineProperty).Я оставляю оба Proxy / Reflect в качестве упражнения для читателя, так что, кто бы ни сделал пулл-реквест для правильного использования их в Q — я буду рад совместить это. Удачи!
Надеюсь, вам понравилась статья. Вы можете следить за новостями и делиться предложениями в Github, Twitter или подписываться через rss.
