首先了解概念然后学习框架
当第一次接触React会有很多的困惑。文章将介绍React和它的基本理念。
通过阅读文章你将会对于为什么需要React和Redux或者其他状态容器有一个更好的理解。
学习时你不需要使用 JSX,ES6/ES*, Webpack,热加载,理解虚拟DOM,甚至不需要使用React自己。
首先:
让我们看一下 TodoMVC code for jQuery。
您将注意到有一个方法render(),每当事件被触发或数据被更新时,它就会被调用。让我们构建一个例子,当改变一个输入值,触发render方法更新DOM。
var state = {value: null};
$('#input').on('keyup', function() {
state.value = $(this).val().trim();
render();
});
function render() {
$('#output').html(state.value);
}
render();
复制代码
我们用一个全局状态state变量来保存同步所有的值。在一次输入后做了两件事:
- 它更新了全局状态state。
- 调用render方法。现在render方法根据全局状态state来更新DOM。
记住这个例子。我们马上就会讲到。
下面是另一个需要考虑的地方:
function output(text) {
return '<div>' + text + '</div>';
}
复制代码
调用output('foo') 将返回 '<div>foo</div>'。
现在考虑下面的例子:
function h2(text) {
return '<h2>' + text + '</h2>';
}
function div(text) {
return '<div>' + text + '</div>';
}
function header(text) {
return div(h2(text));
}
console.log(header('foo') === '<div><h2>foo</h2></div>'); //true;
复制代码
我们编写的方法的返回值基于一个字符串参数。用同样的参数调用header方法总是返回一个同样的元素字符串。如果你曾经在React中考虑过无状态组件的话,这是一个简化版本。React中无状态组件将返回一个React元素而不是一个简单的元素字符串。
我们知道我们可以创建返回元素字符串的函数。让我们回到最初的示例,并扩展它显示add按钮和项目列表。
var state = {items: [], id: 0};
$('#add').on('click', function (e) {
var value = $('#input').val().trim();
$('#input').val('');
state.items.push({id: state.id++, text: value, completed: false});
render();
});
$('#list').on('click', '.item', function () {
var toggleId = parseInt($(this).attr('id'));
state.items.forEach(function (el) {
if (el.id === toggleId) {
el.completed = !el.completed;
}
});
render();
});
function render() {
var items = state.items.map(function (item) {
var completed = item.completed ? 'completed' : '';
return '<li class="item + ' + completed + '" id="' + item.id + '">(' + item.id + ') ' + item.text + '</li>';
}).join('');
var html = '<ul>' + items + '</ul>';
$('#list').html(html);
}
render();
复制代码
我们有一个简单的待办列表,包括切换项目状态(待办或完成)的能力。
用一组已定义的事件函数更新全局状态state然后调用我们的render方法。然后render方法创建项目列表并且添加项目到项目列表中。我们添加了状态来简化事件和元素之间的交互。而不是定义每个事件和元素以及它们各自的关系,我们总是在一个行为被触发后立即更新。它简化了处理复杂的交互。当状态改变时,我们总是调用render方法。
这已经很有效了。我们可以通过输入框输入标题来添加条目,我们可以通过点击条目本身来切换条目状态。
现在render方法看起来有些乱。让我们尝试创建一个基于传入的props来返回元素字符串的方法。
function ItemRow(props) {
var className = props.completed? ' item completed' : 'item';
return '<li class="' + className +'">' + props.text + '</li>';
}
function ItemsList(props) {
return '<ul>' + props.items.map(ItemRow).join('') + '</ul>';
}
复制代码
我们已经清理了render函数。
function render() {
$('#list').html(ItemsList({items : state.items}));
}
复制代码
如果render自身不知道状态,并且期望使用一个输入的参数代替呢?这很容易实现,我们可以用props简单的重构render方法。(这就是React组件所期望的。)
function render(props) {
$('#list').html(ItemsList({items : props.items}));
}
复制代码
render不需要知道外部状态。这使我们能够简单地调用render方法传入任何已定义的状态。这也意味着传入的状态不改变那么重新绘制将一次又一次地返回相同的结果。虽然我们应该记住,这是Dom的一个副作用,但让我们暂时忽略这个事实。
通过将显式状态与绘制部分分离,我们可以轻松实现撤销/重做。这意味着我们可以创建一个历史记录并在每次更改时保存。
另一个优化是将根节点作为参数传入,而不是在render函数中显式地定义节点。
function render(props, node) {
node.html(ItemsList({items : props.items}));
}
复制代码
因此,现在我们可以简单地调用render方法并且传入已定义的状态和根节点。
render(state, $('#list'));
复制代码
但是,在更新状态之后,如何不需要显式地调用render方法呢?
让我们构建一个store,当状态从应用的任何地方更新时,只需调用我们的render方法。这是第一次尝试。虽然这个实现是非常基础的,这是创建一个更高级的状态容器的好起点。
function createStore(initialState) {
var _state = initialState || {}, _listeners = [];
function updateListeners(state) {
_listeners.forEach(function(listener) {
listener.cb(state);
});
}
return {
setState: function(state) {
_state = state;
updateListeners(state);
},
getState: function() {
return _state;
},
onUpdate: function(name, cb) {
_listeners.push({name: name, cb: cb});
}
};
}
复制代码
我们可以简单地通过存储setState方法更新状态。一旦状态改变后,我们的render函数会被调用。
var store = createStore(state);
store.onUpdate('rootRender', function(state) {
render(state, $('#list'));
});
复制代码
我们现在看到了什么?
我们已经看到单向数据流的简单原理。我们将状态传递给render函数,状态沿着函数的层次结构传入。例如ItemList传递一个适当的props给ItemRow。我们已经创建了组件,并将这些组件组合成更大的组件。记得标题的例子,我们将div和h2函数组合到header中。这里我们处理的是纯函数。这使得我们所有的更新都可以预测。我们现在对我们的状态也有一个清晰的认识。
在React中,这会以一种非常有效的方式进行。通过实现虚拟DOM、单向数据流等实现架构,优化渲染。
...我们可以专注于研究React真正的优势:架构,单向数据流,从DSLs中解脱,明确的预期和静态思想模型。
Dan Abramov(medium.com/@dan_abramo…)
我们能做的还有很多,包括建立一个改进的状态容器, 重构监听器,实现撤销/重做和更多好的功能。