Каждый день мы используем различные фреймворки, которые сопровождают нашу работу каждый день. Используя эти рамки с целью нашего освобождения, мало кто действительно заботится о том, что происходит за кулисами. Я также использую множество фреймворков, и благодаря этим популярным фреймворкам я получил некоторые знания, поэтому я хочу поделиться этими вещами.
Каждое название представляет собой независимую тему, и вы можете выбирать интересные чтения в соответствии с вашими потребностями.
Строка в DOM
Друзья, часто использующие jquery, должны быть знакомы со следующим кодом:
var text = $('<div>hello, world</div>');
$('body').append(text)
Результатом выполнения приведенного выше кода является добавление узла div на страницу. Если оставить в стороне jQuery, код может стать немного сложнее:
var strToDom = function(str) {
var temp = document.createElement('div');
temp.innerHTML = str;
return temp.childNodes[0];
}
var text = strToDom('<div>hello, world</div>');
document.querySelector('body').appendChild(text);
Этот код точно такой же, как с использованием jQuery, Haha, jQuery - это то же самое. Если вы так думаете, вы не правы. В чем разница между выполнением следующих двух кодов:
var tableTr = $('<tr><td>Simple text</td></tr>');
$('body').append(tableTr);
var tableTr = strToDom('<tr><td>Simple text</td></tr>');
document.querySelector('body').appendChild(tableTr);
На первый взгляд проблем нет. Если вы воспользуетесь инструментами разработчика для просмотра структуры страницы, вы обнаружите:
strToDom
только что создал текстовый узел, а не настоящийtr
Этикетка. Причина в том, что строка, содержащая элемент HTML, прогоняется в браузере через парсер, парсер игнорирует теги, которые не размещены в правильном контексте, поэтому мы получаем только текстовый узел.
jQuery
Как решить эту проблему?Проанализировав исходный код, я нашел следующий код:
var wrapMap = {
option: [1, '<select multiple="multiple">', '</select>'],
legend: [1, '<fieldset>', '</fieldset>'],
area: [1, '<map>', '</map>'],
param: [1, '<object>', '</object>'],
thead: [1, '<table>', '</table>'],
tr: [2, '<table><tbody>', '</tbody></table>'],
col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
_default: [1, '<div>', '</div>']
};
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
Каждый элемент требует специальной обработки для выделения массива. Идея состоит в том, чтобы получить то, что нам нужно, чтобы построить правильный уровень вложенности элементов DOM и зависимостей. Например,tr
элемент, нам нужно создать два уровня вложенности:table
,tbody
.
С помощью этой таблицы отображения карты мы можем получить нужные нам окончательные метки. Следующий код демонстрирует, как<tr><td>hello word</td></tr>
взято изtr
:
var match = /<\s*\w.*?>/g.exec(str);
var tag = match[0].replace(/</g, '').replace(/>/g, '');
Осталось только вернуть DOM-элемент согласно соответствующему контексту, и в конечном итоге мы получимstrToDom
Внесите окончательные изменения:
var strToDom = function(str) {
var wrapMap = {
option: [1, '<select multiple="multiple">', '</select>'],
legend: [1, '<fieldset>', '</fieldset>'],
area: [1, '<map>', '</map>'],
param: [1, '<object>', '</object>'],
thead: [1, '<table>', '</table>'],
tr: [2, '<table><tbody>', '</tbody></table>'],
col: [2, '<table><tbody></tbody><colgroup>', '</colgroup></table>'],
td: [3, '<table><tbody><tr>', '</tr></tbody></table>'],
_default: [1, '<div>', '</div>']
};
wrapMap.optgroup = wrapMap.option;
wrapMap.tbody = wrapMap.tfoot = wrapMap.colgroup = wrapMap.caption = wrapMap.thead;
wrapMap.th = wrapMap.td;
var element = document.createElement('div');
var match = /<\s*\w.*?>/g.exec(str);
if(match != null) {
var tag = match[0].replace(/</g, '').replace(/>/g, '');
var map = wrapMap[tag] || wrapMap._default, element;
str = map[1] + str + map[2];
element.innerHTML = str;
// Descend through wrappers to the right content
var j = map[0]+1;
while(j--) {
element = element.lastChild;
}
} else {
// if only text is passed
element.innerHTML = str;
element = element.lastChild;
}
return element;
}
пройти черезmatch != null
Определите, создавать ли метку или текстовый узел. На этот раз через браузер мы можем создать действительное DOM-дерево. Наконец, используя цикл while, пока мы не получим нужную метку, и, наконец, вернем эту метку.
Внедрение зависимостей AngularJS
Когда мы начали использовать AngularJS, его двусторонняя привязка данных была впечатляющей. Еще одна удивительная особенность — внедрение зависимостей. Вот простой пример:
function TodoCtrl($scope, $http) {
$http.get('users/users.json').success(function(data) {
$scope.users = data;
});
}
Это типичная запись контроллера AngularJS: сделав HTTP-запрос, получить данные из файла JSON и назначить данные для$scope.users
. Фреймворк AngularJS автоматически$scope
а также$http
Ввести в контроллер. Посмотрим, как это реализовано.
Глядя на пример, мы хотим отобразить имя пользователя на странице.Для простоты мы используем имитацию поддельных данных для имитации http-запроса:
var dataMockup = ['John', 'Steve', 'David'];
var body = document.querySelector('body');
var ajaxWrapper = {
get: function(path, cb) {
console.log(path + ' requested');
cb(dataMockup);
}
}
var displayUsers = function(domEl, ajax) {
ajax.get('/api/users', function(users) {
var html = '';
for(var i=0; i < users.length; i++) {
html += '<p>' + users[i] + '</p>';
}
domEl.innerHTML = html;
});
}
displayUsers(body, ajaxWrapper)
displayUsers(body, ajaxWrapper)
Для выполнения требуются две зависимости: body и ajaxWrapper. Наша цель — вызвать displayUsers() напрямую, без передачи аргументов, и это будет работать так, как мы ожидаем.
Большинство фреймворков предоставляют механизм внедрения зависимостей с помощью модуля, обычно называемогоinjector. Все зависимости прописаны здесь и предоставляют внешние интерфейсы доступа:
var injector = {
storage: {},
register: function(name, resource) {
this.storage[name] = resource;
},
resolve: function(target) {
}
};
Ключевая реализация Resolve: она получает целевой объект, оборачивает цель, возвращая замыкание, и вызывает его. Например:
resolve: function(target) {
return function() {
target();
};
}
Таким образом, мы можем вызывать нужные нам зависимые функции.
Следующим шагом является получение списка параметров цели, здесь я имею в виду реализацию AngularJS:
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
...
function annotate(fn) {
...
fnText = fn.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
...
}
Я заблокировал другие детали кода, оставив только те части, которые нам полезны.annotate
Соответствующий наш собственныйresolve
. Он будет преобразован в строку через целевую функцию, и комментарии будут удалены, и, наконец, будет получена информация о параметрах:
resolve: function(target) {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
fnText = target.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS);
console.log(argDecl);
return function() {
target();
}
}
Откройте консоль:
Второй элемент массива argDecl содержит все параметры, которые можно получить по имени параметраinjectorЗависимости, хранящиеся в . Ниже приведена конкретная реализация:
resolve: function(target) {
var FN_ARGS = /^function\s*[^\(]*\(\s*([^\)]*)\)/m;
var STRIP_COMMENTS = /((\/\/.*$)|(\/\*[\s\S]*?\*\/))/mg;
fnText = target.toString().replace(STRIP_COMMENTS, '');
argDecl = fnText.match(FN_ARGS)[1].split(/, ?/g);
var args = [];
for(var i=0; i<argDecl.length; i++) {
if(this.storage[argDecl[i]]) {
args.push(this.storage[argDecl[i]]);
}
}
return function() {
target.apply({}, args);
}
}
пройти через.split(/, ?/g)
преобразовать строкуdomEl, ajax
в массив, проверивinjectorЗарегистрирована ли зависимость с тем же именем вtarget
функция.
Код вызова должен выглядеть так:
injector.register('domEl', body);
injector.register('ajax', ajaxWrapper);
displayUsers = injector.resolve(displayUsers);
displayUsers();
Преимущество такой реализации в том, что мы внедряем domEl и ajax в любую нужную функцию. Мы даже можем реализовать настройку приложения. Больше не нужно передавать параметры, стоимость просто передатьregister
а такжеresolve
.
Наш автовпрыск пока что не идеален и имеет два недостатка:
1. Функция не поддерживает пользовательские параметры.
2. Сжатие онлайн-кода приводит к изменению имен параметров, что делает невозможным получение корректных зависимостей.
эти два вопросаAngualrJS
Все решено, и если вам интересно, вы можете прочитать мою другую статью:Как реализовать внедрение зависимостей в javascript, в котором подробно описано полное решение для внедрения зависимостей.
Вычисленные свойства Ember
Вероятно, первое, о чем думает большинство людей, когда они слышат о вычисляемых свойствах, этоVue
серединаComputed
Вычисляемые свойства. по фактуEmber
Платформа также предоставляет такую функцию для свойств вычисляемых свойств. Это небольшой поворот, давайте посмотрим на официальный пример:
App.Person = Ember.Object.extend({
firstName: null,
lastName: null,
fullName: function() {
return this.get('firstName') + ' ' + this.get('lastName');
}.property('firstName', 'lastName')
});
var ironMan = App.Person.create({
firstName: "Kobe",
lastName: "Bryant"
});
ironMan.get('fullName') // "Kobe Bryant"
Объекты Person имеют свойства firstName и lastName. Вычисляемое свойство fullName возвращает строку подключения, содержащую полное имя человека. Странно то, что функция fullName использует.property
метод. Давайте взглянемproperty
код:
Function.prototype.property = function() {
var ret = Ember.computed(this);
// ComputedProperty.prototype.property expands properties; no need for us to
// do so here.
return ret.property.apply(ret, arguments);
};
Настройте прототип объекта глобальной функции, добавив новые свойства. Запуск некоторой логики во время определения класса — хороший подход.
Ember
использоватьgetter
а такжеsetter
управлять данными объекта. Это упрощает реализацию вычисляемых свойств, поскольку у нас есть еще один слой для работы с фактическими переменными. Но было бы интереснее, если бы мы могли использовать вычисляемые свойства с обычными js-объектами. Например:
var User = {
firstName: 'Kobe',
lastName: 'Bryant',
name: function() {
// getter + setter
}
};
console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
Как обычное свойство, имя по сути представляет собой функцию, которая получает или устанавливает имя и фамилию.
В JavaScript есть встроенная функция, которая может помочь нам с этой идеей:
var User = {
firstName: 'Kobe',
lastName: 'Bryant',
};
Object.defineProperty(User, "name", {
get: function() {
return this.firstName + ' ' + this.lastName;
},
set: function(value) {
var parts = value.toString().split(/ /);
this.firstName = parts[0];
this.lastName = parts[1] ? parts[1] : this.lastName;
}
});
Object.defineProperty
Методы могут принимать объекты, имена свойств объекта,getter
а такжеsetter
. Все, что нам нужно сделать, это написать логику реализации этих двух методов. Запустив приведенный выше код, мы получим желаемый результат:
console.log(User.name); // Kobe Bryant
User.name = 'LeBron James';
console.log(User.firstName); // LeBron
console.log(User.lastName); // James
Object.defineProperty
Хотя это то, что мы хотим, очевидно, мы не хотим писать это каждый раз. В идеале мы хотели бы предоставить интерфейс. В этом разделе мы напишемComputize
функция, которая будет обрабатывать объект и каким-то образом преобразовывать функцию имени в свойство с таким же именем.
var Computize = function(obj) {
return obj;
}
var User = Computize({
firstName: 'Kobe',
lastName: 'Bryant',
name: function() {
...
}
});
Мы хотим использовать метод имени в качестве установщика, а также в качестве получателя. Это похоже на вычисляемые свойства Ember.
Теперь добавим нашу собственную логику в прототип объекта функции:
Function.prototype.computed = function() {
return { computed: true, func: this };
};
Таким образом, вычисляемая функция может вызываться непосредственно после определения каждой функции.
name: function() {
...
}.computed()
Свойство name больше не функция, а объект:{ computed: true, func: this }
. вcomputed
равныйtrue
, func
Атрибут указывает на исходную функцию.
Настоящее волшебство происходит при реализации помощника Computize. Он перебирает все свойства объекта, используя object.defineproperty для всех вычисляемых свойств:
var Computize = function(obj) {
for(var prop in obj) {
if(typeof obj[prop] == 'object' && obj[prop].computed === true) {
var func = obj[prop].func;
delete obj[prop];
Object.defineProperty(obj, prop, {
get: func,
set: func
});
}
}
return obj;
}
Примечание. Мы удалили имя вычисляемого свойства, поскольку Object.defineProperty работает только с неопределенными свойствами в некоторых браузерах.
Вот окончательная версия объекта пользователя с использованием функции .computed():
var User = Computize({
firstName: 'Kobe',
lastName: 'Bryant',
name: function() {
if(arguments.length > 0) {
var parts = arguments[0].toString().split(/ /);
this.firstName = parts[0];
this.lastName = parts[1] ? parts[1] : this.lastName;
}
return this.firstName + ' ' + this.lastName;
}.computed()
});
Логика функции заключается в том, чтобы определить, есть ли параметры.Если есть параметры, параметры делятся напрямую, и имя и фамилия назначаются соответственно, и, наконец, возвращается полное имя.
конец
За большими фреймворками и библиотеками стоит опыт многих отличных предшественников. Изучая эти фреймворки, мы можем лучше понять принципы, лежащие в основе этих фреймворков, и иметь возможность развиваться без фреймворка, что очень важно.