youngwind / blog

梁少峰的个人博客
4.66k stars 385 forks source link

preact 源码学习系列之二:组件的渲染与更新 #104

Open youngwind opened 7 years ago

youngwind commented 7 years ago

在上一篇 #103 中,我们已经掌握了“如何解析渲染一段 JSX 结构”。今天,我们进一步研究:如何解析、渲染和更新组件。

变量的解析

在研究组件解析之前,我们先来看个更简单的例子。

import {h, render, Component} from '../../preact';

class Person extends Component {
    constructor() {
        super();
        this.state = {
            name: "youngwind"
        }
    }

    render() {
        return (
            <div>
                {this.state.name}
            </div>
        )
    }
}

render(<Person />, document.body);

问题:在渲染 Person 组件的时候,如何将 this.state.name 解析成真实值 "youngwind" 呢?是用正则匹配替换吗? 答案:不需要处理,因为 babel 已经帮我们处理好了。 请看编译后的代码。

变量的解析 由图中我们可以看出:由于 this.state.name被当做函数参数传递给 h 函数,因此 h 函数在执行的时候会对其进行自动求值。也就是说,对于变量的解析,我们并不需要做任何特殊处理。

组件标签的解析

再看一个更复杂的例子。

import {h, render, Component} from '../../preact';

class Person extends Component {
    constructor() {
        super();
        this.state = {
            name: "youngwind",
            age: 25
        }
    }

    render() {
        return (
            <div>
                <Name name={this.state.name}/>
                <Age age={this.state.age}/>
            </div>
        )
    }
}

class Name extends Component {
    render(props) {
        return (
            <div>
                {props.name}
            </div>
        )
    }
}

class Age extends Component {
    render(props) {
        return (
            <div>
                {props.name}
            </div>
        )
    }
}

render(<Person />, document.body);

编译后的代码如下:

组件的解析 由图中我们可以发现:babel 进行 JSX 解析时,对普通标签和组件标签的处理是类似的,只不过有两点不同。

  1. 对于组件来说,h 函数的第一个参数不是标签字符串,而是组件类;
  2. 所谓传递给组件的 props,其实就相当于给普通标签定义 attributes。

组件的渲染与更新

有了上面的基础,我们来看看如何实现组件的渲染和更新,举个例子。

import {h, render, Component} from '../../preact';

class Person extends Component {
    constructor() {
        super();
        this.state = {
            name: "youngwind",
            age: 25
        }
    }

    change() {
        let {name, age} = this.state;
        this.setState({
            name: name + '啦',
            age: age + 1
        });
    }

    render() {
        return (
            <div>
                <button onclick={this.change.bind(this)}>改变</button>
                <Name name={this.state.name}/>
                <Age age={this.state.age}/>
            </div>
        )
    }
}

class Name extends Component {
    render(props) {
        return (
            <div>
                <label>姓名:</label>
                <span>{props.name}</span>
            </div>
        )
    }
}

class Age extends Component {
    render(props) {
        return (
            <div>
                <label>年龄:</label>
                <span>{props.age}</span>
            </div>
        )
    }
}

render(<Person />, document.body);

这段代码,我们期望的功能是:

  1. 通过 props 给 Name 和 Age 传递参数,初次渲染的时候分别显示 "youngwind" 和 "25";
  2. 点击“改变”按钮,调用 setState,触发 Name 和 Age 的重新渲染,显示 "youngwind啦" 和 "26"。

具体的实现逻辑较为繁复,难以用文字描述清楚,我画了个流程图,可以对比着我实现的代码看。 preact组件渲染与更新逻辑图

效果

最终实现的效果如下所示。(注意,效果图中控制台会输出一些生命周期的信息,这部分的代码我在 demo 中省略了,完整的代码请参考这里demo

后话

虽然在本文中已经实现了组件的更新,但是,并没有应用虚拟 DOM 的 diff 算法,之后有时间再去研究研究。

BetaSu commented 5 years ago

感谢博主分享的文章,真是简洁明了。我照着您的源码重新实现,在过程中遇到一个疑问,想请教博主一下: 在源码 91~96行, build函数中

// 判断新子节点是否已经存在原有 DOM 中
newChildren.forEach((newChild, i) => {
    if (children[i] !== newChild) {
        out.appendChild(newChild);
    }
});

这里为什么要使用appendChild,不应该直接用 newChildren 替换掉 children么。