资讯专栏INFORMATION COLUMN

preact源码分析,有毒

zhangwang / 1300人阅读

摘要:最近读了读源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点建议对着源码看和虚拟结点是对真实元素的一个对象表示由创建方法在根据指定结点名称属性子节点来创建之前会对子节点进行处理,包括当前要创建的不是

最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看
vnode和h()

虚拟结点是对真实DOM元素的一个js对象表示,由h()创建

h()方法在根据指定结点名称、属性、子节点来创建vnode之前,会对子节点进行处理,包括

当前要创建的vnode不是组件,而是普通标签的话,文本子节点是null,undefined,转成"",文本子节点是number类型,转成字符串

连续相邻的两个子节点都是文本结点,合并成一个

例如:

h("div",{ id: "foo", name : "bar" },[
            h("p",null,"test1"),
            "hello",
            null
            "world", 
            h("p",null,"test2")
        ]
)

对应的vnode={

    nodeName:"div",
    attributes:{
        id:"foo",
        name:"bar"
    },
    [
        {
            nodeName:"p",
            children:["test1"]
        },
        "hello world",
        {
            nodeName:"p",
            children:["test2"]
        }
    ]

}
render()

render()就是react中的ReactDOM.render(vnode,parent,merge),将一个vnode转换成真实DOM,插入到parent中,只有一句话,重点在diff中

return diff(merge, vnode, {}, false, parent, false);
diff

diff主要做三件事

调用idff()生成真实DOM

挂载dom

在组件及所有子节点diff完成后,统一执行收集到的组件的componentDidMount()

重点看idiff

idiff(dom,vnode)处理vnode的三种情况

vnode是一个js基本类型值,直接替换dom的文本或dom不存在,根据vnode创建新的文本返回

vnode.nodeName是function 即当前vnode表示一个组件

vnode.nodeName是string 即当前vnode表示一个对普通html元素的js表示

一般我们写react应用,最外层有一个类似的组件,渲染时ReactDOM.render(>,root),这时候diff走的就是第二步,根据vnode.nodeName==="function"来构建组件,执行buildComponentFromVNode(),实例化组件,子组件等等

第三种情况一般出现在组件的定义是以普通标签包裹的,组件内部状态发生改变了或者初次实例化时,要render组件了,此时,要将当前组件现有的dom与执行compoent.render()方法得到的新的vnode进行Diff,来决定当前组件要怎么更新DOM

class Comp1 extends Component{

    render(){
        return 
{ list.map(x=>{ return

{x.txt}

}) }
} //而不是 //render(){ // return //} }
普通标签元素及子节点的diff

我们以一个真实的组件的渲染过程来对照着走一下表示普通dom及子节点的vnode和真实dom之间的diff过程

假设现在有这样一个组件

class App extends Component {

  constructor(props) {
    super(props);
    this.state = {
      change: false,
      data: [1, 2, 3, 4]
    };
  }

 change(){
    this.setState(preState => {
        return {
            change: !preState.change,
            data: [11, 22, 33, 44]
        };
    });
 }

  render(props) {
    const { data, change } = this.state;
    return (
      
{data.map((x, index) => { if (index == 2 && this.state.change) { return

{x}

; } return

{x}

; })} {!change ?

hello world

: null}
); } }
初次渲染

App组件初次挂载后的DOM结构大致表示为

dom = {
       tageName:"DIV",
       childNodes:[
           
           

1

,

2

,

3

,

4

,

hello world

] }
更新

点击一下按钮,触发setState,状态发生变化,App组件实例入渲染队列,一段时间后(异步的),渲染队列中的组件被渲染,实例.render执行,此时生成的vnode结构大致是

vnode= {
    nodeName:"div"
    children:[
        { nodeName:"button", children:["change"] },
        { nodeName:"p", attributes:{key:"0"}, children:[11]},
        { nodeName:"p", attributes:{key:"1"}, children:[22]},
         { nodeName:"h2", attributes:{key:"2"}, children:[33]},
        { nodeName:"p", attributes:{key:"3"}, children:[44]},
    ]
 }

//少了最后的h1元素,第三个p元素变成了h2

然后在renderComponent方法内diff上面的dom和vnode diff(dom,vnode),此时在diff内部调用的idff方法内,执行的就是上面说的第三种情况vnode.nodeType是普通标签,关于renderComponent后面介绍

首先dom和vnode标签名是一样的,都是div(如果不一样,要通过vnode.nodeName来创建一个新元素,并把dom子节点复制到这个新元素下),并且vnode有多个children,所以直接进入innerDiffNode(dom,vnode.children)函数

innerDiffNode(dom,vchildren)工作流程

对dom结点下的子节点遍历,根据是否有key,放入两个数组keyed和children(那些没有key放到这个里)

遍历vchildren,为当前的vchild找一个相对应的dom下的子节点child,例如,key一样的,如果vchild没有key,就从children数组中找标签名一样的

child=idiff(child, vchild); 递归diff,根据vchild来得到处理后的child,将child应用到当前父元素dom下

接着看上面的例子

dom子节点遍历 得到两个数组

keyed=[
    

1

,

2

,

3

,

4

] children=[ ,

hello world

]

迭代vnode的children数组

存在key相等的

vchild={ nodeName:"p", attributes:{key:"0"}, children:[11]},
child=keyed[0]=

1

存在标签名改变的

vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]},
child=keyed[2]=

3

,

存在标签名相等的

vchild={ nodeName:"button", children:["change"] },
child=,

然后对vchild和child进行diff

child=idff(child,vchild)

看一组子元素的更新

看上面那组存在keys相等的子元素的diff,vchild.nodeName=="p"是个普通标签,所以还是走的idff内的第三种情况。

但这里vchild只有一个后代元素,并且child只有一个文本结点,可以明确是文本替换的情况,源码中这样处理,而不是进入innerDiffNode,算是一点优化

let fc = out.firstChild,
        props = out[ATTR_KEY],
        vchildren = vnode.children;

    if (props == null) {
        props = out[ATTR_KEY] = {};
        for (let a = out.attributes, i = a.length; i--;) props[a[i].name] = a[i].value;
    }

    // Optimization: fast-path for elements containing a single TextNode:
    if (!hydrating && vchildren && vchildren.length === 1 && typeof vchildren[0] === "string" && fc != null && fc.splitText !== undefined && fc.nextSibling == null) {
        if (fc.nodeValue != vchildren[0]) {
            fc.nodeValue = vchildren[0];
        }
    }

所有执行child=idiff(child,vchild)

child=

11

//文本值更新了

然后将这个child放入当前dom下的合适位置,一个子元素的更新就完成了

如果vchild.children数组有多个元素,又会进行vchild的子元素的迭代diff

至此,diff算是说了一半了,另一半是vnode表示一个组件的情况,进行组件渲染或更新diff

组件的渲染、diff与更新

和组件的渲染,diff相关的方法主要有三个,依次调用关系

buildComponentFromVNode

组件之前没有实例化过,实例化组件,为组件应用props,setComponentProps()

组件已经实例化过,属于更新阶段,setComponentProps()

setComponentProps

在setComponentProps(compInst)内部进行两件事

根据当前组件实例是首次实例化还是更新属性来调用组件的componentWillMount或者componentWillReceiveProps

判断是否时强制渲染,renderComponent()或者把组件入渲染队列,异步渲染

renderComponent

renderComponent内会做这些事:

判断组件是否更新,更新的话执行componentWillUpdate(),

判断shouldComponentUpdate()的结果,决定是否跳过执行组件的render方法

需要render,执行组件render(),返回一个vnode,diff当前组件表示的页面结构上的真实DOM和返回的这个vnode,应用更新.(像上面说明的那个例子一样)

依然从例子入手,假设现在有这样一个组件

class Welcom extends Component{

    render(props){
        return 

{props.text}

} } class App extends Component { constructor(props){ super(props) this.state={ text:"hello world" } } change(){ this.setState({ text:"now changed" }) } render(props){ return

preact

} } render(,root) vnode={ nodeName:App, }

首次render

render(,root)执行,进入diff(),vnode.nodeName==App,进入buildComponentFromVNode(null,vnode)

程序首次执行,页面还没有dom结构,所以此时buildComponentFromVNode第一个参数是null,进入实例化App组件阶段

c = createComponent(vnode.nodeName, props, context);
if (dom && !c.nextBase) {
    c.nextBase = dom;
    // passing dom/oldDom as nextBase will recycle it if unused, so bypass recycling on L229:
    oldDom = null;
}
setComponentProps(c, props, SYNC_RENDER, context, mountAll);
dom = c.base;

在setComponentProps中,执行component.componentWillMount(),组件入异步渲染队列,在一段时间后,组件渲染,执行
renderComponent()

rendered = component.render(props, state, context);

根据上面的定义,这里有

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:["change"]
        },
        {
            nodeName:"h1",
            children:["preact"]
        },{
            nodeName:Welcom,
            attributes:{
                text:"hello world"
            }
        }
    ]
}

nodeName是普通标签,所以执行

base = diff(null, rendered) 
//这里需要注意的是,renderd有一个组件child,所以在diff()-->idiff()[**走第三种情况**]---->innerDiffNode()中,对这个组件child进行idiff()时,因为是组件,所以走第二种情况,进入buildComponentFromVNode,相同的流程

component.base=base //这里的baes是vnode diff完成后生成的真实dom结构,组件实例上有个base属性,指向这个dom

base大体表示为

base={
    tageName:"DIV",
       childNodes:[
        
           

preact

hello world

] } 然后为当前dom元素添加一些组件的信息 base._component = component; base._componentConstructor = component.constructor;

至此,初始化的这次组件渲染就差不多了,buildComponentFromVNode返回dom,即实例化的App的c.base,在diff()中将dom插入页面

更新

然后现在点击按钮,setState()更新状态,setState源码中

let s = this.state;
if (!this.prevState) this.prevState = extend({}, s);
extend(s, typeof state==="function" ? state(s, this.props) : state);
/**
* _renderCallbacks保存回调列表
*/
if (callback) (this._renderCallbacks = (this._renderCallbacks || [])).push(callback);
enqueueRender(this);

组件入队列了,延迟后执行renderComponent()

这次,在renderComponent中,因为当前App的实例已经有一个base属性,所以此时实例属于更新阶段isUpdate = component.base =true,执行实例的componentWillUpdate()方法,如果实例的shouldComponentUpdate()返回true,实例进入render阶段。

这时候根据新的props,state

rendered = component.render(props, state, context);

rendered={
    nodeName:"div",
    children:[
        {
            nodeName:"button",
            children:["change"]
        },
        {
            nodeName:"h1",
            children:["preact"]
        },{
            nodeName:Welcom,
            attributes:{
                text:"now changed" //这里变化
            }
        }
    ]
}

然后,像第一次render一样,base = diff(cbase, rendered),但这时候,cbase是上一次render后产生的dom,即实例.base,然后页面引用更新后的新的dom.rendered的那个组件子元素(Welcom)同样执行一次更新过程,进入buildComponentFromVNode(),走一遍buildComponentFromVNode()-->setComponentProps()--->renderComponent()--->render()--->diff(),直到数据更新完毕

总结

preact src下只有15个js文件,但一篇文章不能覆盖所有点,这里只是记录了一些主要的流程,最后放一张有毒的图

github

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/93440.html

相关文章

  • 帮你读懂preact源码(一)

    摘要:是一个最小的库,但由于其对尺寸的追求,它的很多代码可读性比较差,市面上也很少有全面且详细介绍的文章,本篇文章希望能帮助你学习的源码。建议与源码一起阅读本文。 作为一名前端,我们需要深入学习react的运行机制,但是react源码量已经相当庞大,从学习的角度,性价比不高,所以学习一个react mini库是一个深入学习react的一个不错的方法。 preact是一个最小的react mi...

    XboxYan 评论0 收藏0
  • Preact了解一个类React的框架是怎么实现的(二): 元素diff

    摘要:本系列文章将重点分析类似于的这类框架是如何实现的,欢迎大家关注和讨论。作为一个极度精简的库,函数是属于本身的。 前言   首先欢迎大家关注我的掘金账号和Github博客,也算是对我的一点鼓励,毕竟写东西没法获得变现,能坚持下去也是靠的是自己的热情和大家的鼓励。  之前分享过几篇关于React的文章: React技术内幕: key带来了什么 React技术内幕: setState的秘密...

    张巨伟 评论0 收藏0
  • 帮你读懂preact源码(三)

    摘要:对回收的处理在中,回收调用了两个方法,节点的回收一般会调用,组件的回收会调用。个人理解从以上源码阅读中我们可以看到,最大的性能问题在于递归的,中的与也是为了缓解这个问题。为不同类型的更新分配优先级。 对回收的处理 在preact中,回收调用了两个方法,dom节点的回收一般会调用recollectNodeTree,组件的回收会调用unmountComponent。 preact复用dom...

    yuanxin 评论0 收藏0
  • 帮你读懂preact源码(二)

    摘要:最后删除新的树中不存在的节点。而中会记录对其做了相应的优化,节点的的情况下,不做移动操作。这种情况,在中得到了优化,通过四个指针,在每次循环中先处理特殊情况,并通过缩小指针范围,获得性能上的提升。 上篇文章已经介绍过idff的处理逻辑主要分为三块,处理textNode,element及component,但具体怎么处理component还没有详细介绍,接下来讲一下preact是如何处理...

    Warren 评论0 收藏0
  • Luy 1.0 :一个React-like轮子的诞生

    摘要:司徒正美的一款了不起的化方案,支持到。行代码内实现一个胡子大哈实现的作品其实就是的了源码学习个人文章源码学习个人文章源码学习个人文章源码学习个人文章这几片文章的作者都是司徒正美,全面的解析和官方的对比。 前言 在过去的一个多月中,为了能够更深入的学习,使用React,了解React内部算法,数据结构,我自己,从零开始写了一个玩具框架。 截止今日,终于可以发布第一个版本,因为就在昨天,我...

    codecook 评论0 收藏0

发表评论

0条评论

zhangwang

|高级讲师

TA的文章

阅读更多
最新活动
阅读需要支付1元查看
<