├── chn ├── 01.example.srt ├── 03.srt ├── 04.srt ├── 15.srt ├── 08.srt ├── 07.srt ├── 01.srt ├── 05.srt ├── 13.srt ├── 12.srt ├── 02.srt ├── 10.srt ├── 14.srt ├── 06.srt ├── 18.srt ├── 09.srt ├── 11.srt └── 16.srt ├── eng ├── 01.example.srt ├── 03.srt ├── 01.srt ├── 04.srt └── 02.srt ├── translation ├── 10 ├── 11 ├── 12 ├── 13 ├── 14 ├── 15 ├── 16 ├── 18 ├── 20 ├── 24 ├── 26 ├── 28 ├── 29 ├── 30 ├── 08 ├── 05 ├── 07 ├── 06 └── 09 ├── src ├── 10 ├── 11 ├── 12 ├── 13 ├── 14 ├── 15 ├── 16 ├── 17 ├── 18 ├── 20 ├── 24 ├── 26 ├── 27 ├── 28 ├── 29 ├── 30 ├── 03 ├── 04 ├── 01 ├── 08 ├── 07 ├── 05 ├── 02 ├── 06 └── 09 └── README.md /chn/01.example.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,620 --> 00:00:02,420 3 | 无论你的 app 是简单如计数器, 4 | 5 | 2 6 | 00:00:02,420 --> 00:00:06,850 7 | 还是拥有大量 UI 和状态转换的复杂应用, 8 | 9 | 3 10 | 00:00:07,000 --> 00:00:11,000 11 | 都要记住 Redux 的第一原则, 12 | 13 | 4 14 | 00:00:11,000 --> 00:00:13,000 15 | 就是要将你的应用的整个状态用单个 JavaScript 对象去代表。 16 | 17 | -------------------------------------------------------------------------------- /eng/01.example.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,620 --> 00:00:02,420 3 | The first principle of Redux 4 | 5 | 2 6 | 00:00:02,420 --> 00:00:06,850 7 | is whether your app is a really simple one like this counter example, 8 | 9 | 3 10 | 00:00:07,000 --> 00:00:11,000 11 | or a complex application with a lot of UI, and change of state, 12 | 13 | 4 14 | 00:00:11,000 --> 00:00:13,000 15 | you are going to represent the whole state of your application as a single JavaScript object. 16 | 17 | -------------------------------------------------------------------------------- /translation/10: -------------------------------------------------------------------------------- 1 | 在上一次的示例中 我使用了NPM中的“expect”和“deep-freeze"库来为我的测试做断言 这次我将测试“toggleTodo”这个方法 该方法取一个“todo”对象并将其“completed”属性翻转 如果“completed”的原始值为“false” 其返回结果将变为“true” 如果是“true” 其结果将变为“false” 2 | 3 | 就像上一节课一样 我将从写一个可变的版本开始来使当前的测试通过 这个版本只是将传入对象的 completed 属性的值翻转一下 4 | 5 | 当成功执行后 我们知道可变对象在Redux中是不允许的 为了达到这个目的 我在我的to-do对象上使用了“deepFreeze” 但是这样我将再也不能改变它的“completed”属性了 6 | 7 | 这个问题的一个方法是创建一个新对象 这个对象复制了原始对象所有属性但除了“complete”这个将被翻转的属性 如果我们后面再给新对象添加新属性 我们可能会忘记更新这段代码来包含他们 8 | 9 | 这就是为什么我建议你使用“Object.assign”这个ES6新方法的原因 它让你可以将多个对象的属性赋给目标对象 注意“Object.assign”参数顺序如何与JavaScript分配操作符相对应 10 | 11 | 左边的参数是赋值所有属性的对象 所以它是可变的 这就是为什么我们传一个空对象给第一个参数 所以我们不会改变任何已存在的数据 “Object.assign”的每一个其他参数将被认为一个源对象 源对象的属性将复制到目标对象 12 | 13 | 重要的一点是如果多个源对象中有相同名称的属性但有不同的属性值 最终替换的是最后一个源对象的值 并且这就是我们用来复写“completed”属性的值而不管原来“todo”对象里面的值是什么 14 | 15 | 最后 你需要记住“Object.assign”是一个ES6的新方法 所以它不是在所有浏览器中都能使用 你应该使用一个polyfill 可以使用Babel或者一个标准的“Object.assign"polyfill 这样可以避免让你的网站崩溃 16 | 17 | 另外一个选项不要求一个polyfill来使用新对象传播操作符 它不是ES6的一部分 尽管如此 它已经在ES7中被提议 它相当受欢迎 可以通过设置Babel中的“stage-2”属性来使用它 18 | -------------------------------------------------------------------------------- /src/03: -------------------------------------------------------------------------------- 1 | Before we proceed any further, it's important that you understand the difference between the pure and impure functions. The pure functions are the functions whose returned value depends solely on the values of their arguments. 2 | 3 | Pure functions do not have any observable side effects, such as network or database calls. The pure functions just calculate the new value. You can be confident that if you call the pure function with the same set of arguments, you're going to get the same returned value. They are predictable. 4 | 5 | Also, pure functions do not modify the values passed to them. For example, squareAll function that accepts an array does not overwrite the items inside this array. Instead, it returns a new array by using items map. 6 | 7 | On the opposite, impure functions may call the database or the network, they may have side effects, they may operate on the DOM, and they may override the values that you pass to them. This is going to be an important distinction because some of the functions that you're going to write in Redux have to be pure, and you need to be mindful of that. 8 | -------------------------------------------------------------------------------- /src/26: -------------------------------------------------------------------------------- 1 | In the previous lesson, we implemented the "Provider" component that uses the react advanced context feature to make the store from the props available to every component in our app. 2 | 3 | So if we pass it through the "Provider", we can read it in any other component from the context, which is really convenient for the container components. In fact, this is so convenient that you don't need to actually write the "Provider" yourself, because it is included in a special library called "react-redux". 4 | 5 | And note that it is not the same as Redux. This is a different library. These are React bindings to the Redux library. You can import the "Provider" by destructuring the "ReactRedux" global object in JSbin, or if you use Babbel, and something like NPM, you can import "Provider" with the braces, because it's a named export from "react-redux" package. Or if you write ES5 code, you can write "var Provider=require('react-redux').Provider", or "var Provider=ReactRedux.Provider". 6 | 7 | Just like the "Provider" we wrote before, the "Provider" that comes with "react-redux" exposes the store you passed this prop on the context, so the components can specify the context types, and then use this context's store to subscribe to the store updates and dispatch actions. 8 | 9 | -------------------------------------------------------------------------------- /chn/03.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,420 --> 00:00:02,210 3 | 在我们继续之前 4 | 5 | 2 6 | 00:00:02,210 --> 00:00:08,140 7 | 你必须理解纯函数和非纯函数的区别 8 | 9 | 3 10 | 00:00:08,140 --> 00:00:16,190 11 | 纯函数的返回值只依赖于它们的参数 12 | 13 | 4 14 | 00:00:16,190 --> 00:00:20,350 15 | 纯函数没有任何可见的副作用 16 | 17 | 5 18 | 00:00:20,350 --> 00:00:23,130 19 | 比如访问网络或数据库 20 | 21 | 6 22 | 00:00:23,560 --> 00:00:26,560 23 | 纯函数只用来产生新值 24 | 25 | 7 26 | 00:00:26,560 --> 00:00:30,510 27 | 可以很确定 如果使用相同的参数 28 | 29 | 8 30 | 00:00:30,510 --> 00:00:33,050 31 | 去调用纯函数 32 | 33 | 9 34 | 00:00:33,050 --> 00:00:35,740 35 | 你将必然得到相同的返回值 36 | 37 | 10 38 | 00:00:35,740 --> 00:00:37,180 39 | 这是可以预知的 40 | 41 | 11 42 | 00:00:37,740 --> 00:00:42,330 43 | 同时 纯函数不会修改传递进来的参数 44 | 45 | 12 46 | 00:00:42,330 --> 00:00:43,320 47 | 比如 48 | 49 | 13 50 | 00:00:43,320 --> 00:00:49,200 51 | squaerAll 函数接受一个数组 但并没有改写这个数组中的元素 52 | 53 | 14 54 | 00:00:49,200 --> 00:00:53,070 55 | 相反 它通过调用 items.map() 返回一个新的数组 56 | 57 | 15 58 | 00:00:54,220 --> 00:00:55,260 59 | 相反的是 60 | 61 | 16 62 | 00:00:55,260 --> 00:00:59,640 63 | 非纯函数可能访问数据库或网络 64 | 65 | 17 66 | 00:00:59,760 --> 00:01:01,550 67 | 它们可能会产生副作用 68 | 69 | 18 70 | 00:01:01,550 --> 00:01:03,310 71 | 它们可能操作 DOM 72 | 73 | 19 74 | 00:01:03,310 --> 00:01:06,920 75 | 它们可能改写传递进来的参数 76 | 77 | 20 78 | 00:01:08,250 --> 00:01:10,780 79 | 这是很重要的区别 80 | 81 | 21 82 | 00:01:10,780 --> 00:01:16,280 83 | 因为在使用 Redux 时 有些函数实现必须是纯函数 84 | 85 | 22 86 | 00:01:16,280 --> 00:01:18,280 87 | 你需要留意这一点 -------------------------------------------------------------------------------- /src/04: -------------------------------------------------------------------------------- 1 | You might have heard that the UI or the view layer is most predictable when it is described as a pure function of the application state. This approach was pioneered by React but is now being picked up by other frameworks, such as Ember and Angular. 2 | 3 | Redux complements this approach with another idea, that the state mutations in your app need to be described as a pure function that takes the previous state and the action being dispatched and returns the next state of your application. 4 | 5 | Inside any Redux application, there is one particular function that takes the state of the whole application and the action being dispatched and returns the next state of the whole application. It is important that it does not modify the state given to it. It has to be pure, so it has to return a new object. 6 | 7 | Even in large applications, there is still just a single function that manages how the next state is calculated based on the previous state of the whole application and the action being dispatched. It does not have to be slow. 8 | 9 | For example, if I change the visibility filter, I have to create a new object for the whole state, but I can keep the reference to the previous version of the todos rate, because it has not changed when I changed the visibility filter. This is what makes Redux fast. 10 | 11 | Now you know the third and the last principle of Redux. To describe state mutations, you have to write a function that takes the previous state of the app, the action being dispatched, and returns the next state of the app. This function has to be pure. This function is called the "Reducer." 12 | -------------------------------------------------------------------------------- /src/15: -------------------------------------------------------------------------------- 1 | In the previous lesson we learned how to use the reducer composition pattern to let different reducers handle different parts of the state tree, and then combine their results. 2 | 3 | In fact this pattern is so common that it's present in most Redux applications. And this is why Redux provides a function called "combineReducers" that lets you avoid writing this code by hand. And instead, it generates the top level reducer for you. 4 | 5 | The only argument to "combineReducers" is an object. And this object lets me specify the mapping between the state field names, and the reducers managing them. The return value of the "combineReducers"'s call is a reducer function, which is pretty much equivalent to the reducer function I wrote by hand previously. 6 | 7 | The keys of the object I configure combined reducers with correspond to the fields of the state object is going to manage. The values of the object I passed to "combineReducers", are the reducers you should call to update the corresponding state fields. 8 | 9 | This "combineReducers" call says that the "todos" field inside the state object managers will be updated by the "todos" reducer, and the "visibilityFilter" field inside the state object will be updated by calling the "visibilityFilter" reducer. And the results will be assembled into a single object. In other words, it behaves pretty much exactly as the function commented down below. 10 | 11 | Finally, I will establish a useful convention. I will always name my reducers after the state keys they manage. Since the key names and the value names are now the same, I can omit the values thanks to the ES6 object literal shorthand notation. 12 | 13 | In this lesson, you learned how to generate a simple reducer that calls many reducers to manage parts of its state by using the "combineReducers" utility function. 14 | -------------------------------------------------------------------------------- /src/01: -------------------------------------------------------------------------------- 1 | The first principle of Redux is whether your app is a really simple one like this counter example, or a complex application with a lot of UI, and change of state, you are going to represent the whole state of your application as a single JavaScript object. 2 | 3 | All mutations, and changes the state in Redux are explicit. It is possible to keep track of all of them. In this case, I am logging every state change in the application in the console. You can see that, in the counter example, there isn't really much state to keep track of so it can be represented by a JavaScript number. 4 | 5 | Here is a different example, a list of independent counters that I can add and remove. In this case, a single number is not enough to represent the state of the application, so we use an array of JavaScript numbers. In a more complex application, there is more state to keep track of. 6 | 7 | This is a typical to-do app, where I can add to-dos, I can cross them as completed ones, and I can change their current filter. Looking back at the history of the state changes, we can see that the initial state of the app was a JavaScript object, containing an array under the to-do string, and a string seen show all, under visible, the filter. 8 | 9 | When I added the first to-do, it was added to the to-dos array, inside our state object. The to-do itself, is described by a plain child script object, saying it was not completed, and the text was saved. Every further change that the app, whether when I was crossing out the to-dos, or when I changed the visibility filter, resulted in this change to this state object, described in our whole application. 10 | 11 | Now you know the first principle of Redux, which is that, everything that changes in your application, including the data and the UI state, is contained in a single object, we call the state or the state tree. 12 | -------------------------------------------------------------------------------- /src/08: -------------------------------------------------------------------------------- 1 | In the simplest counter example, I update the document body manually any time the store state changes. But, of course, this approach does not scale to complex applications. So instead of manually updating the DOM, I'm going to use React. 2 | 3 | I'm adding two script tags corresponding to React and react-dom packages and a root div to render to. Now I can call the ReactDOM.render with my root component. The "render" function is called any time this store state changes, so I can safely pass the current state of this store as a prop to my root component. 4 | 5 | Since this state is held inside the Redux store, the counter component can be a simple function, which is a supported way of declaring components since React 14. 6 | 7 | Now I want to add, decrement, and increment buttons to the component, but I don't want to hard-code the Redux dependency into the component. So I just add "onIncrement" and "onDecrement" props as callbacks. And In my "render" method, I pass the callbacks that call store.dispatch with appropriate actions. Now the application state is updated when I click the buttons. 8 | 9 | Now let's recap how this application works. The counter component is what I call a dumb component. It does not contain any business logic. It only specifies how the current application state transforms into renderable output and how the callbacks, passed via props, are bound to the event handlers. 10 | 11 | When we render a counter, we specify that its value should be taken from the Redux store current state. And when the user presses "increment" or "decrement," we dispatch corresponding action to the Redux store. Our reducer specifies how the next state is calculated based on the current state and the action being dispatched. 12 | 13 | And finally, we subscribe to the Redux store, so our render function runs any time the state changes, so the counter gets the current state. 14 | -------------------------------------------------------------------------------- /translation/26: -------------------------------------------------------------------------------- 1 | In the previous lesson, we implemented the "Provider" component that uses the react advanced context feature to make the store from the props available to every component in our app. 2 | 3 | 在前面的课程中,我们通过使用 React 高级的 context 特性实现了 Provider 组件,使属性中的 store 对我们应用中的每一个组件都有效。 4 | 5 | So if we pass it through the "Provider", we can read it in any other component from the context, which is really convenient for the container components. In fact, this is so convenient that you don't need to actually write the "Provider" yourself, because it is included in a special library called "react-redux". 6 | 7 | 所以如果我们通过 Provider 传递它,我们就可以在其它任意的组件中通过 context 读取它,这对于容器组件来说真的很方便。实际上,由于如此方便,我们不需要真的自己去写 Provider,因为它已经包括在一个特别的 react-redux 库中了。 8 | 9 | And note that it is not the same as Redux. This is a different library. These are React bindings to the Redux library. You can import the "Provider" by destructuring the "ReactRedux" global object in JSbin, or if you use Babbel, and something like NPM, you can import "Provider" with the braces, because it's a named export from "react-redux" package. Or if you write ES5 code, you can write "var Provider=require('react-redux').Provider", or "var Provider=ReactRedux.Provider". 10 | 11 | 而且注意这和 Redux 不同,这是一个不同的库,这些是把 React 绑定到 Redux 的库。你可以通过解构在 JSBin 中的 ReactRedux 全局对象来导入 Provider。或者如果你使用 Babel 和 NPM 之类 (的工具),你可以用大括号导入 Provider,因为它是一个来自 react-redux 包的命名导出。或者如果你写 ES5 代码,你可以写成 "var Provider = require('react-redux').Provider",或者 "var Provider = ReactRedux.Provider"。 12 | 13 | Just like the "Provider" we wrote before, the "Provider" that comes with "react-redux" exposes the store you passed this prop on the context, so the components can specify the context types, and then use this context's store to subscribe to the store updates and dispatch actions. 14 | 15 | 正如我们之前所写的 Provider 那样,来自 react-redux 的 Provider 也暴露你传递到 context 上的 store 属性。所以组件可以指定 context 类型。然后使用这个 context 的 store 来订阅 store 的更新和分送动作。 -------------------------------------------------------------------------------- /src/13: -------------------------------------------------------------------------------- 1 | In the previous lesson we created a reducer that can handle two actions, adding a new todo, and toggling an existing todo. Right now, the code to update the todo item or to create a new one is placed right inside of the todos reducer. 2 | 3 | This function is hard to understand because it makes us two different concerns, how the todos array is updated, and how individual todos are updated. This is not a problem unique to Redux. Any time a function does too many things, you want to extract other functions from it, and call them so that every function only addresses a single concern. 4 | 5 | In this case, I decided that creating and updating a todo in response to an action is a separate operation, and needs to be handled by a separate function called "todo". As a matter of convention, I decided that it should also accept two arguments, the current state and the action being dispatched, and it should return the next state. 6 | 7 | But in this case, this state refers to the individual todo, and not to the list of todos. Finally, there is no magic in Redux to make it work. We extracted the "todo" reducer from the "todos" reducer, so now we need to call it for every todo, and assemble the results into an array. 8 | 9 | While this is not required in this particular example, I suggest that you always have the default case where you return the current state to avoid all bugs in the future. The part described in this lesson is pervasive in Redux's development, and is called reducer composition. 10 | 11 | Different reducers specify how different parts of the state tree are updated in response to actions. Reducers are also normal JavaScript functions, so they can call other reducers to delegate and abstract a way handling of updates of some parts of this tree they manage. 12 | 13 | This pattern can be applied many times, and while there is still a single top level reducer managing the state of your app, you will find it convenient to express it as many reducers call on each other, each contributing to a part of the application state tree. 14 | -------------------------------------------------------------------------------- /src/07: -------------------------------------------------------------------------------- 1 | In the previous video we looked at how to implement a simple counter example using the "createStore" function provided by Redux and the "store" object it returns that provides the "getState" method to get the current application state, the "dispatch" method, to change the current application state by dispatching an action, and the "subscribe" method to subscribe to the changes and re-render our application with the current state of the app. 2 | 3 | If you're like me you prefer to understand the tools that you're using. In this tutorial we're going to re-implement the "createStore" function provided by Redux from scratch. The first and the only, from what we know so far, argument to the "createStore" function is the reducer function provided by the application. 4 | 5 | And we know that the store holds the current state. We keep it in a variable, and the "getState" function is going to return the current value of that variable. This function, combined with the "dispatch" function and the "subscribe" function on a single object is what we call the Redux store. 6 | 7 | Because the "subscribe" function can be called many times, we need to keep track of all the change listeners. And any time it is called we want to push the new listener into the array. Dispatching an action is the only way to change the internal state. 8 | 9 | And in order to calculate the new state we call the reducer with the current state and the action being dispatched. After the state was updated, we need to notify every change listener, by calling it. 10 | 11 | There is an important missing piece here. We haven't provided a way to unsubscribe a listener. But instead of adding a dedicated "unsubscribe" method, we'll just return a function from the "subscribe" method that removes this listener from the "listeners" array. 12 | 13 | Finally, by the time the store is returned we wanted to have the initial state populated. So we're going to dispatch a dummy action just to get the reducer to return the initial value. 14 | 15 | This implementation of the Redux store, apart from a few minor details in edge cases, is the createStore which was shipped with Redux. 16 | -------------------------------------------------------------------------------- /src/05: -------------------------------------------------------------------------------- 1 | The first function we're going to write is the reducer for the counter example. And reducer accepts state and action as arguments and returns the next state. But before jumping into the implementation, we're going to make certain assertions using Michael Jackson's Expect library. We're going to assert that when the state of the counter is 0 and you pass an "INCREMENT" action it should return 1. And similarly it should return 2 when this state is 1 and you increment. 2 | 3 | We're going to add a couple of tests that test how "DECREMENT" works, which is that it decrements from 2 to 1 and from 1 to 0 and we're going to add a log to tell if our tests are successful. 4 | 5 | So if you run this test, they're actually going to fail because we haven't even begun to implement our reducer. We're going to start by checking the action type and if the action type is "INCREMENT", we're going to return state plus 1, but if it is "DECREMENT" we're going to return state minus 1. 6 | 7 | If you run the tests we will find that this is enough to get them to pass. However, there are still some flaws in our current implementation of the counter reducer. For example, I think that if we dispatch an action that it does not understand, it should return the current state of the application. 8 | 9 | However, if we check for that we will see that this test fails, because we currently don't handle unknown actions. So I'm going to add an else clause that returns the current state. And the tests pass now. 10 | 11 | Another issue is that while the reducer is normally in control of the application state, currently it does not specify the initial state. In the case of counter example that would be 0. The convention we use in Redux is that if the reducer receives "undefined" as the state argument, it must return what it considers to be the initial state of the application. In this case it will be 0. 12 | 13 | Now come a few cosmetic tweaks. I'll replace this bunch of tweaks with a switch statement and I'm going to replace this condition with ES6 default argument, which looks better. I'm also going to replace the function declaration with an arrow function, which has clearer semantics in ES6. 14 | -------------------------------------------------------------------------------- /eng/03.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,420 --> 00:00:02,210 3 | Before we proceed any further 4 | 5 | 2 6 | 00:00:02,210 --> 00:00:08,140 7 | it's important that you understand the difference between the pure and impure functions 8 | 9 | 3 10 | 00:00:08,140 --> 00:00:16,190 11 | The pure functions are the functions whose returned value depends solely on the values of their arguments 12 | 13 | 4 14 | 00:00:16,190 --> 00:00:20,350 15 | Pure functions do not have any observable side effects 16 | 17 | 5 18 | 00:00:20,350 --> 00:00:23,130 19 | such as network or database calls 20 | 21 | 6 22 | 00:00:23,560 --> 00:00:26,560 23 | The pure functions just calculate the new value 24 | 25 | 7 26 | 00:00:26,560 --> 00:00:30,510 27 | You can be confident that if you call the pure function 28 | 29 | 8 30 | 00:00:30,510 --> 00:00:33,050 31 | with the same set of arguments 32 | 33 | 9 34 | 00:00:33,050 --> 00:00:35,740 35 | you're going to get the same returned value 36 | 37 | 10 38 | 00:00:35,740 --> 00:00:37,180 39 | They are predictable 40 | 41 | 11 42 | 00:00:37,740 --> 00:00:42,330 43 | Also pure functions do not modify the values passed to them 44 | 45 | 12 46 | 00:00:42,330 --> 00:00:43,320 47 | For example 48 | 49 | 13 50 | 00:00:43,320 --> 00:00:49,200 51 | squareAll function that accepts an array does not overwrite the items inside this array 52 | 53 | 14 54 | 00:00:49,200 --> 00:00:53,070 55 | Instead it returns a new array by using items map 56 | 57 | 15 58 | 00:00:54,220 --> 00:00:55,260 59 | On the opposite 60 | 61 | 16 62 | 00:00:55,260 --> 00:00:59,640 63 | impure functions may call the database or the network 64 | 65 | 17 66 | 00:00:59,760 --> 00:01:01,550 67 | they may have side effects 68 | 69 | 18 70 | 00:01:01,550 --> 00:01:03,310 71 | they may operate on the DOM 72 | 73 | 19 74 | 00:01:03,310 --> 00:01:06,920 75 | and they may override the values that you pass to them 76 | 77 | 20 78 | 00:01:08,250 --> 00:01:10,780 79 | This is going to be an important distinction 80 | 81 | 21 82 | 00:01:10,780 --> 00:01:16,280 83 | because some of the functions that you're going to write in Redux have to be pure 84 | 85 | 22 86 | 00:01:16,280 --> 00:01:18,280 87 | and you need to be mindful of that 88 | 89 | -------------------------------------------------------------------------------- /src/12: -------------------------------------------------------------------------------- 1 | In this lesson, we will continue creating the reducer for Todo list application. The only action that this reducer currently handles is called 'ADD_TODO'. We also created a test that makes sure that when the reducer is called with an empty array as a state and the 'ADD_TODO' action, it returns an array with a single Todo element. 2 | 3 | In this lesson, we will follow the same approach to implement another action called 'TOGGLE_TODO'. We're going to start with a test again. This time, we're testing a different action and we have a different initial state. The state before calling the reducer now includes two different todos with id 0 and 1. Notice how both of them have their completed fields set to false. 4 | 5 | Next, I declare the action. The action is an object with the type property which is a 'TOGGLE_TODO' string and the id of the todo that I want to be toggled. I declare the state that I expect to receive after calling the reducer. And it's pretty much the same as before calling the reducer. However, I expect the todo with the id specified in the action or 1 in this case. The change is completed field. 6 | 7 | The reducer must be a pure function. So it is a matter of precaution, I called freeze on the state and the action. Finally, just like in the previous lesson, I'm asserting that the result of calling my reducer with the state before and the action is going to be deeply equal to the state after. 8 | 9 | Now my test is a function, so I need to call it at the end of the file. And if I run it, it fails because I have not implemented handling this action yet. 10 | 11 | I'm adding a new switch case to my reducer. And I remember that I shouldn't change the original array, so I'm using the array map method to produce a new array. 12 | 13 | The function I pass as an argument will be called for every todo. So if it's not a todo I'm looking for, I don't want to change it. I just return it as is. However, if the todo is the one we want to toggle, I'm going to return a new object that has all the properties of the original todo object thanks to the object spread operator, but also an inverted value of the completed field. 14 | 15 | Now both of our tests run successfully. And we have an implementation of the reducer that can add and toggle todos. 16 | -------------------------------------------------------------------------------- /src/02: -------------------------------------------------------------------------------- 1 | The second principle of Redux is that the state tree is read only. You cannot modify or write to it. Instead, anytime you want to change the state, you need to dispatch an action. 2 | 3 | An action is a plain JavaScript object describing the change. Just like the state is the minimal representation of the data in your app, the action is the minimal representation of the change to that data. 4 | 5 | The structure of the action object is up to you. The only requirement is that it has a tie property, which is not undefined. We suggest using strings, because they are serializable. 6 | 7 | In different apps, you're going to have different types of actions. For example, in a counter app we only have increment and decrement actions. We don't pass any additional information, because this is all that is needed to describe these changes. 8 | 9 | But say, for a counter release example, we have more actions. We have add counter action, we have a remove counter action, and anytime I change the individual counter, you can see that the increment and the decrement actions now have index. Because we need to describe which particular counter was changed. 10 | 11 | This approach scales well to medium and complex applications. Anytime I add a todo, the components don't really know how exactly it's been added. All they know is that they need to dispatch an action with a type, add todo, and the text of the todo and a sequential ID. 12 | 13 | If I toggle a todo, again, the components don't know how it happens. All they need to do is to dispatch an action with a type, toggle todo and pass in the ID of the todo I want to toggle. 14 | 15 | The same is true for the visibility filter. Anytime I click on this control to change the currently visible todos, what really happens is this component dispatches an action with a type, set visibility filter, and pass in the desired filter string, filter filled. 16 | 17 | But these are all plain objects, describing what happens in a wrap. 18 | 19 | Now you know the second principle of Redux -- the state is read only. The only way to change the state tree is by dispatching an action. An action is a plain JavaScript object, describing in the minimal way what changed in the application. Whether it is initiated by a network request or by user interaction, any data that gets into the Redux application gets there by actions. 20 | -------------------------------------------------------------------------------- /src/10: -------------------------------------------------------------------------------- 1 | Like in previous example, I use "expect" and "deep-freeze" libraries from NPM to make my test assertions. And this time, I'm testing a function called "toggleTodo" that takes our "todo" object and flips its "completed" field. If "completed" was "false", it should be "true" in the return value. If it was "true", it should be "false". 2 | 3 | Just like in the previous lesson, I'm going to start by writing a mutated version that passes the current test. So, A mutated version just flips the "completed" field, reassigns it on the passed object. 4 | 5 | While it works, we know that mutations are not allowed in Redux. So to enforce this, I'm calling "deepFreeze" on my to-do object. I'm not allowed to change its "completed" field anymore. 6 | 7 | One way out of this would be to create the new object with every field copied from the original object except the "completed" field, which would be flipped. However, if we later add new properties to the new object, we might forget to update this piece of code to include them. 8 | 9 | This is why I suggest you to use "Object.assign" method, which is new to ES6. It lets you assign properties of several objects onto the target object. Note how the "Object.assign" argument order corresponds to that of the JavaScript assignment operator. 10 | 11 | The left argument is the one whose properties are going to be assigned, so it's going to be mutated. This is why we're passing an empty object as the first argument, so we don't mutate any existing data. Every further argument to "Object .assign" will be considered one of the source objects whose properties will be copied to the target object. 12 | 13 | It is important that if several sources specify different values for the same property, the last one wins. And this is what we use to override the completed field despite what the original "todo" object says. 14 | 15 | Finally, you need to remember that "Object.assign" is a new method in ES6, so it is not natively available in all the browsers. You should use a polyfill, either the one that ships with Babel or a standalone "Object.assign" polyfill, to use it without risking crashing your website. 16 | 17 | Another option that doesn't require a polyfill is to use the new object spread operator, which is not part of ES6. However, it is proposed for ES7. It is fairly popular, and it is enabled in Babel if you use the "stage-2" preset. 18 | -------------------------------------------------------------------------------- /src/06: -------------------------------------------------------------------------------- 1 | I added Redux to our application as a script act from CDNJS. This is the UMD build, so it exports a single global variable called Redux, with a capital R. And in real applications, I suggest you to use NPM instead and a module bundler like Webpack or Browserify, but the UMD build will suffice for our example. 2 | 3 | I'm going to need just a single function from Redux called "createStore". I'm using ES6 destruction syntax here. It's equivalent to writing, "var createStore = Redux.createStore;" or, if you use NPM and something like Babel to transpile your ES6, you can write, "import { createStore }" notice the parenthesis, "from Redux". 4 | 5 | This store binds together the three principles of Redux. It holds the current application's state object. It lets you dispatch actions. When you create it, you need to specify the reducer that tells how state is updated with actions. 6 | 7 | In this example, we're calling "createStore" with counter as the reducer that manages the state updates. This store has three important methods. 8 | 9 | The first method of this store is called "getState". And it retrieves the current state of the Redux store. If we run this, we're going to see zero because this is the initial state of our application. 10 | 11 | The second and the most commonly used store method is called "dispatch". And it lets you dispatch actions to change the state of your application. If we log this store state after dispatch, we're going to see that it has changed. 12 | 13 | Of course, always log to the console gets boring, so we're actually going to render something to the body now, with the help of the third Redux store method, called "subscribe". It lets you register a callback that the Redux store will call any time an action has been dispatched, so then you can update the UI of your application. It will reflect the current application state. 14 | 15 | I'm being very naive now. I'm not using React or anything. I'm just rendering the counter into the document body. Any time the body is clicked, I'm going to dispatch an action to increment this counter. 16 | 17 | Now if you pay close attention, you will notice that the initial state, the 0, was not rendered. And this is because I'm rendering inside the subscribe callback, but it doesn't actually fire the very first time. 18 | 19 | So I extract this logic into "render" method. I subscribe the "render" method to this store. I also call it once to render the initial state. Now it renders the 0, and the click increments the counter. This is our first working Redux application. 20 | -------------------------------------------------------------------------------- /src/14: -------------------------------------------------------------------------------- 1 | In the previous lesson, we established the pattern of reducer composition where one reducer can be called by another reducer to update items inside an array. 2 | 3 | If we create a store with this reducer and log its state, we will find that the initial state of it is an empty array of todos. If we dispatch an 'ADD_TODO' action, we will find that the corresponding todo has been added to the state array. 4 | 5 | If we dispatch another 'ADD_TODO' action, the corresponding todo will also be added at the end of the array, and dispatch a 'TOGGLE_TODO' action with id 0 will flip the completed field of the todo with id 0. 6 | 7 | Representing the whole state of the application as an array of todos works for a simple example, but what if we want to store more information? For example, we may want to let the user choose which todos are currently visible with the visibility filter such as 'SHOW_COMPLETED', 'SHOW_ALL', or 'SHOW_ACTIVE'. 8 | 9 | The state of the visibility filter is a simple string representing the current filter. It is changed by 'SET_VISIBILITY_FILTER' action. 10 | 11 | To store this new information, I don't need to change the existing reducers. I will use the reducer composition pattern and create a new reducer that calls the existing reducers to manage parts of its state and combines the results in a single state object. 12 | 13 | Now that the first time it runs, it will pass undefined as the state of the child reducers because the initial state of the combined reducer is an empty object, so all its fields are undefined. This gets the child reducers to return their initial states and populates the state object for the first time. 14 | 15 | When an action comes in, it calls the reducers with the pass of the state that they manage and the action and combines the results into the new state object. 16 | 17 | This is another example of the reducer composition pattern, but this time we use it to combine several reducers into a single reducer that we will now use to create our store. The initial state of the combined reducer now contains the initial states of independent reducers. Any time an action comes in, those reducers handle the action independently. 18 | 19 | This pattern helps scale Redux's development because different people on the team can work on different reducers handling the same actions without running into each other and causing merge conflicts. 20 | 21 | Finally, I'm dispatching the 'SET_VISIBILITY_FILTER' action. You can see that it doesn't affect the todos, but the "visibilityFilter" field has been updated. 22 | -------------------------------------------------------------------------------- /translation/15: -------------------------------------------------------------------------------- 1 | In the previous lesson we learned how to use the reducer composition pattern to let different reducers handle different parts of the state tree, and then combine their results. 2 | 在上一课中,我们学习了如何使用 reducer 组合模式来让不同的 reducer 处理状态树的不同部分,然后再合并它们的结果。 3 | 4 | In fact this pattern is so common that it's present in most Redux applications. 5 | 实际上,这种模式是如此的常见,以至于在大部分 Redux 应用中它都存在。 6 | 7 | And this is why Redux provides a function called "combineReducers" that lets you avoid writing this code by hand. 8 | 而这就是为什么,Redux 提供了一个 combineReducers 函数,来让你不用自己从头写这段代码。 9 | 10 | And instead, it generates the top level reducer for you. 11 | combineReducers 函数会帮你生成这个最高阶的 reducer。 12 | 13 | The only argument to "combineReducers" is an object. 14 | combineRecuers 函数的唯一参数是一个对象。 15 | 16 | And this object lets me specify the mapping between the state field names, and the reducers managing them. 17 | 这个对象让我可以声明,状态对象的字段的名称,以及对应的管理该字段的 reducer 之间的映射关系。 18 | 19 | The return value of the "combineReducers"'s call is a reducer function, which is pretty much equivalent to the reducer function I wrote by hand previously. 20 | combineReducers 函数的返回值是一个 reducer 函数,它基本上等价于我之前手写的 reducer 函数。 21 | 22 | The keys of the object I configure "combineReducers" with correspond to the fields of the state object is going to manage. 23 | 我传递给 combineReducers 的对象的键,对应了它所返回的 reducer 所要管理的状态对象的字段。 24 | 25 | The values of the object I passed to "combineReducers", are the reducers you should call to update the corresponding state fields. 26 | 我传递给 combineReducers 的对象的值,就是更新对应的状态字段时所需要调用的 reducer。 27 | 28 | This "combineReducers" call says that the "todos" field inside the state object managers will be updated by the "todos" reducer, and the "visibilityFilter" field inside the state object will be updated by calling the "visibilityFilter" reducer. 29 | 这个 combineReducers 函数调用的意思是,状态对象的 todos 属性会被 todos 这个 reducer 更新,而状态对象的 visibilityFilter 属性会被 visibilityFilter 这个 reducer 更新。 30 | 31 | And the results will be assembled into a single object. 32 | 然后这些更新的结果会被合并到一个单一的对象里。 33 | 34 | In other words, it behaves pretty much exactly as the function commented down below. 35 | 换句话说,它的行为基本上就像下面注释里所写的一样。 36 | 37 | Finally, I will establish a useful convention. 38 | 最后,我要提出一个非常有用的约定。 39 | 40 | I will always name my reducers after the state keys they manage. 41 | 在任何情况下,我都会以 reducer 所要管理的状态对象的键来命名我的 reducer。 42 | 43 | Since the key names and the value names are now the same, I can omit the values thanks to the ES6 object literal shorthand notation. 44 | 这样,归功于 ES6 的对象属性的简洁表示法,我就可以忽略掉值,因为键和值的名字都是相同的。 45 | 46 | In this lesson, you learned how to generate a simple reducer that calls many reducers to manage parts of its state by using the "combineReducers" utility function. 47 | 在本课中,你学习了如何使用 combineReducers 工具函数,来生成一个简单的 reducer,这个 reducer 会调用多个 reducer 来分别管理状态树的某个部分。 48 | 49 | -------------------------------------------------------------------------------- /chn/04.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,520 --> 00:00:01,600 3 | 你大概已经听说过了 4 | 5 | 2 6 | 00:00:01,740 --> 00:00:06,700 7 | 当被描述成一个应用的状态的纯函数时 8 | 9 | 3 10 | 00:00:06,700 --> 00:00:09,240 11 | UI 或者视图层是最为明确的 12 | 13 | 4 14 | 00:00:10,020 --> 00:00:12,360 15 | 这一方法最先被 React 所提出 16 | 17 | 5 18 | 00:00:12,360 --> 00:00:14,440 19 | 如今其他框架也都逐渐开始学习这一方法 20 | 21 | 6 22 | 00:00:14,440 --> 00:00:16,300 23 | 例如 Ember 和 Angular 24 | 25 | 7 26 | 00:00:17,500 --> 00:00:20,880 27 | Redux 则通过另一个点子完善了这一方法 28 | 29 | 8 30 | 00:00:21,100 --> 00:00:24,200 31 | 那就是 应用中的状态变化 32 | 33 | 9 34 | 00:00:24,280 --> 00:00:26,940 35 | 需要被描述成一个纯函数 36 | 37 | 10 38 | 00:00:26,940 --> 00:00:31,120 39 | 这个函数会接受前一个状态和被分发动作 40 | 41 | 11 42 | 00:00:31,120 --> 00:00:35,220 43 | 然后返回应用的下一个状态 44 | 45 | 12 46 | 00:00:36,080 --> 00:00:38,820 47 | 在任何一个 Redux 应用中 48 | 49 | 13 50 | 00:00:38,820 --> 00:00:40,980 51 | 都存在这么一个特定的函数 52 | 在任何一个 Redux 应用中 53 | 54 | 14 55 | 00:00:40,980 --> 00:00:45,860 56 | 它以整个应用的状态和被分发的动作为参数 57 | 58 | 15 59 | 00:00:46,000 --> 00:00:49,680 60 | 返回应用的下一个状态 61 | 62 | 16 63 | 00:00:49,680 --> 00:00:54,040 64 | 很重重要的一点是 它没有更改传入的状态 65 | 66 | 17 67 | 00:00:54,040 --> 00:00:55,340 68 | 它必须得是纯函数 69 | 70 | 18 71 | 00:00:58,400 --> 00:00:59,500 72 | 所以它必然会返回一个新的状态 73 | 74 | 19 75 | 00:00:59,500 --> 00:01:01,700 76 | 即使在大型应用中 77 | 78 | 20 79 | 00:01:01,940 --> 00:01:04,100 80 | 这样的函数也只有一个 81 | 82 | 21 83 | 00:01:04,460 --> 00:01:05,420 84 | 这个函数描述 85 | 86 | 22 87 | 00:01:11,380 --> 00:01:11,460 88 | 如何根据应用的上一个状态和被分发的动作 89 | 90 | 23 91 | 00:01:11,460 --> 00:01:13,460 92 | 计算出下一个状态 93 | 94 | 24 95 | 00:01:14,200 --> 00:01:15,780 96 | 这个函数并不一定会影响性能 97 | 98 | 25 99 | 00:01:15,960 --> 00:01:19,060 100 | 比如 如果我更改了过滤器标签 101 | 102 | 26 103 | 00:01:19,060 --> 00:01:22,300 104 | 我需要为整个应用的状态创建一个新的对象 105 | 106 | 27 107 | 00:01:22,300 --> 00:01:27,480 108 | 但是我仍然可以保留对之前 todos 数组的引用 109 | 110 | 28 111 | 00:01:27,480 --> 00:01:29,220 112 | 因为在我更改过滤器标签时 113 | 114 | 29 115 | 00:01:29,220 --> 00:01:31,440 116 | 这个数组并没有被改变 117 | 因为在我更改过滤器标签时 118 | 119 | 30 120 | 00:01:31,440 --> 00:01:31,600 121 | 因为在我更改过滤器标签时 122 | 123 | 31 124 | 00:01:31,600 --> 00:01:34,820 125 | 这就是 Redux 性能很好的原因 126 | 127 | 32 128 | 00:01:34,820 --> 00:01:38,520 129 | 现在你已经了解了 Redux 的第三个原则 130 | 131 | 33 132 | 00:01:38,860 --> 00:01:40,540 133 | 为了描述状态的变化 134 | 135 | 34 136 | 00:01:40,540 --> 00:01:40,560 137 | 你需要创建一个函数 138 | 为了描述状态的变化 139 | 140 | 35 141 | 00:01:40,560 --> 00:01:42,060 142 | 你需要创建一个函数 143 | 144 | 36 145 | 00:01:42,220 --> 00:01:45,840 146 | 它接受应用的上一个状态和被分发的动作 147 | 148 | 37 149 | 00:01:45,840 --> 00:01:48,360 150 | 然后返回应用的下一个状态 151 | 152 | 38 153 | 00:01:48,360 --> 00:01:50,460 154 | 而这个函数必须是一个纯函数 155 | 156 | 39 157 | 00:01:51,000 --> 00:01:53,520 158 | 它被称作 reducer 159 | 160 | -------------------------------------------------------------------------------- /chn/15.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,250 --> 00:00:01,370 3 | 在上一课中 4 | 5 | 2 6 | 00:00:01,370 --> 00:00:04,570 7 | 我们学习了如何使用 reducer 组合模式 8 | 9 | 3 10 | 00:00:04,570 --> 00:00:08,950 11 | 来让不同的 reducer 处理状态树的不同部分 12 | 13 | 4 14 | 00:00:08,950 --> 00:00:08,970 15 | 然后再合并它们的结果 16 | 来让不同的 reducer 处理状态树的不同部分 17 | 18 | 5 19 | 00:00:08,970 --> 00:00:11,170 20 | 然后再合并它们的结果 21 | 22 | 6 23 | 00:00:12,370 --> 00:00:14,800 24 | 实际上 这种模式是如此的常见 25 | 26 | 7 27 | 00:00:14,800 --> 00:00:18,020 28 | 以至于在大部分 Redux 应用中它都存在 29 | 30 | 8 31 | 00:00:18,070 --> 00:00:22,700 32 | 而这就是为什么 Redux 提供了一个 combineReducers 函数 33 | 34 | 9 35 | 00:00:22,820 --> 00:00:26,070 36 | 来让你不用自己从头写这段代码 37 | 38 | 10 39 | 00:00:26,120 --> 00:00:30,270 40 | combineReducers 函数会帮你生成这个最高阶的 reducer 41 | 42 | 11 43 | 00:00:30,970 --> 00:00:34,400 44 | combineRecuers 函数的唯一参数是一个对象 45 | 46 | 12 47 | 00:00:34,520 --> 00:00:37,450 48 | 这个对象让我可以声明 49 | 50 | 13 51 | 00:00:37,450 --> 00:00:39,450 52 | 状态对象的字段的名称 53 | 54 | 14 55 | 00:00:39,450 --> 00:00:41,650 56 | 以及对应的管理该字段的 reducer 之间的映射关系 57 | 58 | 15 59 | 00:00:42,350 --> 00:00:47,070 60 | combineReducers 函数的返回值是一个 reducer 函数 61 | 62 | 16 63 | 00:00:47,070 --> 00:00:52,420 64 | 它基本上等价于我之前手写的 reducer 函数 65 | 66 | 17 67 | 00:00:53,620 --> 00:00:57,800 68 | 我传递给 combineReducers 的对象的键 69 | 70 | 18 71 | 00:00:57,970 --> 00:01:02,400 72 | 对应了它所返回的 reducer 所要管理的状态对象的字段 73 | 74 | 19 75 | 00:01:04,170 --> 00:01:07,800 76 | 我传递给 combineReducers 的对象的值 77 | 78 | 20 79 | 00:01:07,900 --> 00:01:12,970 80 | 就是更新对应的状态字段时所需要调用的 reducer 81 | 82 | 21 83 | 00:01:14,070 --> 00:01:16,500 84 | 这个 combineReducers 函数调用的意思是 85 | 86 | 22 87 | 00:01:16,500 --> 00:01:22,250 88 | 状态对象的 todos 属性会被 todos 这个 reducer 更新 89 | 90 | 23 91 | 00:01:22,350 --> 00:01:25,570 92 | 而状态对象的 visibilityFilter 属性 93 | 94 | 24 95 | 00:01:25,570 --> 00:01:29,570 96 | 会被 visibilityFilter 这个 reducer 更新 97 | 98 | 25 99 | 00:01:29,670 --> 00:01:33,220 100 | 然后这些更新的结果会被合并到一个单一的对象里 101 | 102 | 26 103 | 00:01:34,050 --> 00:01:38,950 104 | 换句话说 它的行为基本上就像下面注释里所写的一样 105 | 106 | 27 107 | 00:01:41,020 --> 00:01:43,920 108 | 最后 我要提出一个非常有用的约定 109 | 110 | 28 111 | 00:01:44,470 --> 00:01:46,720 112 | 在任何情况下 我都会以 reducer 所要管理的状态对象的键 113 | 114 | 29 115 | 00:01:46,720 --> 00:01:48,820 116 | 来命名我的 reducer 117 | 118 | 30 119 | 00:01:49,570 --> 00:01:53,000 120 | 这样 归功于 ES6 的对象属性的简洁表示法 121 | 122 | 31 123 | 00:01:53,000 --> 00:01:54,670 124 | 我就可以忽略掉值 125 | 126 | 32 127 | 00:01:54,720 --> 00:01:58,650 128 | 因为键和值的名字都是相同的 129 | 130 | 33 131 | 00:02:00,200 --> 00:02:01,070 132 | 在本课中 133 | 134 | 34 135 | 00:02:01,070 --> 00:02:04,150 136 | 你学习了如何使用 combineReducers 工具函数 137 | 138 | 35 139 | 00:02:04,150 --> 00:02:06,050 140 | 来生成一个简单的 reducer 141 | 142 | 36 143 | 00:02:06,050 --> 00:02:11,620 144 | 这个 reducer 会调用多个 reducer 来分别管理状态树的某个部分 145 | 146 | -------------------------------------------------------------------------------- /chn/08.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,320 --> 00:00:02,340 3 | 在这个最简单的计数器例子中 4 | 5 | 2 6 | 00:00:02,340 --> 00:00:04,720 7 | 每当 store 的状态改变时 8 | 9 | 3 10 | 00:00:04,720 --> 00:00:07,020 11 | 我会手动更新 document.body 12 | 13 | 4 14 | 00:00:07,160 --> 00:00:11,440 15 | 但是 这种方法当然不能扩展到复杂的应用中 16 | 17 | 5 18 | 00:00:11,580 --> 00:00:14,340 19 | 所以我准备使用 React 20 | 21 | 6 22 | 00:00:14,340 --> 00:00:16,340 23 | 而不是手动更新 DOM 24 | 25 | 7 26 | 00:00:16,960 --> 00:00:18,920 27 | 我添加了两个 script 标签 28 | 29 | 8 30 | 00:00:18,920 --> 00:00:21,660 31 | 分别对应 react 和 react-dom 这两个包 32 | 33 | 9 34 | 00:00:21,660 --> 00:00:23,760 35 | 我还添加了一个 id 为 "root" 的 div 标签 用来渲染 React 部件 36 | 37 | 10 38 | 00:00:23,860 --> 00:00:28,180 39 | 现在我就可以用我的根部件来调用 ReactDOM.render() 了 40 | 41 | 11 42 | 00:00:29,100 --> 00:00:33,040 43 | render() 函数在每一次 store 的状态改变时都会被调用 44 | 45 | 12 46 | 00:00:33,160 --> 00:00:36,380 47 | 所以我可以放心地将 store 的当前状态 48 | 49 | 13 50 | 00:00:36,380 --> 00:00:38,580 51 | 以属性的形式传递给根部件 52 | 53 | 14 54 | 00:00:39,340 --> 00:00:42,520 55 | 因为这个状态储存在 Redux store 里面 56 | 57 | 15 58 | 00:00:42,620 --> 00:00:45,860 59 | 所以 counter 部件可以是一个简单的函数 60 | 61 | 16 62 | 00:00:46,000 --> 00:00:50,400 63 | 这是从 React 14 开始就支持的一种声明部件的方式 64 | 65 | 17 66 | 00:00:51,400 --> 00:00:56,000 67 | 现在我想在部件里加上 + 和 - 按钮 68 | 69 | 18 70 | 00:00:56,000 --> 00:01:00,660 71 | 但我不想把 Redux 依赖写死在部件里 72 | 73 | 19 74 | 00:01:00,860 --> 00:01:05,900 75 | 所以我添加了 onIncrement 和 onDecrement 属性作为回调函数 76 | 77 | 20 78 | 00:01:06,120 --> 00:01:07,960 79 | 然后在我的 render() 方法里 80 | 81 | 21 82 | 00:01:08,100 --> 00:01:09,940 83 | 我把回调函数传了进去 84 | 85 | 22 86 | 00:01:09,940 --> 00:01:14,320 87 | 这些回调函数会用合适的动作来调用 store.dispatch() 88 | 89 | 23 90 | 00:01:16,160 --> 00:01:20,360 91 | 现在 当我点击按钮时 应用的状态就会被更新 92 | 93 | 24 94 | 00:01:21,160 --> 00:01:24,200 95 | 让我们来回顾一下应用的整个工作原理 96 | 97 | 25 98 | 00:01:25,080 --> 00:01:28,360 99 | 我把 counter 部件叫作展示(笨拙)部件 100 | 101 | 26 102 | 00:01:28,560 --> 00:01:31,020 103 | 它不包含任何的业务逻辑 104 | 105 | 27 106 | 00:01:31,260 --> 00:01:37,560 107 | 它只说明了当前的应用状态是如何转化成可渲染的输出 108 | 109 | 28 110 | 00:01:37,880 --> 00:01:43,880 111 | 还有通过属性传递进来的回调函数如何绑定到事件处理器上 112 | 113 | 29 114 | 00:01:44,860 --> 00:01:46,680 115 | 当我们渲染一个计数器时 116 | 117 | 30 118 | 00:01:46,820 --> 00:01:51,440 119 | 我们指定了它的值应该来自于 Redux store 的当前状态 120 | 121 | 31 122 | 00:01:51,780 --> 00:01:55,200 123 | 而当用户按下 + 或者 - 时 124 | 125 | 32 126 | 00:01:55,200 --> 00:01:58,640 127 | 我们会分发相应的动作到 Redux store 128 | 129 | 33 130 | 00:01:59,360 --> 00:02:05,240 131 | 我们的 reducer 则指定了如何基于当前状态 132 | 133 | 34 134 | 00:02:05,240 --> 00:02:07,240 135 | 和被分发的动作 计算出下一个状态 136 | 137 | 35 138 | 00:02:07,460 --> 00:02:10,400 139 | 最后 我们订阅到 Redux store 140 | 141 | 36 142 | 00:02:10,480 --> 00:02:15,040 143 | 这样我们的 render() 函数就可以在每次状态变化时运行 144 | 145 | 37 146 | 00:02:15,040 --> 00:02:17,780 147 | 让计数器获取到它的当前状态 148 | 149 | -------------------------------------------------------------------------------- /src/18: -------------------------------------------------------------------------------- 1 | In the last lesson, we implemented a simple UI for the todo list application that is able to add new todos and view the existing todos in the list. 2 | 3 | To add the todos, we dispatched the "ADD_TODO" action. In this lesson, we're going to dispatch the "TOGGLE_TODO" action to toggle the completed state of the todos by clicking on them. 4 | 5 | I'm scrolling down to my React component. And I've got a list item here corresponding to the todo, so I'm adding the "onClick" handler. So when the user clicks on the list item, I want to dispatch an action to my store with a type "TOGGLE_TODO" and the id of the todo being toggled, which I get from the todo object. 6 | 7 | The event handler knows which todo it corresponds to, so it is able to pass its id in the action. 8 | 9 | In the user interface, I want the completed todos to appear crossed out, so I'm adding this style attribute to the list item. And I'm going to use the "textDecoration" property, which is going to be a "line-through" when todo completed is true, and "none" when todo completed is false, so I get a normal looking todo. 10 | 11 | Now, if I add a couple of todos, I can click on them and they're going to appear toggled, and I can toggle them back. Isn't that satisfying? 12 | 13 | Let's recap how toggling the todo actually works. 14 | 15 | It starts with me dispatching the "TOGGLE_TODO" action inside my click handler, with a type "TOGGLE_TODO" and the id, which is the id of the todo being rendered. I get the todo object as an argument to the array map callback inside my render method where I render all the todos. 16 | 17 | When an action is dispatched, the store will call the root reducer, which will call the "todos" reducer with the array of todos and the action. In this case, the action type is "TOGGLE_TODO", so the "todos" reducer delegates handling of every todo to the "todo" reducer with a "map" function to call it for every todo item. So the "todo" reducer receives the todo as state, and the action. 18 | 19 | Again, we switch on the action type, and it matches "TOGGLE_TODO" string. And now, for every todo whose id does not match the id specified in the action, we just return the previous state, that is the todo object, as it was. 20 | 21 | However, if the id of the todo matches the one specified in the action, we're going to return a new object with all the properties of the original todo, but with the "completed" field equal to the opposite value of what it was. 22 | 23 | The updated todo item will be included in the "todos" field under the new application state. And because we subscribe the render function is going to get the next state of the application in "store.getState()" and pass the new version of the todos to the "TodoApp" component, which is going to render the updated todos. 24 | 25 | Finally, this style of the list item, the bands on the todo completed field, which we just updated, which is why it re-renders in a cross-child state. 26 | -------------------------------------------------------------------------------- /translation/08: -------------------------------------------------------------------------------- 1 | In the simplest counter example, I update the document body manually any time the store state changes. 2 | 在这个最简单的计数器例子中,每当 store 的状态改变时,我会手动更新 document.body。 3 | 4 | But, of course, this approach does not scale to complex applications. 5 | 但是,这种方法当然不能扩展到复杂的应用中。 6 | 7 | So instead of manually updating the DOM, I'm going to use React. 8 | 所以我准备使用 React,而不是手动更新 DOM。 9 | 10 | 11 | I'm adding two script tags corresponding to React and react-dom packages and a root div to render to. 12 | 我添加了两个 script 标签,分别对应 react 和 react-dom 这两个包。我还添加了一个 id 为 "root" 的 div 标签,用来渲染我的 React 部件。 13 | 14 | Now I can call the ReactDOM.render with my root component. 15 | 现在我就可以用我的根部件来调用 ReactDOM.render() 了。 16 | 17 | 18 | The "render" function is called any time this store state changes, so I can safely pass the current state of this store as a prop to my root component. 19 | render() 函数在每一次 store 的状态改变时都会被调用,所以我可以放心地将 store 的当前状态以属性的形式传递给根部件。 20 | 21 | Since this state is held inside the Redux store, the counter component can be a simple function, which is a supported way of declaring components since React 14. 22 | 因为这个状态储存在 Redux store 里面,所以 counter 部件可以是一个简单的函数,这是从 React 14 开始就支持的一种声明部件的方式。 23 | 24 | Now I want to add, decrement, and increment buttons to the component, but I don't want to hard-code the Redux dependency into the component. 25 | 现在我想在部件里加上 + 和 - 按钮,但我不想把 Redux 依赖写死在部件里。 26 | 27 | So I just add "onIncrement" and "onDecrement" props as callbacks. 28 | 所以我添加了 onIncrement 和 onDecrement 属性作为回调函数。 29 | 30 | And In my "render" method, I pass the callbacks that call store.dispatch with appropriate actions. 31 | 然后在我的 render() 方法里,我把回调函数传了进去,这些回调函数会用合适的动作来调用 store.dispatch()。 32 | 33 | Now the application state is updated when I click the buttons. 34 | 现在,当我点击按钮时,应用的状态就会被更新。 35 | 36 | Now let's recap how this application works. 37 | 让我们来回顾一下应用的整个工作原理。 38 | 39 | The counter component is what I call a dumb component. 40 | 我把 counter 部件叫作展示(笨拙)部件。 41 | 42 | It does not contain any business logic. 43 | 它不包含任何的业务逻辑。 44 | 45 | It only specifies how the current application state transforms into renderable output and how the callbacks, passed via props, are bound to the event handlers. 46 | 它只说明了当前的应用状态是如何转化成可渲染的输出,还有通过属性传递进来的回调函数如何绑定到事件处理器上。 47 | 48 | When we render a counter, we specify that its value should be taken from the Redux store current state. 49 | 当我们渲染一个计数器时,我们指定了它的值应该来自于 Redux store 的当前状态。 50 | 51 | 52 | And when the user presses "increment" or "decrement," we dispatch corresponding action to the Redux store. 53 | 而当用户按下 + 或者 - 时,我们会分发相应的动作到 Redux store。 54 | 55 | Our reducer specifies how the next state is calculated based on the current state and the action being dispatched. 56 | 我们的 reducer 则指定了如何基于当前状态和被分发的动作,计算出下一个状态。 57 | 58 | And finally, we subscribe to the Redux store, so our render function runs any time the state changes, so the counter gets the current state. 59 | 最后,我们订阅到 Redux store,这样我们的 render() 函数就可以在每次状态变化时运行,让计数器获取到它的当前状态。 60 | -------------------------------------------------------------------------------- /translation/13: -------------------------------------------------------------------------------- 1 | In the previous lesson we created a reducer that can handle two actions, adding a new todo, and toggling an existing todo. Right now, the code to update the todo item or to create a new one is placed right inside of the todos reducer. 2 | 3 | 在上节课程中,我们创建了一个可以处理两个 action 的 reducer:增加一个新的 todo 和完成/撤销已有的 todo。而现在,这段更新/添加一个 todo 的代码,是放在 todos 这个 reducer 里面的。 4 | 5 | This function is hard to understand because it makes us two different concerns, how the todos array is updated, and how individual todos are updated. This is not a problem unique to Redux. Any time a function does too many things, you want to extract other functions from it, and call them so that every function only addresses a single concern. 6 | 7 | 这个函数令人费解,因为我们要考虑两个不同的问题:todos 数组是如何被更新的;以及每个独立的 todo 是如何被更新的。不仅仅是在 Redux 中有这个问题,任何时候,如果一个函数做了太多事情,你都会想从中提取出其他函数,然后再调用它们,这样每一个函数就只需要解决一个问题。 8 | 9 | In this case, I decided that creating and updating a todo in response to an action is a separate operation, and needs to be handled by a separate function called "todo". As a matter of convention, I decided that it should also accept two arguments, the current state and the action being dispatched, and it should return the next state. 10 | 11 | 在这个例子中,我认为根据动作来创建和更新一个 todo 是个独立的操作,这个操作应该用一个独立的 todo 函数去处理。出于 Redux 的约定,我决定让这个 todo 函数接受两个参数:当前状态和被分发的动作,同时它应该返回下一个状态。 12 | 13 | But in this case, this state refers to the individual todo, and not to the list of todos. Finally, there is no magic in Redux to make it work. We extracted the "todo" reducer from the "todos" reducer, so now we need to call it for every todo, and assemble the results into an array. 14 | 15 | 但是在 todo 这个函数中,状态指向的是某个具体的 todo,而不是整个 todo 的列表。最后,不出意外,在 Redux 中它就可以顺利的运行了。我们已近从 "todos" reducer 中提取了 "todo" reducer,所以现在我们需要对每一个 todo 调用这个函数,然后将结果合并到一个数组当中。 16 | 17 | While this is not required in this particular example, I suggest that you always have the default case where you return the current state to avoid all bugs in the future. The part described in this lesson is pervasive in Redux's development, and is called reducer composition. 18 | 19 | 虽然在这个例子中我们没必要这样做,但我还是建议你都有一个默认的 case 返回当前的状态,以避免以后产生 bug。这节课程中所描述的内容在 Redux 的开发中是无处不在的,我们称它叫 reducer 组合。 20 | 21 | Different reducers specify how different parts of the state tree are updated in response to actions. Reducers are also normal JavaScript functions, so they can call other reducers to delegate and abstract a way handling of updates of some parts of this tree they manage. 22 | 23 | 不同的 reducer 定义了状态树的不同部分是如何根据动作作出更新。Reducers 是普通的 JavaScript 函数,所以我们可以调用其他的 reducers 作为代理,抽象出一种方式,来处理对它们所管理的状态树的某些部分的更新。 24 | 25 | This pattern can be applied many times, and while there is still a single top level reducer managing the state of your app, you will find it convenient to express it as many reducers calling each other, each contributing to a part of the application state tree. 26 | 27 | 这种方式可以被应用很多次,虽然还是有一个单独的上层 reducer 来管理你的应用的状态,但你会发现当,将它表述为多个 reducer 互相调用,每一个 reducer 分别负责状态树的某一个部分,这种方式是非常方便的。 28 | 29 | 30 | -------------------------------------------------------------------------------- /translation/05: -------------------------------------------------------------------------------- 1 | The first function we're going to write is the reducer for the counter example. And reducer accepts state and action as arguments and returns the next state. But before jumping into the implementation, we're going to make certain assertions using Michael Jackson's Expect library. We're going to assert that when the state of the counter is 0 and you pass an "INCREMENT" action it should return 1. And similarly it should return 2 when this state is 1 and you increment. 2 | 3 | 我们要写的第一个方法是计数器的 reducer。reducer 接受状态和动作作为参数并返回新的状态。但在开始实现之前,我们先用 Michael Jackson 的 Expect 库来写断言测试。我们将确保当计数器的状态为 0 时,传递一个 "INCREMENT" 的动作后,它将返回 1。相似地,当状态为 1 时,你递增它,它将返回 2。 4 | 5 | We're going to add a couple of tests that test how "DECREMENT" works, which is that it decrements from 2 to 1 and from 1 to 0 and we're going to add a log to tell if our tests are successful. 6 | 7 | 我们将增加一些测试来看看 "DECREMENT" 是怎么工作的,它会将 2 递减成 1,将 1 递减成 0。我们还将增加一些输出来告诉我们测试是否成功。 8 | 9 | So if you run this test, they're actually going to fail because we haven't even begun to implement our reducer. We're going to start by checking the action type and if the action type is "INCREMENT", we're going to return "state + 1", but if it is "DECREMENT" we're going to return "state - 1". 10 | 11 | 如果这时你运行这个测试,它会失败,因为我们甚至还没有开始实现 reducer。我们从检查动作的类型开始,如果动作的类型是 "INCREMENT",我们返回 state + 1,但如果是 "DECREMENT",我们返回 state - 1。 12 | 13 | If you run the tests we will find that this is enough to get them to pass. However, there are still some flaws in our current implementation of the counter reducer. For example, I think that if we dispatch an action that it does not understand, it should return the current state of the application. 14 | 15 | 如果这时你再运行测试,我们会发现现在这个 reducer 已经足够让测试通过。但是,这个计数器 reducer 的实现仍然有一些不足。比如,如果我们分发一个它不理解的动作,它应该返回应用当前的状态。 16 | 17 | However, if we check for that we will see that this test fails, because we currently don't handle unknown actions. So I'm going to add an else clause that returns the current state. And the tests pass now. 18 | 19 | 然而,我们这时运行测试将会发现测试失败了,因为我们目前还没有处理未知的动作。所以我将添加一个 else 语句来返回当前的状态,看,现在测试通过了。 20 | 21 | Another issue is that while the reducer is normally in control of the application state, currently it does not specify the initial state. In the case of counter example that would be 0. The convention we use in Redux is that if the reducer receives "undefined" as the state argument, it must return what it considers to be the initial state of the application. In this case it will be 0. 22 | 23 | 另外一个问题是,虽然 reducer 通常控制着整个应用的状态,但它现在却没有明确的初始状态。在计数器这个例子中,初始状态应该是 0。使用 Redux 有一个约定,当 reducer 接收到一个 "undefined" 的 state 参数时,它必须返回它认为的应用的初始状态,在这个例子中初始状态应该是 0。 24 | 25 | Now come a few cosmetic tweaks. I'll replace this bunch of tweaks with a switch statement and I'm going to replace this condition with ES6 default argument, which looks better. I'm also going to replace the function declaration with an arrow function, which has clearer semantics in ES6. 26 | 27 | 现在我们来美化一下代码。我们用 switch 语句来替代这部分代码,使用更好看 ES6 默认参数语法来替代这个条件判断。同时,我用 ES6 中具有更清晰语义的箭头函数替代这个函数声明。 -------------------------------------------------------------------------------- /src/29: -------------------------------------------------------------------------------- 1 | Finally, let's take a look at the "FilterLink" container component that renders a "Link" with an active property and a click handler. First, I will write the "mapStateToProps" function, which I'll call, "mapStateToLinkProps", because I have everything in a single file. 2 | 3 | And it's going to accept the state of the Redux store and return the props that should be passed to the "Link". And we only have a single such prop called "active" that determines whether the "Link" displays the current visibility filter. 4 | 5 | When I paste this expression from the "render" method, I see that it references the "filter" prop of the "FilterLink" component. To tell whether a "Link" is active, we need to compare this prop with the "visibilityFilter" value from the Redux store state. 6 | 7 | It is fairly common to use the container props when calculating the child props, so this is why props are passed as a second argument to "mapStateToProps". I will rename it to "ownProps" to make it clear we are talking about the container component's own props and not the props that are passed through the child, which is the return value of "mapStateToProps". 8 | 9 | The second function I'm writing here is "mapDispatchToProps" or, to avoid name clashes in this JSbin, "mapDispatchToLinkProps". The only argument so far is the "dispatch" function. And I'm going to need to look at the container component I wrote by hand to see what props depend on the "dispatch" function. 10 | 11 | In this case, this is just the click handler where I dispatch the "SET_VISIBILITY_FILTER" action. The only prop I'm passing down is called, "onClick". And I declare it as an arrow function with no arguments, and I paste the dispatch code. But it references the props again, so I need to add "ownProps" as an argument, the second argument, to "mapDispatchToProps" function to you. 12 | 13 | Finally, I will call the "connect" function from "react-redux" library to generate the "FilterLink" container component. I pass the relevant "mapStateToProps" function as the first argument, the "mapDispatchToProps" as the second argument, and I call the function again with a "Link" component which should be rendered. Now I can remove the old "FilterLink" implementation. 14 | 15 | Let's recap the data flow in this example. The "Footer" component renders three "FilterLink"s, and each of them has a different "filter" prop that specifies which filter it corresponds to. This prop will be available on the "ownProps" object that both "mapDispatchToProps" and "mapStateToProps" will receive as the second argument. 16 | 17 | We pass these two functions to the "connect" call, which will return a container component called, "FilterLink". The "FilterLink" will take the props that will return from the "mapDispatchToProps" and "mapStateToProps" and pass them as props to the "Link" component that it wraps. 18 | 19 | We can now use the "FilterLink" container component and specify just the "Filter", but the underlying "Link" component will have received the calculated "active" and "onClick" values. 20 | 21 | -------------------------------------------------------------------------------- /translation/07: -------------------------------------------------------------------------------- 1 | In the previous video we looked at how to implement a simple counter example using the "createStore" function provided by Redux and the "store" object it returns that provides the "getState" method to get the current application state, the "dispatch" method, to change the current application state by dispatching an action, and the "subscribe" method to subscribe to the changes and re-render our application with the current state of the app. 2 | 3 | 在前面的视频中,我们已经看到了是如何实现一个简单的计数器的例子。通过 Redux 提供的 createStore 方法,得到一个 store 对象;通过 store 对象的 getState 方法,得到应用当前的状态;使用 dispatch 方法分发动作去改变应用的状态;使用 subscribe 方法去订阅 store 的变化,然后使用当前的状态去重新渲染整个应用。 4 | 5 | If you're like me you prefer to understand the tools that you're using. In this tutorial we're going to re-implement the "createStore" function provided by Redux from scratch. The first and the only, from what we know so far, argument to the "createStore" function is the reducer function provided by the application. 6 | 7 | 如果你像我一样,你会喜欢去深究正在使用的工具的内部原理。所以,在这个教程中,我们将从头开始来重新实现 Redux 提供的 createStore 方法。到目前为止就我们所知,createStore 唯一的参数就是应用提供的 reducer。 8 | 9 | And we know that the store holds the current state. We keep it in a variable, and the "getState" function is going to return the current value of that variable. This function, combined with the "dispatch" function and the "subscribe" function on a single object is what we call the Redux store. 10 | 11 | 而且我们知道,store 维护着当前的状态。我们用一个变量来存储状态,并且用 getState 方法来返回这个变量的当前值。这个方法 ( 指 getState ),和 dispatch 方法,subscribe 方法,一起组成了我们称作 Redux store 的对象。 12 | 13 | Because the "subscribe" function can be called many times, we need to keep track of all the change listeners. And any time it is called we want to push the new listener into the array. Dispatching an action is the only way to change the internal state. 14 | 15 | 因为 subscribe 方法会被调用很多次,所以我们需要记录下所有的监听者。只要它被调用,我们就把新的监听者添加到 ( 监听者 ) 数组里。分发动作是唯一可以改变内部状态的途径。 16 | 17 | And in order to calculate the new state we call the reducer with the current state and the action being dispatched. After the state was updated, we need to notify every change listener, by calling it. 18 | 19 | 为了得到新的状态,我们使用当前的状态和分发过来的动作作为参数,调用 reducer。在状态被更新后,我们通过调用每一个监听者来通知它们。 20 | 21 | There is an important missing piece here. We haven't provided a way to unsubscribe a listener. But instead of adding a dedicated "unsubscribe" method, we'll just return a function from the "subscribe" method that removes this listener from the "listeners" array. 22 | 23 | 这里漏掉了一个很重要的地方,我们没有提供方法来取消订阅一个监听者。为了避免增加一个专门的 unsubcribe 方法,我们从 subscribe 方法中返回一个可以将此监听者从监听者数组中移除的方法。 24 | 25 | Finally, by the time the store is returned we wanted to have the initial state populated. So we're going to dispatch a dummy action just to get the reducer to return the initial value. 26 | 27 | 最后,在 store 被返回之前,我们希望先填充初始状态的值。所以我们在这里分发一个空的动作来让 reducer 返回初始状态值。 28 | 29 | This implementation of the Redux store, apart from a few minor details in edge cases, is the createStore which was shipped with Redux. 30 | 31 | 这个 Redux store 的实现,除去一些边缘情况的小细节外,就是 Redux 中 createStore 的最终实现了。 -------------------------------------------------------------------------------- /chn/07.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,860 --> 00:00:02,100 3 | 在前面的视频中 4 | 5 | 2 6 | 00:00:02,100 --> 00:00:05,600 7 | 我们已经看到了如何实现一个简单的计数器的例子 8 | 9 | 3 10 | 00:00:05,740 --> 00:00:09,120 11 | 通过 Redux 提供的 createStore() 方法 12 | 13 | 4 14 | 00:00:09,280 --> 00:00:11,500 15 | 得到一个 store 对象 16 | 17 | 5 18 | 00:00:11,680 --> 00:00:15,660 19 | 通过 store 对象的 getState() 方法 得到应用当前的状态 20 | 21 | 6 22 | 00:00:15,800 --> 00:00:21,180 23 | 使用 dispatch() 方法分发动作去改变应用的状态 24 | 25 | 7 26 | 00:00:21,460 --> 00:00:24,820 27 | 使用 subscribe() 方法去订阅 store 的变化 28 | 29 | 8 30 | 00:00:24,820 --> 00:00:29,240 31 | 然后使用当前的状态去重新渲染整个应用 32 | 33 | 9 34 | 00:00:29,560 --> 00:00:33,360 35 | 如果你像我一样 你会喜欢去深究正在使用的工具的内部原理 36 | 37 | 10 38 | 00:00:33,460 --> 00:00:36,740 39 | 所以 在这个教程中 我们将从头开始来重新实现 40 | 41 | 11 42 | 00:00:36,740 --> 00:00:40,900 43 | Redux 提供的 createStore() 方法 44 | 45 | 12 46 | 00:00:42,520 --> 00:00:48,060 47 | 到目前为止就我们所知 createStore() 唯一的参数 48 | 49 | 13 50 | 00:00:48,220 --> 00:00:51,340 51 | 就是应用提供的 reducer 52 | 53 | 14 54 | 00:00:51,720 --> 00:00:55,080 55 | 而且我们知道 store 维护着当前的状态 56 | 57 | 15 58 | 00:00:55,220 --> 00:00:57,020 59 | 所以我们用一个变量来存储状态 60 | 61 | 16 62 | 00:00:57,200 --> 00:01:02,640 63 | 并且用 getState() 方法来返回这个变量的当前值 64 | 65 | 17 66 | 00:01:03,820 --> 00:01:08,660 67 | getState() dispatch() 还有 subscribe() 这三个方法 68 | 69 | 18 70 | 00:01:08,740 --> 00:01:12,780 71 | 一起组成了我们称作 Redux store 的对象 72 | 73 | 19 74 | 00:01:14,020 --> 00:01:16,800 75 | 因为 subscribe 方法会被调用很多次 76 | 77 | 20 78 | 00:01:16,800 --> 00:01:19,600 79 | 所以我们需要记录下所有的监听者 80 | 81 | 21 82 | 00:01:19,820 --> 00:01:21,280 83 | 只要它被调用 84 | 85 | 22 86 | 00:01:21,280 --> 00:01:24,460 87 | 我们就把新的监听者添加到监听者数组里 88 | 89 | 23 90 | 00:01:25,540 --> 00:01:29,940 91 | 分发动作是唯一可以改变内部状态的途径 92 | 93 | 24 94 | 00:01:30,180 --> 00:01:32,440 95 | 为了得到新的状态 96 | 97 | 25 98 | 00:01:32,440 --> 00:01:36,140 99 | 我们以当前的状态和分发过来的动作为参数来调用 reducer 100 | 101 | 26 102 | 00:01:36,480 --> 00:01:38,400 103 | 在状态被更新后 104 | 105 | 27 106 | 00:01:38,520 --> 00:01:42,440 107 | 我们通过调用每一个监听者来通知它们 108 | 109 | 28 110 | 00:01:44,520 --> 00:01:46,620 111 | 这里漏掉了一个很重要的地方 112 | 113 | 29 114 | 00:01:46,620 --> 00:01:50,020 115 | 我们没有提供方法来取消订阅一个监听者 116 | 117 | 30 118 | 00:01:50,240 --> 00:01:53,620 119 | 为了避免增加一个专门的 unsubscribe() 方法 120 | 121 | 31 122 | 00:01:53,880 --> 00:01:57,680 123 | 我们从 subscribe() 方法中返回一个 124 | 125 | 32 126 | 00:01:57,680 --> 00:02:01,280 127 | 可以将此监听者从 listeners 数组中移除的方法 128 | 129 | 33 130 | 00:02:03,480 --> 00:02:06,640 131 | 最后 在 store 被返回之前 132 | 133 | 34 134 | 00:02:06,800 --> 00:02:10,220 135 | 我们希望先填充初始状态的值 136 | 137 | 35 138 | 00:02:10,340 --> 00:02:13,040 139 | 所以我们在这里分发一个空的动作 140 | 141 | 36 142 | 00:02:13,140 --> 00:02:17,140 143 | 来让 reducer 返回初始状态值 144 | 145 | 37 146 | 00:02:18,140 --> 00:02:20,540 147 | 这个 Redux store 的实现 148 | 149 | 38 150 | 00:02:20,540 --> 00:02:23,660 151 | 除去一些边缘情况的小细节外 152 | 153 | 39 154 | 00:02:23,840 --> 00:02:26,980 155 | 就是 Redux 中 createStore() 的最终实现了 156 | 157 | -------------------------------------------------------------------------------- /chn/01.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,620 --> 00:00:02,420 3 | Redux 的第一原则就是 4 | 5 | 2 6 | 00:00:02,420 --> 00:00:06,850 7 | 无论你的应用是像这个计数器一样的简单示例 8 | 9 | 3 10 | 00:00:07,000 --> 00:00:11,000 11 | 还是拥有超多 UI 和状态转换的复杂应用 12 | 13 | 4 14 | 00:00:11,000 --> 00:00:13,820 15 | 你需要用一个单一的 JavaScript 对象 16 | 17 | 5 18 | 00:00:13,820 --> 00:00:16,780 19 | 来代表你整个应用的状态 20 | 21 | 6 22 | 00:00:17,940 --> 00:00:22,680 23 | 在 Redux 中 所有的变化和状态转换都是显式的 24 | 25 | 7 26 | 00:00:22,970 --> 00:00:26,340 27 | 所以 你可以跟踪所有的这些变化 28 | 29 | 8 30 | 00:00:26,480 --> 00:00:30,480 31 | 在这个例子中 我将应用的每一个状态变化 32 | 33 | 9 34 | 00:00:30,480 --> 00:00:31,880 35 | 打印到控制台里 36 | 37 | 10 38 | 00:00:32,500 --> 00:00:35,680 39 | 你可以看到 在我们的计数器示例中 40 | 41 | 11 42 | 00:00:35,720 --> 00:00:38,520 43 | 并没有太多的状态需要我们去维护 44 | 45 | 12 46 | 00:00:38,680 --> 00:00:42,650 47 | 用一个 JavaScript 数值就可以代表整个应用的状态了 48 | 49 | 13 50 | 00:00:43,450 --> 00:00:45,260 51 | 这是另一个例子 52 | 53 | 14 54 | 00:00:45,380 --> 00:00:47,420 55 | 这里有一堆独立的计数器 56 | 57 | 15 58 | 00:00:47,420 --> 00:00:49,040 59 | 而我可以随意添加和移除 60 | 61 | 16 62 | 00:00:49,740 --> 00:00:51,520 63 | 在这个例子里 一个单一的数值 64 | 65 | 17 66 | 00:00:51,520 --> 00:00:54,500 67 | 并不足以代表整个应用的状态 68 | 69 | 18 70 | 00:00:54,700 --> 00:00:57,940 71 | 所以我们用了一个 JavaScript 数组 72 | 73 | 19 74 | 00:00:59,370 --> 00:01:01,380 75 | 在一个更复杂的应用中 76 | 77 | 20 78 | 00:01:01,380 --> 00:01:03,320 79 | 会有更多的需要维护的状态 80 | 81 | 21 82 | 00:01:03,680 --> 00:01:05,360 83 | 这是一个典型的待办事项应用 84 | 85 | 22 86 | 00:01:05,360 --> 00:01:06,880 87 | 我可以在其中添加待办事项 88 | 89 | 23 90 | 00:01:07,110 --> 00:01:09,340 91 | 可以把它划掉来标记成已完成 92 | 93 | 24 94 | 00:01:09,340 --> 00:01:11,760 95 | 还可以改变当前的过滤器 96 | 97 | 25 98 | 00:01:13,600 --> 00:01:16,500 99 | 看一下状态转换的历史纪录 100 | 101 | 26 102 | 00:01:16,600 --> 00:01:20,580 103 | 我们可以看到应用的初始状态是一个 JavaScript 对象 104 | 105 | 27 106 | 00:01:20,720 --> 00:01:24,280 107 | 这个对象的“todos”属性下是一个数组 108 | 109 | 28 110 | 00:01:24,280 --> 00:01:28,100 111 | 而“visibilityFilter”属性则是一个字符串“SHOW_ALL” 112 | 113 | 29 114 | 00:01:28,200 --> 00:01:30,440 115 | 当我添加第一个待办事项时 116 | 117 | 30 118 | 00:01:30,440 --> 00:01:34,880 119 | 它被添加到“状态”对象里的“todos”数组 120 | 121 | 31 122 | 00:01:35,080 --> 00:01:37,120 123 | 而这个待办事项本身 124 | 125 | 32 126 | 00:01:37,120 --> 00:01:39,700 127 | 则是由一个普通的 JavaScript 对象所代表 128 | 129 | 33 130 | 00:01:39,720 --> 00:01:43,060 131 | 这里它被标记为未完成,而它的内容则已经被保存 132 | 133 | 34 134 | 00:01:43,220 --> 00:01:45,940 135 | 这个应用之后的每个变化 136 | 137 | 35 138 | 00:01:45,940 --> 00:01:48,620 139 | 无论是我划掉一个待办事项 140 | 141 | 36 142 | 00:01:48,760 --> 00:01:51,240 143 | 还是我改变“visibilityFilter” 144 | 145 | 37 146 | 00:01:51,340 --> 00:01:55,080 147 | 都会让“状态”对象发生改变 148 | 149 | 38 150 | 00:01:55,080 --> 00:01:57,600 151 | 而这个对象代表了我们的整个应用 152 | 153 | 39 154 | 00:01:58,280 --> 00:02:00,800 155 | 现在你已经了解了 Redux 的第一原则 156 | 157 | 40 158 | 00:02:00,900 --> 00:02:04,220 159 | 那就是 所有在应用中的改变 160 | 161 | 41 162 | 00:02:04,250 --> 00:02:06,660 163 | 包括数据和 UI 状态 164 | 165 | 42 166 | 00:02:06,660 --> 00:02:08,900 167 | 都包含在一个单一的对象里 168 | 169 | 43 170 | 00:02:08,900 --> 00:02:11,320 171 | 我们把它称为“状态”或者“状态树” 172 | 173 | -------------------------------------------------------------------------------- /chn/05.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,480 --> 00:00:02,680 3 | 我们要写的第一个方法 4 | 5 | 2 6 | 00:00:02,820 --> 00:00:05,940 7 | 是计数器的 reducer 8 | 9 | 3 10 | 00:00:06,240 --> 00:00:09,660 11 | 这个 reducer 接受状态和动作作为参数 12 | 13 | 4 14 | 00:00:09,820 --> 00:00:11,380 15 | 并返回新的状态 16 | 17 | 5 18 | 00:00:11,600 --> 00:00:14,260 19 | 但在开始实现之前 20 | 21 | 6 22 | 00:00:14,460 --> 00:00:17,620 23 | 我们会用 Michael Jackson 的 Expect 库 24 | 25 | 7 26 | 00:00:17,680 --> 00:00:20,020 27 | 来写断言测试 28 | 29 | 8 30 | 00:00:20,480 --> 00:00:21,980 31 | 我们将确保 32 | 33 | 9 34 | 00:00:22,120 --> 00:00:24,300 35 | 当计数器的状态为 0 时 36 | 37 | 10 38 | 00:00:24,440 --> 00:00:26,640 39 | 传递一个 "INCREMENT" 动作后 40 | 41 | 11 42 | 00:00:26,640 --> 00:00:28,640 43 | 它会返回 1 44 | 45 | 12 46 | 00:00:28,640 --> 00:00:30,820 47 | 相似地 当状态为 1 时 48 | 49 | 13 50 | 00:00:30,940 --> 00:00:33,560 51 | 你递增它 它将返回 2 52 | 53 | 14 54 | 00:00:33,860 --> 00:00:35,920 55 | 我们将增加一些测试 56 | 57 | 15 58 | 00:00:35,920 --> 00:00:38,520 59 | 来看看 "DECREMENT" 是怎么工作的 60 | 61 | 16 62 | 00:00:38,880 --> 00:00:42,600 63 | 它会从2递减成1 从1递减成0 64 | 65 | 17 66 | 00:00:42,980 --> 00:00:45,500 67 | 我们还将增加一些输出 68 | 69 | 18 70 | 00:00:45,500 --> 00:00:48,300 71 | 来告诉我们测试是否成功 72 | 73 | 19 74 | 00:00:49,240 --> 00:00:51,300 75 | 如果这时你运行这个测试 76 | 77 | 20 78 | 00:00:51,420 --> 00:00:56,460 79 | 它会失败 因为我们甚至还没有开始实现这个 reducer 80 | 81 | 21 82 | 00:00:57,120 --> 00:01:00,040 83 | 我们从检查动作的类型开始 84 | 85 | 22 86 | 00:01:00,200 --> 00:01:04,060 87 | 如果动作的类型是 "INCREMENT" 我们返回 state+1 88 | 89 | 23 90 | 00:01:04,280 --> 00:01:09,200 91 | 但如果是 "DECREMENT" 我们返回 state-1 92 | 93 | 24 94 | 00:01:10,580 --> 00:01:11,940 95 | 如果这时你再运行测试 96 | 97 | 25 98 | 00:01:11,940 --> 00:01:14,760 99 | 我们会发现现在这个 reducer 已经足够让测试通过 100 | 101 | 26 102 | 00:01:14,960 --> 00:01:20,660 103 | 但是 这个计数器 reducer 的实现仍然有一些不足 104 | 105 | 27 106 | 00:01:21,340 --> 00:01:27,300 107 | 比如 如果我们分发一个它不理解的动作 108 | 109 | 28 110 | 00:01:27,300 --> 00:01:30,600 111 | 它应该返回应用当前的状态 112 | 113 | 29 114 | 00:01:31,100 --> 00:01:35,060 115 | 然而 我们这时运行测试将会发现测试失败了 116 | 117 | 30 118 | 00:01:35,240 --> 00:01:38,280 119 | 因为我们目前还没有处理未知的动作 120 | 121 | 31 122 | 00:01:38,840 --> 00:01:41,560 123 | 所以我将添加一个 else 语句 124 | 125 | 32 126 | 00:01:41,560 --> 00:01:43,560 127 | 来返回当前的状态 128 | 129 | 33 130 | 00:01:44,140 --> 00:01:45,700 131 | 看 现在测试通过了 132 | 133 | 34 134 | 00:01:46,320 --> 00:01:51,460 135 | 另外一个问题是 虽然 reducer 通常控制着整个应用的状态 136 | 137 | 35 138 | 00:01:51,460 --> 00:01:54,420 139 | 但它现在却没有明确的初始状态 140 | 141 | 36 142 | 00:01:55,260 --> 00:01:58,000 143 | 在计数器这个例子中 初始状态应该是 0 144 | 145 | 37 146 | 00:01:58,440 --> 00:02:00,780 147 | 使用 Redux 有一个约定 148 | 149 | 38 150 | 00:02:00,920 --> 00:02:06,140 151 | 当 reducer 接收到 undefined 作为 state 参数时 152 | 153 | 39 154 | 00:02:06,260 --> 00:02:11,620 155 | 它必须返回它认为的应用的初始状态 156 | 157 | 40 158 | 00:02:11,820 --> 00:02:13,960 159 | 在这个例子中初始状态应该是 0 160 | 161 | 41 162 | 00:02:14,640 --> 00:02:17,060 163 | 现在我们来美化一下代码 164 | 165 | 42 166 | 00:02:17,140 --> 00:02:20,280 167 | 我们用 switch 语句来替代这部分代码 168 | 169 | 43 170 | 00:02:20,600 --> 00:02:27,000 171 | 使用更好看 ES6 默认参数语法来替代这个条件判断 172 | 173 | 44 174 | 00:02:27,200 --> 00:02:29,980 175 | 同时 我用 ES6 中具有更清晰语义的箭头函数 176 | 177 | 45 178 | 00:02:30,220 --> 00:02:35,300 179 | 替代这个函数声明 180 | 181 | -------------------------------------------------------------------------------- /chn/13.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,420 --> 00:00:01,750 3 | 在上节课程中 4 | 5 | 2 6 | 00:00:01,750 --> 00:00:05,220 7 | 我们创建了一个可以处理两个 action 的 reducer 8 | 9 | 3 10 | 00:00:05,320 --> 00:00:09,050 11 | 增加一个新的 todo 和完成/撤销已有的 todo 12 | 13 | 4 14 | 00:00:09,520 --> 00:00:14,150 15 | 而现在,这段更新/添加一个 todo 的代码 16 | 17 | 5 18 | 00:00:14,150 --> 00:00:17,170 19 | 是放在 todos 这个 reducer 里面的 20 | 21 | 6 22 | 00:00:17,800 --> 00:00:19,950 23 | 这个函数令人费解 24 | 25 | 7 26 | 00:00:19,950 --> 00:00:22,570 27 | 因为我们要考虑两个不同的问题 28 | 29 | 8 30 | 00:00:22,670 --> 00:00:25,220 31 | todos 数组是如何被更新的 32 | 33 | 9 34 | 00:00:25,270 --> 00:00:28,000 35 | 以及每个独立的 todo 是如何被更新的 36 | 37 | 10 38 | 00:00:28,600 --> 00:00:31,050 39 | 不仅仅是在 Redux 中有这个问题 40 | 41 | 11 42 | 00:00:31,050 --> 00:00:33,620 43 | 任何时候 如果一个函数做了太多事情 44 | 45 | 12 46 | 00:00:33,700 --> 00:00:36,420 47 | 你都会想从中提取出其他函数 48 | 49 | 13 50 | 00:00:36,450 --> 00:00:37,620 51 | 然后再调用它们 52 | 53 | 14 54 | 00:00:37,620 --> 00:00:41,020 55 | 这样每一个函数就只需要解决一个问题 56 | 57 | 15 58 | 00:00:41,720 --> 00:00:43,670 59 | 在这个例子中 60 | 61 | 16 62 | 00:00:43,700 --> 00:00:49,950 63 | 我认为根据动作来创建和更新一个 todo 是个独立的操作 64 | 65 | 17 66 | 00:00:49,950 --> 00:00:53,470 67 | 这个操作应该用一个独立的 todo 函数去处理 68 | 69 | 18 70 | 00:00:54,570 --> 00:00:57,070 71 | 出于 Redux 的约定 72 | 73 | 19 74 | 00:00:57,070 --> 00:00:59,750 75 | 我决定让这个 todo 函数接受两个参数 76 | 77 | 20 78 | 00:00:59,750 --> 00:01:02,820 79 | 当前状态和被分发的动作 80 | 81 | 21 82 | 00:01:02,820 --> 00:01:04,970 83 | 同时它应该返回下一个状态 84 | 85 | 22 86 | 00:01:05,170 --> 00:01:06,320 87 | 但是在 todo 这个函数中 88 | 89 | 23 90 | 00:01:06,420 --> 00:01:09,470 91 | 状态指向的是某个具体的 todo 92 | 93 | 24 94 | 00:01:09,470 --> 00:01:11,470 95 | 而不是整个 todo 的列表 96 | 97 | 25 98 | 00:01:12,120 --> 00:01:15,500 99 | 最后 不出意外 在 Redux 中它就可以顺利的运行了 100 | 101 | 26 102 | 00:01:15,670 --> 00:01:19,420 103 | 我们已近从 "todos" reducer 中提取了 "todo" reducer 104 | 105 | 27 106 | 00:01:19,420 --> 00:01:22,220 107 | 所以现在我们需要对每一个 todo 调用这个函数 108 | 109 | 28 110 | 00:01:22,220 --> 00:01:24,650 111 | 然后将结果合并到一个数组当中 112 | 113 | 29 114 | 00:01:28,720 --> 00:01:31,870 115 | 虽然在这个例子中我们没必要这样做 116 | 117 | 30 118 | 00:01:31,870 --> 00:01:36,470 119 | 但我还是建议你都有一个默认的 case 返回当前的状态 120 | 121 | 31 122 | 00:01:36,470 --> 00:01:38,650 123 | 以避免以后产生 bug 124 | 125 | 32 126 | 00:01:39,120 --> 00:01:43,720 127 | 这节课程中所描述的内容在 Redux 的开发中是无处不在的 128 | 129 | 33 130 | 00:01:43,750 --> 00:01:46,120 131 | 我们称它叫 reducer 组合 132 | 133 | 34 134 | 00:01:46,500 --> 00:01:53,270 135 | 不同的 reducer 定义了状态树的不同部分是如何根据动作作出更新 136 | 137 | 35 138 | 00:01:53,870 --> 00:01:57,020 139 | Reducers 是普通的 JavaScript 函数 140 | 141 | 36 142 | 00:01:57,020 --> 00:01:59,770 143 | 所以我们可以调用其他的 reducers 作为代理 144 | 145 | 37 146 | 00:01:59,770 --> 00:02:01,070 147 | 抽象出一种方式 148 | 149 | 38 150 | 00:02:01,070 --> 00:02:05,050 151 | 来处理对它们所管理的状态树的某些部分的更新 152 | 153 | 39 154 | 00:02:05,370 --> 00:02:07,750 155 | 这种方式可以被应用很多次 156 | 157 | 40 158 | 00:02:07,750 --> 00:02:12,100 159 | 虽然还是有一个单独的上层 reducer 来管理你的应用的状态 160 | 161 | 41 162 | 00:02:12,270 --> 00:02:13,800 163 | 但你会发现 164 | 165 | 42 166 | 00:02:13,800 --> 00:02:16,300 167 | 当将它表述为多个 reducer 互相调用 168 | 169 | 43 170 | 00:02:16,350 --> 00:02:18,770 171 | 每一个 reducer 分别负责状态树的某一个部分 172 | 173 | 44 174 | 00:02:18,770 --> 00:02:22,020 175 | 这种方式是非常方便的。 176 | 177 | -------------------------------------------------------------------------------- /src/30: -------------------------------------------------------------------------------- 1 | So far we have covered the container components, the presentational components, the reducers, and the store. But we have not covered the concept of action creators, which you might see in the Redux talks and examples. 2 | 3 | Let's consider the following example. I dispatch the "ADD_TODO" action from inside the "button onClick" handler. And this is fine. However, it references the "nextTodoId" variable, which I declared alongside the "AddTodo" component. 4 | 5 | Normally, it would be local. However, what if another component wants to dispatch the "ADD_TODO" action? It would need to have the access to "nextTodoId" somehow. And while I could make this variable global, it's not a very good idea. 6 | 7 | Instead, it would be best if the components dispatching the "ADD_TODO" action did not have to worry about specifying the ID. Because the only information they really pass is the text of the todo being added. 8 | 9 | I don't want to generate the ID inside the reducer, because that would make it non-deterministic. However, I can extract this code generating the action object into a function I will call "addTodo". 10 | 11 | I pass the input value to "addTodo". And "addTodo" is just a function that takes the text of the todo and constructs an action object representing "ADD_TODO" action. So it has the type, "ADD_TODO", it takes care of generating the unique ID and it includes the text. 12 | 13 | Although extraction such functions is not required, it is very common pattern in Redux applications to keep them maintainable. So, like all these functions, we usually place action creators separately from components or from reducers. 14 | 15 | I will now extract other action creators from the components. And I see that I have a "SET_VISIBILITY_FILTER" with "dispatch" here, so I will change this to call the "setVisibilityFilter" action creator with "ownProps.filter" as the argument and is going to return the action that needs to be dispatched, so I'm declaring the "setVisibilityFilter" function. 16 | 17 | This is what I call an action creator, because it takes the arguments about the action and it returns the action object with the type "SET_VISIBILITY_FILTER" and the "filter" itself. 18 | 19 | You might think that this kind of code is boilerplate and you'd rather dispatch the action inline inside the component. However, don't underestimate how action creators document your software, because they tell your team what kinds of actions the components can dispatch, and this kind of information can be invaluable in large applications. 20 | 21 | I will now scroll down to the last place where I call "dispatch" with an inline action object. And I will now extract that to a "toggleTodo" action creator, to which I pass the ID of the todo as the argument. 22 | 23 | I'm now scrolling up to my action creators and I will add a new one that I call "toggleTodo". It accepts the ID as the argument and it returns the action of the type, "TOGGLE_TODO", and this ID. 24 | 25 | Let's take a moment to consider how convenient it is to have all the action creators in a single place so that I can use them from components and tests without worrying about the action's internal structure. 26 | 27 | Know that whether you use action creators or not, the data flow is exactly the same, because I just call the action creator to get the action object and then I call "dispatch" just like I did before, passing the action. 28 | -------------------------------------------------------------------------------- /src/09: -------------------------------------------------------------------------------- 1 | In this lesson, I use Expect Library to make test assertions, and "deepFreeze" to make sure that my code is free of mutations. 2 | 3 | Let's say that I want to implement a counter list application. I would need to write a few functions that operate on its state, and its state is an array of JavaScript numbers representing the individual counters. 4 | 5 | The first function I want to write is called "addCounter", and all it should do is to append a 0 at the end of the passed array. 6 | 7 | At first, I use the array "push" method to add a new item at the end of the array, and it works. However, we need to learn to avoid mutations in Redux, and I'm enforcing this by calling "deepFreeze" on the original array. 8 | 9 | Now my attempt to "push" does not work. It cannot add a new property to a frozen object. Instead of "push", I'm going to use the "concat" method, which does not modify the original array. 10 | 11 | Now the tests pass without mutations, and I can also use the new ES6 array spread operator to write the same code in a more concise way. 12 | 13 | My next function is called "removeCounter", and it accepts two arguments, an array of numbers, and the index of the number to skip from the array. 14 | 15 | So if I've got three numbers and I'm passing 1 as the second argument, I expect to receive an array with two numbers with the second item skipped in the result array. 16 | 17 | Usually, to delete an item from the array, I would use the "splice" method. However, splice is a mutation method, so you can't use it in Redux. 18 | 19 | I'm going to "deepFreeze" the array object, and now I need to figure out a different way to remove an item from the array without mutating it. 20 | 21 | I'm using a method called "slice" here, and it doesn't have anything to do with "splice". It is not mutating, and it gives me a part of the array from some beginning to some end index. 22 | 23 | So what I'm doing is that I'm taking the parts before the index I want to skip and after the index I want to skip, and I concatenate them to get a new array. 24 | 25 | Finally, instead of writing it as a method chain with "concat" calls, I can use the ES6 array spread operator to write it more concisely. 26 | 27 | Now that we implemented adding and removing counters, let's implement incrementing the counter. The "incrementCounter" function takes your arguments, the array and the index of the counter that should be incremented, so the return value has the same count of items, but one of them is incremented. 28 | 29 | Directly setting the array value at index works, but this is a mutation. So if we add a "deepFreeze" call, it's not going to work anymore, so how do we replace a single value in the array without mutating it? 30 | 31 | It turns out the answer is really similar to how we remove an item. We want to take the slice before the index, concat it with a single item array with a new value, and then concat it with the rest of the original array. 32 | 33 | Finally, with the ES6 spread operator, we can spread over the left part of the array, specify the new item, and then spread over the right part of the original array, and this looks much nicer. 34 | 35 | In this lesson, you learned how to use the "concat" method or the spread operator, and the "slice" method to add, remove, and change items in arrays without mutating them, and how to protect yourself with "deepFreeze" from mutation in your tests. 36 | 37 | -------------------------------------------------------------------------------- /translation/12: -------------------------------------------------------------------------------- 1 | In this lesson, we will continue creating the reducer for Todo list application. The only action that this reducer currently handles is called 'ADD_TODO'. We also created a test that makes sure that when the reducer is called with an empty array as a state and the 'ADD_TODO' action, it returns an array with a single Todo element. 2 | 3 | 在本次课程中,我们将会继续给我们的 Todo List 应用完善 reducer 。当前的 reducer 能处理的 action 只有 ADD_TODO 。我们也添加了相应的测试来确保,当传入参数 state 为空数组以及 action 为 ADD_TODO 的 reducer 被调用时,它返回一个只包含一个 Todo 的数组。 4 | 5 | In this lesson, we will follow the same approach to implement another action called 'TOGGLE_TODO'. We're going to start with a test again. This time, we're testing a different action and we have a different initial state. The state before calling the reducer now includes two different todos with id 0 and 1. Notice how both of them have their completed fields set to false. 6 | 7 | 在本次课程中,我们将遵循同样的方法来实现一个新的 action -- TOGGLE_TODO 。 我们同样从测试开始,这一次将测试新的 action 及新的初始 state 。在调用 reducer 之前, state 包含了两个不同的 todo , id 分别是 0 和 1 。注意设置这两个 todo 的 completed 字段为 false 。 8 | 9 | Next, I declare the action. The action is an object with the type property which is a 'TOGGLE_TODO' string and the id of the todo that I want to be toggled. I declare the state that I expect to receive after calling the reducer. And it's pretty much the same as before calling the reducer. However, I expect the todo with the id specified in the action or 1 in this case. The change is completed field. 10 | 11 | 接着,我声明了一个 action 对象。这个对象的 type 属性是字符串 "TOGGLE_TODO" ,而它的 id 属性则是我所想要完成/撤销的 todo 的 id。然后我声明了调用 reducer 之后期待返回的 state 。它和我们调用的 reducer 之前 state 的长得基本一样。但是我们要把 action 指定 id 的 todo ,也就是 id 为 1 的 todo ,修改其 completed 为 true。 12 | 13 | The reducer must be a pure function. So it is a matter of precaution, I called freeze on the state and the action. Finally, just like in the previous lesson, I'm asserting that the result of calling my reducer with the state before and the action is going to be deeply equal to the state after. 14 | 15 | reducer 一定要是纯函数。所以作为一种预防措施,我调用了 deepFreeze() 来确保 state 和 action 不被修改。最后,和上节课一样,我断言 reducer 在接收 stateBefore 和 action 之后的运行结果等于 stateAfter 。 16 | 17 | Now my test is a function, so I need to call it at the end of the file. And if I run it, it fails because I have not implemented handling this action yet. 18 | 19 | 现在我的测试是一个函数,所以我需要在文件的最后调用一下。然后运行测试,噢它失败了,那是因为我还没有在 reducer 中处理相应的 action。 20 | 21 | I'm adding a new switch case to my reducer. And I remember that I shouldn't change the original array, so I'm using the array map method to produce a new array. 22 | 23 | 我现在添加一个新的 switch 条件到 reducer 中。这时我想起来我不应该去修改原有的数组,所以我用数组的 map() 函数来创建一个新的数组。 24 | 25 | The function I pass as an argument will be called for every todo. So if it's not a todo I'm looking for, I don't want to change it. I just return it as is. However, if the todo is the one we want to toggle, I'm going to return a new object that has all the properties of the original todo object thanks to the object spread operator, but also an inverted value of the completed field. 26 | 27 | 我传入 map() 的函数会被每个 todo 所调用。因此要是当前的 todo 不是我 action 中指定的 todo,我不想要修改它,所以直接返回。要是当前的 todo 就是我想要切换的那个,我将返回一个新的 todo 对象,它将包含所有原 todo 的属性(感谢对象展开运算符),但 completed 字段的值将取反运算。 28 | 29 | Now both of our tests run successfully. And we have an implementation of the reducer that can add and toggle todos. 30 | 31 | 现在我们所有的测试都通过了,并且我们实现了一个能够添加和切换 todo 的 reducer 。 32 | -------------------------------------------------------------------------------- /src/11: -------------------------------------------------------------------------------- 1 | Just like in the previous two lessons, I'm using "expect" library to make test assertions and "deep-freeze" library to prevent accidental mutations in my code. In this lesson, I will create the reducer for a todo list application whose state is described an array of todos. 2 | 3 | Just to remind you what a reducer is, it's a pure function you write to implement the update logic of your application -- that is, how the next state is calculated given the current state and the action being dispatched. 4 | 5 | Before writing a reducer, I want to have a way of knowing whether its code is correct, so I'm starting by writing a test for it. I'm declaring two variables, the "stateBefore", which is an empty array, and the action being dispatched, which is an action describing user adding a new todo with some ID and a text. 6 | 7 | I am also declaring the state I expect to get after calling the reducer. And like state before, it is an array, but this time, it has a single element representing the todo that was just added. So it has the same ID and the text as the action object. It also has an additional field called, "completed," that I want to be initialized to be "false". 8 | 9 | We want to make sure that the reducer is a pure function, so I'm calling deepFreeze both on the state and the action. Finally, I am ready to use the expect library to verify that if I call the "todos" reducer with the state before and the action object, I'm going to get the result that is deeply equal to the state after I just declared. 10 | 11 | This concludes my first test. Now I can call it just like a regular JavaScript function. And if it doesn't throw in the expect call, I'm going to see a message saying that the tests have passed. 12 | 13 | Of course, it fails because the reducer is not implemented yet. It's an empty function. So it returns undefined instead of the array with a single item that I expect in the test. 14 | 15 | To fix this, I would need my reducer to take a look at the action type property, which is a string. When it matches the "ADD_TODO" string, which I specify as the action type in my test, to satisfy the test I need to return a new array which includes all items from the original array but also a new todo item that has its ID and text copied from the action object and a "completed" field set to "false". 16 | 17 | Finally, I add a default case to my switch statement because every reducer has to return the current state for any unknown action. 18 | 19 | Now the tests run successfully. Let's recap the data flow in this example to see why. 20 | 21 | First, I create the state array, which is an empty array, and the action object inside my test function. I'm passing them as arguments to my reducer function, called, "todos." The "todos" reducer accepts the state and the action as arguments and takes a look at the action type. 22 | 23 | In this case, the action type is a string saying "ADD_TODO", so it matches the switch case inside the reducer. The reducer returns a new array which contains the items from the old array and the new item representing the added todo. 24 | 25 | However, the state we passed from the test was actually an empty array, so, at the end, we're going to get an array with a single item, which is the new todo. 26 | 27 | Finally, we compare the return value to an array with a single todo item to make sure that the reducer works as intended. The equality check passes. So this makes the test successful. 28 | -------------------------------------------------------------------------------- /src/24: -------------------------------------------------------------------------------- 1 | In the previous lessons, we used this store top-level variable to refer to the Redux store. The components that access the store, such as the container components, read the state from it, subscribe to the store, and dispatch actions on the store using this store top-level variable. 2 | 3 | This approach works fine for JSbin example where everything is in a single file. However, it doesn't scale to real applications for several reasons. 4 | 5 | First of all, it makes your container components harder to test because they reference a specific store, but you might want to supply a different mock store in the test. Secondly, it makes it very hard to implement universal applications that are rendered on the server, because on the server, you want to supply a different store instance for every request because different requests have different data. 6 | 7 | I'm going to start by moving the store creation code to the bottom of the file where I render my React components. I'm going to change it slightly. And instead of creating the store top-level variable, I will pass the store I create as a prop to the top-level component, so it is completely injectable into it. 8 | 9 | Every container component needs a reference to the store so unfortunately, we have to pass it down to every component as a prop. It's less effort than passing different data through every component, but it's still inconvenient. So, don't worry, we'll find a better solution later, but for now, we need to see the problem. 10 | 11 | The problem is that the container components need to have the store instance to get the state from it, dispatch actions and subscribe to the changes. So this time, I'm changing the container component to take the store from the props using the ES6 destruction syntax, which just means "store equals props.store." 12 | 13 | And I'm doing the same here. I'm just taking the store from the props so I can call "dispatch" on it. 14 | 15 | I need to make similar changes to other container components. And in this case, I have this "AddTodo" component, which is not exactly a container component, but it still needs the store to dispatch the "ADD_TODO" action, so I added it as a prop. And I'm also going to add the store to the "Footer" component because, unfortunately, "FilterLink" needs it. 16 | 17 | And the "Footer" component renders "FilterLink". This is not convenient, but as I said, we'll figure out a way to avoid this later. But for now, we need to pass the store down so that every container component, such as "FilterLink", can use it to subscribe to the changes, to read the state and to dispatch actions without relying on a top-level variable being available. 18 | 19 | I'm changing the "render" method to read the store from the props. And now, all containers read the store instance from the props, and don't rely on a top-level variable that I removed. 20 | 21 | Note that this change did not change the behavior or the data flow of this application. The container components subscribe to the store, just like before, and update their states in response to its changes. 22 | 23 | However, what changed is how they access the store. Previously, they would access a top-level variable, but this approach does not scale to real-world applications. And this is why, right now, I'm passing down the store as a prop, so the container components can subscribe to it. 24 | 25 | In the future lessons, we will see how to pass the store down to the container components implicitly but without introducing the top-level variable. 26 | -------------------------------------------------------------------------------- /src/16: -------------------------------------------------------------------------------- 1 | In the previous lesson, we learned to use the "combineReducers" function, which comes with Redux and generates one reducer from several other reducers, delegating to them paths of the state tree. 2 | 3 | To gain a deeper understanding of how exactly "combineReducers" works, we will implement it from scratch in this lesson. 4 | 5 | "combineReducers" is a function, so I'm writing a function declaration. And its only argument is the mapping between the state keys and the reducers, so I'm just going to call it "reducers". 6 | 7 | The return value is supposed to be a reducer itself, so this is a function that returns another function. And the signature of the return function is a reducer signature. It has the state and the action. 8 | 9 | Now, I'm calling the "object.keys" method, which gives me all the keys of the reducers object. In our example, this is "todos" and the "visibilityFilter". 10 | 11 | Next, I'm calling the "reduce" method on the keys, because I want to produce a single value, such as the next state, by accumulating over every reducer key and calling the corresponding reducer. 12 | 13 | Each reducer passed through the "combineReducers" function is only responsible for updating a part of the state. This is why I'm saying that the next state by the given key can be calculated by calling the corresponding reducer by the given key with the current state by the given key and the action. 14 | 15 | The array reduce wants me to return the next accumulated value from the call back, so I'm returning the next state. And I'm also specifying an empty object as the initial next state, before all the keys are processed. 16 | 17 | And there we have it. This is a working re-implementation of "combineReducers" utility from Redux. 18 | 19 | Let's briefly recap how it works. I'm calling "combineReducers" with an object whose values are the reducer functions and keys are the state field they manage. Inside the generated reducer, I'm retrieving all the keys of the reducers I passed to "combineReducers", which is an array of strings, "todos" and "visibilityFilter". 20 | 21 | I'm starting with an empty object for my next state and I'm using the reduce operation of these keys to fill it gradually. 22 | 23 | Notice that I'm mutating the next state object on every iteration. This is not a problem, because it is the object I created inside the reducer. It is not something passed from outside, so reducer stays a pure function. 24 | 25 | To calculate the next state for a given key, it calls the corresponding reducer function, such as "todos" or "visibilityFilter". 26 | 27 | The generated reducer will pass through the child reducer only if part of its state by the key. So if its state is a single object, it's only going to pass the relevant part, such as "todos" or "visibilityFilter", depending on the current key, and save the result in the next state by the same key. 28 | 29 | Finally, we use the array reduce operation with the empty object as the initial next state, that is being filled on every iteration until it is the returned value of the whole reduce operation. 30 | 31 | In this lesson, you learned how to implement the "combineReducers" utility that comes with Redux from scratch. 32 | 33 | It is not essential to use in Redux, so it is fine if you don't fully understand how it works yet. However, it is a good idea to practice functional programming and understand functions can take other functions as arguments and return other functions, because knowing this will help you get more productive in Redux in the long term. 34 | -------------------------------------------------------------------------------- /chn/12.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,170 --> 00:00:01,050 3 | 在本次课程中 4 | 5 | 2 6 | 00:00:01,050 --> 00:00:05,700 7 | 我们将会继续给我们的 Todo List 应用完善 reducer 8 | 9 | 3 10 | 00:00:06,000 --> 00:00:10,850 11 | 当前的 reducer 能处理的 action 只有 ADD_TODO 12 | 13 | 4 14 | 00:00:11,520 --> 00:00:14,770 15 | 我们也添加了相应的测试来确保 16 | 17 | 5 18 | 00:00:14,770 --> 00:00:19,770 19 | 当传入参数 state 为空数组以及 action 为 ADD_TODO 的 reducer 被调用时 20 | 21 | 6 22 | 00:00:19,850 --> 00:00:23,520 23 | 它返回一个只包含一个 Todo 的数组 24 | 25 | 7 26 | 00:00:25,070 --> 00:00:25,950 27 | 在本次课程中 28 | 29 | 8 30 | 00:00:25,950 --> 00:00:29,950 31 | 我们将遵循同样的方法来实现一个新的 action -- 32 | 33 | 9 34 | 00:00:29,950 --> 00:00:31,700 35 | TOGGLE_TODO 36 | 37 | 10 38 | 00:00:32,870 --> 00:00:35,650 39 | 我们同样从测试开始 40 | 41 | 11 42 | 00:00:36,000 --> 00:00:41,100 43 | 这一次将测试新的 action 及新的初始 state 44 | 45 | 12 46 | 00:00:42,070 --> 00:00:44,570 47 | 在调用 reducer 之前 48 | 49 | 13 50 | 00:00:44,670 --> 00:00:49,020 51 | state 包含了两个不同的 todo , id 分别是 0 和 1 52 | 53 | 14 54 | 00:00:49,850 --> 00:00:54,250 55 | 注意设置这两个 todo 的 completed 字段为 false 56 | 57 | 15 58 | 00:00:55,000 --> 00:00:57,000 59 | 接着 我声明了一个 action 对象 60 | 61 | 16 62 | 00:00:57,020 --> 00:01:02,500 63 | 这个对象的 type 属性是字符串 "TOGGLE_TODO" 64 | 65 | 17 66 | 00:01:02,700 --> 00:01:06,150 67 | 而它的 id 属性则是我所想要完成/撤销的 todo 的 id 68 | 69 | 18 70 | 00:01:06,950 --> 00:01:11,600 71 | 然后我声明了调用 reducer 之后期待返回的 state 72 | 73 | 19 74 | 00:01:11,950 --> 00:01:15,820 75 | 它和我们调用的 reducer 之前 state 的长得基本一样 76 | 77 | 20 78 | 00:01:16,050 --> 00:01:21,150 79 | 但是我们要把 action 指定 id 的 todo 80 | 81 | 21 82 | 00:01:21,150 --> 00:01:23,120 83 | 也就是 id 为 1 的 todo 84 | 85 | 22 86 | 00:01:23,120 --> 00:01:25,450 87 | 修改其 completed 为 true 88 | 89 | 23 90 | 00:01:25,900 --> 00:01:28,400 91 | reducer 一定要是纯函数 92 | 93 | 24 94 | 00:01:28,400 --> 00:01:30,420 95 | 所以作为一种预防措施 96 | 97 | 25 98 | 00:01:30,420 --> 00:01:33,570 99 | 我调用了 deepFreeze() 来确保 state 和 action 不被修改 100 | 101 | 26 102 | 00:01:34,220 --> 00:01:36,870 103 | 最后 和上节课一样 104 | 105 | 27 106 | 00:01:36,870 --> 00:01:46,550 107 | 我断言 reducer 在接收 stateBefore 和 action 之后的运行结果等于 stateAfter 108 | 109 | 28 110 | 00:01:47,270 --> 00:01:48,950 111 | 现在我的测试是一个函数 112 | 113 | 29 114 | 00:01:48,950 --> 00:01:51,550 115 | 所以我需要在文件的最后调用一下 116 | 117 | 30 118 | 00:01:51,750 --> 00:01:53,070 119 | 然后运行测试 120 | 121 | 31 122 | 00:01:53,070 --> 00:01:54,150 123 | 噢它失败了 124 | 125 | 32 126 | 00:01:54,150 --> 00:01:57,320 127 | 那是因为我还没有在 reducer 中处理相应的 action 128 | 129 | 33 130 | 00:01:58,650 --> 00:02:01,720 131 | 我现在添加一个新的 switch 条件到 reducer 中 132 | 133 | 34 134 | 00:02:02,000 --> 00:02:05,570 135 | 这时我想起来我不应该去修改原有的数组 136 | 137 | 35 138 | 00:02:05,670 --> 00:02:09,550 139 | 所以我用数组的 map() 函数来创建一个新的数组 140 | 141 | 36 142 | 00:02:10,220 --> 00:02:14,370 143 | 我传入 map() 的函数会被每个 todo 所调用 144 | 145 | 37 146 | 00:02:14,720 --> 00:02:17,150 147 | 因此要是当前的 todo 不是我 action 中指定的 todo 148 | 149 | 38 150 | 00:02:17,150 --> 00:02:18,470 151 | 我不想要修改它 152 | 153 | 39 154 | 00:02:18,470 --> 00:02:20,570 155 | 所以直接返回 156 | 157 | 40 158 | 00:02:20,820 --> 00:02:24,170 159 | 要是当前的 todo 就是我想要切换的那个 160 | 161 | 41 162 | 00:02:24,420 --> 00:02:26,800 163 | 我将返回一个新的 todo 对象 164 | 165 | 42 166 | 00:02:26,800 --> 00:02:33,100 167 | 它将包含所有原 todo 的属性(感谢对象展开运算符) 168 | 169 | 43 170 | 00:02:33,100 --> 00:02:37,250 171 | 但 completed 字段的值将取反运算 172 | 173 | 44 174 | 00:02:38,620 --> 00:02:41,550 175 | 现在我们所有的测试都通过了 176 | 177 | 45 178 | 00:02:41,570 --> 00:02:47,150 179 | 并且我们实现了一个能够添加和切换 todo 的 reducer 180 | 181 | -------------------------------------------------------------------------------- /chn/02.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,260 --> 00:00:04,740 3 | Redux 的第二原则:状态树 (state tree) 是只读的 4 | 5 | 2 6 | 00:00:04,920 --> 00:00:07,300 7 | 你不能修改或者对它进行写入操作 8 | 9 | 3 10 | 00:00:07,720 --> 00:00:10,480 11 | 所以 每当你希望改变状态的时候 12 | 13 | 4 14 | 00:00:10,700 --> 00:00:12,900 15 | 你需要分发一个动作 (action) 16 | 17 | 5 18 | 00:00:13,900 --> 00:00:17,880 19 | 一个动作其实就是一个描述变化的普通 JavaScript 对象 20 | 21 | 6 22 | 00:00:18,400 --> 00:00:23,120 23 | 就像状态是应用中的数据的一个最小化抽象 24 | 25 | 7 26 | 00:00:23,380 --> 00:00:28,420 27 | 动作就是数据改变的最小化抽象 28 | 29 | 8 30 | 00:00:29,340 --> 00:00:32,160 31 | 你可以自己决定动作对象的结构 32 | 33 | 9 34 | 00:00:32,360 --> 00:00:35,640 35 | 唯一的要求就是它必须有一个“type”属性 36 | 37 | 10 38 | 00:00:35,780 --> 00:00:37,300 39 | 而且这个“type”属性不能够是“undefined” 40 | 41 | 11 42 | 00:00:37,420 --> 00:00:39,060 43 | 我们会建议使用字符串 44 | 45 | 12 46 | 00:00:39,200 --> 00:00:40,960 47 | 因为字符串是可以序列化的 48 | 49 | 13 50 | 00:00:41,700 --> 00:00:42,860 51 | 在不同的应用中 52 | 53 | 14 54 | 00:00:42,880 --> 00:00:45,060 55 | 你会需要用到不同类型的动作 56 | 57 | 15 58 | 00:00:45,060 --> 00:00:50,740 59 | 比如 在一个计数器里我们只需要“INCREMENT”和“DECREMENT”两个动作 60 | 61 | 16 62 | 00:00:50,860 --> 00:00:53,940 63 | 而不需要再传入其它额外的信息 64 | 65 | 17 66 | 00:00:53,940 --> 00:00:57,460 67 | 因为这些已经足够描述应用的变化了 68 | 69 | 18 70 | 00:00:58,700 --> 00:01:01,940 71 | 但是在这个多计数器的例子里 72 | 73 | 19 74 | 00:01:01,940 --> 00:01:03,340 75 | 我们有更多的动作 76 | 77 | 20 78 | 00:01:03,340 --> 00:01:05,340 79 | 我们有“ADD_COUNTER”动作 80 | 81 | 21 82 | 00:01:05,760 --> 00:01:07,800 83 | 有“REMOVE_COUNTER”动作 84 | 85 | 22 86 | 00:01:08,380 --> 00:01:12,160 87 | 而当每一次我改变某个计数器时 88 | 89 | 23 90 | 00:01:12,320 --> 00:01:17,960 91 | 你可以看到“INCREMENT”和“DECREMENT”动作附带了“index”这个信息 92 | 93 | 24 94 | 00:01:18,220 --> 00:01:22,780 95 | 这是因为我们需要定位 到底是哪一个计数器发生了改变 96 | 97 | 25 98 | 00:01:24,380 --> 00:01:28,720 99 | 这个方法可以很好地扩展到中型和更为复杂的应用 100 | 101 | 26 102 | 00:01:29,540 --> 00:01:35,340 103 | 每一次我添加一个待办事项 部件本身其实不知道它到底是怎么被添加的 104 | 105 | 27 106 | 00:01:35,600 --> 00:01:39,960 107 | 它所知道的只是要分发一个动作 这个动作包含了值为“ADD_TODO”的属性“type” 108 | 109 | 28 110 | 00:01:39,960 --> 00:01:44,580 111 | 待办事项的内容 以及一个序列化的 ID 112 | 113 | 29 114 | 00:01:45,220 --> 00:01:50,060 115 | 如果我完成/撤销一个待办事项 跟之前的状况一样 组件不会知道它是怎么发生的 116 | 117 | 30 118 | 00:01:50,300 --> 00:01:54,140 119 | 它也只会知道需要分发一个动作 这个动作包含了值为“ADD_TODO”的属性“type” 120 | 121 | 31 122 | 00:01:54,140 --> 00:01:59,120 123 | 以及传入我想完成/撤销的待办事项的 ID 124 | 125 | 32 126 | 00:01:59,800 --> 00:02:02,420 127 | 对于过滤标签的操作也是如此 128 | 129 | 33 130 | 00:02:02,640 --> 00:02:04,960 131 | 每一次我点击这些过滤标签 132 | 133 | 34 134 | 00:02:05,080 --> 00:02:07,480 135 | 来改变现在可见的待办事项时 136 | 137 | 35 138 | 00:02:07,620 --> 00:02:13,280 139 | 这个部件实际上分发了一个动作 140 | 141 | 36 142 | 00:02:13,280 --> 00:02:15,060 143 | 这个动作的“type”属性是“SET_VISIBILITY_FILTER” 144 | 145 | 37 146 | 00:02:15,200 --> 00:02:19,480 147 | 而“filter”属性则是对应的过滤器类型 148 | 149 | 38 150 | 00:02:19,640 --> 00:02:22,100 151 | 但所有的这些都是普通的 JavaScript 对象 152 | 153 | 39 154 | 00:02:22,160 --> 00:02:24,800 155 | 这些对象描述了应用中所发生的事情 156 | 157 | 40 158 | 00:02:26,680 --> 00:02:29,400 159 | 现在你已经了解了 Redux 的第二原则 —— 160 | 161 | 41 162 | 00:02:29,500 --> 00:02:31,340 163 | 状态是只读的 164 | 165 | 42 166 | 00:02:31,620 --> 00:02:36,020 167 | 唯一一个改变状态树的方式就是分发一个动作 168 | 169 | 43 170 | 00:02:36,520 --> 00:02:38,980 171 | 一个动作就是一个普通的 JavaScript 对象 172 | 173 | 44 174 | 00:02:39,100 --> 00:02:43,300 175 | 它以最简单的方式描绘了应用里面发生的改变 176 | 177 | 45 178 | 00:02:43,780 --> 00:02:48,500 179 | 无论是由网络请求还是用户交互发起的数据 180 | 181 | 46 182 | 00:02:48,620 --> 00:02:54,320 183 | 要想进入到 Redux 应用内部 都只能通过动作来实现 184 | -------------------------------------------------------------------------------- /eng/01.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,620 --> 00:00:02,420 3 | The first principle of Redux 4 | 5 | 2 6 | 00:00:02,420 --> 00:00:06,850 7 | is that whether your app is a really simple one like this counter example, 8 | 9 | 3 10 | 00:00:07,000 --> 00:00:11,000 11 | or a complex application with a lot of UI, and changes of state, 12 | 13 | 4 14 | 00:00:11,000 --> 00:00:16,910 15 | you are going to represent the whole state of your application as a single JavaScript object. 16 | 17 | 5 18 | 00:00:17,940 --> 00:00:22,680 19 | All mutations, and changes of state in Redux are explicit. 20 | 21 | 6 22 | 00:00:22,970 --> 00:00:26,340 23 | So, eh, it is possible to keep track of all of them. 24 | 25 | 7 26 | 00:00:26,480 --> 00:00:31,820 27 | In this case, I am logging every state change in the application in the console. 28 | 29 | 8 30 | 00:00:32,500 --> 00:00:35,680 31 | You can see that, in the counter example, 32 | 33 | 9 34 | 00:00:35,720 --> 00:00:38,520 35 | there isn't really much state to keep track of, 36 | 37 | 10 38 | 00:00:38,680 --> 00:00:42,650 39 | so it can be represented by a JavaScript number. 40 | 41 | 11 42 | 00:00:43,450 --> 00:00:49,250 43 | Here is a different example, a list of independent counters that I can add and remove. 44 | 45 | 12 46 | 00:00:49,740 --> 00:00:54,580 47 | In this case, a single number is not enough to represent the state of the application, 48 | 49 | 13 50 | 00:00:54,700 --> 00:00:57,940 51 | so we use an array of JavaScript numbers. 52 | 53 | 14 54 | 00:00:59,370 --> 00:01:03,450 55 | In a more complex application, there is more state to keep track of. 56 | 57 | 15 58 | 00:01:03,680 --> 00:01:07,110 59 | This is a typical to-do app, where I can add to-dos, 60 | 61 | 16 62 | 00:01:07,110 --> 00:01:09,340 63 | I can cross them as completed ones, 64 | 65 | 17 66 | 00:01:09,340 --> 00:01:11,760 67 | and I can change their current filter. 68 | 69 | 18 70 | 00:01:13,600 --> 00:01:16,500 71 | Looking back at the history of the state changes, 72 | 73 | 19 74 | 00:01:16,600 --> 00:01:20,580 75 | we can see that the initial state of the app was a JavaScript object, 76 | 77 | 20 78 | 00:01:20,720 --> 00:01:24,280 79 | containing an array under the todos key, 80 | 81 | 21 82 | 00:01:24,280 --> 00:01:28,100 83 | and a string seen SHOW_ALL, under visibilityFilter. 84 | 85 | 22 86 | 00:01:28,200 --> 00:01:30,440 87 | When I added the first to-do, 88 | 89 | 23 90 | 00:01:30,440 --> 00:01:34,880 91 | it was added to the todos array, inside our state object. 92 | 93 | 24 94 | 00:01:35,080 --> 00:01:39,720 95 | The to-do itself, is described by a plain JavaScript object, 96 | 97 | 25 98 | 00:01:39,720 --> 00:01:43,060 99 | saying it was not completed, and the text was saved. 100 | 101 | 26 102 | 00:01:43,220 --> 00:01:45,940 103 | Every further change that the app, 104 | 105 | 27 106 | 00:01:45,940 --> 00:01:48,620 107 | whether when I was crossing out the to-dos, 108 | 109 | 28 110 | 00:01:48,760 --> 00:01:51,240 111 | or when I changed the visibilityFilter, 112 | 113 | 29 114 | 00:01:51,340 --> 00:01:55,080 115 | resulted in this change to this state object, 116 | 117 | 30 118 | 00:01:55,080 --> 00:01:57,600 119 | described in our whole application. 120 | 121 | 31 122 | 00:01:58,280 --> 00:02:00,800 123 | Now you know the first principle of Redux, 124 | 125 | 32 126 | 00:02:00,900 --> 00:02:04,220 127 | which is that, everything that changes in your application, 128 | 129 | 33 130 | 00:02:04,250 --> 00:02:06,660 131 | including the data and the UI state, 132 | 133 | 34 134 | 00:02:06,660 --> 00:02:11,380 135 | is contained in a single object, we call the state or the state tree. 136 | -------------------------------------------------------------------------------- /translation/06: -------------------------------------------------------------------------------- 1 | I added Redux to our application as a script act from CDNJS. This is the UMD build, so it exports a single global variable called Redux, with a capital R. And in real applications, I suggest you to use NPM instead and a module bundler like Webpack or Browserify, but the UMD build will suffice for our example. 2 | 3 | 我把托管在 CDNJS 上 Redux 库添加到应用。这是 UMD 版本,所以它会导出一个单一的全局变量 Redux,注意是大写的 R。在真实的应用中,我建议你使用 NPM 和 Webpack 或者 Browserify 这样的模块打包工具来替代,但在我们这个例子中,UMD 版本就够用了。 4 | 5 | I'm going to need just a single function from Redux called "createStore". I'm using ES6 destruction syntax here. It's equivalent to writing, "var createStore = Redux.createStore;" or, if you use NPM and something like Babel to transpile your ES6, you can write, "import { createStore }" notice the parenthesis, "from Redux". 6 | 7 | 目前我只需要 Redux 中的一个方法 createStore。这里我使用 ES6 的解构语法。这等价于,"var createStore = Redux.createStore;" 或者,如果你使用 NPM 和像 Babel 这样的转换器来转换你的 ES6 代码,你也可以这样写,"import { createStore }" 注意大括号,"from Redux"。 8 | 9 | This store binds together the three principles of Redux. It holds the current application's state object. It lets you dispatch actions. When you create it, you need to specify the reducer that tells how state is updated with actions. 10 | 11 | 这个 store 把 Redux 的三大原则结合在一起。它保存了应用当前的状态。它允许你向它分发动作。当你创建 store 的时候,你需要指定 reducer 来让它知道当动作分发时,状态要如何更新。 12 | 13 | In this example, we're calling "createStore" with counter as the reducer that manages the state updates. This store has three important methods. 14 | 15 | 在这个例子中,我们调用 createStore 时指定 counter 作为 reducer 来管理状态的更新。这个 store 有三个重要的方法。 16 | 17 | The first method of this store is called "getState". And it retrieves the current state of the Redux store. If we run this, we're going to see 0 because this is the initial state of our application. 18 | 19 | 第一个方法叫 getState。它可以获取 Redux store 当前的状态。如果这时我们执行这个方法,我们将看到 0,因为这是我们这个应用的初始状态。 20 | 21 | The second and the most commonly used store method is called "dispatch". And it lets you dispatch actions to change the state of your application. If we log this store state after dispatch, we're going to see that it has changed. 22 | 23 | 第二个,也是我们最常使用的方法,叫 dispatch。它允许你分发动作来改变应用的状态。如果我们在分发动作后打印 store 的状态,我们可以看到状态发生了改变。 24 | 25 | Of course, always log to the console gets boring, so we're actually going to render something to the body now, with the help of the third Redux store method, called "subscribe". It lets you register a callback that the Redux store will call any time an action has been dispatched, so then you can update the UI of your application. It will reflect the current application state. 26 | 27 | 当然,一直只是打印到控制台有点无聊,我们实际上想渲染一些东西到 body 上,Redux store 的第三个方法 subcribe 可以帮助我们做到这一点。它允许你注册一个回调,只要一个动作被分发,Redux store 就会调用这个回调,这样你就可以更新应用的 UI,它 ( UI ) 反映了应用当前的状态。 28 | 29 | I'm being very naive now. I'm not using React or anything. I'm just rendering the counter into the document body. Any time the body is clicked, I'm going to dispatch an action to increment this counter. 30 | 31 | 我现在用很简单的办法,我不使用 React 或其它,我只是把计数器渲染到网页的 body 上。只要单击 body,我就会分发一个动作来使计数器加 1。 32 | 33 | Now if you pay close attention, you will notice that the initial state, the 0, was not rendered. And this is because I'm rendering inside the subscribe callback, but it doesn't actually fire the very first time. 34 | 35 | 如果你观察得很仔细的话,你会发现初始状态,0,没有被渲染。这是因为渲染只发生在订阅的回调里,但它在最开始的时候 (状态被初始化的时候) 并没有被触发。 36 | 37 | So I extract this logic into "render" method. I subscribe the "render" method to this store. I also call it once to render the initial state. Now it renders the 0, and the click increments the counter. This is our first working Redux application. 38 | 39 | 所以我把这个逻辑抽取出来到 render 方法中。我订阅 render 方法到 store。我同时手动调用 render 方法一次,让它渲染初始状态。现在它显示 0,每次单击都会使计数器递增。这就是我们的第一个 Redux 应用。 40 | -------------------------------------------------------------------------------- /eng/04.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,360 --> 00:00:02,500 3 | You might have heard that the UI 4 | 5 | 2 6 | 00:00:02,500 --> 00:00:05,020 7 | or the view layer is most predictable 8 | 9 | 3 10 | 00:00:05,100 --> 00:00:09,180 11 | when it is described as a pure function of the application state. 12 | 13 | 4 14 | 00:00:10,100 --> 00:00:12,380 15 | This approach was pioneered by React, 16 | 17 | 5 18 | 00:00:12,380 --> 00:00:14,440 19 | but is now being picked up by other frameworks, 20 | 21 | 6 22 | 00:00:14,440 --> 00:00:16,440 23 | such as Ember and Angular. 24 | 25 | 7 26 | 00:00:17,300 --> 00:00:21,040 27 | Redux complements this approach with another idea, 28 | 29 | 8 30 | 00:00:21,040 --> 00:00:24,220 31 | that the state mutations in your app 32 | 33 | 9 34 | 00:00:24,220 --> 00:00:27,020 35 | need to be described as a pure function 36 | 37 | 10 38 | 00:00:27,020 --> 00:00:29,020 39 | that takes the previous state 40 | 41 | 11 42 | 00:00:29,020 --> 00:00:31,020 43 | and the action being dispatched 44 | 45 | 12 46 | 00:00:31,020 --> 00:00:34,800 47 | and returns the next state of your application. 48 | 49 | 13 50 | 00:00:36,180 --> 00:00:38,860 51 | Inside any Redux application, 52 | 53 | 14 54 | 00:00:38,860 --> 00:00:40,860 55 | there is one particular function 56 | 57 | 15 58 | 00:00:40,860 --> 00:00:43,980 59 | that takes the state of the whole application 60 | 61 | 16 62 | 00:00:43,980 --> 00:00:45,980 63 | and the action being dispatched 64 | 65 | 17 66 | 00:00:45,980 --> 00:00:49,560 67 | and returns the next state of the whole application. 68 | 69 | 18 70 | 00:00:50,140 --> 00:00:54,000 71 | It is important that it does not modify the state given to it. 72 | 73 | 19 74 | 00:00:54,000 --> 00:00:58,300 75 | It has to be pure, so it has to return a new object. 76 | 77 | 20 78 | 00:00:59,520 --> 00:01:01,800 79 | Even in large applications, 80 | 81 | 21 82 | 00:01:01,800 --> 00:01:04,220 83 | there is still just a single function 84 | 85 | 22 86 | 00:01:04,220 --> 00:01:08,540 87 | that manages how the next state is calculated 88 | 89 | 23 90 | 00:01:08,540 --> 00:01:11,560 91 | based on the previous state of the whole application 92 | 93 | 24 94 | 00:01:11,560 --> 00:01:13,560 95 | and the action being dispatched. 96 | 97 | 25 98 | 00:01:14,040 --> 00:01:15,980 99 | It does not have to be slow. 100 | 101 | 26 102 | 00:01:15,980 --> 00:01:19,040 103 | For example, if I change the visibility filter, 104 | 105 | 27 106 | 00:01:19,160 --> 00:01:22,060 107 | I have to create a new object for the whole state, 108 | 109 | 28 110 | 00:01:22,320 --> 00:01:24,420 111 | but I can keep the reference 112 | 113 | 29 114 | 00:01:24,420 --> 00:01:27,380 115 | to the previous version of the todos rate, 116 | 117 | 30 118 | 00:01:27,380 --> 00:01:29,280 119 | because it has not changed 120 | 121 | 31 122 | 00:01:29,280 --> 00:01:31,540 123 | when I changed the visibility filter. 124 | 125 | 32 126 | 00:01:31,800 --> 00:01:34,280 127 | This is what makes Redux fast. 128 | 129 | 33 130 | 00:01:35,100 --> 00:01:38,520 131 | Now you know the third and the last principle of Redux. 132 | 133 | 34 134 | 00:01:38,700 --> 00:01:42,040 135 | To describe state mutations you have to write a function 136 | 137 | 35 138 | 00:01:42,180 --> 00:01:44,260 139 | that takes the previous state of the app, 140 | 141 | 36 142 | 00:01:44,260 --> 00:01:45,820 143 | the action being dispatched 144 | 145 | 37 146 | 00:01:45,820 --> 00:01:48,300 147 | and returns the next state of the app. 148 | 149 | 38 150 | 00:01:48,380 --> 00:01:50,600 151 | And this function has to be pure 152 | 153 | 39 154 | 00:01:50,600 --> 00:01:52,600 155 | This function is called the Reducer 156 | 157 | -------------------------------------------------------------------------------- /chn/10.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,100 --> 00:00:01,460 3 | 就像在之前的示例中一样 4 | 5 | 2 6 | 00:00:01,460 --> 00:00:04,480 7 | 我使用了 NPM 中的 expect 和 deep-freeze 库 8 | 9 | 3 10 | 00:00:04,480 --> 00:00:06,480 11 | 来为我的测试做断言 12 | 13 | 4 14 | 00:00:06,760 --> 00:00:10,340 15 | 这次我将测试 toggleTodo() 这个方法 16 | 17 | 5 18 | 00:00:10,660 --> 00:00:12,760 19 | 该方法取一个 todo 对象 20 | 21 | 6 22 | 00:00:12,760 --> 00:00:14,760 23 | 并将其 completed 属性翻转 24 | 25 | 7 26 | 00:00:14,800 --> 00:00:16,760 27 | 如果 completed 的原始值为 false 28 | 29 | 8 30 | 00:00:16,900 --> 00:00:19,120 31 | 其返回结果将变为 true 32 | 33 | 9 34 | 00:00:19,240 --> 00:00:21,400 35 | 如果是 true 其结果将变为 false 36 | 37 | 10 38 | 00:00:22,000 --> 00:00:23,760 39 | 就像上一节课一样 40 | 41 | 11 42 | 00:00:23,760 --> 00:00:28,140 43 | 我将从写一个可变的版本开始 尝试通过当前的测试 44 | 45 | 12 46 | 00:00:28,400 --> 00:00:34,920 47 | 这个版本只是将传入对象的 completed 属性的值翻转一下 48 | 49 | 13 50 | 00:00:35,120 --> 00:00:36,100 51 | 当测试通过之后 52 | 53 | 14 54 | 00:00:36,100 --> 00:00:38,760 55 | 我们知道可变对象在 Redux 中是不允许的 56 | 57 | 15 58 | 00:00:38,940 --> 00:00:40,300 59 | 为了达到这个目的 60 | 61 | 16 62 | 00:00:40,300 --> 00:00:42,560 63 | 我在我的 todo 对象上使用了 deepFreeze 64 | 65 | 17 66 | 00:00:42,780 --> 00:00:46,080 67 | 这样我将再也不能改变它的 completed 属性了 68 | 69 | 18 70 | 00:00:46,820 --> 00:00:49,980 71 | 解决这个问题的方法之一就是创建一个新对象 72 | 73 | 19 74 | 00:00:49,980 --> 00:00:53,140 75 | 这个对象复制了原始对象所有属性 76 | 77 | 20 78 | 00:00:53,380 --> 00:00:55,860 79 | 不过 complete 属性的值是翻转之后的值 80 | 81 | 21 82 | 00:00:56,980 --> 00:01:00,340 83 | 但是 如果我们后面再给新对象添加新属性 84 | 85 | 22 86 | 00:01:00,340 --> 00:01:03,500 87 | 我们可能会忘记更新这段代码来包含这些新的属性 88 | 89 | 23 90 | 00:01:04,200 --> 00:01:06,360 91 | 这就是为什么我建议你使用 92 | 93 | 24 94 | 00:01:06,360 --> 00:01:09,680 95 | Object.assign() 这个 ES6 的新方法 96 | 97 | 25 98 | 00:01:09,860 --> 00:01:16,380 99 | 它让你可以将多个对象的属性赋给目标对象 100 | 101 | 26 102 | 00:01:17,260 --> 00:01:20,140 103 | 注意 Object.assign() 参数顺序 104 | 105 | 27 106 | 00:01:20,140 --> 00:01:23,740 107 | 如何与 JavaScript分配操作符相对应 108 | 109 | 28 110 | 00:01:23,940 --> 00:01:28,700 111 | 左边的参数是我们要赋予新属性的目标对象 112 | 113 | 29 114 | 00:01:28,700 --> 00:01:30,700 115 | 所以它会被改变 116 | 117 | 30 118 | 00:01:30,700 --> 00:01:34,200 119 | 这就是为什么我们传一个空对象给第一个参数 120 | 121 | 31 122 | 00:01:34,280 --> 00:01:36,620 123 | 这样我们就不会改变任何已存在的数据 124 | 125 | 32 126 | 00:01:37,340 --> 00:01:42,280 127 | Object.assign() 的每一个其他参数将被认为一个源对象 128 | 129 | 33 130 | 00:01:42,380 --> 00:01:45,880 131 | 源对象的属性将复制到目标对象 132 | 133 | 34 134 | 00:01:46,500 --> 00:01:49,740 135 | 重要的一点是如果多个源对象中 136 | 137 | 35 138 | 00:01:49,740 --> 00:01:52,320 139 | 有相同名称的属性但有不同的属性值 140 | 141 | 36 142 | 00:01:52,320 --> 00:01:54,000 143 | 那么最终替换的是最后一个源对象的值 144 | 145 | 37 146 | 00:01:54,080 --> 00:01:58,060 147 | 并且这就是我们用来最后覆盖 completed 属性的方式 148 | 149 | 38 150 | 00:01:58,060 --> 00:02:00,860 151 | 而不管原来 todo 对象里面原来的 completed 属性是什么 152 | 153 | 39 154 | 00:02:01,740 --> 00:02:06,500 155 | 最后 你需要记住 Object.assign() 是一个 ES6 的新方法 156 | 157 | 40 158 | 00:02:06,500 --> 00:02:10,000 159 | 所以它不是在所有浏览器中都能使用 160 | 161 | 41 162 | 00:02:10,680 --> 00:02:12,340 163 | 你应该使用一个 polyfill 164 | 165 | 42 166 | 00:02:12,420 --> 00:02:16,580 167 | 可以使用 Babel 或者一个标准的 Object.assign() polyfill 168 | 169 | 43 170 | 00:02:16,660 --> 00:02:20,040 171 | 这样可以避免让你的网站崩溃 172 | 173 | 44 174 | 00:02:21,040 --> 00:02:23,980 175 | 另外一个不需要 polyfill 的方案 176 | 177 | 45 178 | 00:02:23,980 --> 00:02:27,040 179 | 就是使用新的对象扩展操作符 180 | 181 | 46 182 | 00:02:27,040 --> 00:02:29,260 183 | 它不是 ES6 的一部分 184 | 185 | 47 186 | 00:02:29,260 --> 00:02:32,040 187 | 但它已经在 ES7 的议案中出现 188 | 189 | 48 190 | 00:02:32,040 --> 00:02:33,580 191 | 它相当受欢迎 192 | 193 | 49 194 | 00:02:33,580 --> 00:02:38,300 195 | 可以通过设置 Babel 中的 Stage 2 preset 来使用它 196 | 197 | -------------------------------------------------------------------------------- /chn/14.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,250 --> 00:00:01,400 3 | 在上一节中 4 | 5 | 2 6 | 00:00:01,400 --> 00:00:04,400 7 | 我们建立了一种 reducer 组合模式 8 | 9 | 3 10 | 00:00:04,400 --> 00:00:07,270 11 | 在模式中 一个 reducer 可以被另外一个 reducer 调用 12 | 13 | 4 14 | 00:00:07,270 --> 00:00:09,700 15 | 进行数组中元素的更新 16 | 17 | 5 18 | 00:00:10,420 --> 00:00:13,820 19 | 如果我们使用这个 reducer 去创建一个 store 并打印出它的 state 20 | 21 | 6 22 | 00:00:14,000 --> 00:00:18,570 23 | 我们将发现它的初始 state 是一个空的 todos 数组 24 | 25 | 7 26 | 00:00:18,650 --> 00:00:21,450 27 | 如果我们分发一个‘ADD_TODO’动作 28 | 29 | 8 30 | 00:00:21,450 --> 00:00:26,000 31 | 我们会发现相应的 todo 将被添加到 state 数组中 32 | 33 | 9 34 | 00:00:26,470 --> 00:00:29,000 35 | 如果我们发送另外一个‘ADD_TODO’动作 36 | 37 | 10 38 | 00:00:29,000 --> 00:00:32,720 39 | 相对应的 todo 也将被添加到数组的末尾 40 | 41 | 11 42 | 00:00:32,820 --> 00:00:35,050 43 | 还有分发一个 id 为 0 的‘TOGGLE_TODO’的动作 44 | 45 | 12 46 | 00:00:35,050 --> 00:00:40,170 47 | 会翻转 id 为 0 的 todo 的 completed 字段 48 | 49 | 13 50 | 00:00:40,920 --> 00:00:43,450 51 | 在一个简单的示例中 52 | 53 | 14 54 | 00:00:43,470 --> 00:00:47,020 55 | 将应用的整个 state 表示成一个 todos 数组没有什么问题 56 | 57 | 15 58 | 00:00:47,070 --> 00:00:50,100 59 | 但如果我们想要存储更多信息呢? 60 | 61 | 16 62 | 00:00:50,250 --> 00:00:54,820 63 | 例如 我们可能想要让用户通过“显示过滤器” 64 | 65 | 17 66 | 00:00:54,820 --> 00:00:59,870 67 | 比如‘SHOW_COMPLETED’ ‘SHOW_ALL’ 或者 ‘SHOW_ACTIVE’来选择哪些 todo 是当前可见的 68 | 69 | 18 70 | 00:01:00,370 --> 00:01:05,350 71 | 显示过滤器的 state 用一个简单的字符串来表示当前过滤器 72 | 73 | 19 74 | 00:01:05,350 --> 00:01:08,600 75 | 它通过‘SET_VISIBILITY_FILTER’动作来改变 76 | 77 | 20 78 | 00:01:09,100 --> 00:01:10,770 79 | 为了存储这个新的信息 80 | 81 | 21 82 | 00:01:10,770 --> 00:01:13,770 83 | 我不需要改变已经存在的 reducers 84 | 85 | 22 86 | 00:01:13,950 --> 00:01:16,550 87 | 我将使用 reducer 组合模式 88 | 89 | 23 90 | 00:01:16,550 --> 00:01:20,670 91 | 创建一个新的 reducer 来调用已经存在的 reducers 92 | 93 | 24 94 | 00:01:20,670 --> 00:01:22,550 95 | 来管理它的不同部分的状态 96 | 97 | 25 98 | 00:01:22,550 --> 00:01:25,820 99 | 并且将结果合并到一个单独的 state 对象里面 100 | 101 | 26 102 | 00:01:26,320 --> 00:01:28,050 103 | 现在第一次运行它 104 | 105 | 27 106 | 00:01:28,050 --> 00:01:31,650 107 | 将传递 undefined 作为子 reducers 的 state 108 | 109 | 28 110 | 00:01:31,770 --> 00:01:35,250 111 | 因为组合 reducer 的初始 state 是一个空的对象 112 | 113 | 29 114 | 00:01:35,250 --> 00:01:37,720 115 | 所以它的所有属性都是 undefined 116 | 117 | 30 118 | 00:01:38,200 --> 00:01:42,220 119 | 这使得第一次执行时 子 reducers 返回它们的初始 state 120 | 121 | 31 122 | 00:01:42,220 --> 00:01:45,300 123 | 并且填充到 state 对象中 124 | 125 | 32 126 | 00:01:45,670 --> 00:01:46,870 127 | 当一个传递动作进来时 128 | 129 | 33 130 | 00:01:46,870 --> 00:01:52,020 131 | 它调用了全部 reducer 并把他们管理的对应的 state 部分和动作传递进去 132 | 133 | 34 134 | 00:01:52,020 --> 00:01:55,420 135 | 然后将结果合并到一个新的 state 对象中 136 | 137 | 35 138 | 00:01:56,100 --> 00:01:59,520 139 | 这是另外一个 reducer 组合模式的例子 140 | 141 | 36 142 | 00:01:59,520 --> 00:02:04,420 143 | 但这一次我们使用它将多个 reducers 组合成一个单独的 reducer 144 | 145 | 37 146 | 00:02:04,420 --> 00:02:06,950 147 | 我们用它来创建我们的 store 148 | 149 | 38 150 | 00:02:07,850 --> 00:02:13,670 151 | 组合后的 reducer 初始 state 现在包含独立 reducers 的初始state 152 | 153 | 39 154 | 00:02:13,670 --> 00:02:15,670 155 | 每当动作到来时 156 | 157 | 40 158 | 00:02:15,670 --> 00:02:19,400 159 | 那些reducers将独立处理这些动作 160 | 161 | 41 162 | 00:02:20,320 --> 00:02:23,400 163 | 这个模式使 Redux 开发更具扩展性 164 | 165 | 42 166 | 00:02:23,400 --> 00:02:27,500 167 | 因为在团队中的不同的人可以去开发不同的 reducer 168 | 169 | 43 170 | 00:02:27,500 --> 00:02:30,950 171 | 在处理相同动作时不会产生冲突 172 | 173 | 44 174 | 00:02:30,950 --> 00:02:32,600 175 | 还能避免合并(代码)的冲突 176 | 177 | 45 178 | 00:02:33,750 --> 00:02:37,120 179 | 最后 我将分发‘SET_VISIBILITY_FILTER’动作 180 | 181 | 46 182 | 00:02:37,220 --> 00:02:39,570 183 | 你可以看到它并不影响 todos 184 | 185 | 47 186 | 00:02:39,570 --> 00:02:41,570 187 | 但是“显示过滤器”字段被更新了。 188 | 189 | -------------------------------------------------------------------------------- /translation/14: -------------------------------------------------------------------------------- 1 | In the previous lesson, we established the pattern of reducer composition where one reducer can be called by another reducer to update items inside an array. 2 | 3 | 在上一节课中,我们建立了一种 reducer 组合模式。在模式中,一个 reducer 可以被另外一个 reducer 调用,进行数组中元素的更新。 4 | 5 | If we create a store with this reducer and log its state, we will find that the initial state of it is an empty array of todos. If we dispatch an 'ADD_TODO' action, we will find that the corresponding todo has been added to the state array. 6 | 7 | 如果我们使用这个 reducer 去创建一个 store 并且打印出它的 state, 我们将发现它的初始 state 是一个空的 todo 数组。如果我们分发一个‘ADD_TODO’动作,我们会发现相应的 todo 将被添加到 state 数组中。 8 | 9 | If we dispatch another 'ADD_TODO' action, the corresponding todo will also be added at the end of the array. And dispatch a 'TOGGLE_TODO' action with id 0 will flip the completed field of the todo with id 0. 10 | 11 | 如果我们发送另外一个‘ADD_TODO’动作,相对应的 todo 也将被添加到数组的末尾。还有分发一个 id 为 0 的‘TOGGLE_TODO’的动作,会翻转 id 为 0 的 todo 的 completed 字段。 12 | 13 | Representing the whole state of the application as an array of todos works for a simple example, but what if we want to store more information? For example, we may want to let the user choose which todos are currently visible with the visibility filter such as 'SHOW_COMPLETED', 'SHOW_ALL', or 'SHOW_ACTIVE'. 14 | 15 | 在一个简单的示例中,将应用的整个 state 表示成一个 todos 数组没有什么问题,但如果我们想要存储更多信息呢?例如 我们可能想要让用户通过“显示过滤器”,比如‘SHOW_COMPLETED’ ‘SHOW_ALL’ 或者 ‘SHOW_ACTIVE’来选择哪些 todo 是当前可见的。 16 | 17 | The state of the visibility filter is a simple string representing the current filter. It is changed by 'SET_VISIBILITY_FILTER' action. 18 | 19 | 显示过滤器的 state 用一个简单的字符串来表示当前过滤器,它通过‘SET_VISIBILITY_FILTER’动作来改变。 20 | 21 | To store this new information, I don't need to change the existing reducers. I will use the reducer composition pattern and create a new reducer that calls the existing reducers to manage parts of its state and combines the results in a single state object. 22 | 23 | 为了存储这个新的信息,我不需要改变已经存在的 reducer. 我将使用 reducer 组合模式,创建一个新的 reducer 来调用已经存在的 reducer ,来管理它的不同部分的状态,并且将结果合并到一个单独的 state 对象里面。 24 | 25 | Now that the first time it runs, it will pass undefined as the state of the child reducers because the initial state of the combined reducer is an empty object, so all its fields are undefined. This gets the child reducers to return their initial states and populates the state object for the first time. 26 | 27 | 现在第一次运行它,将传递 undefined 作为子 reducers 的 state, 因为组合 reducer 的初始 state 是一个空的对象,所以它的所有属性都是 undefined, 这使得第一次执行时子 reducers 返回它们的初始 state 并且填充到 state 对象中。 28 | 29 | When an action comes in, it calls the reducers with the pass of the state that they manage and the action and combines the results into the new state object. 30 | 31 | 当一个传递动作进来时,它调用了全部 reducer 并把他们管理的对应的 state 部分和动作传递进去,然后将结果合并到一个新的 state 对象中。 32 | 33 | This is another example of the reducer composition pattern, but this time we use it to combine several reducers into a single reducer that we will now use to create our store. The initial state of the combined reducer now contains the initial states of independent reducers. Any time an action comes in, those reducers handle the action independently. 34 | 35 | 这是另外一个 reducer 组合模式的例子,但这一次我们使用它将多个 reducer 组合成一个单独的 reducer, 我们用它来创建我们的 store. 组合后的 reducer 初始 state 现在包含独立 reducer 的初始state. 每当动作到来时,那些 reducer 将独立处理这些动作。 36 | 37 | This pattern helps scale Redux's development because different people on the team can work on different reducers handling the same actions without running into each other and causing merge conflicts. 38 | 39 | 这个模式使 Redux 开发更具扩展性。因为在团队中的不同的人可以去开发不同的 reducer, 在处理相同动作时不会产生冲突,还能避免合并(代码)的冲突。 40 | 41 | Finally, I'm dispatching the 'SET_VISIBILITY_FILTER' action. You can see that it doesn't affect the todos, but the "visibilityFilter" field has been updated. 42 | 43 | 最后,我将分发‘SET_VISIBILITY_FILTER’动作,你可以看到它并不影响 todos, 但是“显示过滤器”字段被更新了。 44 | -------------------------------------------------------------------------------- /chn/06.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,460 --> 00:00:03,200 3 | 我把托管在 CDNJS 上面的 Redux 库 4 | 5 | 2 6 | 00:00:03,360 --> 00:00:06,580 7 | 以 script 标签的形式添加到应用里 8 | 9 | 3 10 | 00:00:06,720 --> 00:00:09,520 11 | 这是 UMD(Universal Module Definition) 版本 12 | 13 | 4 14 | 00:00:09,520 --> 00:00:13,020 15 | 所以它会暴露一个单一的全局变量 Redux 16 | 17 | 5 18 | 00:00:13,060 --> 00:00:14,300 19 | 注意是大写的 R 20 | 21 | 6 22 | 00:00:14,720 --> 00:00:19,600 23 | 在实际的应用中 我建议你使用 NPM 24 | 25 | 7 26 | 00:00:19,700 --> 00:00:23,340 27 | 还有 Webpack 或者 Browserify 这样的模块打包工具 28 | 29 | 8 30 | 00:00:23,460 --> 00:00:27,180 31 | 但在我们的例子中 UMD 版本就够用了 32 | 33 | 9 34 | 00:00:27,600 --> 00:00:32,880 35 | 目前我只需要 Redux 中的一个方法 createStore() 36 | 37 | 10 38 | 00:00:33,860 --> 00:00:36,860 39 | 这里我使用 ES6 的解构语法 40 | 41 | 11 42 | 00:00:36,860 --> 00:00:42,920 43 | 这等价于“var createStore = Redux.createStore;” 44 | 45 | 12 46 | 00:00:43,040 --> 00:00:49,300 47 | 或者 如果你使用 NPM 和像 Babel 这样的转译器来转译你的 ES6 代码 48 | 49 | 13 50 | 00:00:49,500 --> 00:00:54,860 51 | 你也可以这样写“import \ from Redux;” 注意是大括号 52 | 53 | 14 54 | 00:00:57,960 --> 00:01:02,060 55 | store 把 Redux 的三大原则结合在一起 56 | 57 | 15 58 | 00:01:02,420 --> 00:01:05,620 59 | 它保存了应用当前的状态 60 | 61 | 16 62 | 00:01:05,820 --> 00:01:07,920 63 | 它允许你分发动作 64 | 65 | 17 66 | 00:01:08,100 --> 00:01:09,520 67 | 当你创建 store 的时候 68 | 69 | 18 70 | 00:01:09,520 --> 00:01:16,260 71 | 你需要指定 reducer 来让它知道当动作分发时 状态要如何更新 72 | 73 | 19 74 | 00:01:16,760 --> 00:01:17,760 75 | 在这个例子中 76 | 77 | 20 78 | 00:01:17,800 --> 00:01:19,460 79 | 我们调用 createStore 时 80 | 81 | 21 82 | 00:01:19,520 --> 00:01:23,720 83 | 指定 counter 作为 reducer 来管理状态的更新 84 | 85 | 22 86 | 00:01:24,400 --> 00:01:27,520 87 | 这个 store 有三个重要的方法 88 | 89 | 23 90 | 00:01:27,920 --> 00:01:31,460 91 | 第一个方法叫 getState() 92 | 93 | 24 94 | 00:01:31,860 --> 00:01:35,680 95 | 它可以获取 Redux store 当前的状态 96 | 97 | 25 98 | 00:01:36,420 --> 00:01:38,780 99 | 如果这时我们执行这个方法 我们将看到 0 100 | 101 | 26 102 | 00:01:38,920 --> 00:01:42,800 103 | 因为这是我们这个应用的初始状态 104 | 105 | 27 106 | 00:01:44,140 --> 00:01:48,640 107 | 第二个 也是我们最常使用的方法 叫 dispatch() 108 | 109 | 28 110 | 00:01:48,800 --> 00:01:53,200 111 | 它允许你分发动作来改变应用的状态 112 | 113 | 29 114 | 00:01:53,760 --> 00:01:56,620 115 | 如果我们在分发动作后打印 store 的状态 116 | 117 | 30 118 | 00:01:56,720 --> 00:01:59,140 119 | 我们可以看到状态发生了改变 120 | 121 | 31 122 | 00:01:59,800 --> 00:02:02,760 123 | 当然 一直只是打印到控制台有点无聊 124 | 125 | 32 126 | 00:02:02,940 --> 00:02:06,000 127 | 我们实际上想渲染一些东西到 body 上 128 | 129 | 33 130 | 00:02:06,100 --> 00:02:10,260 131 | Redux store 的第三个方法 subscribe() 可以帮助我们做到这一点 132 | 133 | 34 134 | 00:02:10,860 --> 00:02:12,820 135 | 它允许你注册一个回调 136 | 137 | 35 138 | 00:02:12,820 --> 00:02:16,940 139 | 只要一个动作被分发 Redux store 就会调用这个回调 140 | 141 | 36 142 | 00:02:17,100 --> 00:02:20,420 143 | 这样你就可以更新应用的 UI 144 | 145 | 37 146 | 00:02:20,500 --> 00:02:23,160 147 | 而它(UI)反映了应用当前的状态 148 | 149 | 38 150 | 00:02:23,460 --> 00:02:25,340 151 | 我现在只用很简单的办法 152 | 153 | 39 154 | 00:02:25,340 --> 00:02:27,200 155 | 不使用 React 或其它库 156 | 157 | 40 158 | 00:02:27,240 --> 00:02:30,880 159 | 我只是把计数器渲染到网页的 body 上 160 | 161 | 41 162 | 00:02:30,980 --> 00:02:32,960 163 | 只要单击 body 164 | 165 | 42 166 | 00:02:33,100 --> 00:02:36,920 167 | 我就会分发一个动作来使计数器加 1 168 | 169 | 43 170 | 00:02:37,480 --> 00:02:43,120 171 | 如果你观察得很仔细的话 你会发现初始状态 0 没有被渲染 172 | 173 | 44 174 | 00:02:43,320 --> 00:02:47,900 175 | 这是因为渲染只发生在订阅的回调里 176 | 177 | 45 178 | 00:02:48,020 --> 00:02:51,320 179 | 但它在最开始的时候并没有被触发 180 | 181 | 46 182 | 00:02:51,440 --> 00:02:54,340 183 | 所以我把这个逻辑抽取出来到 render() 方法中 184 | 185 | 47 186 | 00:02:54,500 --> 00:02:57,940 187 | 我订阅 render() 方法到 store 188 | 189 | 48 190 | 00:02:58,080 --> 00:03:01,860 191 | 我同时手动调用 render() 方法一次 让它渲染初始状态 192 | 193 | 49 194 | 00:03:02,640 --> 00:03:06,600 195 | 现在它显示 0 而且每次单击都会使计数器递增 196 | 197 | 50 198 | 00:03:06,600 --> 00:03:09,580 199 | 这就是我们的第一个能用的 Redux 应用 200 | 201 | -------------------------------------------------------------------------------- /src/28: -------------------------------------------------------------------------------- 1 | In the previous lesson, we used the "connect" function from "react-edux" bindings library to generate the container component that renders our presentational component. I specify how to calculate the props to inject from the current Redux store state and the callback props to inject from the "dispatch" function on the Redux store. 2 | 3 | Normally, I would keep these functions, called "mapStateToProps" and "mapDispatchToProps", but I'm working in a single file right now. And I need to write these functions for your other container components, so I'm going to rename them to something more specific, which you don't have to do in your code if you keep every component in its own file. 4 | 5 | I will also remove the line breaks here to make it clear that these functions are only relevant for generating this particular container component. 6 | 7 | Now I'm scrolling up to the "AddTodo" component, which is not clearly presentational or a container component. However, it uses the store. It reads the store from the context to dispatch an action when the button is clicked. And it has to declare the "contextTypes" to be able to grab the store from the context. 8 | 9 | Context is an unstable API, so it's best to avoid using it in your application code. Instead of reading the store from the context, I will read the "dispatch" function from the props because I only need the "dispatch" here. I don't need the whole store. 10 | 11 | And I will create a container component with "connect" that will inject the "dispatch" function as a prop. I will remove the "contextTypes" because the component generated by "connect" function will take care of reading the store from the context. 12 | 13 | Because I changed the "AddTodo" declaration from the const to the let binding, I can reassign it now so that the consuming component does not need to specify the "dispatch" prop because it will be injected by the component generated by the "connect" code. 14 | 15 | The first argument to the "connect" function is "mapStateToProps", but there aren't any props for "AddTodo" component that depend on the current state, so I return an empty object. The second argument to "connect" is "mapDispatchToProps", but "AddTodo" component doesn't need any callback props. It just accepts the "dispatch" function itself, so I'm returning it as a prop with the same name. 16 | 17 | Finally, I'm calling the function for a second time to specify the component I want to wrap, in this case, "AddTodo" itself. The generated container component will not pass any props dependent on the state, but it will pass "dispatch" itself as a function so that the component can read it from the props and use it without worrying about context or specifying "contextTypes". 18 | 19 | However, it is wasteful to even subscribe to the store if we don't calculate any props from the state. So I'm replacing the "mapStateToProps" function with an "null", which tells "connect" that there is no need to subscribe to the store. 20 | 21 | Additionally, it's pretty common pattern to inject just the "dispatch" function. So this is why if you specify "null" or any falsy value in "connect" as the second argument, you're going to get "dispatch" injected as a prop. So in fact, I can just remove all arguments here. And the default behavior will be to not subscribe to the store and to inject just the "dispatch" function as a prop. 22 | 23 | Let's recap what happens to the components here. The "AddTodo" component that I declare accepts "dispatch" as a prop, but it doesn't know how to get the store. It just hopes that someone is going to pass the "dispatch" to it. 24 | 25 | The "connect" code without any arguments is going to generate a container component that does not subscribe to the store. However, that will pass "dispatch" to the component that it wraps. And in this case, it wraps my "AddTodo" component. 26 | 27 | The second "connect" call returns the generated container component. And I'm assigning it to "AddTodo". So I'm reassigning the let binding the second time. 28 | 29 | And when the further code references "AddTodo", it's going to reference the container component that does not need the "dispatch" prop and that will pass the "dispatch" prop to my inner "AddTodo" component that I don't have a reference to anymore. 30 | -------------------------------------------------------------------------------- /src/20: -------------------------------------------------------------------------------- 1 | In the last few lessons, we created a user interface for a simple React and Redux todo list where I can add items, toggle them as completed, and change the currently visible todos. 2 | 3 | And we did that by having a single "TodoApp" component that has the input, the button for adding todos, the list of currently visible todos with click handler. And it has these three links that let us change the currently visible todos. 4 | 5 | The single component approach worked so far. However, we really want to have many components that can be used, tested, and changed by different people separately. So we're going to refactor our application in this lesson. 6 | 7 | The first component I want to extract from the "TodoApp" component is the "Todo" component that renders a single list item. I am declaring the "Todo" component as a function which React 14 allows me to do. I'm not sure which props it's going to have, so I'll leave them blank for now. And I will just paste the list item I copied before. 8 | 9 | The first thing I'm doing is removing the special "key" property because it's only needed when I enumerate an array. And I'll use it later when enumerating many todos. 10 | 11 | One of my goals with this refactoring is to make every component as flexible as it is reasonable. Right now, I have hardcoded that clicking a todo always causes the "TOGGLE_TODO" action. And this is perfectly fine to do in your app. 12 | 13 | However, I prefer to have a bunch of components that don't specify any behaviors, and that are only concerned with how things look or how they render. I call such components the presentational components. 14 | 15 | I would like to give todo a presentational component, so I removed the "onClick" handler. And I promote it to be a prop so that anyone who uses "Todo" component can specify what happens on the click. And you don't have to do this in your Redux apps, but I find it to be a very convenient pattern. 16 | 17 | Finally, while it doesn't make a lot of difference, I prefer to keep it explicit what is the data that the component needs to render. So instead of passing a todo object, I pass "completed" and "text" fields as separate props. 18 | 19 | Note how the "Todo" component is now a purely presentational component and doesn't specify any behavior. But it knows how to render "ADD_TODO". 20 | 21 | The next component I create is called "TodoList". And it's also a presentational component. It's only concerned with how things look. 22 | 23 | And it accepts an array of todos. And it's going to render an unordered list of them by calling the "todos.map" function and rendering a "Todo" component for every todo. It tells React to use todo ID as the unique key for the elements. And it spreads over the "todo" object properties so the "text" and "completed" end up as props on the "Todo" component. 24 | 25 | Now I need to specify what happens when a todo is clicked. And I could have dispatched an action here. And again, that would be fine, but I want it to a presentational component, so I'm going to call another function, called "onTodoClick", and pass the ID of the todo, and let it decide what should happen. So "onTodoClick" is another prop for the "TodoList". 26 | 27 | Both "Todo" and "TodoList" are presentational components, so we need something I call, "container components" to actually pass the data from the store and to specify the behavior. In this case, the top level "TodoAppComponent" acts as a container component. And We will see more examples of container components in the future lessons. 28 | 29 | In this case, it just renders the "TodoList" with visible todos as the "todos", and with a callback that says that when "onTodoClick" is called with a todo ID, we should dispatch an action on the store with the type "TOGGLE_TODO" and the ID of the todo. 30 | 31 | Let's recap again how this works. The "TodoApp" component renders a "TodoList", and it passes a function to it that can dispatch an action. The "TodoList" component renders the "Todo" component and passes "onClick" prop which calls "onTodoClick". 32 | 33 | The "Todo" component just uses the "onClick" prop it receives and binds it to the list item's "onClick". This way, when it's called, the "onTodoClick" is called, and this dispatches the action and updates the visible todos because the action updates the store. 34 | -------------------------------------------------------------------------------- /translation/18: -------------------------------------------------------------------------------- 1 | In the last lesson, we implemented a simple UI for the todo list application that is able to add new todos and view the existing todos in the list. 2 | 3 | 在上节课,我们为 todo 列表应用实现了一个简单的 UI,可以添加新的 todo,查看已存在的 todo。 4 | 5 | To add the todos, we dispatched the "ADD_TODO" action. In this lesson, we're going to dispatch the "TOGGLE_TODO" action to toggle the completed state of the todos by clicking on them. 6 | 7 | 为了添加新的 todo,我们发起了 ADD_TODO 动作。在这节课,我们将通过点击 todo,发起 TOGGLE_TODO 的动作来切换 todo 的已完成状态。 8 | 9 | I'm scrolling down to my React component. And I've got a list item here corresponding to the todo, so I'm adding the "onClick" handler. So when the user clicks on the list item, I want to dispatch an action to my store with a type "TOGGLE_TODO" and the id of the todo being toggled, which I get from the todo object. 10 | 11 | 我往下滚动到 ( 定义 ) 我的 React 组件的地方。在这里我得到一个 todo 对应的列表元素,所以我 ( 在这 ) 添加一个 onClick 的处理方法。当用户单击列表元素时,我想发起一个动作到 store 中,它具有 TOGGLE_TODO 的类型,并且还有即将被切换 ( 已完成状态 ) 的 todo 的 id,id 从这个 todo 对象中得到。 12 | 13 | The event handler knows which todo it corresponds to, so it is able to pass its id in the action. 14 | 15 | 这个事件处理方法知道它所对应的 todo,所以它能够在动作中传递它 ( todo ) 的 id。 16 | 17 | In the user interface, I want the completed todos to appear crossed out, so I'm adding this style attribute to the list item. And I'm going to use the "textDecoration" property, which is going to be a "line-through" when todo completed is true, and "none" when todo completed is false, so I get a normal looking todo. 18 | 19 | 在用户界面上,我想让已完成的 todo 显示成删除的样子,所以我为这个列表元素添加 style 属性。我将使用 textDecoration 属性,当 todo 的已完成状态为真时,它的值为 line-through,当 todo 的已完成状态为假时,它的值为 none。所以现在我得到看上去正常的 todo。 20 | 21 | Now, if I add a couple of todos, I can click on them and they're going to appear toggled, and I can toggle them back. Isn't that satisfying? 22 | 23 | 现在,如果我添加一些 todo,我可以点击它们,它们的状态会被切换,我还可以把它们的状态切换回来,还不错吧。 24 | 25 | Let's recap how toggling the todo actually works. 26 | 27 | 让我们来回顾一下切换 todo ( 的状态 ) 究竟是怎么工作的。 28 | 29 | It starts with me dispatching the "TOGGLE_TODO" action inside my click handler, with a type "TOGGLE_TODO" and the id, which is the id of the todo being rendered. I get the todo object as an argument to the array map callback inside my render method where I render all the todos. 30 | 31 | 它是从我的点击处理方法中发起 TOGGLE_TODO 的动作开始的,这个动作具有 TOGGLE_TODO 的类型,和当前正在被渲染的 todo 的 id。在渲染所有 todo 的 render 方法中,我从数组的 map 回调函数中得到作为参数的 todo 对象。 32 | 33 | When an action is dispatched, the store will call the root reducer, which will call the "todos" reducer with the array of todos and the action. In this case, the action type is "TOGGLE_TODO", so the "todos" reducer delegates handling of every todo to the "todo" reducer with a "map" function to call it for every todo item. So the "todo" reducer receives the todo as state, and the action. 34 | 35 | 当一个动作被分发,store 将会调用根 reducer,根 reducer 又将会调用 todos reducer 来处理 todos 数组和此动作。在这个例子中,这个动作的类型是 TOGGLE_TODO,所以 todos reducer 以代理的方式,在 map 函数中调用 todo reducer 来处理每一个 todo。所以 todo reducer 接收 todo 作为状态,还接收动作。 36 | 37 | Again, we switch on the action type, and it matches "TOGGLE_TODO" string. And now, for every todo whose id does not match the id specified in the action, we just return the previous state, that is the todo object, as it was. 38 | 39 | 我们继续对比动作的类型,它匹配 TOGGLE_TODO 字符串。现在,对于每一个和动作中指定的 id 不匹配的 todo,我们就返回之前的状态,即原来的 todo 对象。 40 | 41 | However, if the id of the todo matches the one specified in the action, we're going to return a new object with all the properties of the original todo, but with the "completed" field equal to the opposite value of what it was. 42 | 43 | 然而,如果 todo 的 id 和动作中指定的 id 相匹配,我们将返回一个新的对象,它具有原来 todo 所有的属性,但是 completed 字段的值和原来是相反的。 44 | 45 | The updated todo item will be included in the "todos" field under the new application state. And because we subscribe the render function is going to get the next state of the application in "store.getState()" and pass the new version of the todos to the "TodoApp" component, which is going to render the updated todos. 46 | 47 | 更新后的 todo 元素将会被包含在应用的新状态的 todos 字段中。而且因为我们订阅了 render 方法,它将从 store.getState() 中得到应用的下一状态,然后将新的 todos 传递给 TodoApp 组件,TodoApp 组件将渲染更新后的 todos。 48 | 49 | Finally, this style of the list item, the bands on the todo "completed" field, which we just updated, which is why it re-renders in a cross-child state. 50 | 51 | 最后,列表元素的这个 style,和 todo 的 completed 字段绑定,我们刚更新它,这就是为什么它重新渲染时显示删除线。 52 | -------------------------------------------------------------------------------- /translation/29: -------------------------------------------------------------------------------- 1 | Finally, let's take a look at the "FilterLink" container component that renders a "Link" with an active property and a click handler. First, I will write the "mapStateToProps" function, which I'll call, "mapStateToLinkProps", because I have everything in a single file. 2 | 3 | 最后,让我们来看一下 FilterLink 容器组件,它渲染拥有激活属性和点击处理方法的 Link 组件。首先,我来写 mapStateToProps 方法,我把它命名为 mapStateToLinkProps,因为我把所有内容都写在一个文件里。 4 | 5 | And it's going to accept the state of the Redux store and return the props that should be passed to the "Link". And we only have a single such prop called "active" that determines whether the "Link" displays the current visibility filter. 6 | 7 | 它将接受 Redux store 的状态,并返回应该被传递给 Link 组件的属性。我们 (这里) 只有一个叫 active 的属性,它用来决定 Link 组件显示的是不是当前可见性过滤值。 8 | 9 | When I paste this expression from the "render" method, I see that it references the "filter" prop of the "FilterLink" component. To tell whether a "Link" is active, we need to compare this prop with the "visibilityFilter" value from the Redux store state. 10 | 11 | 当我从 render 方法中粘贴这个表达式时,我发现它引用了 FilterLink 组件的 filter 属性。为了表明一个 Link 组件是否激活,我们需要把这个属性和 Redux store 的状态中的 visibilityFilter 值进行比较。 12 | 13 | It is fairly common to use the container props when calculating the child props, so this is why props are passed as a second argument to "mapStateToProps". I will rename it to "ownProps" to make it clear we are talking about the container component's own props and not the props that are passed through the child, which is the return value of "mapStateToProps". 14 | 15 | 使用容器 (组件的) 属性去计算子 (组件的) 属性是相当常见的,所以这就是为什么 props 被作为 mapStateToProps 的第二个参数进行传递,我将把它重命名为 ownProps,使它更清晰的表明,我们谈论的是容器组件自己的属性,而不是作为 mapStateToProps 的返回值,用于传递给子组件的属性。 16 | 17 | The second function I'm writing here is "mapDispatchToProps" or, to avoid name clashes in this JSbin, "mapDispatchToLinkProps". The only argument so far is the "dispatch" function. And I'm going to need to look at the container component I wrote by hand to see what props depend on the "dispatch" function. 18 | 19 | 这里我写的第二个方法是 mapDispatchToProps,或者,为了避免在 JSBin 中的命名冲突,(它实际叫) mapDsipatchToLinkProps,目前为止它唯一的参数是 dispatch 方法。我将需要看看我手写的容器组件有哪些属性依赖 dispatch 方法。 20 | 21 | In this case, this is just the click handler where I dispatch the "SET_VISIBILITY_FILTER" action. The only prop I'm passing down is called, "onClick". And I declare it as an arrow function with no arguments, and I paste the dispatch code. But it references the props again, so I need to add "ownProps" as an argument, the second argument, to "mapDispatchToProps" function to you. 22 | 23 | 在这个例子中,这是分发了 SET_VISIBILITY_FILTER 动作的点击处理方法。我将往下传递的唯一属性是 onClick,而且我用无参的箭头函数来声明它,我粘贴 dispatch 的代码,但是它又引用了 (容器组件的) 属性,所以我需要添加 ownProps 参数,作为 mapDispatchToProps 的第二个参数。 24 | 25 | Finally, I will call the "connect" function from "react-redux" library to generate the "FilterLink" container component. I pass the relevant "mapStateToProps" function as the first argument, the "mapDispatchToProps" as the second argument, and I call the function again with a "Link" component which should be rendered. Now I can remove the old "FilterLink" implementation. 26 | 27 | 最后,我将调用来自 react-redux 库的 connect() 方法,用来生成 FilterLink 容器组件,我传递相应的 mapStateToProps 方法作为第一个参数,mapDispatchToProps 作为第二个参数,然后我用应该被渲染的 Link 组件 (作为参数) 再一次调用这个方法。现在我可以移除掉旧的 FilterLink 实现了。 28 | 29 | Let's recap the data flow in this example. The "Footer" component renders three "FilterLink"s, and each of them has a different "filter" prop that specifies which filter it corresponds to. This prop will be available on the "ownProps" object that both "mapDispatchToProps" and "mapStateToProps" will receive as the second argument. 30 | 31 | 让我们来回顾一下这个例子中的数据流。Footer 组件渲染三个 FilterLink,每一个 FilterLink 都有一个不同的 filter 属性,用来指定它对应的过滤值。这个属性将用于 ownProps 对象,它是 mapDispatchToProps 和 mapStateToProps 接收的第二个参数。 32 | 33 | We pass these two functions to the "connect" call, which will return a container component called, "FilterLink". The "FilterLink" will take the props that will return from the "mapDispatchToProps" and "mapStateToProps" and pass them as props to the "Link" component that it wraps. 34 | 35 | 我们传递这两个方法给 connect() 调用,它将返回一个容器组件叫 FilterLink。FilterLink 将使用 mapDispatchToProps 和 mapStateToProps 返回的值作为属性传递给它包装的 Link 组件。 36 | 37 | We can now use the "FilterLink" container component and specify just the "Filter", but the underlying "Link" component will have received the calculated "active" and "onClick" values. 38 | 39 | 我们现在可以用 FilterLink 容器组件,而且只需指定 filter (属性),但底层的 Link 组件可以接收计算得到的 active 和 onClick 值。 -------------------------------------------------------------------------------- /src/27: -------------------------------------------------------------------------------- 1 | In the previous lesson, I added "react-redux" bindings to the project and I used a "Provider" component from "react-redux" to pass the store down the context so that the container components can read the store from the context and subscribe to its changes. All container components are very similar. 2 | 3 | They need to re-render when the store state changes, they need to unsubscribe from the store when they unmount and they take the current state of the Redux store and use it to render the presentational components with some props that they calculate from the state of the store, and they also need to specify the "contextTypes" to get the store from the context. 4 | 5 | I'm going to write this component in a different way now. And I'll declare a function called "mapsStateToProps" which takes the redux store state and returns the props that I need to pass to the presentational "TodoList" component, to render it with the current state. 6 | 7 | In this case, there is just a single prop called "todos". So I copy-paste this expression to the "mapStateToProps "function. It returns the props that depend on the current state of the redux store. So in this case, this is just the "todos" prop. 8 | 9 | I'm creating another function that I call "mapDispatchToProps". And it accepts the "dispatch" method from the store as the only argument and returns the props that should be passed to the "TodoList" component and they depend on the "dispatch" method. 10 | 11 | The only prop that uses "store.dispatch" is called "onTodoClick". So I copy-paste the "onTodoClick", into "mapDispatchToProps". Now that I don't have the reference to the store here anymore. So instead, I'm changing it just to use the "dispatch", which is provided as an argument to "mapDispatchToProps". 12 | 13 | I will add some punctuation to make it appear easier on my eyes. "onTodoClick" is a function that accepts the ID of the todo, and dispatches an action. Now, I've got two different functions. 14 | 15 | The first one maps the redux store state to the props of the "TodoList" component that are related to the data from the redux store. The second function maps the "dispatch" method of the store to the callback props of "TodoList" component. It specifies the behavior, that is, which callback prop dispatches which action. 16 | 17 | To gather this, your function has described a container component so well that instead of writing it, I can generate it by using the "connect" function provided by "react-redux" library. If you use Babble and NPM, you will likely import it like this instead. And don't forget the curly braces. 18 | 19 | Now, instead of declaring a class, I'm going to declare a variable. And I will call the "connect" method to obtain it. I'm passing "mapStateToProps" as the first argument and "mapDispatchToProps" as the second argument. And notice that this is a curry function, so I have to call it once again. And this time, I pass the presentational component that I wanted to wrap and pass the props to it. 20 | 21 | The "connect" function will generate the component, just like the one I previously wrote by hand. So I don't need to write the code to subscribe to the store or to specify the "contextTypes", because the "connect" function takes care of that. 22 | 23 | Now, let's recap how to generate the container component using the "connect" function. First, I'll write a function called "mapStateToProps" that takes the state of the redux store and returns the props for the presentational component calculated from it. 24 | 25 | These props will be updated anytime the state changes. Next, I write a function that I call "mapDispatchToProps". It takes the "stores.dispatch" method as its first argument. And it returns the props that used the "dispatch" method to dispatch actions, so it returns the callback props needed for the presentational component. 26 | 27 | To create the container component from them, I import "connect" from the "react-redux" library and I call it passing "mapStateToProps" as the first argument and "mapDispatchToProps" as the second argument. 28 | 29 | Finally, I close the function call paren and I open another paren, because this is a curry function and it needs to be called twice. And the last argument is the presentational component that I want to connect to the redux store. 30 | 31 | The result of the "connect" call is the container component that is going to render my presentational component. It will calculate the props to pass to the presentational component by merging the objects returned from "mapStateToProps", "mapDispatchToProps", and its own props. 32 | -------------------------------------------------------------------------------- /eng/02.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,260 --> 00:00:04,740 3 | The second principle of Redux is that the state tree is read-only. 4 | 5 | 2 6 | 00:00:04,920 --> 00:00:07,300 7 | You cannot modify or write to it. 8 | 9 | 3 10 | 00:00:07,720 --> 00:00:10,480 11 | Instead, anytime you want to change the state, 12 | 13 | 4 14 | 00:00:10,700 --> 00:00:12,900 15 | you need to dispatch an action. 16 | 17 | 5 18 | 00:00:13,900 --> 00:00:17,880 19 | An action is a plain JavaScript object describing the change. 20 | 21 | 6 22 | 00:00:18,400 --> 00:00:23,120 23 | Just like the state is the minimal representation of the data in your app, 24 | 25 | 7 26 | 00:00:23,380 --> 00:00:28,420 27 | the action is the minimal representation of the change to that data. 28 | 29 | 8 30 | 00:00:29,340 --> 00:00:32,160 31 | The structure of the action object is up to you. 32 | 33 | 9 34 | 00:00:32,360 --> 00:00:35,640 35 | The only requirement is that it has a "type" property, 36 | 37 | 10 38 | 00:00:35,780 --> 00:00:37,300 39 | which is not "undefined". 40 | 41 | 11 42 | 00:00:37,420 --> 00:00:39,060 43 | We suggest using strings, 44 | 45 | 12 46 | 00:00:39,200 --> 00:00:40,960 47 | because they are serializable. 48 | 49 | 13 50 | 00:00:41,700 --> 00:00:42,860 51 | In different apps, 52 | 53 | 14 54 | 00:00:42,880 --> 00:00:45,060 55 | you're going to have different types of actions. 56 | 57 | 15 58 | 00:00:45,060 --> 00:00:50,740 59 | For example, in a counter app we only have "INCREMENT" and "DECREMENT" actions. 60 | 61 | 16 62 | 00:00:50,860 --> 00:00:53,940 63 | And we don't pass any additional information, 64 | 65 | 17 66 | 00:00:53,940 --> 00:00:57,460 67 | because this is all that is needed to describe these changes. 68 | 69 | 18 70 | 00:00:58,700 --> 00:01:01,940 71 | But say, for a counter release example, 72 | 73 | 19 74 | 00:01:01,940 --> 00:01:03,340 75 | we have more actions. 76 | 77 | 20 78 | 00:01:03,340 --> 00:01:05,340 79 | We have "ADD_COUNTER" action, 80 | 81 | 21 82 | 00:01:05,760 --> 00:01:07,800 83 | we have a "REMOVE_COUNTER" action, 84 | 85 | 22 86 | 00:01:08,380 --> 00:01:12,160 87 | and anytime I change the individual counter, 88 | 89 | 23 90 | 00:01:12,320 --> 00:01:17,960 91 | you can see that the "INCREMENT" and the "DECREMENT actions now have index. 92 | 93 | 24 94 | 00:01:18,220 --> 00:01:22,780 95 | Because we need to describe which particular counter was changed. 96 | 97 | 25 98 | 00:01:24,380 --> 00:01:28,720 99 | This approach scales well to medium and complex applications. 100 | 101 | 26 102 | 00:01:29,540 --> 00:01:35,340 103 | Anytime I add a todo, the components don't really know how exactly it's been added. 104 | 105 | 27 106 | 00:01:35,600 --> 00:01:39,960 107 | All they know is that they need to dispatch an action with a type, 108 | 109 | 28 110 | 00:01:39,960 --> 00:01:44,580 111 | "ADD_TODO", and the text of the todo and a sequential ID. 112 | 113 | 29 114 | 00:01:45,220 --> 00:01:50,060 115 | And, If I toggle a todo, again, the components don't know how it happens. 116 | 117 | 30 118 | 00:01:50,300 --> 00:01:54,140 119 | All they need to do is to dispatch an action with a type, 120 | 121 | 31 122 | 00:01:54,140 --> 00:01:59,120 123 | toggle todo and pass in the ID of the todo I want to toggle. 124 | 125 | 32 126 | 00:01:59,800 --> 00:02:02,420 127 | The same is true for the visibility filter. 128 | 129 | 33 130 | 00:02:02,640 --> 00:02:04,960 131 | Anytime I click on this control 132 | 133 | 34 134 | 00:02:05,080 --> 00:02:07,480 135 | to change the currently visible todos, 136 | 137 | 35 138 | 00:02:07,620 --> 00:02:13,280 139 | what really happens is this component dispatches an action with a type, 140 | 141 | 36 142 | 00:02:13,280 --> 00:02:15,060 143 | "SET_VISIBILITY_FILTER", 144 | 145 | 37 146 | 00:02:15,200 --> 00:02:19,480 147 | and pass in the desired filter string, filter filled. 148 | 149 | 38 150 | 00:02:19,640 --> 00:02:22,100 151 | But these are all plain objects, 152 | 153 | 39 154 | 00:02:22,160 --> 00:02:24,800 155 | describing what happens in a wrap. 156 | 157 | 40 158 | 00:02:26,680 --> 00:02:29,400 159 | Now you know the second principle of Redux -- 160 | 161 | 41 162 | 00:02:29,500 --> 00:02:31,340 163 | the state is read-only. 164 | 165 | 42 166 | 00:02:31,620 --> 00:02:36,020 167 | The only way to change the state tree is by dispatching an action. 168 | 169 | 43 170 | 00:02:36,520 --> 00:02:38,980 171 | An action is a plain JavaScript object, 172 | 173 | 44 174 | 00:02:39,100 --> 00:02:43,300 175 | describing in the minimal way what changed in the application. 176 | 177 | 45 178 | 00:02:43,780 --> 00:02:48,500 179 | Whether it is initiated by a network request or by user interaction, 180 | 181 | 46 182 | 00:02:48,620 --> 00:02:54,320 183 | any data that gets into the Redux application gets there by actions. 184 | 185 | -------------------------------------------------------------------------------- /translation/24: -------------------------------------------------------------------------------- 1 | In the previous lessons, we used this store top-level variable to refer to the Redux store. The components that access the store, such as the container components, read the state from it, subscribe to the store, and dispatch actions on the store using this store top-level variable. 2 | 3 | 在前面的课程中,我们使用这个最顶层的 store 变量来关联 Redux store。访问 store 的组件,比如那些容器组件,它们都使用这个最顶层的 store 变量来从 store 读取状态,订阅 store,以及分发动作到 store。 4 | 5 | This approach works fine for JSbin example where everything is in a single file. However, it doesn't scale to real applications for several reasons. 6 | 7 | 这种方法对于一个 JSBin 的例子来说工作正常,因为所有的东西都在一个文件里。但是,有几个原因使它不能扩展到真正的应用中。 8 | 9 | First of all, it makes your container components harder to test because they reference a specific store, but you might want to supply a different mock store in the test. Secondly, it makes it very hard to implement universal applications that are rendered on the server, because on the server, you want to supply a different store instance for every request because different requests have different data. 10 | 11 | 首先,它使你的容器组件很难测试,因为它们关联到了一个具体的 store,但是你可能想在测试中使用一个不同的假的 store。其次,它导致很难实现在服务器端进行渲染的通用应用,因为在服务器端,你想为每一个请求提供不同的 store 实例,因为不同的请求携带不同的数据。 12 | 13 | I'm going to start by moving the store creation code to the bottom of the file where I render my React components. I'm going to change it slightly. And instead of creating the store top-level variable, I will pass the store I create as a prop to the top-level component, so it is completely injectable into it. 14 | 15 | 我将开始把创建 store 的代码移到文件底部,在渲染我的 React 组件的地方。我将稍微地改变它。我将把创建的 store 作为属性传递给最顶层的组件,替代创建最顶层的 store 变量,这样它是完全可注入的。 16 | 17 | Every container component needs a reference to the store so unfortunately, we have to pass it down to every component as a prop. It's less effort than passing different data through every component, but it's still inconvenient. So, don't worry, we'll find a better solution later, but for now, we need to see the problem. 18 | 19 | 每一个容器组件都需要一个 store 的引用,所以很不幸,我们需要把它作为属性往下传递给每一个组件。比起传递不同的数据给每一个组件,这样更省力,但它仍然不方便。所以,别急,我们之后会找到一个更好的解决办法。但是现在,我们需要看看这个问题。 20 | 21 | The problem is that the container components need to have the store instance to get the state from it, dispatch actions and subscribe to the changes. So this time, I'm changing the container component to take the store from the props using the ES6 destruction syntax, which just means "store equals props.store." 22 | 23 | 这个问题就是容器组件需要 store 的实例来获取状态,分发动作和订阅变化。所以这次,我把容器组件改变为使用 ES6 解构语法从属性中获取 store,这里的意思是 store 等于 props.store。 24 | 25 | And I'm doing the same here. I'm just taking the store from the props so I can call "dispatch" on it. 26 | 27 | 我在这里做同样的操作,我从 props 中获取 store,这样我就能调用它的 dispatch() 方法了。 28 | 29 | I need to make similar changes to other container components. And in this case, I have this "AddTodo" component, which is not exactly a container component, but it still needs the store to dispatch the "ADD_TODO" action, so I added it as a prop. And I'm also going to add the store to the "Footer" component because, unfortunately, "FilterLink" needs it. 30 | 31 | 我需要对其它的容器组件做相似的改变,在这种情况下,这个 AddTodo 组件,实际并不是一个真正的容器组件,但它仍然需要 store 来分发 ADD_TODO 动作,所以我添加它作为属性。而且我也为 Footer 组件添加 store 属性,因为很不幸,FilterLink 需要它。 32 | 33 | And the "Footer" component renders "FilterLink". This is not convenient, but as I said, we'll figure out a way to avoid this later. But for now, we need to pass the store down so that every container component, such as "FilterLink", can use it to subscribe to the changes, to read the state and to dispatch actions without relying on a top-level variable being available. 34 | 35 | Footer 组件渲染 FilterLink 组件,这并不方便,但正如我之前所说的,我们之后会找个办法来解决这个问题。但是现在,我们需要往下传递 store,这样每一个容器组件,比如 FilterLink,就能在不依赖顶层变量的情况下,使用它来订阅变化,读取状态和分发动作, 36 | 37 | I'm changing the "render" method to read the store from the props. And now, all containers read the store instance from the props, and don't rely on a top-level variable that I removed. 38 | 39 | 我改变 render() 方法为从属性中读取 store。而且现在,所有的容器都从属性中读取 store 实例,不再依赖我之前移除的顶层变量。 40 | 41 | Note that this change did not change the behavior or the data flow of this application. The container components subscribe to the store, just like before, and update their states in response to its changes. 42 | 43 | 注意这个变化并没有改变这个应用的行为或数据流。这些容器组件订阅到 store,和之前一样,随着它的变化而更新它们自己的状态。 44 | 45 | However, what changed is how they access the store. Previously, they would access a top-level variable, but this approach does not scale to real-world applications. And this is why, right now, I'm passing down the store as a prop, so the container components can subscribe to it. 46 | 47 | 然而,改变的只是他们访问 store 的方式。之前,它们访问一个顶层变量,但这种方法不能扩展到真实世界的应用。这就是为什么,现在,我把 store 作为属性向下传递,这样容器组件就能订阅它。 48 | 49 | In the future lessons, we will see how to pass the store down to the container components implicitly but without introducing the top-level variable. 50 | 51 | 在将来的课程中,我们将看到在不需要顶层变量的情况下,如何隐式地向下传递 store 组容器组件。 -------------------------------------------------------------------------------- /translation/09: -------------------------------------------------------------------------------- 1 | In this lesson, I use Expect Library to make test assertions, and "deepFreeze" to make sure that my code is free of mutations. 2 | 3 | 在这节课,我使用 Expect 库来做一些断言测试,deepFreeze() 方法可以确保我的代码是不会被改变的。 4 | 5 | Let's say that I want to implement a counter list application. I would need to write a few functions that operate on its state, and its state is an array of JavaScript numbers representing the individual counters. 6 | 7 | 比方说,我想要实现一个计数器列表的应用。我需要写一些方法来操作它的状态,而且它的状态是一个 Javascript 数值数组,每一个数值代表一个独立的计数器。 8 | 9 | The first function I want to write is called "addCounter", and all it should do is to append a 0 at the end of the passed array. 10 | 11 | 第一个方法我想写的是 addCounter(), 它需要做的是把一个 0 添加到传递进来的数组末尾。 12 | 13 | At first, I use the array "push" method to add a new item at the end of the array, and it works. However, we need to learn to avoid mutations in Redux, and I'm enforcing this by calling "deepFreeze" on the original array. 14 | 15 | 首先,我使用数组的 push() 方法来添加一个新元素到数组的末尾,工作正常。然而,我们需要学会在 Redux 中避免变化,现在我使用 deepFreeze() 方法强制原来的数组不能变化。 16 | 17 | Now my attempt to "push" does not work. It cannot add a new property to a frozen object. Instead of "push", I'm going to use the "concat" method, which does not modify the original array. 18 | 19 | 现在 push() 方法不管用了。它不能添加一个新的属性到一个被冻结的对象上。为了替代 push(),我将使用 concat() 方法,它不会修改原来的数组。 20 | 21 | Now the tests pass without mutations, and I can also use the new ES6 array spread operator to write the same code in a more concise way. 22 | 23 | 现在在没有改变原有数组的情况下通过了测试,而且我可以使用 ES6 的数组展开运算符 ( ... ) 来简化代码。 24 | 25 | My next function is called "removeCounter", and it accepts two arguments, an array of numbers, and the index of the number to skip from the array. 26 | 27 | 我的下一个方法叫 removeCounter(),它接受两个参数,一个是数值数组,一个是需要从数组中移除的那个数值的索引。 28 | 29 | So if I've got three numbers and I'm passing 1 as the second argument, I expect to receive an array with two numbers with the second item skipped in the result array. 30 | 31 | 所以如果我有 3 个数值,同时我传递 1 作为第二个参数,我期望得到一个只有 2 个数值的数组,原来数组中第二个值被跳过了。 32 | 33 | Usually, to delete an item from the array, I would use the "splice" method. However, splice is a mutation method, so you can't use it in Redux. 34 | 35 | 通常情况下,从数组中删除一个元素,我会使用 splice() 方法。但是,splice() 是一个会改变原有数组的方法,所以你不能在 Redux 中使用它。 36 | 37 | I'm going to "deepFreeze" the array object, and now I need to figure out a different way to remove an item from the array without mutating it. 38 | 39 | 我会使用 deepFreeze() 来“冻结”数组对象,现在我需要找到另一个办法来从数组中移除一个元素,同时又不修改它。 40 | 41 | I'm using a method called "slice" here, and it doesn't have anything to do with "splice". It is not mutating, and it gives me a part of the array from some beginning to some end index. 42 | 43 | 我在这里使用一个叫 slice() 的方法,它跟 splice() 没有任何关系。它不会改变 ( 原来的数组 ),它返回数组中从某个开始的索引,到某个结束的索引之间的那部分。 44 | 45 | So what I'm doing is that I'm taking the parts before the index I want to skip and after the index I want to skip, and I concatenate them to get a new array. 46 | 47 | 所以我现在正在做的就是,拿到在我想要跳过的索引之前的那部分,和在我想要跳过的索引之后的那部分,然后把它们拼接在一起,从而得到一个新的数组。 48 | 49 | Finally, instead of writing it as a method chain with "concat" calls, I can use the ES6 array spread operator to write it more concisely. 50 | 51 | 最后,为了避免使用 concat() 进行链式方法调用,我可以使用 ES6 的数组展开运算符 ( ... ) 来简化。 52 | 53 | Now that we implemented adding and removing counters, let's implement incrementing the counter. The "incrementCounter" function takes your arguments, the array and the index of the counter that should be incremented, so the return value has the same count of items, but one of them is incremented. 54 | 55 | 现在我们已经实现了添加和删除计数器的功能,让我们来实现递增计数器。incrementCounter() 方法,接收一个数组,和数组中计数值要递增的计数器的索引作为参数。返回值是具有相同元素个数的数组,但其中有一个元素被递增了。 56 | 57 | Directly setting the array value at index works, but this is a mutation. So if we add a "deepFreeze" call, it's not going to work anymore, so how do we replace a single value in the array without mutating it? 58 | 59 | 直接设置数组中对应索引的值是可以工作的,但这会对原有数组作出改动。所以如果我们增加一个 deepFreeze() 调用,这种做法就不行了。那么我们要怎样替换数组中的一个值同时又不修改它呢? 60 | 61 | It turns out the answer is really similar to how we remove an item. We want to take the slice before the index, concat it with a single item array with a new value, and then concat it with the rest of the original array. 62 | 63 | 实际上,这个问题的答案和从数组中移除一个元素类似。我们想拿到索引之前的那部分,拼接一个新值的元素,然后再拼接原来数组中剩下的那部分。 64 | 65 | Finally, with the ES6 spread operator, we can spread over the left part of the array, specify the new item, and then spread over the right part of the original array, and this looks much nicer. 66 | 67 | 最后,通过 ES6 的展开操作符,我们可以展开数组的左边部分,再指定一个新元素,然后展开数数组的右边部分,看上去好多了。 68 | 69 | In this lesson, you learned how to use the "concat" method or the spread operator, and the "slice" method to add, remove, and change items in arrays without mutating them, and how to protect yourself with "deepFreeze" from mutation in your tests. 70 | 71 | 在这节课,你学到了,如何使用 concat() 方法或是展开运算符,和 slice() 方法,去添加,移除,修改数组中的元素同时不改变原有数组,如何在你的测试中,使用 deepFreeze() 来避免自己被改变。 72 | -------------------------------------------------------------------------------- /chn/18.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,200 --> 00:00:01,250 3 | 在上节课 4 | 5 | 2 6 | 00:00:01,250 --> 00:00:05,570 7 | 我们为 todo 列表应用实现了一个简单的 UI 8 | 9 | 3 10 | 00:00:05,850 --> 00:00:08,320 11 | 可以添加新的 todo 12 | 13 | 4 14 | 00:00:08,320 --> 00:00:10,950 15 | 查看已存在的 todo 16 | 17 | 5 18 | 00:00:11,450 --> 00:00:12,600 19 | 为了添加新的 todo 20 | 21 | 6 22 | 00:00:12,600 --> 00:00:14,870 23 | 我们发起了 ADD_TODO 动作 24 | 25 | 7 26 | 00:00:14,970 --> 00:00:15,870 27 | 在这节课 28 | 29 | 8 30 | 00:00:15,870 --> 00:00:18,520 31 | 我们将通过点击 todo 32 | 33 | 9 34 | 00:00:18,520 --> 00:00:23,020 35 | 发起 TOGGLE_TODO 的动作来切换 todo 的已完成状态 36 | 37 | 10 38 | 00:00:23,850 --> 00:00:26,300 39 | 我往下滚动到 ( 定义 ) 我的 React 组件的地方 40 | 41 | 11 42 | 00:00:26,300 --> 00:00:30,100 43 | 在这里我得到一个 todo 对应的列表元素 44 | 45 | 12 46 | 00:00:30,100 --> 00:00:32,670 47 | 所以我 ( 在这 ) 添加一个 onClick 的处理方法 48 | 49 | 13 50 | 00:00:32,670 --> 00:00:36,070 51 | 当用户单击列表元素时 52 | 53 | 14 54 | 00:00:36,070 --> 00:00:38,920 55 | 我想发起一个动作到 store 中 56 | 57 | 15 58 | 00:00:38,920 --> 00:00:40,620 59 | 它具有 TOGGLE_TODO 的类型 60 | 61 | 16 62 | 00:00:40,620 --> 00:00:43,350 63 | 并且还有即将被切换 ( 已完成状态 ) 的 todo 的 id 64 | 65 | 17 66 | 00:00:43,350 --> 00:00:45,570 67 | id 从这个 todo 对象中得到 68 | 69 | 18 70 | 00:00:46,850 --> 00:00:50,050 71 | 这个事件处理方法知道它所对应的 todo 72 | 73 | 19 74 | 00:00:50,050 --> 00:00:53,020 75 | 所以它能够在动作中传递它 ( todo ) 的 id 76 | 77 | 20 78 | 00:00:54,050 --> 00:00:55,350 79 | 在用户界面上 80 | 81 | 21 82 | 00:00:55,350 --> 00:00:58,870 83 | 我想让已完成的 todo 显示成删除的样子 84 | 85 | 22 86 | 00:00:58,870 --> 00:01:02,820 87 | 所以我为这个列表元素添加 style 属性 88 | 89 | 23 90 | 00:01:02,970 --> 00:01:06,950 91 | 我将使用 textDecoration 属性 92 | 93 | 24 94 | 00:01:07,150 --> 00:01:09,950 95 | 当 todo 的已完成状态为真时 96 | 97 | 25 98 | 00:01:09,950 --> 00:01:12,270 99 | 它的值为 line-through 100 | 101 | 26 102 | 00:01:12,420 --> 00:01:13,920 103 | 当 todo 的已完成状态为假时 104 | 105 | 27 106 | 00:01:13,920 --> 00:01:15,570 107 | 它的值为 none 108 | 109 | 28 110 | 00:01:15,570 --> 00:01:18,450 111 | 所以现在我得到看上去正常的 todo 112 | 113 | 29 114 | 00:01:18,820 --> 00:01:21,700 115 | 现在,如果我添加一些 todo 116 | 117 | 30 118 | 00:01:21,700 --> 00:01:23,350 119 | 我可以点击它们 120 | 121 | 31 122 | 00:01:23,350 --> 00:01:25,920 123 | 它们的状态会被切换 124 | 125 | 32 126 | 00:01:25,920 --> 00:01:27,600 127 | 我还可以把它们的状态切换回来 128 | 129 | 33 130 | 00:01:29,770 --> 00:01:31,320 131 | 还不错吧 132 | 133 | 34 134 | 00:01:31,800 --> 00:01:34,720 135 | 让我们来回顾一下切换 todo ( 的状态 ) 究竟是怎么工作的 136 | 137 | 35 138 | 00:01:35,620 --> 00:01:40,550 139 | 它是从我的点击处理方法中发起 TOGGLE_TODO 的动作开始的 140 | 141 | 36 142 | 00:01:40,550 --> 00:01:42,370 143 | 这个动作具有 TOGGLE_TODO 的类型 144 | 145 | 37 146 | 00:01:42,370 --> 00:01:46,620 147 | 和当前正在被渲染的 todo 的 id 148 | 149 | 38 150 | 00:01:47,000 --> 00:01:49,370 151 | 在渲染所有 todo 的 render 方法中 152 | 153 | 39 154 | 00:01:49,370 --> 00:01:54,850 155 | 我从数组的 map 回调函数中得到作为参数的 todo 对象 156 | 157 | 40 158 | 00:01:56,050 --> 00:01:57,550 159 | 当一个动作被分发 160 | 161 | 41 162 | 00:01:57,550 --> 00:01:59,750 163 | store 将会调用根 reducer 164 | 165 | 42 166 | 00:01:59,870 --> 00:02:02,420 167 | 根 reducer 又将会调用 todos reducer 168 | 169 | 43 170 | 00:02:02,420 --> 00:02:05,320 171 | 来处理 todos 数组和此动作在这个例子中 172 | 173 | 44 174 | 00:02:05,570 --> 00:02:08,370 175 | 在这个例子中 这个动作的类型是 TOGGLE_TODO 176 | 177 | 45 178 | 00:02:08,570 --> 00:02:11,270 179 | 所以 todos reducer 以代理的方式 180 | 181 | 46 182 | 00:02:11,270 --> 00:02:18,300 183 | 在 map 函数中调用 todo reducer 来处理每一个 todo 184 | 185 | 47 186 | 00:02:18,370 --> 00:02:22,700 187 | 所以 todo reducer 接收 todo 作为状态 188 | 189 | 48 190 | 00:02:22,700 --> 00:02:23,920 191 | 还接收动作 192 | 193 | 49 194 | 00:02:24,470 --> 00:02:26,470 195 | 我们继续对比动作的类型 196 | 197 | 50 198 | 00:02:26,470 --> 00:02:28,920 199 | 它匹配 TOGGLE_TODO 字符串 200 | 201 | 51 202 | 00:02:29,320 --> 00:02:35,170 203 | 现在,对于每一个和动作中指定的 id 不匹配的 todo 204 | 205 | 52 206 | 00:02:35,170 --> 00:02:37,170 207 | 我们就返回之前的状态 208 | 209 | 53 210 | 00:02:37,170 --> 00:02:39,720 211 | 即原来的 todo 对象 212 | 213 | 54 214 | 00:02:39,870 --> 00:02:45,350 215 | 然而,如果 todo 的 id 和动作中指定的 id 相匹配 216 | 217 | 55 218 | 00:02:45,350 --> 00:02:47,550 219 | 我们将返回一个新的对象 220 | 221 | 56 222 | 00:02:47,550 --> 00:02:50,300 223 | 它具有原来 todo 所有的属性 224 | 225 | 57 226 | 00:02:50,300 --> 00:02:55,270 227 | 但是 completed 字段的值和原来是相反的 228 | 229 | 58 230 | 00:02:55,920 --> 00:03:02,470 231 | 更新后的 todo 元素将会被包含在应用的新状态的 todos 字段中 232 | 233 | 59 234 | 00:03:02,670 --> 00:03:05,650 235 | 而且因为我们订阅了 render 方法 236 | 237 | 60 238 | 00:03:05,650 --> 00:03:10,000 239 | 它将从 store.getState() 中得到应用的下一状态 240 | 241 | 61 242 | 00:03:10,000 --> 00:03:14,550 243 | 然后将新的 todos 传递给 TodoApp 组件 244 | 245 | 62 246 | 00:03:14,550 --> 00:03:17,700 247 | TodoApp 组件将渲染更新后的 todos 248 | 249 | 63 250 | 00:03:18,770 --> 00:03:21,220 251 | 最后,列表元素的这个 style 252 | 253 | 64 254 | 00:03:21,220 --> 00:03:23,720 255 | 和 todo 的 completed 字段绑定 256 | 257 | 65 258 | 00:03:23,720 --> 00:03:25,100 259 | 我们刚更新它 260 | 261 | 66 262 | 00:03:25,100 --> 00:03:30,820 263 | 这就是为什么它重新渲染时显示删除线 264 | 265 | -------------------------------------------------------------------------------- /translation/30: -------------------------------------------------------------------------------- 1 | So far we have covered the container components, the presentational components, the reducers, and the store. 2 | 目前为止,我们已经讲解了容器组件,展示组件,reducer 以及 store。 3 | 4 | But we have not covered the concept of action creators, which you might see in the Redux talks and examples. 5 | 但我们还没讲到动作生成器 (action creator) 的概念,而你可能会在 Redux 相关的演讲或者示例代码里面见到。 6 | 7 | Let's consider the following example. 8 | 让我们来考虑下面的例子。 9 | 10 | I dispatch the "ADD_TODO" action from inside the "button onClick" handler. 11 | 我在 button 组件的 onClick 里面分发了一个 "ADD_TODO" 动作。 12 | 13 | And this is fine. 14 | 这没什么问题。 15 | 16 | However, it references the "nextTodoId" variable, which I declared alongside the "AddTodo" component. 17 | 但是,它引用了变量 nextTodoID,这个变量我是跟 AddTodo 组件一起声明的。 18 | 19 | Normally, it would be local. 20 | 通常,它会是本地变量。 21 | 22 | However, what if another component wants to dispatch the "ADD_TODO" action? 23 | 不过,如果另一个组件也想要分发 "ADD_TODO" 动作怎么办? 24 | 25 | It would need to have the access to "nextTodoId" somehow. 26 | 它会需要通过某个途径访问到 nextTodoId。 27 | 28 | And while I could make this variable global, it's not a very good idea. 29 | 虽然我可以将变量改成全局变量,但这不是一个好的做法。 30 | 31 | Instead, it would be best if the components dispatching the "ADD_TODO" action did not have to worry about specifying the ID. Because the only information they really pass is the text of the todo being added. 32 | 最好的做法是,分发 "ADD_TODO" 动作的组件不需要指明 ID。因为它们传递的信息其实就是 todo 的内容而已。 33 | 34 | I don't want to generate the ID inside the reducer, because that would make it non-deterministic. 35 | 我不想在 reducer 里面去生成 ID,因为那样会让 reducer 变得不确定。 36 | 37 | However, I can extract this code generating the action object into a function I will call "addTodo". 38 | 不过,我可以将这段生成动作对象的代码提取到一个函数中,我将它命名为 addTodo()。 39 | 40 | I pass the input value to "addTodo". 41 | 我将 input.value 传递给 addTodo()。 42 | 43 | And "addTodo" is just a function that takes the text of the todo and constructs an action object representing "ADD_TODO" action. 44 | 而 addTodo() 只不过是一个函数,它接受 todo 的内容,然后构造出一个动作对象来代表 "ADD_TODO" 动作。 45 | 46 | So it has the type, "ADD_TODO", it takes care of generating the unique ID and it includes the text. 47 | 一句话总结,"ADD_TODO" 负责生成唯一的 ID,而且包含了 todo 的内容。 48 | 49 | Although extraction such functions is not required, it is very common pattern in Redux applications to keep them maintainable. 50 | 虽然提取这样的函数不是必须的,但是在 Redux 中它是一种非常普遍的做法,用来提高应用的可维护性。 51 | 52 | So, like all these functions, we usually place action creators separately from components or from reducers. 53 | 所以就像这里的这些函数,我们通常把 action creators 跟组件和 reducers 分开放置。 54 | 55 | I will now extract other action creators from the components. 56 | 我现在会从组件抽取其它的动作生成器。 57 | 58 | And I see that I have a "SET_VISIBILITY_FILTER" with "dispatch" here, so I will change this to call the "setVisibilityFilter" action creator with "ownProps.filter" as the argument and is going to return the action that needs to be dispatched, so I'm declaring the "setVisibilityFilter" function. 59 | 我看到这里有一个 "SET_VISIBILITY_FILTER" 的 dispatch() 调用,所以我要修改这里的代码,让它以 ownProps.filter 作为参数调用 setVisibilityFilter() 这个 action creator,它会返回要分发的动作。所以我这里声明 setVisibilityFilter() 函数 60 | 61 | This is what I call an action creator, because it takes the arguments about the action and it returns the action object with the type "SET_VISIBILITY_FILTER" and the "filter" itself. 62 | 我把类似这样的函数称为 action creator,因为它会接收有关动作信息的参数,然后返回一个动作对象,类型是 "SET_VISIBILITY_FILTER",还包含了 filter 的值。 63 | 64 | You might think that this kind of code is boilerplate and you'd rather dispatch the action inline inside the component. 65 | 你可能觉得这样的代码有点像模版,你宁愿将代码直接写在组件里面。 66 | 67 | However, don't underestimate how action creators document your software, because they tell your team what kinds of actions the components can dispatch, and this kind of information can be invaluable in large applications. 68 | 但是,不要低估 action creators 的作用,因为你的协作者可以通过它们明确知道组件能够分发的动作,而这个信息对于编写大型应用来说是非常有价值的。 69 | 70 | I will now scroll down to the last place where I call "dispatch" with an inline action object. 71 | 我现在会滚动到最后一个直接用动作对象调用 dispatch() 的地方。 72 | 73 | And I will now extract that to a "toggleTodo" action creator, to which I pass the ID of the todo as the argument. 74 | 而我现在会将这些代码提取到一个 toggleTodo() action creato,我会将 todo 的 ID 作为参数传给它。 75 | 76 | I'm now scrolling up to my action creators and I will add a new one that I call "toggleTodo". It accepts the ID as the argument and it returns the action of the type, "TOGGLE_TODO", and this ID. 77 | 现在我要添加一个新的 action creator 叫 toggleTodo()。它接收 ID 作为参数,然后返回一个动作对象,类型是 "TOGGLE_TODO",还有 todo 的 ID。 78 | 79 | Let's take a moment to consider how convenient it is to have all the action creators in a single place so that I can use them from components and tests without worrying about the action's internal structure. 80 | 让我们来想想,如果你把所有的 action creator 都放在一个统一的地方,可以任意从组件和测试中使用,而不需要担心动作的内部结构,这事多么方便。 81 | 82 | Know that whether you use action creators or not, the data flow is exactly the same, because I just call the action creator to get the action object and then I call "dispatch" just like I did before, passing the action. 83 | 要知道,无论你是否使用 action creator,数据流都是完全一样的,因为我只是调用了 action creator 来获得动作对象,然后再调用 dispatch(),把动作传递进去。 84 | -------------------------------------------------------------------------------- /translation/11: -------------------------------------------------------------------------------- 1 | Just like in the previous two lessons, I'm using "expect" library to make test assertions and "deep-freeze" library to prevent accidental mutations in my code. 2 | 就像在前两节课一样,我使用 expect 库来进行测试断言,还有 deep-freeze 库来避免我的代码对对象作出意外的修改。 3 | 4 | In this lesson, I will create the reducer for a todo list application whose state is described an array of todos. 5 | 在这节课中,我会为一个 todo 列表应用创造一个 reducer, 我们会用一个 todo 的数组来描述这个应用的状态。 6 | 7 | Just to remind you what a reducer is, it's a pure function you write to implement the update logic of your application -- that is, how the next state is calculated given the current state and the action being dispatched. 8 | 提醒一下,一个 reducer 就是一个纯函数,用来实现应用的更新逻辑 —— 也就是给定当前状态和被分发的动作,如何计算出下一个状态。 9 | 10 | Before writing a reducer, I want to have a way of knowing whether its code is correct, so I'm starting by writing a test for it. 11 | 在开始实现一个 reducer 之前,我希望有一种方式可以知道我的代码是否正确,所以我会先从为 reducer 写测试开始。 12 | 13 | I'm declaring two variables, the "stateBefore", which is an empty array, and the action being dispatched, which is an action describing user adding a new todo with some ID and a text. 14 | 我现在定义两个变量我现在定义两个变量,一个是 stateBefore, 它是一个空数组,另一个是被分发的动作,它描述了用户添加一个新的带有 ID 和内容的 todo。 15 | 16 | I am also declaring the state I expect to get after calling the reducer. 17 | 我还声明了在调用 reducer 之后,我期望得到的状态。 18 | 19 | And like state before, it is an array, but this time, it has a single element representing the todo that was just added. 20 | 就像之前的状态一样,它是一个数组,但不同的是,这一次它会有一个元素代表着刚刚添加的 todo。 21 | 22 | So it has the same ID and the text as the action object. 23 | 所以它会有和动作对象一样的 ID 和内容。 24 | 25 | It also has an additional field called, "completed," that I want to be initialized to be "false". 26 | 它还有一个额外的属性叫作 "complted",我希望把它初始化为 false。 27 | 28 | We want to make sure that the reducer is a pure function, so I'm calling deepFreeze both on the state and the action. 29 | 我们希望保证 reducer 是一个纯函数,所以我会对状态和动作调用 deepFreeze()。 30 | 31 | Finally, I am ready to use the expect library to verify that if I call the "todos" reducer with the state before and the action object, I'm going to get the result that is deeply equal to the "stateAfter" I just declared. 32 | 最后,我已经准备好使用 expect 库来验证,如果我对前一个状态和动作对象调用 todos reducer,那么我得到的结果将会和我刚刚声明的 stateAfter 深层相等. 33 | 34 | This concludes my first test. 35 | 好了,这就是我的第一个测试。 36 | 37 | Now I can call it just like a regular JavaScript function. 38 | 现在,我可以像调用一个普通 JavaScript 函数一样调用它。 39 | 40 | And if it doesn't throw in the expect call, I'm going to see a message saying that the tests have passed. 41 | 而且,如果它不抛出异常的话,我会看到一个信息,表明测试已经通过。 42 | 43 | Of course, it fails because the reducer is not implemented yet. 44 | 当然,这个测试现在无法通过,因为 reducer 还没有被实现。 45 | 46 | It's an empty function. 47 | 它是一个空的函数。 48 | 49 | So it returns undefined instead of the array with a single item that I expect in the test. 50 | 所以它现在返回 undefined,而不是我所期望的一个带有单一元素的数组。 51 | 52 | To fix this, I would need my reducer to take a look at the action type property, which is a string. 53 | 为了修复这个问题,我会需要 reducer 去检查动作的 type 属性,这个属性的值是个字符串。 54 | 55 | When it matches the "ADD_TODO" string, which I specify as the action type in my test, to satisfy the test I need to return a new array which includes all items from the original array but also a new todo item that has its ID and text copied from the action object and a "completed" field set to "false". 56 | 当它匹配 "ADD_TODO" 字符串时,也就是我在测试中指明的动作类型,要满足测试条件,我需要返回一个新的数组。这个数组包含了原有数组的所有元素,还有一个新的 todo 元素,这个元素的 ID 和 text 是从动作对象那里拷贝过来的,同时它的 completed 属性会被设为 false。 57 | 58 | Finally, I add a default case to my switch statement because every reducer has to return the current state for any unknown action. 59 | 最后,我给 switch 语句加了一个缺省条件,因为在遇到未知的动作时,每一个 reducer 都需要返回当前的状态。 60 | 61 | Now the tests run successfully. 62 | 现在测试就通过了。 63 | 64 | Let's recap the data flow in this example to see why. 65 | 让我们回顾一下这个例子里的数据流,来看一下为什么测试会通过。 66 | 67 | First, I create the state array, which is an empty array, and the action object inside my test function. 68 | 首先,在我的测试函数里,我创建了一个空的状态数组,还有动作对象。 69 | 70 | I'm passing them as arguments to my reducer function, called, "todos." 71 | 我将它们以参数的形式传递给我的 reducer 函数 todos。 72 | 73 | The "todos" reducer accepts the state and the action as arguments and takes a look at the action type. 74 | todos reducer 接受状态和动作作为参数,然后检查动作的类型。 75 | 76 | In this case, the action type is a string saying "ADD_TODO", so it matches the switch case inside the reducer. 77 | 在这个例子中,动作的类型是 "ADD_TODO",所以它跟 reducer 中的 switch 条件是匹配的。 78 | 79 | The reducer returns a new array which contains the items from the old array and the new item representing the added todo. 80 | reducer 返回了一个新数组,这个数组包含了原来数组的元素,以及一个代表被添加的 todo 的新元素。 81 | 82 | However, the state we passed from the test was actually an empty array, so, at the end, we're going to get an array with a single item, which is the new todo. 83 | 不过,测试中我们传进去的状态是个空数组,所以最后,我们会得到一个只有单一元素的数组,而这个元素就是新的 todo。 84 | 85 | Finally, we compare the return value to an array with a single todo item to make sure that the reducer works as intended. 86 | 最后,我们将返回值和只有一个 todo 元素的数组对比,确保 reducer 像我们预想的那样运行。 87 | 88 | The equality check passes. 89 | 这里的等价检查通过了。 90 | 91 | So this makes the test successful. 92 | 这样测试就全部通过了。 93 | -------------------------------------------------------------------------------- /translation/16: -------------------------------------------------------------------------------- 1 | In the previous lesson, we learned to use the "combineReducers" function, which comes with Redux and generates one reducer from several other reducers, delegating to them paths of the state tree. 2 | 在上一课中,我们学习了使用 Redux 中的 combineReducers() 函数,这个函数可以将多个 reducer 合并成一个 reducer,而这些 reducer 分别管理了状态树的不同部分。 3 | 4 | To gain a deeper understanding of how exactly "combineReducers" works, we will implement it from scratch in this lesson. 5 | 为了深入理解 combineReducers() 函数,我们会在这节课中从头实现它。 6 | 7 | "combineReducers" is a function, so I'm writing a function declaration. And its only argument is the mapping between the state keys and the reducers, so I'm just going to call it "reducers". 8 | combineReducers() 是一个函数,所以我要写一个函数声明。这个函数唯一的参数是状态的键和 reducer 之间的映射关系,所以我把它叫做 reducers. 9 | 10 | The return value is supposed to be a reducer itself, so this is a function that returns another function. And the signature of the return function is a reducer signature. It has the state and the action. 11 | 这个函数的返回值本身也应该是一个 reducer,所以这是一个返回另一个函数的函数。而被返回的函数的签名也应该是一个 reducer 函数的签名,也就是一个状态和一个动作。 12 | 13 | Now, I'm calling the "Object.keys" method, which gives me all the keys of the reducers object. In our example, this is "todos" and the "visibilityFilter". 14 | 现在,我要调用 Object.keys() 这个方法,它会返回包含 reducers 对象的所有键的数组。在我们的这个例子中,我们得到的是 "todos" 和 "visibilityFilter"。 15 | 16 | Next, I'm calling the "reduce" method on the keys, because I want to produce a single value, such as the next state, by accumulating over every reducer key and calling the corresponding reducer. 17 | 接下来,我根据这些键来调用(数组的) reduce() 方法,因为我想通过遍历每一个 reducer 键来调用对应的 reducer 函数,最后把他们叠加起来,生成一个单一的值作为下一个状态。 18 | 19 | Each reducer passed through the "combineReducers" function is only responsible for updating a part of the state. This is why I'm saying that the next state by the given key can be calculated by calling the corresponding reducer by the given key with the current state by the given key and the action. 20 | 每个传入 combineReducers() 函数的 reducer,都只负责更新状态树中的某个部分。这就是为什么我说,要计算出某个健的下一个状态,只要用这个健对应的当前状态和动作,调用这个健对应的 reducer 就可以了。 21 | 22 | The array reduce wants me to return the next accumulated value from the callback, so I'm returning the nextState. And I'm also specifying an empty object as the initial nextState, before all the keys are processed. 23 | 这个数组的 reduce() 方法想要我在回调中返回下一个需要叠加的值,所以我在这里返回 nextState。同时我指定了一个空对象作为 nextState 的初始值,它会在任何键被处理之前初始化。 24 | 25 | And there we have it. This is a working re-implementation of "combineReducers" utility from Redux. 26 | 现在我们就完成了。这是一个 Redux 的 combineReducer() 函数的重新实现,而且它是可用的。 27 | 28 | Let's briefly recap how it works. I'm calling "combineReducers" with an object whose values are the reducer functions and keys are the state field they manage. Inside the generated reducer, I'm retrieving all the keys of the reducers I passed to "combineReducers", which is an array of strings, "todos" and "visibilityFilter". 29 | 让我们简短地回顾下这是如何实现的。我调用了 combineReducers() 函数,它的参数是对象,这个对象的值是多个 reducer 函数,而它的键则是对应的 reducer 函数所管理的状态字段。在这个函数最后生成的 reducer 中,我提取了所有传入到 combineReducers() 函数的 reducer 的键,也就是 "todos" 和 "visibilityFilter"。 30 | 31 | I'm starting with an empty object for my nextState and I'm using the reduce operation over these keys to fill it gradually. 32 | 我先用一个空对象初始化 nextState,然后用数组的 reduce() 函数来遍历这些键,逐步填充 nextState 对象。 33 | 34 | Notice that I'm mutating the nextState object on every iteration. This is not a problem, because it is the object I created inside the reducer. It is not something passed from outside, so reducer stays a pure function. 35 | 注意,我在每一次遍历中都修改了 nextState 对象。这么做并没什么问题,因为这个对象是我在 reducer 中创建的。它不是从外部传入的,所以 reducer 还是一个纯函数。 36 | 37 | To calculate the next state for a given key, it calls the corresponding reducer function, such as "todos" or "visibilityFilter". 38 | 要计算出一个键的下一状态,我们会调用对应的 reducer 函数,比如 todos() 和 visibilityFilter() 函数。 39 | 40 | The generated reducer will pass to the child reducer only a part of its state by the key. So if its state is a single object, it's only going to pass the relevant part, such as "todos" or "visibilityFilter", depending on the current key, and save the result in the nextState by the same key. 41 | 我们只会给子 reducer 传递那个键所对应的状态的一部分。所以如果生成的 reducer 的状态是一个单独的对象, 它只会传递给键所对应的那部分状态,例如 todos 和 visibilityFilter,然后在 nextState 用同样的键把结果保存下来。 42 | 43 | Finally, we use the array reduce operation with the empty object as the initial nextState, that is being filled on every iteration until it is the returned value of the whole reduce operation. 44 | 最后,我们使用数组的 reduce() 操作,它以一个空对象作为 nextState 的初始值,然后在每一次迭代中填充它,直到它可以作为整个 reduce 操作的结果被返回。 45 | 46 | In this lesson, you learned how to implement the "combineReducers" utility that comes with Redux from scratch. 47 | 这节课中,你学习了如何从头实现 Redux 中的 combineReducers() 工具函数。 48 | 49 | It is not essential to use in Redux, so it is fine if you don't fully understand how it works yet. However, it is a good idea to practice functional programming and understand functions can take other functions as arguments and return other functions, because knowing this will help you get more productive in Redux in the long term. 50 | 如果你还没有完全明白它的工作原理,没有关系,因为这对于使用 Redux 并不是非常重要。但是,练习函数式编程,以及理解函数可以接受其它函数为参数,然后返回其它函数,是一个很好的主义。因为长期来说,理解这些会让你在使用 Redux 时更具生产力。 51 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 使用工具 2 | 请自行 goolge 并安装 aegisub 3 | 4 | # 文档目录结构 5 | 6 | * 视频文件因为 size 较大,所以放在百度盘上 7 | * `src/` 中为英文的 transcript 8 | * `translation/` 中为翻译成中文的 transcript 9 | * `chn/` 中为打轴后的中文字幕文件,格式为 `.srt` 10 | 11 | 12 | # 翻译规范 13 | * 英文的逗号,句号,分号通通换成空格(行末直接去掉) 14 | * 保留问号和感叹号,使用中文的 15 | * 括号使用英文括号,左右各留一个空格 16 | * 破折号保留,使用中文的 17 | * 省略号使用三个英文句号 18 | * 引号保留,使用中文的 19 | * 代码相关:除了字符串以外,所有的返回值,函数名,数值等都不带引号。函数名在函数名后加上英文括号,以区分一般符号和函数名。 20 | 21 | # 协作形式 22 | 23 | 翻译者: 24 | 25 | * 请从标记为 `待领取` 的 issues 中领取任务,写清楚预估的 DDL,并分配给自己。任务有三类: 26 | * `听校` 任务需要对照视频,更正 `src/` 中的英文 transcript 可能的错误 27 | * `翻译` 任务需要将 `src/` 中的 transcript 翻译成中文 transcript 并放入 `translation/`。 28 | * `打轴` 任务需要将 `translation/` 中的中文提取出来,打上时间轴之后,以 `.srt` 格式放在 `chn/` 文件夹 29 | * 如果超过预估时间无法完成请在 issue 中 at 管理员,说明接替者信息或请管理员重新分配该任务; 30 | * 完成任务后提交 PR 31 | 32 | 校对员: 33 | * 将 `待校对` 的 PR 更改为 `校对中`,并进行校对 34 | * 校对通过则将 PR 改为 `已校对`,并分配给管理员 35 | * 校对不通过则将 PR 改为 `待修改`,并分配给 PR 提交者 36 | 37 | 管理员 38 | * 将 `待校对` 的 PR 分配给校对员 39 | * 若 `已校对` 的 PR 可以合并,则直接合并。合并后请及时更新进度表,以及添加新的 issues 40 | * 若 `已校对` 的 PR 不能合并,分配给 PR 提交者并将 PR 标记为 `待修改` 41 | 42 | 43 | 现任管理员:@hymRedemption 44 | 45 | 现任校对员:@[mrwiredancer](https://github.com/Mr-Wiredancer) 46 | 47 | # 进度表 48 | 章节 | 听校| 状态 | 翻译| 状态 | 打轴 | 状态 49 | ---- | ---- | ---- | ---- | --- | ---- | ---- 50 | 01 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️|[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 51 | 02 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️|[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 52 | 03 |[baurine](https://github.com/baurine) | ✔️ |[baurine](https://github.com/baurine) | ✔️|[baurine](https://github.com/baurine) | ✔️ 53 | 04 |[lx7575000](https://github.com/lx7575000), [HYM](https://github.com/hymRedemption) |✔️ |[lx7575000](https://github.com/lx7575000), [HYM](https://github.com/hymRedemption) | ✔️ |[lx7575000](https://github.com/lx7575000), [HYM](https://github.com/hymRedemption) | ✔️ 54 | 05 | [mrwiredancer](https://github.com/Mr-Wiredancer)| ✔️ | [baurine](https://github.com/baurine) | ✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 55 | 06 | [baurine](https://github.com/baurine) |✔️ | [baurine](https://github.com/baurine) |✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 56 | 07 |[mrwiredancer](https://github.com/Mr-Wiredancer) |✔️ | [baurine](https://github.com/baurine) |✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 57 | 08 |[mrwiredancer](https://github.com/Mr-Wiredancer) |✔️ |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️| [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 58 | 09 |[baurine](https://github.com/baurine)| ✔️ |[baurine](https://github.com/baurine) | ✔️ | [HYM](https://github.com/hymRedemption) | ✔️ 59 | 10 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ |[zhaozhiming](https://github.com/zhaozhiming) | ✔️ | [HYM](https://github.com/hymRedemption) | ✔️ 60 | 11 |[mrwiredancer](https://github.com/Mr-Wiredancer) |✔️ |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [HYM](https://github.com/hymRedemption) | ✔️ 61 | 12 |[baurine](https://github.com/baurine) |✔️ | [ZED](https://github.com/zedzhang)| ✔️ | [ceoimon](https://github.com/ceoimon)| ✔️ 62 | 13 |[baurine](https://github.com/baurine) |✔️ |[alfredcc](https://github.com/alfredcc) | ✔️ | [ceoimon](https://github.com/ceoimon)| ✔️ 63 | 14 |[baurine](https://github.com/baurine) |✔️ |[zhaozhiming](https://github.com/zhaozhiming) | ✔️| [ceoimon](https://github.com/ceoimon)| ✔️ 64 | 15 |[baurine](https://github.com/baurine)|✔️ |[mrwiredancer](https://github.com/Mr-Wiredancer) |✔️ | [ceoimon](https://github.com/ceoimon)| ✔️ 65 | 16 |[baurine](https://github.com/baurine)|✔️ |[alfredcc](https://github.com/alfredcc) |✔️ | [ceoimon](https://github.com/ceoimon)| ✔️ 66 | 17 |[mrwiredancer](https://github.com/Mr-Wiredancer)|✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer)| ✔️| [ceoimon](https://github.com/ceoimon)| ✔️ 67 | 18 |[baurine](https://github.com/baurine)|✔️ | [baurine](https://github.com/baurine) | ✔️| [ceoimon](https://github.com/ceoimon)| ✔️ 68 | 19 |[baurine](https://github.com/baurine)|✔️ | [baurine](https://github.com/baurine) | ✔️| [ceoimon](https://github.com/ceoimon)| ✔️ 69 | 20 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️| 70 | 21 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ |[zhaozhiming](https://github.com/zhaozhiming) | ✔️ 71 | 22 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [baurine](https://github.com/baurine) | ✔️ 72 | 23 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [howard](https://github.com/hayeah) | ✔️ 73 | 24 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [baurine](https://github.com/baurine) | ✔️ 74 | 25 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ 75 | 26 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ |[baurine](https://github.com/baurine)|✔️ 76 | 27 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️|[zhaozhiming](https://github.com/zhaozhiming) | ✔️ 77 | 28 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | mrwiredancer | ✔️ 78 | 29 |[mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️|[baurine](https://github.com/baurine)|✔️ 79 | 30 | [mrwiredancer](https://github.com/Mr-Wiredancer) | ✔️ | mrwiredancer | doing 80 | -------------------------------------------------------------------------------- /chn/09.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,380 --> 00:00:01,160 3 | 在这节课 4 | 5 | 2 6 | 00:00:01,160 --> 00:00:04,040 7 | 我使用 Expect 库来做一些断言测试 8 | 9 | 3 10 | 00:00:04,180 --> 00:00:08,400 11 | deepFreeze() 方法可以确保我的代码是不会被改变的 12 | 13 | 4 14 | 00:00:08,900 --> 00:00:09,500 15 | 比方说 16 | 17 | 5 18 | 00:00:09,500 --> 00:00:13,100 19 | 我想要实现一个计数器列表的应用 20 | 21 | 6 22 | 00:00:13,380 --> 00:00:17,060 23 | 我需要写一些方法来操作它的状态 24 | 25 | 7 26 | 00:00:17,260 --> 00:00:20,580 27 | 而且它的状态是一个 Javascript 数值数组 28 | 29 | 8 30 | 00:00:20,580 --> 00:00:23,000 31 | 每一个数值代表一个独立的计数器 32 | 33 | 9 34 | 00:00:23,400 --> 00:00:26,780 35 | 第一个方法我想写的是 addCounter() 36 | 37 | 10 38 | 00:00:26,780 --> 00:00:32,120 39 | 它需要做的是把一个 0 添加到传递进来的数组末尾 40 | 41 | 11 42 | 00:00:33,300 --> 00:00:34,000 43 | 首先 44 | 45 | 12 46 | 00:00:34,000 --> 00:00:36,000 47 | 我使用数组的 push() 方法 48 | 49 | 13 50 | 00:00:36,000 --> 00:00:38,640 51 | 来添加一个新元素到数组的末尾 52 | 53 | 14 54 | 00:00:38,980 --> 00:00:39,880 55 | 工作正常 56 | 57 | 15 58 | 00:00:40,280 --> 00:00:43,740 59 | 然而 我们需要学会在 Redux 中避免变化 60 | 61 | 16 62 | 00:00:43,820 --> 00:00:48,400 63 | 现在我使用 deepFreeze() 方法强制原来的数组不能变化 64 | 65 | 17 66 | 00:00:49,160 --> 00:00:51,580 67 | 现在 push() 方法不管用了 68 | 69 | 18 70 | 00:00:51,980 --> 00:00:55,260 71 | 它不能添加一个新的属性到一个被冻结的对象上 72 | 73 | 19 74 | 00:00:55,900 --> 00:00:59,000 75 | 为了替代 push() 我将使用 concat() 方法 76 | 77 | 20 78 | 00:00:59,160 --> 00:01:02,140 79 | 它不会修改原来的数组 80 | 81 | 21 82 | 00:01:03,080 --> 00:01:05,660 83 | 现在在没有改变原有数组的情况下通过了测试 84 | 85 | 22 86 | 00:01:05,780 --> 00:01:12,600 87 | 而且我可以使用 ES6 的数组展开运算符来简化代码 88 | 89 | 23 90 | 00:01:14,980 --> 00:01:17,580 91 | 我的下一个方法叫 removeCounter() 92 | 93 | 24 94 | 00:01:17,580 --> 00:01:19,580 95 | 它接受两个参数 96 | 97 | 25 98 | 00:01:19,660 --> 00:01:21,180 99 | 一个是数值数组 100 | 101 | 26 102 | 00:01:21,180 --> 00:01:24,600 103 | 一个是需要从数组中移除的那个数值的索引 104 | 105 | 27 106 | 00:01:24,720 --> 00:01:27,080 107 | 所以如果我有 3 个数值 108 | 109 | 28 110 | 00:01:27,160 --> 00:01:29,860 111 | 同时我传递 1 作为第二个参数 112 | 113 | 29 114 | 00:01:29,860 --> 00:01:32,580 115 | 我期望得到一个只有 2 个数值的数组 116 | 117 | 30 118 | 00:01:32,580 --> 00:01:35,540 119 | 原来数组中第二个值被跳过了 120 | 121 | 31 122 | 00:01:36,420 --> 00:01:37,040 123 | 通常情况下 124 | 125 | 32 126 | 00:01:37,040 --> 00:01:38,820 127 | 从数组中删除一个元素 128 | 129 | 33 130 | 00:01:38,820 --> 00:01:40,820 131 | 我会使用 splice() 方法 132 | 133 | 34 134 | 00:01:41,200 --> 00:01:43,960 135 | 但是 splice() 是一个会改变原有数组的方法 136 | 137 | 35 138 | 00:01:43,960 --> 00:01:46,220 139 | 所以你不能在 Redux 中使用它 140 | 141 | 36 142 | 00:01:46,800 --> 00:01:49,240 143 | 我会使用 deepFreeze() 来“冻结”数组对象 144 | 145 | 37 146 | 00:01:49,460 --> 00:01:53,860 147 | 现在我需要找到另一个办法来从数组中移除一个元素 148 | 149 | 38 150 | 00:01:53,860 --> 00:01:55,280 151 | 同时又不修改它 152 | 153 | 39 154 | 00:01:56,160 --> 00:01:58,740 155 | 我在这里使用一个叫 slice() 的方法 156 | 157 | 40 158 | 00:01:58,740 --> 00:02:01,880 159 | 它跟 splice() 没有任何关系 160 | 161 | 41 162 | 00:02:02,000 --> 00:02:03,280 163 | 它不会改变 ( 原来的数组 ) 164 | 165 | 42 166 | 00:02:03,440 --> 00:02:06,020 167 | 它返回数组中从某个开始的索引 168 | 169 | 43 170 | 00:02:06,060 --> 00:02:08,480 171 | 到某个结束的索引之间的那部分 172 | 173 | 44 174 | 00:02:08,540 --> 00:02:10,200 175 | 所以我现在正在做的就是 176 | 177 | 45 178 | 00:02:10,200 --> 00:02:14,380 179 | 拿到在我想要跳过的索引之前的那部分 180 | 181 | 46 182 | 00:02:14,380 --> 00:02:16,380 183 | 和在我想要跳过的索引之后的那部分 184 | 185 | 47 186 | 00:02:16,460 --> 00:02:19,100 187 | 然后把它们拼接在一起 从而得到一个新的数组 188 | 189 | 48 190 | 00:02:20,880 --> 00:02:25,200 191 | 最后 为了避免使用 concat() 进行链式方法调用 192 | 193 | 49 194 | 00:02:25,300 --> 00:02:30,380 195 | 我可以使用 ES6 的数组展开运算符来简化 196 | 197 | 50 198 | 00:02:33,140 --> 00:02:36,600 199 | 我们已经实现了添加和删除计数器的功能 200 | 201 | 51 202 | 00:02:36,600 --> 00:02:39,540 203 | 现在让我们来实现让计数器递增的功能 204 | 205 | 52 206 | 00:02:39,980 --> 00:02:41,840 207 | incrementCounter() 方法 208 | 209 | 53 210 | 00:02:41,840 --> 00:02:47,880 211 | 接收一个数组和数组中计数值要递增的计数器的索引作为参数 212 | 213 | 54 214 | 00:02:48,180 --> 00:02:51,640 215 | 返回值是具有相同元素个数的数组 216 | 217 | 55 218 | 00:02:51,720 --> 00:02:53,780 219 | 但其中有一个元素被递增了 220 | 221 | 56 222 | 00:02:54,780 --> 00:02:58,160 223 | 直接设置数组中对应索引的值是可行的 224 | 225 | 57 226 | 00:02:58,160 --> 00:03:00,160 227 | 但这会对原有数组作出改动 228 | 229 | 58 230 | 00:03:00,160 --> 00:03:02,280 231 | 所以如果我们增加一个 deepFreeze() 调用 232 | 233 | 59 234 | 00:03:02,280 --> 00:03:04,280 235 | 这种做法就不行了 236 | 237 | 60 238 | 00:03:04,980 --> 00:03:09,680 239 | 那么我们要怎样替换数组中的一个值同时又不修改它呢? 240 | 241 | 61 242 | 00:03:10,420 --> 00:03:14,560 243 | 实际上 这个问题的答案和从数组中移除一个元素类似 244 | 245 | 62 246 | 00:03:15,320 --> 00:03:17,940 247 | 我们想拿到索引之前的那部分 248 | 249 | 63 250 | 00:03:17,940 --> 00:03:21,660 251 | 拼接一个新值的元素 252 | 253 | 64 254 | 00:03:21,740 --> 00:03:25,040 255 | 然后再拼接原来数组中剩下的那部分 256 | 257 | 65 258 | 00:03:25,740 --> 00:03:28,240 259 | 最后 通过 ES6 的展开操作符 260 | 261 | 66 262 | 00:03:28,240 --> 00:03:30,900 263 | 我们可以展开数组的左边部分 264 | 265 | 67 266 | 00:03:30,900 --> 00:03:32,780 267 | 再指定一个新元素 268 | 269 | 68 270 | 00:03:32,780 --> 00:03:35,900 271 | 然后展开数数组的右边部分 272 | 273 | 69 274 | 00:03:35,900 --> 00:03:37,520 275 | 这样看上去就好多了 276 | 277 | 70 278 | 00:03:38,300 --> 00:03:39,740 279 | 这节课你学到了 280 | 281 | 71 282 | 00:03:39,740 --> 00:03:44,800 283 | 如何使用 concat() 方法或是展开运算符和 slice() 方法 284 | 285 | 72 286 | 00:03:44,800 --> 00:03:49,220 287 | 去添加 移除 修改数组中的元素同时不改变原有数组 288 | 289 | 73 290 | 00:03:49,320 --> 00:03:50,900 291 | 以及如何在你的测试中 292 | 293 | 74 294 | 00:03:50,900 --> 00:03:54,120 295 | 使用 deepFreeze() 来避免自己被改变 296 | 297 | -------------------------------------------------------------------------------- /chn/11.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,000 --> 00:00:02,240 3 | 就像在前两节课一样 4 | 5 | 2 6 | 00:00:02,240 --> 00:00:05,680 7 | 我使用 expect 库来进行测试断言 8 | 9 | 3 10 | 00:00:05,800 --> 00:00:10,300 11 | 还有 deep-freeze 库来避免我的代码对对象作出意外的修改 12 | 13 | 4 14 | 00:00:10,700 --> 00:00:11,520 15 | 在这节课中 16 | 17 | 5 18 | 00:00:11,520 --> 00:00:14,900 19 | 我会为一个 todo 列表应用创造一个 reducer 20 | 21 | 6 22 | 00:00:14,980 --> 00:00:18,020 23 | 我们会用一个 todo 的数组来描述这个应用的状态 24 | 25 | 7 26 | 00:00:18,680 --> 00:00:22,860 27 | 提醒一下 reducer 就是一个纯函数 28 | 29 | 8 30 | 00:00:22,860 --> 00:00:26,860 31 | 用来实现应用的更新逻辑 32 | 33 | 9 34 | 00:00:27,020 --> 00:00:30,860 35 | 也就是给定当前状态和被分发的动作 36 | 37 | 10 38 | 00:00:30,860 --> 00:00:32,860 39 | 如何计算出下一个状态 40 | 41 | 11 42 | 00:00:33,640 --> 00:00:35,420 43 | 在开始实现一个 reducer 之前 44 | 45 | 12 46 | 00:00:35,420 --> 00:00:38,400 47 | 我希望有一种方式可以知道我的代码是否正确 48 | 49 | 13 50 | 00:00:38,700 --> 00:00:41,740 51 | 所以我会先从为 reducer 写测试开始 52 | 53 | 14 54 | 00:00:42,340 --> 00:00:44,220 55 | 我现在定义两个变量 56 | 57 | 15 58 | 00:00:44,220 --> 00:00:46,760 59 | 一个是 stateBefore 它是一个空数组 60 | 61 | 16 62 | 00:00:46,760 --> 00:00:48,580 63 | 另一个是被分发的动作 64 | 65 | 17 66 | 00:00:48,580 --> 00:00:54,740 67 | 它描述了用户添加一个新的带有 ID 和内容的 todo 68 | 69 | 18 70 | 00:00:55,460 --> 00:01:00,100 71 | 我还声明了在调用 reducer 之后期望得到的状态 72 | 73 | 19 74 | 00:01:00,320 --> 00:01:02,720 75 | 就像之前的状态一样 它是一个数组 76 | 77 | 20 78 | 00:01:02,860 --> 00:01:03,660 79 | 但不同的是 80 | 81 | 21 82 | 00:01:03,660 --> 00:01:08,120 83 | 这一次它会有一个元素代表着刚刚添加的 todo 84 | 85 | 22 86 | 00:01:08,120 --> 00:01:12,300 87 | 所以它会有和动作对象一样的 ID 和内容 88 | 89 | 23 90 | 00:01:12,540 --> 00:01:15,760 91 | 它还有一个额外的属性叫作 completed 92 | 93 | 24 94 | 00:01:15,880 --> 00:01:18,840 95 | 我希望把它初始化为 false 96 | 97 | 25 98 | 00:01:20,080 --> 00:01:23,380 99 | 我们希望保证 reducer 是一个纯函数 100 | 101 | 26 102 | 00:01:23,520 --> 00:01:28,080 103 | 所以我会对状态和动作调用 deepFreeze() 104 | 105 | 27 106 | 00:01:28,840 --> 00:01:33,280 107 | 最后 我已经准备好使用 expect 库来验证 108 | 109 | 28 110 | 00:01:33,320 --> 00:01:38,780 111 | 如果我对前一个状态和动作对象调用 todos reducer 112 | 113 | 29 114 | 00:01:38,980 --> 00:01:44,980 115 | 那么我得到的结果将会和我刚刚声明的 stateAfter 深层相等 116 | 117 | 30 118 | 00:01:45,720 --> 00:01:47,880 119 | 好了 这就是我的第一个测试 120 | 121 | 31 122 | 00:01:47,880 --> 00:01:51,580 123 | 现在 我可以像调用一个普通 JavaScript 函数一样调用它 124 | 125 | 32 126 | 00:01:51,740 --> 00:01:54,460 127 | 而且 如果它不抛出异常的话 128 | 129 | 33 130 | 00:01:54,560 --> 00:01:58,100 131 | 我会看到一个信息 表明测试已经通过 132 | 133 | 34 134 | 00:01:59,680 --> 00:02:01,060 135 | 当然 这个测试现在无法通过 136 | 137 | 35 138 | 00:02:01,060 --> 00:02:04,200 139 | 因为 reducer 还没有被实现 140 | 141 | 36 142 | 00:02:04,200 --> 00:02:05,780 143 | 它是一个空的函数 144 | 145 | 37 146 | 00:02:05,880 --> 00:02:07,680 147 | 所以它现在返回 undefined 148 | 149 | 38 150 | 00:02:07,680 --> 00:02:12,740 151 | 而不是我所期望的一个带有单一元素的数组 152 | 153 | 39 154 | 00:02:13,680 --> 00:02:14,520 155 | 为了修复这个问题 156 | 157 | 40 158 | 00:02:14,520 --> 00:02:18,680 159 | 我会需要 reducer 去检查动作的 type 属性 160 | 161 | 41 162 | 00:02:18,680 --> 00:02:19,920 163 | 这个属性的值是个字符串 164 | 165 | 42 166 | 00:02:20,280 --> 00:02:22,900 167 | 当它匹配 "ADD_TODO" 字符串时 168 | 169 | 43 170 | 00:02:22,900 --> 00:02:25,940 171 | 也就是我在测试中指明的动作类型 172 | 173 | 44 174 | 00:02:26,260 --> 00:02:27,940 175 | 要满足测试条件 176 | 177 | 45 178 | 00:02:27,940 --> 00:02:30,200 179 | 我需要返回一个新的数组 180 | 181 | 46 182 | 00:02:30,280 --> 00:02:33,340 183 | 这个数组包含了原有数组的所有元素 184 | 185 | 47 186 | 00:02:33,460 --> 00:02:35,880 187 | 还有一个新的 todo 元素 188 | 189 | 48 190 | 00:02:36,020 --> 00:02:40,480 191 | 这个元素的 ID 和 text 是从动作对象那里拷贝过来的 192 | 193 | 49 194 | 00:02:40,640 --> 00:02:43,200 195 | 同时它的 completed 属性会被设为 false 196 | 197 | 50 198 | 00:02:44,360 --> 00:02:47,840 199 | 最后 我给 switch 语句加了一个缺省条件 200 | 201 | 51 202 | 00:02:47,840 --> 00:02:51,500 203 | 因为在遇到未知的动作时 204 | 205 | 52 206 | 00:02:51,500 --> 00:02:53,500 207 | 每一个 reducer 都需要返回当前的状态 208 | 209 | 53 210 | 00:02:54,740 --> 00:02:56,920 211 | 现在测试就通过了 212 | 213 | 54 214 | 00:02:57,340 --> 00:03:00,040 215 | 让我们回顾一下这个例子里的数据流 216 | 217 | 55 218 | 00:03:00,040 --> 00:03:01,360 219 | 来看一下为什么测试会通过 220 | 221 | 56 222 | 00:03:03,260 --> 00:03:05,620 223 | 首先 在我的测试函数里 224 | 225 | 57 226 | 00:03:05,620 --> 00:03:07,100 227 | 我创建了一个空的状态数组 228 | 229 | 58 230 | 00:03:07,100 --> 00:03:08,680 231 | 还有动作对象 232 | 233 | 59 234 | 00:03:12,020 --> 00:03:16,440 235 | 我将它们以参数的形式传递给我的 reducer 函数 todos 236 | 237 | 60 238 | 00:03:17,640 --> 00:03:22,340 239 | 接受状态和动作作为参数 240 | 241 | 61 242 | 00:03:22,460 --> 00:03:24,880 243 | 然后检查动作的类型 244 | 245 | 62 246 | 00:03:26,200 --> 00:03:30,400 247 | 在这个例子中 动作的类型是 "ADD_TODO" 248 | 249 | 63 250 | 00:03:30,720 --> 00:03:34,360 251 | 所以它跟 reducer 中的 switch 条件是匹配的 252 | 253 | 64 254 | 00:03:35,540 --> 00:03:37,920 255 | reducer 返回了一个新数组 256 | 257 | 65 258 | 00:03:38,060 --> 00:03:40,640 259 | 这个数组包含了原来数组的元素 260 | 261 | 66 262 | 00:03:40,640 --> 00:03:43,960 263 | 以及一个代表被添加的 todo 的新元素 264 | 265 | 67 266 | 00:03:44,560 --> 00:03:49,640 267 | 不过 测试中我们传进去的状态是个空数组 268 | 269 | 68 270 | 00:03:49,940 --> 00:03:54,120 271 | 所以我们会得到一个只有单一元素的数组 272 | 273 | 69 274 | 00:03:54,120 --> 00:03:56,120 275 | 而这个元素就是新的 todo 276 | 277 | 70 278 | 00:03:57,260 --> 00:04:02,940 279 | 最后 我们将返回值和只有一个 todo 元素的数组对比 280 | 281 | 71 282 | 00:04:03,080 --> 00:04:06,260 283 | 确保 reducer 像我们预想的那样运行 284 | 285 | 72 286 | 00:04:06,800 --> 00:04:08,800 287 | 这里的等价检查通过了 288 | 289 | 73 290 | 00:04:09,040 --> 00:04:11,440 291 | 这样测试就全部通过了 292 | 293 | -------------------------------------------------------------------------------- /chn/16.srt: -------------------------------------------------------------------------------- 1 | 1 2 | 00:00:00,220 --> 00:00:01,300 3 | 在上一课中 4 | 5 | 2 6 | 00:00:01,300 --> 00:00:04,220 7 | 我们学习了使用 Redux 中的 combineReducers() 函数 8 | 9 | 3 10 | 00:00:04,220 --> 00:00:10,000 11 | 这个函数可以将多个 reducer 合并成一个 reducer 12 | 13 | 4 14 | 00:00:10,050 --> 00:00:13,020 15 | 而这些 reducer 分别管理了状态树的不同部分 16 | 17 | 5 18 | 00:00:14,200 --> 00:00:18,900 19 | 为了深入理解 combineReducers() 函数 20 | 21 | 6 22 | 00:00:19,200 --> 00:00:22,200 23 | 我们会在这节课中从头实现它 24 | 25 | 7 26 | 00:00:24,320 --> 00:00:26,570 27 | combineReducers() 是一个函数 28 | 29 | 8 30 | 00:00:26,600 --> 00:00:29,270 31 | 所以我要写一个函数声明 32 | 33 | 9 34 | 00:00:29,300 --> 00:00:34,670 35 | 这个函数唯一的参数是状态的键和 reducer 之间的映射关系 36 | 37 | 10 38 | 00:00:34,750 --> 00:00:37,220 39 | 所以我把它叫做 reducers 40 | 41 | 11 42 | 00:00:37,770 --> 00:00:41,350 43 | 这个函数的返回值本身也应该是一个 reducer 44 | 45 | 12 46 | 00:00:41,520 --> 00:00:44,920 47 | 所以这是一个返回另一个函数的函数 48 | 49 | 13 50 | 00:00:45,050 --> 00:00:49,450 51 | 而被返回的函数的签名也应该是一个 reducer 函数的签名 52 | 53 | 14 54 | 00:00:49,450 --> 00:00:51,450 55 | 也就是一个状态和一个动作 56 | 57 | 15 58 | 00:00:53,420 --> 00:00:56,170 59 | 现在,我要调用 Object.keys() 这个方法 60 | 61 | 16 62 | 00:00:56,220 --> 00:01:00,070 63 | 它会返回包含 reducers 对象的所有键的数组 64 | 65 | 17 66 | 00:01:00,170 --> 00:01:01,270 67 | 在我们的这个例子中 68 | 69 | 18 70 | 00:01:01,270 --> 00:01:04,200 71 | 我们得到的是 "todos" 和 "visibilityFilter" 72 | 73 | 19 74 | 00:01:05,420 --> 00:01:09,450 75 | 接下来 我根据这些键来调用(数组的) reduce() 方法 76 | 77 | 20 78 | 00:01:09,520 --> 00:01:12,300 79 | 因为我想通过遍历每一个 reducer 键来调用对应的 reducer 函数 80 | 81 | 21 82 | 00:01:12,300 --> 00:01:15,250 83 | 最后把他们叠加起来 84 | 85 | 22 86 | 00:01:15,250 --> 00:01:19,520 87 | 生成一个单一的值作为下一个状态 88 | 89 | 23 90 | 00:01:20,550 --> 00:01:24,100 91 | 每个传入 combineReducers() 函数的 reducer 92 | 93 | 24 94 | 00:01:24,220 --> 00:01:28,100 95 | 都只负责更新状态树中的某个部分 96 | 97 | 25 98 | 00:01:28,750 --> 00:01:31,420 99 | 这就是为什么我说 100 | 101 | 26 102 | 00:01:31,450 --> 00:01:35,350 103 | 要计算出某个健的下一个状态 104 | 105 | 27 106 | 00:01:35,350 --> 00:01:38,670 107 | 只要用这个健对应的当前状态和动作 108 | 109 | 28 110 | 00:01:38,670 --> 00:01:43,220 111 | 调用这个健对应的 reducer 就可以了 112 | 113 | 29 114 | 00:01:46,120 --> 00:01:51,520 115 | 这个数组的 reduce() 方法想要我在回调中返回下一个需要叠加的值 116 | 117 | 30 118 | 00:01:51,520 --> 00:01:53,750 119 | 所以我在这里返回 nextState 120 | 121 | 31 122 | 00:01:53,950 --> 00:01:56,820 123 | 同时我指定了一个空对象作为 nextState 的初始值 124 | 125 | 32 126 | 00:01:56,920 --> 00:02:01,200 127 | 它会在任何键被处理之前初始化 128 | 129 | 33 130 | 00:02:02,120 --> 00:02:03,370 131 | 现在我们就完成了 132 | 133 | 34 134 | 00:02:03,370 --> 00:02:07,600 135 | 这是一个 Redux 的 combineReducer() 函数的重新实现 136 | 137 | 35 138 | 00:02:07,600 --> 00:02:09,320 139 | 而且它是可用的 140 | 141 | 36 142 | 00:02:12,020 --> 00:02:14,450 143 | 让我们简短地回顾下这是如何实现的 144 | 145 | 37 146 | 00:02:14,650 --> 00:02:17,170 147 | 我调用了 combineReducers() 函数 它的参数是对象 148 | 149 | 38 150 | 00:02:17,200 --> 00:02:19,670 151 | 这个对象的值是多个 reducer 函数 152 | 153 | 39 154 | 00:02:19,670 --> 00:02:22,170 155 | 而它的键则是对应的 reducer 函数所管理的状态字段 156 | 157 | 40 158 | 00:02:23,370 --> 00:02:25,620 159 | 在这个函数最后生成的 reducer 中 160 | 161 | 41 162 | 00:02:25,650 --> 00:02:30,650 163 | 我提取了所有传入到 combineReducers() 函数的 reducer 的键 164 | 165 | 42 166 | 00:02:30,720 --> 00:02:34,600 167 | 也就是 "todos" 和 "visibilityFilter" 168 | 169 | 43 170 | 00:02:36,200 --> 00:02:39,570 171 | 我先用一个空对象初始化 nextState 172 | 173 | 44 174 | 00:02:39,620 --> 00:02:42,800 175 | 然后用数组的 reduce() 函数来遍历这些键 176 | 177 | 45 178 | 00:02:42,800 --> 00:02:44,620 179 | 逐步填充 nextState 对象 180 | 181 | 46 182 | 00:02:45,870 --> 00:02:50,100 183 | 注意 我在每一次遍历中都修改了 nextState 对象 184 | 185 | 47 186 | 00:02:50,520 --> 00:02:51,850 187 | 这么做并没什么问题 188 | 189 | 48 190 | 00:02:51,850 --> 00:02:55,250 191 | 因为这个对象是我在 reducer 中创建的 192 | 193 | 49 194 | 00:02:55,250 --> 00:02:57,850 195 | 它不是从外部传入的 196 | 197 | 50 198 | 00:02:57,850 --> 00:03:00,150 199 | 所以 reducer 还是一个纯函数 200 | 201 | 51 202 | 00:03:01,650 --> 00:03:04,400 203 | 要计算出一个键的下一状态 204 | 205 | 52 206 | 00:03:04,400 --> 00:03:07,320 207 | 我们会调用对应的 reducer 函数 208 | 209 | 53 210 | 00:03:07,320 --> 00:03:10,300 211 | 比如 todos() 和 visibilityFilter() 函数 212 | 213 | 54 214 | 00:03:12,150 --> 00:03:18,700 215 | 我们只会给子 reducer 传递那个键所对应的状态的一部分 216 | 217 | 55 218 | 00:03:19,170 --> 00:03:21,770 219 | 所以如果生成的 reducer 的状态是一个单独的对象 220 | 221 | 56 222 | 00:03:21,770 --> 00:03:24,370 223 | 它只会传递给键所对应的那部分状态 224 | 225 | 57 226 | 00:03:24,370 --> 00:03:29,120 227 | 例如 todos 和 visibilityFilter 228 | 229 | 58 230 | 00:03:29,220 --> 00:03:33,420 231 | 然后在 nextState 用同样的键把结果保存下来 232 | 233 | 59 234 | 00:03:34,820 --> 00:03:37,620 235 | 最后 我们使用数组的 reduce() 操作 236 | 237 | 60 238 | 00:03:37,620 --> 00:03:41,150 239 | 它以一个空对象作为 nextState 的初始值 240 | 241 | 61 242 | 00:03:41,150 --> 00:03:43,950 243 | 然后在每一次迭代中填充它 244 | 245 | 62 246 | 00:03:43,950 --> 00:03:48,500 247 | 直到它可以作为整个 reduce 操作的结果被返回 248 | 249 | 63 250 | 00:03:50,250 --> 00:03:51,870 251 | 这节课中 252 | 253 | 64 254 | 00:03:51,870 --> 00:03:57,720 255 | 你学习了如何从头实现 Redux 中的 combineReducers() 工具函数 256 | 257 | 65 258 | 00:03:58,420 --> 00:04:01,120 259 | 如果你还没有完全明白它的工作原理 260 | 261 | 66 262 | 00:04:01,120 --> 00:04:02,470 263 | 没有关系 264 | 265 | 67 266 | 00:04:02,470 --> 00:04:05,450 267 | 因为这对于使用 Redux 并不是非常重要 268 | 269 | 68 270 | 00:04:05,570 --> 00:04:09,670 271 | 但是 练习函数式编程 272 | 273 | 69 274 | 00:04:09,850 --> 00:04:14,120 275 | 以及理解函数可以接受其它函数为参数 276 | 277 | 70 278 | 00:04:14,120 --> 00:04:16,120 279 | 然后返回其它函数 280 | 281 | 71 282 | 00:04:16,120 --> 00:04:17,400 283 | 是一个很好的主义 284 | 285 | 72 286 | 00:04:17,400 --> 00:04:19,550 287 | 因为长期来说 288 | 289 | 73 290 | 00:04:19,570 --> 00:04:22,550 291 | 理解这些会让你在使用 Redux 时更具生产力 292 | 293 | -------------------------------------------------------------------------------- /src/17: -------------------------------------------------------------------------------- 1 | In the previous lessons, we learned how to split the root reducers into many smaller reducers that manage parts of the state tree. 2 | 3 | And now we have a ready "todoApp" reducer that handles all the actions of our simple ToDo application. So now it's time to implement the view layer. And I'm going to use React in this example. 4 | 5 | I'm adding react and redact-dom packages from the Facebook CDN. I'm also adding a div with ID "root", which is where I'm going to render my react application. 6 | 7 | imilar to the React counter example from the eighth lesson, I declare a "render" function that is going to update DOM in response to the current application state. And I'm going to subscribe to the store changes and call "render" whenever the store changes, and once to render the initial state. 8 | 9 | And the implementation of the render method is going to use react, so it will call "ReactDom.render" for some "TodoApp" component I haven't written yet. And it renders it into the div I created inside the HTML. So it's the div with ID called "root". 10 | 11 | React provides a base class for all components. So I'm grabbing from the react object called "reactComponent". And I'm declaring my own "TooApp" component that extends the react base component. This component is only going to have a "render" function and is going to return a "div". And inside the "div", I'm going to place a botton saying "Add Todo". 12 | 13 | But I don't want to add an input field yet to keep the example simple at first. So I'm dispatching the "ADD_TODO" action, and I'm going to put a test as my checks for the action. So It's going to keep adding todos with the text "Test". 14 | 15 | And, The ID, I need to specify a sequential ID. So This is why I'm declaring a global variable called "nextTodoId", and I'm going to keep incrementing it. So, every time, it's going to emit a new id. 16 | 17 | And finally, I also want to display a list of the todos. So assuming that I have the todos injected as a "todos" prop, I'll call map and for every todo item, I'm going to show a list item showing the text of that particular todo. 18 | 19 | Finally, because I need to the "todos" as a prop, I'm going to pass it to the TodoApp by reading the current store state and written its "todos" field. 20 | 21 | You can see that there is a button "Add Todo" and anytime I press it, I see a new ToDo with a text "Test". Now I'm going to add an "input" inside my "render" function, and I'm using the react callback "ref" API where "ref" is a function, it gets the node corresponding to the "ref", and I'm saving that node with some name, in this case, "this.input". 22 | 23 | So I'm able to read the value of the input inside my event handler. I'm reading "this.input.value". And I'm also able to reset the value after dispatching the action so that the field is cleared. Now if I try to write something to fill and press "Add Todo", the "ADD_TODO" action is dispatched and the field is cleared. 24 | 25 | Let's take a moment to recap how this application works. It starts with a "TodoApp" react component. This component is not aware of how exactly todos are being added. However, it can express its desire to mutate the state by dispatching an action with the type "ADD_TODO". 26 | 27 | For the text field, it uses the current input value and it passes an incrementing ID as the ID of todo. Every todo needs its own ID, and in this approach, we're just going to increment the counter, so it always gives us the next integer as ID. 28 | 29 | It is common for react components to dispatch actions in Redux apps. However, it is equally important to be able to render the current state. My "TodoApp" component assumes that it's going to receive todos as a prop, and it maps over the todos to display a list of them using the ID as a key. 30 | 31 | This component is being rendered in the "render" function that runs any time the store state changes and initially. The render function reads the current state of the store and passes the " todos" array that it gets from the current state of this store to the "TodoApp" component as a prop. 32 | 33 | The "render" function is called on every store change so the "todos" prop is always up to date. This was the rendering part of the redux flow. Let's recap how mutations work in Redux. 34 | 35 | Any state change is caused by a "store.dispatch" call somewhere in the component. When an action is dispatched, the store calls the reducer it was created with, with the current state and the action being dispatched. 36 | 37 | And in our case, this is the "todoApp" reducer, which we obtained by combining "visibilityFilter" and "ToDos" reducer. 38 | 39 | It matches the action type and the switch statement. And if the action type is "ADD_TODO", and indeed, it is equal to "ADD_TODO" string. In this case, it will call the child "todo" reducer, passing it undefined, because there is no state for a new todo that it can pass, and the action. 40 | 41 | And we have a similar switch statement inside the "todo" reducer and the action type is "ADD_TODO". So It returns the initial state of the todo with the ID and text from the action and the "completed" field set to false. 42 | 43 | The "todos" reducer that called it was returned a new array with all existent items and the new item added at the very end. So it adds a need todo to the current state. 44 | 45 | Finally, the combined producer called "todoApp" will use this new array as the new value for the "todos" field in the global state object. It's going to return a new state object where the "todos" field corresponds to the array with the newly added todo item. 46 | 47 | The "todoApp" reducer is the root reducer in this application. It is the one the store was created with. So its next state is a next state of the Redux store, and all the listeners are notified. 48 | 49 | The "render" function is subscribed to the store changes so it is called again, and it gets the fresh state by calling "getState" and it passes the fresh todos to the component, re-rendering it with the new data . 50 | -------------------------------------------------------------------------------- /translation/20: -------------------------------------------------------------------------------- 1 | In the last few lessons, we created a user interface for a simple React and Redux todo list where I can add items, toggle them as completed, and change the currently visible todos. 2 | 在前面几节课中,我们为一个简单的应用,React/Redux 的简单 todo 列表,创建了用户界面,在上面我可以增加项目,完成/撤销项目,以及改变当前可见的项目。 3 | 4 | And we did that by having a single "TodoApp" component that has the input, the button for adding todos, the list of currently visible todos with click handler. And it has these three links that let us change the currently visible todos. 5 | 我们实现这个应用的方式,是通过一个单一的 TodoApp 部件,这个部件有一个输入框,一个添加待办事项的按钮,以及带有点击处理器的当前可见 todo 列表。这个应用还有三个链接,可以让我们改变当前可见的 todo。 6 | 7 | The single component approach worked so far. However, we really want to have many components that can be used, tested, and changed by different people separately. So we're going to refactor our application in this lesson. 8 | 到目前为止,这种单一部件的方式都还不错。不过,我们真正想要的是,能够拥有很多部件,而这些部件能够被使用、测试或者被不同的人所修改。所以这节课,我们会来重构我们的应用。 9 | 10 | The first component I want to extract from the "TodoApp" component is the "Todo" component that renders a single list item. I am declaring the "Todo" component as a function which React 14 allows me to do. I'm not sure which props it's going to have, so I'll leave them blank for now. And I will just paste the list item I copied before. 11 | 第一个我想从 TodoApp 部件中提取出来的是 Todo 部件,它会渲染一个单独的 todo 列表项。我现在将 Todo 部件声明为一个函数,这在 React 14 中是合法的。我不太清楚我会需要哪些属性,所以我会先空着。然后我会将列表项的代码复制过来。 12 | 13 | The first thing I'm doing is removing the special "key" property because it's only needed when I enumerate an array. And I'll use it later when enumerating many todos. 14 | 我首先要做的事情是,移除 key 属性,因为它只在我遍历一个数组的时候才会用到。我会在之后遍历很多 todo 的时候再使用它。 15 | 16 | One of my goals with this refactoring is to make every component as flexible as it is reasonable. Right now, I have hardcoded that clicking a todo always causes the "TOGGLE_TODO" action. And this is perfectly fine to do in your app. 17 | 重构这些代码的目标之一,是要让每个部件都既灵活又合理。现在,点击一个 todo 总是触发 "TOGGLE_TODO" 动作,我把这个逻辑写死在部件里了。在你的应用中你也完全可以这样做。 18 | 19 | However, I prefer to have a bunch of components that don't specify any behaviors, and that are only concerned with how things look or how they render. I call such components the presentational components. 20 | 然而,我希望有一些部件,它们不指明任何行为,而只关心内容如何被渲染。我将这种部件称作展示部件。 21 | 22 | I would like to give todo a presentational component, so I removed the "onClick" handler. And I promote it to be a prop so that anyone who uses "Todo" component can specify what happens on the click. And you don't have to do this in your Redux apps, but I find it to be a very convenient pattern. 23 | 我想把 Todo 变成一个展示部件,所以我将 onClick 处理器移除了。然后我将它变成一个属性,这样任何使用 Todo 部件的人都可以指明部件被点击后的逻辑。你并不需要在你的 Redux 应用中这样做,但我认为它是一种很方便的模式。 24 | 25 | Finally, while it doesn't make a lot of difference, I prefer to keep it explicit what is the data that the component needs to render. So instead of passing a todo object, I pass "completed" and "text" fields as separate props. 26 | 最后,虽然这并没有什么区别,但我还是倾向于将部件所需要渲染的数据明确表示出来。所以我将 completed 和 text 字段以属性的方式传递给部件,而不是传递一个 todo 对象。 27 | 28 | Note how the "Todo" component is now a purely presentational component and doesn't specify any behavior. But it knows how to render "ADD_TODO". 29 | 要注意,我们是如何将 Todo 部件变成一个纯展示部件的。它并没有指明任何行为,但它知道如何渲染 "ADD_TODO"。 30 | 31 | The next component I create is called "TodoList". And it's also a presentational component. It's only concerned with how things look. 32 | 下一个我要创建的部件叫做 TodoList。它也是一个展示部件。它只关心内容的呈现方式。 33 | 34 | And it accepts an array of todos. And it's going to render an unordered list of them by calling the "todos.map" function and rendering a "Todo" component for every todo. It tells React to use todo ID as the unique key for the elements. And it spreads over the "todo" object properties so the "text" and "completed" end up as props on the "Todo" component. 35 | 它会接受一个 todo 的数组。然后它会调用 todos.map() 函数来渲染一个无序的 todo 列表,以及为每一个 todo 渲染一个 Todo 部件。它让 React 使用 todo 的 ID 作为元素的唯一键。然后它对 todo 对象使用了展开运算符,这样 text 和 completed 就会作为属性传递给 Todo 部件。 36 | 37 | Now I need to specify what happens when a todo is clicked. And I could have dispatched an action here. And again, that would be fine, but I want it to a presentational component, so I'm going to call another function, called "onTodoClick", and pass the ID of the todo, and let it decide what should happen. So "onTodoClick" is another prop for the "TodoList". 38 | 现在我需要定义如何处理对一个 Todo 部件的点击。我可以分发一个动作,这当然是可行的。但是我希望它是一个展示部件,所以我调用了另一个函数 onTodoClick(),然后将 todo 的 ID 传递给它,然后让它决定要怎么处理。所以 onTodoClick 成为了 TodoList 部件的另一个属性。 39 | 40 | Both "Todo" and "TodoList" are presentational components, so we need something I call, "container components" to actually pass the data from the store and to specify the behavior. In this case, the top level "TodoAppComponent" acts as a container component. And We will see more examples of container components in the future lessons. 41 | Todo 和 TodoList 都是展示部件,所以我们需要容器部件来从 store 传递数据以及定义行为。在这个例子中,最高层的 TodoAppComponent 就是一个容器部件。在后面的课程中我们会看到更多的容器部件。 42 | 43 | In this case, it just renders the "TodoList" with visible todos as the "todos", and with a callback that says that when "onTodoClick" is called with a todo ID, we should dispatch an action on the store with the type "TOGGLE_TODO" and the ID of the todo. 44 | 在这个例子中,TodoAppComponent 只渲染 TodoList,它以当前可见的 todo 作为 todos。它还传递了一个回调函数,这个函数定义了,当 onTodoClick() 被以一个 todo ID 调用时,我们应该分发一个动作给 store,这个动作的类型是 "TOGGLE_TODO",它的 ID 就是 todo 的 ID。 45 | 46 | Let's recap again how this works. The "TodoApp" component renders a "TodoList", and it passes a function to it that can dispatch an action. The "TodoList" component renders the "Todo" component and passes "onClick" prop which calls "onTodoClick". 47 | 让我们回顾一下。TodoApp 部件渲染了一个 TodoList,而且传递了一个可以分发状态的函数。TodoList 部件渲染了 Todo 部件,而且传递了一个属性 onClick,它会调用 onTodoClick()。 48 | 49 | The "Todo" component just uses the "onClick" prop it receives and binds it to the list item's "onClick". This way, when it's called, the "onTodoClick" is called, and this dispatches the action and updates the visible todos because the action updates the store. 50 | Todo 部件只是使用了 onClick 属性,将它绑定到 todo 列表项的 onClick。这样,当 onClick 被调用时,onTodoClick() 会被调用,它会分发一个动作,然后更新当前可见的 todo,因为动作更新了 store。 51 | -------------------------------------------------------------------------------- /translation/28: -------------------------------------------------------------------------------- 1 | In the previous lesson, we used the "connect" function from "react-edux" bindings library to generate the container component that renders our presentational component. 2 | 在上节课中,我们使用了 react-redux 库中的 connect() 函数来生成容器组件,这个容器组件负责渲染我们的展示组件。 3 | 4 | I specify how to calculate the props to inject from the current Redux store state and the callback props to inject from the "dispatch" function on the Redux store. 5 | 我降到了,如何从当前的 Redux store 状态,计算出要注入的属性;以及从 Redux Store 的 dispatch() 函数,计算出要注入的回调函数属性。 6 | 7 | Normally, I would keep these functions, called "mapStateToProps" and "mapDispatchToProps". 8 | 通常,我会保留这些被命名为 mapStateToProps() 和 mapDispatchToProps() 的函数。 9 | 10 | But I'm working in a single file right now. And I need to write these functions for your other container components, so I'm going to rename them to something more specific, which you don't have to do in your code if you keep every component in its own file. 11 | 但是我现在是在单个文件里写代码,我还需要为它的容器组件写这些函数,所以我要将它们命名得更具体一点。在你的代码里,如果你将每一个组件都放在一个单独的文件里,那么你就不需要这样做。 12 | 13 | I will also remove the line breaks here to make it clear that these functions are only relevant for generating this particular container component. 14 | 我还要把这些换行符去掉,这样看起来代码更清晰,因为这些函数只是为了用来生成这个特定的容器组件。 15 | 16 | Now I'm scrolling up to the "AddTodo" component, which is not clearly presentational or a container component. 17 | 现在我要回到 AddTodo 组件,它现在的角色有点模棱两可,不知道是展示组件还是容器组件。 18 | 19 | However, it uses the store. 20 | 不过,它用到了 store。 21 | 22 | It reads the store from the context to dispatch an action when the button is clicked. 23 | 当一个按钮被点击时,AddTodo 组件从上下文读取了 store,然后分发了一个动作。 24 | 25 | And it has to declare the "contextTypes" to be able to grab the store from the context. 26 | 而且它必须声明 contextTypes 来从上下文获取 store。 27 | 28 | Context is an unstable API, so it's best to avoid using it in your application code. 29 | Context 是一个不稳定的 API,所以在你最好避免使用它。 30 | 31 | Instead of reading the store from the context, I will read the "dispatch" function from the props because I only need the "dispatch" here. I don't need the whole store. 32 | 现在我不再从上下文读取 store,而是从属性里读取 dispatch() 函数,因为这里我只需要用到 dispatch()。我不需要整个 store。 33 | 34 | And I will create a container component with "connect" that will inject the "dispatch" function as a prop. 35 | 现在我要用 connect() 创建一个容器组件,connect() 会将 dispatch() 注入组件作为它的属性。 36 | 37 | I will remove the "contextTypes" because the component generated by "connect" function will take care of reading the store from the context. 38 | 我要删掉 contextTypes,因为用 connect() 函数生成的组件会搞定从上下文读取 store 这个事情。 39 | 40 | Because I changed the "AddTodo" declaration from the const to the let binding, I can reassign it now so that the consuming component does not need to specify the "dispatch" prop because it will be injected by the component generated by the "connect" code. 41 | 因为我将 AddTodo 的声明从 const 改成了 let,所以我可以在这里再次赋值,而不需要给它声明 dispatch 属性。因为 connect() 会在所生成的组件里注入 dispatch。 42 | 43 | The first argument to the "connect" function is "mapStateToProps", but there aren't any props for "AddTodo" component that depend on the current state, so I return an empty object. 44 | connect() 函数的第一个参数是 mapStateToProps,但因为 AddTodo 组件没有依赖任何依赖当前状态的属性,所以这里这是返回一个空对象。 45 | 46 | The second argument to "connect" is "mapDispatchToProps", but "AddTodo" component doesn't need any callback props. 47 | connect() 函数的第二个参数是 mapDispatchToProps。不过 AddTodo 组件不需要任何回调属性。 48 | 49 | It just accepts the "dispatch" function itself, so I'm returning it as a prop with the same name. 50 | 它只接受 dispatch() 函数本身,所以我只要返回一个对象,其中的 dispatch 属性就是这个 dispatch() 函数。 51 | 52 | Finally, I'm calling the function for a second time to specify the component I want to wrap, in this case, "AddTodo" itself. 53 | 最后,我要对我想要打包的组件,也就是 AddTodo,再次调用这个函数。 54 | 55 | The generated container component will not pass any props dependent on the state, but it will pass "dispatch" itself as a function so that the component can read it from the props and use it without worrying about context or specifying "contextTypes". 56 | 这样生成的容器组件不会传递任何依赖于状态的属性,但是它会将 dispatch 作为一个函数传递,这样组件就可以从属性里面读取它,而不需要担心上下文或者声明 contextTypes。 57 | 58 | However, it is wasteful to even subscribe to the store if we don't calculate any props from the state. 59 | 但是,如果我们不需要用到任何状态里面的属性,那么订阅到 store 其实是很多余的。 60 | 61 | So I'm replacing the "mapStateToProps" function with an "null", which tells "connect" that there is no need to subscribe to the store. 62 | 所以我将 mapStateToProps 改成 null,这样的话 connect 就不会帮我订阅 store。 63 | 64 | Additionally, it's pretty common pattern to inject just the "dispatch" function. 65 | 另外,只注入 dispatch() 函数其实是一个蛮普遍的做法。 66 | 67 | So this is why if you specify "null" or any falsy value in "connect" as the second argument, you're going to get "dispatch" injected as a prop. 68 | 所以这也是为什么在 connect() 里面将第二个参数设为 null 或者任何假值,它就会将 dispatch 作为属性注入。 69 | 70 | So in fact, I can just remove all arguments here. 71 | 所以,实际上,我可以删掉这里所有的参数。 72 | 73 | And the default behavior will be to not subscribe to the store and to inject just the "dispatch" function as a prop. 74 | 而这样做,默认地就会不订阅 sotre,以及将 dispatch 函数作为属性注入。 75 | 76 | Let's recap what happens to the components here. The "AddTodo" component that I declare accepts "dispatch" as a prop, but it doesn't know how to get the store. It just hopes that someone is going to pass the "dispatch" to it. 77 | 让我们来回顾一下在组件身上发生了什么。AddTodo 组件接受 dispatch 作为属性,但它并不需要知道怎么拿到 store。它只是希望有人会将 dispatch 传递给它。 78 | 79 | The "connect" code without any arguments is going to generate a container component that does not subscribe to the store. 80 | 不带任何参数地调用 connect() 函数会生成一个不订阅 store 的容器组件。 81 | 82 | However, that will pass "dispatch" to the component that it wraps. And in this case, it wraps my "AddTodo" component. 83 | 不过,它会将 dispatch 传递给它所包裹的组件。在我们的例子中,就是 AddTodo 组件。 84 | 85 | The second "connect" call returns the generated container component. And I'm assigning it to "AddTodo". 86 | 第二个 connect() 调用会返回生成的容器组件。我将它赋值给 AddTodo。 87 | 88 | So I'm reassigning the let binding the second time. 89 | 所以在这里我对 AddTodo 进行第二次赋值。 90 | 91 | And when the further code references "AddTodo", it's going to reference the container component that does not need the "dispatch" prop and that will pass the "dispatch" prop to my inner "AddTodo" component that I don't have a reference to anymore. 92 | 而当其他的代码再次应用 AddTodo 时,它们引用的就是那个不需要 dispatch 属性的容器组件,这个组件会将 dispatch 属性传递给我内部的 AddTodo 组件,而这个组件我将无法再次引用。 93 | --------------------------------------------------------------------------------