{"action":"created","issue":{"url":"https://api.github.com/repos/hujiulong/blog/issues/5","repository_url":"https://api.github.com/repos/hujiulong/blog","labels_url":"https://api.github.com/repos/hujiulong/blog/issues/5/labels{/name}","comments_url":"https://api.github.com/repos/hujiulong/blog/issues/5/comments","events_url":"https://api.github.com/repos/hujiulong/blog/issues/5/events","html_url":"https://github.com/hujiulong/blog/issues/5","id":308594877,"node_id":"MDU6SXNzdWUzMDg1OTQ4Nzc=","number":5,"title":"从零开始实现一个React（二）：组件和生命周期","user":{"login":"hujiulong","id":13267437,"node_id":"MDQ6VXNlcjEzMjY3NDM3","avatar_url":"https://avatars1.githubusercontent.com/u/13267437?v=4","gravatar_id":"","url":"https://api.github.com/users/hujiulong","html_url":"https://github.com/hujiulong","followers_url":"https://api.github.com/users/hujiulong/followers","following_url":"https://api.github.com/users/hujiulong/following{/other_user}","gists_url":"https://api.github.com/users/hujiulong/gists{/gist_id}","starred_url":"https://api.github.com/users/hujiulong/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/hujiulong/subscriptions","organizations_url":"https://api.github.com/users/hujiulong/orgs","repos_url":"https://api.github.com/users/hujiulong/repos","events_url":"https://api.github.com/users/hujiulong/events{/privacy}","received_events_url":"https://api.github.com/users/hujiulong/received_events","type":"User","site_admin":false},"labels":[{"id":894723288,"node_id":"MDU6TGFiZWw4OTQ3MjMyODg=","url":"https://api.github.com/repos/hujiulong/blog/labels/React","name":"React","color":"d93f0b","default":false}],"state":"open","locked":false,"assignee":null,"assignees":[],"milestone":null,"comments":30,"created_at":"2018-03-26T14:24:50Z","updated_at":"2018-11-28T02:09:46Z","closed_at":null,"author_association":"OWNER","body":"# 前言\r\n在上一篇文章[JSX和虚拟DOM](https://github.com/hujiulong/blog/issues/4)中，我们实现了基础的JSX渲染功能，但是React的意义在于组件化。在这篇文章中，我们就要实现React的组件功能。\r\n\r\n# 组件\r\nReact定义组件的方式可以分为两种：函数和类，函数定义可以看做是类定义的一种简单形式。\r\n\r\n## createElement的变化\r\n回顾一下上一篇文章中我们对`React.createElement`的实现：\r\n```js\r\nfunction createElement( tag, attrs, ...children ) {\r\n    return {\r\n        tag,\r\n        attrs,\r\n        children\r\n    }\r\n}\r\n```\r\n这种实现我们前面暂时只用来渲染原生DOM元素，而对于组件，createElement得到的参数略有不同：\r\n如果JSX片段中的某个元素是组件，那么createElement的第一个参数`tag`将会是一个方法，而不是字符串。\r\n\r\n> 区分组件和原生DOM的工作，是`babel-plugin-transform-react-jsx`帮我们做的\r\n\r\n例如在处理`<Welcome name=\"Sara\" />`时，`createElement`方法的第一个参数`tag`，实际上就是我们定义`Welcome`的方法：\r\n```jsx\r\nfunction Welcome( props ) {\r\n    return <h1>Hello, {props.name}</h1>;\r\n}\r\n```\r\n我们不需要对createElement做修改，只需要知道如果渲染的是组件，tag的值将是一个函数\r\n\r\n## 组件基类React.Component\r\n通过类的方式定义组件，我们需要继承`React.Component`：\r\n```jsx\r\nclass Welcome extends React.Component {\r\n    render() {\r\n        return <h1>Hello, {this.props.name}</h1>;\r\n    }\r\n}\r\n```\r\n所以我们就需要先来实现React.Component这个类：\r\n\r\n## Component\r\nReact.Component包含了一些预先定义好的变量和方法，我们来一步一步地实现它：\r\n先定义一个`Component`类：\r\n```jsx\r\nclass Component {}\r\n```\r\n### state & props\r\n通过继承`React.Component`定义的组件有自己的私有状态`state`，可以通过`this.state`获取到。同时也能通过`this.props`来获取传入的数据。\r\n所以在构造函数中，我们需要初始化`state`和`props`\r\n```js\r\n// React.Component\r\nclass Component {\r\n    constructor( props = {} ) {\r\n        this.state = {};\r\n        this.props = props;\r\n    }\r\n}\r\n```\r\n### setState\r\n组件内部的`state`和渲染结果相关，当`state`改变时通常会触发渲染，为了让React知道我们改变了`state`，我们只能通过`setState`方法去修改数据。我们可以通过`Object.assign`来做一个简单的实现。\r\n在每次更新`state`后，我们需要调用`renderComponent`方法来重新渲染组件，`renderComponent`方法的实现后文会讲到。\r\n```js\r\nimport { renderComponent } from '../react-dom/render'\r\nclass Component {\r\n    constructor( props = {} ) {\r\n        // ...\r\n    }\r\n\r\n    setState( stateChange ) {\r\n        // 将修改合并到state\r\n        Object.assign( this.state, stateChange );\r\n        renderComponent( this );\r\n    }\r\n}\r\n```\r\n你可能听说过React的`setState`是异步的，同时它有很多优化手段，这里我们暂时不去管它，在以后会有一篇文章专门来讲`setState`方法。\r\n\r\n## render\r\n上一篇文章中实现的render方法只支持渲染原生DOM元素，我们需要修改`ReactDOM.render`方法，让其支持渲染组件。\r\n修改之前我们先来回顾一下上一篇文章中我们对`ReactDOM.render`的实现：\r\n```js\r\nfunction render( vnode, container ) {\r\n    return container.appendChild( _render( vnode ) );\r\n}\r\n\r\nfunction _render( vnode ) {\r\n\r\n    if ( vnode === undefined || vnode === null || typeof vnode === 'boolean' ) vnode = '';\r\n\r\n    if ( typeof vnode === 'number' ) vnode = String( vnode );\r\n\r\n    if ( typeof vnode === 'string' ) {\r\n        let textNode = document.createTextNode( vnode );\r\n        return textNode;\r\n    }\r\n\r\n    const dom = document.createElement( vnode.tag );\r\n\r\n    if ( vnode.attrs ) {\r\n        Object.keys( vnode.attrs ).forEach( key => {\r\n            const value = vnode.attrs[ key ];\r\n            setAttribute( dom, key, value );\r\n        } );\r\n    }\r\n\r\n    vnode.children.forEach( child => render( child, dom ) );    // 递归渲染子节点\r\n\r\n    return dom; \r\n```\r\n我们需要在其中加一段用来渲染组件的代码：\r\n```js\r\nfunction _render( vnode, container ) {\r\n\r\n    // ...\r\n\r\n    if ( typeof vnode.tag === 'function' ) {\r\n\r\n        const component = createComponent( vnode.tag, vnode.attrs );\r\n\r\n        setComponentProps( component, vnode.attrs );\r\n\r\n        return component.base;\r\n    }\r\n    \r\n    // ...\r\n}\r\n```\r\n## 组件渲染和生命周期\r\n在上面的方法中用到了`createComponent`和`setComponentProps`两个方法，组件的生命周期方法也会在这里面实现。\r\n\r\n> 生命周期方法是一些在特殊时机执行的函数，例如componentDidMount方法会在组件挂载后执行\r\n\r\n`createComponent`方法用来创建组件实例，并且将函数定义组件扩展为类定义组件进行处理，以免其他地方需要区分不同定义方式。\r\n```js\r\n// 创建组件\r\nfunction createComponent( component, props ) {\r\n\r\n    let inst;\r\n    // 如果是类定义组件，则直接返回实例\r\n    if ( component.prototype && component.prototype.render ) {\r\n        inst = new component( props );\r\n    // 如果是函数定义组件，则将其扩展为类定义组件\r\n    } else {\r\n        inst = new Component( props );\r\n        inst.constructor = component;\r\n        inst.render = function() {\r\n            return this.constructor( props );\r\n        }\r\n    }\r\n\r\n    return inst;\r\n}\r\n```\r\n`setComponentProps`方法用来更新`props`，在其中可以实现`componentWillMount`，`componentWillReceiveProps`两个生命周期方法\r\n```js\r\n// set props\r\nfunction setComponentProps( component, props ) {\r\n\r\n    if ( !component.base ) {\r\n        if ( component.componentWillMount ) component.componentWillMount();\r\n    } else if ( component.componentWillReceiveProps ) {\r\n        component.componentWillReceiveProps( props );\r\n    }\r\n\r\n    component.props = props;\r\n\r\n    renderComponent( component );\r\n\r\n}\r\n```\r\nrenderComponent方法用来渲染组件，setState方法中会直接调用这个方法进行重新渲染，在这个方法里可以实现`componentWillUpdate`，`componentDidUpdate`，`componentDidMount`几个生命周期方法。\r\n```js\r\nexport function renderComponent( component ) {\r\n\r\n    let base;\r\n\r\n    const renderer = component.render();\r\n\r\n    if ( component.base && component.componentWillUpdate ) {\r\n        component.componentWillUpdate();\r\n    }\r\n\r\n    base = _render( renderer );\r\n\r\n    if ( component.base ) {\r\n        if ( component.componentDidUpdate ) component.componentDidUpdate();\r\n    } else if ( component.componentDidMount ) {\r\n        component.componentDidMount();\r\n    }\r\n\r\n    if ( component.base && component.base.parentNode ) {\r\n        component.base.parentNode.replaceChild( base, component.base );\r\n    }\r\n\r\n    component.base = base;\r\n    base._component = component;\r\n\r\n}\r\n```\r\n# 渲染组件\r\n现在大部分工作已经完成，我们可以用它来渲染组件了。\r\n## 渲染函数定义组件\r\n渲染前文提到的`Welcome`组件：\r\n```jsx\r\nconst element = <Welcome name=\"Sara\" />;\r\nReactDOM.render(\r\n    element,\r\n    document.getElementById( 'root' )\r\n);\r\n```\r\n在浏览器中可以看到结果：\r\n\r\n![1](https://user-images.githubusercontent.com/13267437/37911259-26c0ff84-3142-11e8-89d5-a3a2257ff2be.png)\r\n\r\n试试更复杂的例子，将多个组件组合起来：\r\n```jsx\r\nfunction App() {\r\n    return (\r\n        <div>\r\n            <Welcome name=\"Sara\" />\r\n            <Welcome name=\"Cahal\" />\r\n            <Welcome name=\"Edite\" />\r\n        </div>\r\n    );\r\n}\r\nReactDOM.render(\r\n    <App />,\r\n    document.getElementById( 'root' )\r\n);\r\n```\r\n在浏览器中可以看到结果：\r\n![2](https://user-images.githubusercontent.com/13267437/37911269-2b312f94-3142-11e8-961d-e5948af46cf6.png)\r\n\r\n## 渲染类定义组件\r\n我们来试一试将刚才函数定义组件改成类定义：\r\n```jsx\r\nclass Welcome extends React.Component {\r\n    render() {\r\n        return <h1>Hello, {this.props.name}</h1>;\r\n    }\r\n}\r\n\r\nclass App extends React.Component {\r\n    render() {\r\n        return (\r\n            <div>\r\n                <Welcome name=\"Sara\" />\r\n                <Welcome name=\"Cahal\" />\r\n                <Welcome name=\"Edite\" />\r\n            </div>\r\n        );\r\n    }\r\n}\r\nReactDOM.render(\r\n    <App />,\r\n    document.getElementById( 'root' )\r\n);\r\n```\r\n运行起来结果和函数定义组件完全一致：\r\n![1](https://user-images.githubusercontent.com/13267437/37911298-414a2678-3142-11e8-9ddd-6b48d4e3be17.png)\r\n\r\n\r\n再来尝试一个能体现出类定义组件区别的例子，实现一个计数器`Counter`，每点击一次就会加1。\r\n并且组件中还增加了两个生命周期函数：\r\n```jsx\r\nclass Counter extends React.Component {\r\n    constructor( props ) {\r\n        super( props );\r\n        this.state = {\r\n            num: 0\r\n        }\r\n    }\r\n\r\n    componentWillUpdate() {\r\n        console.log( 'update' );\r\n    }\r\n\r\n    componentWillMount() {\r\n        console.log( 'mount' );\r\n    }\r\n\r\n    onClick() {\r\n        this.setState( { num: this.state.num + 1 } );\r\n    }\r\n\r\n    render() {\r\n        return (\r\n            <div onClick={ () => this.onClick() }>\r\n                <h1>number: {this.state.num}</h1>\r\n                <button>add</button>\r\n            </div>\r\n        );\r\n    }\r\n}\r\n\r\nReactDOM.render(\r\n    <Counter />,\r\n    document.getElementById( 'root' )\r\n);\r\n```\r\n可以看到结果：\r\n![2](https://user-images.githubusercontent.com/13267437/37910869-20427274-3141-11e8-9deb-fc97f320199d.gif)\r\n\r\nmount只在挂载时输出了一次，后面每次更新时会输出update\r\n\r\n# 后话\r\n至此我们已经从API层面实现了React的核心功能。但是我们目前的做法是每次更新都重新渲染整个组件甚至是整个应用，这样的做法在页面复杂时将会暴露出性能上的问题，DOM操作非常昂贵，而为了减少DOM操作，React又做了哪些事？这就是我们下一篇文章的内容了。\r\n\r\n这篇文章的代码：https://github.com/hujiulong/simple-react/tree/chapter-2\r\n\r\n# 从零开始实现React系列\r\nReact是前端最受欢迎的框架之一，解读其源码的文章非常多，但是我想从另一个角度去解读React：从零开始实现一个React，从API层面实现React的大部分功能，在这个过程中去探索为什么有虚拟DOM、diff、为什么setState这样设计等问题。\r\n\r\n整个系列大概会有四篇左右，我每周会更新一到两篇，我会第一时间在github上更新，有问题需要探讨也请在github上回复我~\r\n> 博客地址: https://github.com/hujiulong/blog\r\n关注点star，订阅点watch\r\n\r\n## 上一篇文章\r\n[从零开始实现一个React（一）：JSX和虚拟DOM ](https://github.com/hujiulong/blog/issues/4)\r\n\r\n## 下一篇文章\r\n[从零开始实现一个React（三）：diff算法 ](https://github.com/hujiulong/blog/issues/6)"},"comment":{"url":"https://api.github.com/repos/hujiulong/blog/issues/comments/442291589","html_url":"https://github.com/hujiulong/blog/issues/5#issuecomment-442291589","issue_url":"https://api.github.com/repos/hujiulong/blog/issues/5","id":442291589,"node_id":"MDEyOklzc3VlQ29tbWVudDQ0MjI5MTU4OQ==","user":{"login":"bigggge","id":9855604,"node_id":"MDQ6VXNlcjk4NTU2MDQ=","avatar_url":"https://avatars3.githubusercontent.com/u/9855604?v=4","gravatar_id":"","url":"https://api.github.com/users/bigggge","html_url":"https://github.com/bigggge","followers_url":"https://api.github.com/users/bigggge/followers","following_url":"https://api.github.com/users/bigggge/following{/other_user}","gists_url":"https://api.github.com/users/bigggge/gists{/gist_id}","starred_url":"https://api.github.com/users/bigggge/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/bigggge/subscriptions","organizations_url":"https://api.github.com/users/bigggge/orgs","repos_url":"https://api.github.com/users/bigggge/repos","events_url":"https://api.github.com/users/bigggge/events{/privacy}","received_events_url":"https://api.github.com/users/bigggge/received_events","type":"User","site_admin":false},"created_at":"2018-11-28T02:09:46Z","updated_at":"2018-11-28T02:09:46Z","author_association":"NONE","body":"为啥要写\r\ninst.constructor = component;\r\n这一句？"}}
{"actor":{"display_login":"bigggge"}}