Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
Некоторое время назад я опубликовал похожую статью про 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.