предисловие
Прежде всего, приглашаю всех подписаться на меняБлог на гитхабе, что можно рассматривать как небольшое поощрение для меня, ведь у меня нет денег, чтобы писать вещи, и я могу продолжать на своем собственном энтузиазме и всеобщем поощрении.
Внутренний интерфейс принадлежит двум мирам Vue и React. Когда дело доходит до Vue, больше всего впечатляет двусторонняя привязка. принцип отзывчивых данных. В этой статье мы не будем дословно анализировать, как адаптивные данные реализованы в Vue, а только рассмотрим, как реализовать простой адаптивный модуль с точки зрения принципа, в надежде быть вам чем-то полезным.
отзывчивые данные
Реактивные данные не появляются из ниоткуда. Для интерфейсной разработки модель данных Model представляет собой обычный объект JavsScript. Представление является воплощением модели. С помощью ответа на событие JavaScript представление может легко модифицировать модель, например:
var model = {
click: false
};
var button = document.getElementById("button");
button.addEventListener("click", function(){
model.click = !model.click;
})
Но когда вы хотите изменить модель, представление также можно соответствующим образом обновить, что относительно сложно. В связи с этим React и View предоставляют два разных решения, вы можете обратиться кэта статья. Среди них отзывчивые данные дают достижимую идею. Что такое реактивные данные? На мой взгляд, реактивные данные — это когда вы изменяете данные, вы можете инициировать ряд других действий в соответствии с установленными вами правилами. На самом деле мы хотим добиться следующего эффекта:
var model = {
name: "javascript"
};
// 使传入的数据变成响应式数据
observify(model);
//监听数据修改
watch(model, "name", function(newValue, oldValue){
console.log("name newValue: ", newValue, ", oldValue: ", oldValue);
});
model.name = "php"; // languange newValue: php, oldValue: javascript
Из приведенного выше эффекта видно, что нам нужно захватить процесс изменения данных. К счастью, ES5 предоставляет свойства дескриптора через методObject.defineProperty
Мы можем установить свойства доступа. Но браузеры более ранних версий, включая IE8, не реализованы.Object.defineProperty
И это не может быть реализовано полифиллом (на самом деле IE8 реализует эту функцию, но она может использоваться только для DOM-объектов и очень ограничена), поэтому эту функцию нельзя реализовать в браузерах младших версий. Вот почему Vue не поддерживает просмотр в IE8 и ниже. пройти черезObject.defineProperty
Мы можем достичь:
Object.defineProperty(obj, "prop", {
enumerable: true,
configurable: true,
set: function(value){
//劫持修改的过程
},
get: function(){
//劫持获取的过程
}
});
скорость отклика данных
Согласно вышеизложенным идеям, мы рассмотрим, как достичьobservify
Функция, если мы хотим сделать объект реактивным, нам нужно обойти каждое свойство в объекте, а также нам нужно ответить на значение, соответствующее каждому свойству. код показывает, как показано ниже:
// 数据响应化
// 使用lodash
function observify(model){
if(_.isObject(model)){
_.each(model, function(value, key){
defineReactive(model, key, value);
});
}
}
//定义对象的单个响应式属性
function defineReactive(obj, key, value){
observify(value);
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set: function(newValue){
var oldValue = value;
value = newValue;
//可以在修改数据时触发其他的操作
console.log("newValue: ", newValue, " oldValue: ", oldValue);
},
get: function(){
return value;
}
});
}
функция вышеobservify
Он реализует отзывчивую обработку объектов, например:
var model = {
name: "MrErHu",
message: {
languange: "javascript"
}
};
observify(model);
model.name = "mrerhu" //newValue: mrerhu oldValue: MrErHu
model.message.languange = "php" //newValue: php oldValue: javascript
model.message = { db: "MySQL" } //newValue: {db: "MySQL"} oldValue: {languange:"javascript"}
Мы знаем, что в JavaScript часто используются не только объекты, массивы также являются очень важной частью. А методов, которые могут изменить сам массив, очень много, так как же мы можем отслеживать изменения, привносимые методами массива в массив? Чтобы решить эту проблему, мы можем заменить собственную функцию нашей пользовательской функцией альтернативным способом и вызвать собственный метод массива в пользовательской функции для достижения того, что мы хотим. Затем мы переделываем нашуdefineReactive
функция.
function observifyArray(array){
//需要变异的函数名列表
var methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayProto = Object.create(Array.prototype);
_.each(methods, function(method){
arrayProto[method] = function(...args){
// 劫持修改数据
var ret = Array.prototype[method].apply(this, args);
//可以在修改数据时触发其他的操作
console.log("newValue: ", this);
return ret;
}
});
Object.setPrototypeOf(array, arrayProto);
}
//定义对象的单个响应式属性
function defineReactive(obj, key, value){
if(_.isArray(value)){
observifyArray(value, dep);
}else {
observify(value);
}
Object.defineProperty(obj, key, {
// 省略......
});
}
我们可以看到我们将数组原生的原型替换成自定义的原型,然后调用数组的变异方法时就会调用我们自定义的函数。 Например:
var model = [1,2,3];
observify(model);
model.push(4); //newValue: [1, 2, 3, 4]
到目前为止我们已经实现了我们的需求,其实我写到这里的时候,我考虑到是否需要实现对数组的键值进行监听,其实作为使用过Vue的用户一定知道,当你利用索引直接设置一个项时,是不会监听到数组的变化的。 Например:
vm.items[indexOfItem] = newValue
Если вы хотите добиться вышеуказанного эффекта, вы можете добиться его следующими способами:
vm.items.splice(indexOfItem, 1, newValue);
Сначала подумайте, можно ли этого добиться. Ответ очевиден. Конечно, это возможно. Массивы на самом деле можно рассматривать как специальные массивы. На самом деле, для массивов индекс числового типа будет окончательно преобразован в строковый тип, например следующий код:
var array = [0,1,2];
array["0"] = 1; //array: [1,1,2]
Чтобы изменить данные, соответствующие числовому индексу, также можноObject.defineProperty
функцию для реализации, например:
var array = [0];
Object.defineProperty(array, 0, {
set: function(newValue){
console.log("newValue: ", newValue);
}
});
array[0] = 1;//newValue: 1
может быть реализовано, но эта функция не реализована, основная причина может быть связана с соображениями производительности (мое предположение). Но Vue предоставляет еще одну глобальную функцию,Vue.set
может быть реализован
Vue.set(vm.array, indexOfItem, newValue)
Мы можем примернодогадкаодин разVue.set
Как это реализовано внутри?Для массивов вам нужно толькоnewValue
Выполните реактивную обработку и назначьте ее массиву, а затем уведомите массив об изменениях. Для объектов, если это свойство, которого раньше не было, можно сначалаnewValue
Отзывчивая обработка (например, вызовobservify(newValue)
), а затем определить прослушиватель для определенных свойств (например, вызов функцииdefineReactive
), и, наконец, выполнить задание, конкретная обработка может сильно различаться, но принцип внутренней реализации должен быть таким (просто предположение).
Мало того, в приведенной выше реализации мы можем обнаружить, что не можем контролировать объект и не можем обнаружить добавление или удаление атрибутов объекта, поэтому, если вы хотите отслеживать значение определенного атрибута, а этот атрибут не не существует в начале, лучше всего присвоить ему значение по умолчанию при инициализации данных, чтобы можно было отслеживать изменение свойства.
Коллекция зависимостей
Мы так много сказали выше, я надеюсь, вы не будете введены в заблуждение, все, что мы сделали выше, это получать уведомления при изменении данных. Вернемся к нашему первоначальному вопросу. Мы надеемся, что когда данные в слое модели изменяются, данные в слое представления соответственно изменяются, и мы смогли отслеживать изменения в данных Следующее, что нужно учитывать, — это изменения в представлении.
Для Vue, даже если вы используетеTemplate
Опишите слой представления, который в конечном итоге будет скомпилирован вrender
функция. Например, в шаблоне описывается:
<h1>{{ name }}</h1>
будет фактически скомпилирован в:
render: function (createElement) {
return createElement('h1', this.name);
}
Теперь есть следующая проблема, если моя Модель выглядит так:
var model = {
name: "MrErHu",
age: 23,
sex: "man"
}
Фактическиrender
В функции используются только свойстваname
, но в Модели есть и другие свойства.Когда данные изменяются, как мы узнаем, когда нам нужно снова вызватьrender
функция. Вы можете подумать, а откуда такая морока с обновлением каждый раз при изменении данныхrender
Функционала не хватает. Это, безусловно, возможно.На самом деле, если мы идем в этом направлении, мы идем в направлении React. На самом деле, если вы не используете виртуальный DOM, если вы вызываете его каждый раз при изменении свойстваrender
Эффективность должна быть низкой. В настоящее время мы вводим сбор зависимостей. Если мы можем знатьrender
Зависит от этих свойств, тогда, когда эти свойства изменяются, мы называем это именноrender
функция, поэтому наша цель не достигнута? это то, что мы называемКоллекция зависимостей.
Принцип сбора зависимостей очень прост.В адаптивных данных мы всегда использовали атрибуты в дескрипторе атрибута.set
метод, и мы знаем, что когда вызывается свойство объекта, дескриптор свойстваget
метод, когдаget
При вызове метода мы будем вызыватьget
Коллекция методов может завершить нашу задачу по сбору зависимостей.
Прежде всего, мы можем подумать о том, как бы мы его спроектировали, если бы нам нужно было написать адаптивный модуль данных с коллекцией зависимостей. Первый подобный эффект, которого мы хотим добиться, это:
var model = {
name: "MrErHu",
program: {
language: "Javascript"
},
favorite: ["React"]
};
//数据响应化
observify(model);
//监听
watch(function(){
return '<p>' + (model.name) + '</p>'
}, function(){
console.log("name: ", model.name);
});
watch(function(){
return '<p>' + (model.program.language) + '</p>'
}, function(){
console.log("language: ", model.program.language);
});
watch(function(){
return '<p>' + (model.favorite) + '</p>'
}, function(){
console.log("favorite: ", model.favorite);
});
model.name = "mrerhu"; //name: mrerhu
model.program.language = "php"; //language: php
model.favorite.push("Vue"); //favorite: [React, Vue]
Чего нам нужно достичьwatch
Первый параметр функции можно рассматривать какrender
функция, выполняяrender
функция, которую мы можем собратьrender
Эти реактивные свойства данных используются внутри функции. Затем при изменении соответствующего атрибута отзывчивых данных срабатывает вторая зарегистрированная нами функция. Таким образом, гранулярность наших атрибутов мониторинга — это каждый атрибут данных ответа. Следуя концепции единой ответственности, мы будемследить за подпискойа такжеобъявлениеразделение обязанностей отдельнымDep
Класс ответственный. Поскольку детализацией прослушивания является каждый атрибут ответных данных, мы будем поддерживатьDep
. Соответственно, мы создаемWatcher
класс, ответственный заDep
Зарегистрируйтесь и вызовите функцию обратного вызова при получении уведомления. Как показано ниже:
Сначала мы осознаемDep
а такжеWatcher
Добрый:
//引入lodash库
class Dep {
constructor(){
this.listeners = [];
}
// 添加Watcher
addWatcher(watcher){
var find = _.find(this.listeners, v => v === watcher);
if(!find){
//防止重复注册
this.listeners.push(watcher);
}
}
// 移除Watcher
removeWatcher(watcher){
var find = _.findIndex(this.listeners, v => v === fn);
if(find !== -1){
this.listeners.splice(watcher, 1);
}
}
// 通知
notify(){
_.each(this.listeners, function(watcher){
watcher.update();
});
}
}
Dep.target = null;
class Watcher {
constructor(callback){
this.callback = callback;
}
//得到Dep通知调用相应的回调函数
update(){
this.callback();
}
}
Затем мы создаем функцию наблюдателя и модифицируем предыдущие реактивные связанные функции:
// 数据响应化
function observify(model){
if(_.isObject(model)){
_.each(model, function(value, key){
defineReactive(model, key, value);
});
}
}
//定义对象的单个响应式属性
function defineReactive(obj, key, value){
var dep = new Dep();
if(_.isArray(value)){
observifyArray(value, dep);
}else {
observify(value);
}
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set: function(newValue){
observify(value);
var oldValue = value;
value = newValue;
//可以在修改数据时触发其他的操作
dep.notify(value);
},
get: function(){
if(!_.isNull(Dep.target)){
dep.addWatcher(Dep.target);
}
return value;
}
});
}
// 数据响应化
function observify(model){
if(_.isObject(model)){
_.each(model, function(value, key){
defineReactive(model, key, value);
});
}
}
//定义对象的单个响应式属性
function defineReactive(obj, key, value){
var dep = new Dep();
if(_.isArray(value)){
observifyArray(value, dep);
}else {
observify(value);
}
Object.defineProperty(obj, key, {
configurable: true,
enumerable: true,
set: function(newValue){
observify(value);
var oldValue = value;
value = newValue;
//可以在修改数据时触发其他的操作
dep.notify(value);
},
get: function(){
if(!_.isNull(Dep.target)){
dep.addWatcher(Dep.target);
}
return value;
}
});
}
function observifyArray(array, dep){
//需要变异的函数名列表
var methods = ['push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse'];
var arrayProto = Object.create(Array.prototype);
_.each(methods, function(method){
arrayProto[method] = function(...args){
var ret = Array.prototype[method].apply(this, args);
dep.notify(this);
return ret;
}
});
Object.setPrototypeOf(array, arrayProto);
}
function watch(render, callback){
var watcher = new Watcher(callback);
Dep.target = watcher;
render();
Dep.target = null;
}
Далее мы можем поэкспериментировать с нашимwatch
функция:
var model = {
name: "MrErHu",
message: {
languange: "javascript"
},
love: ["Vue"]
};
observify(model);
watch(function(){
return '<p>' + (model.name) + '</p>'
}, function(){
console.log("name: ", model.name);
});
watch(function(){
return '<p>' + (model.message.languange) + '</p>'
}, function(){
console.log("message: ", model.message);
});
watch(function(){
return '<p>' + (model.love) + '</p>'
}, function(){
console.log("love: ", model.love);
});
model.name = "mrerhu"; // name: mrerhu
model.message.languange = "php"; // message: { languange: "php"}
model.message = {
target: "javascript"
}; // message: { languange: "php"}
model.love.push("React"); // love: ["Vue", "React"]
На данный момент мы в основном добились желаемого эффекта.Конечно, приведенный выше пример не является полным, но он может в основном показать основные принципы адаптивных данных и зависимости от данных. Конечно, описанное выше реализуется только с использованием дескрипторов данных ES5.С популярностью ES6 мы также можем использовать Proxy (прокси) и Reflect (отражение) для реализации. В качестве первой статьи в этой серии есть и другие пункты, которые не были перечислены один за другим, вы можете следить за мной.Блог на гитхабеПродолжайте обращать внимание, если есть какие-то неточности, пожалуйста, поправьте меня.