preact源码剖析,有毒

近来读了读preact源码,记录点笔记,这里采纳例子的情势,把代码的实行历程带到源码里走一遍,趁便申明一些重要的点,发起对着preact源码看

vnode和h()

假造结点是对实在DOM元素的一个js对象示意,由h()建立

h()要领在依据指定结点称号、属性、子节点来建立vnode之前,会对子节点举行处置惩罚,包含

  1. 当前要建立的vnode不是组件,而是平常标签的话,文簿子节点是null,undefined,转成”,文簿子节点是number范例,转成字符串
  2. 一连相邻的两个子节点都是文本结点,合并成一个

比方:

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重要做三件事

  1. 挪用idff()天生实在DOM
  2. 挂载dom
  3. 在组件及一切子节点diff完成后,一致实行收集到的组件的componentDidMount()

重点看idiff

idiff(dom,vnode)处置惩罚vnode的三种状况

  1. vnode是一个js基础范例值,直接替代dom的文本或dom不存在,依据vnode建立新的文本返回
  2. vnode.nodeName是function 即当前vnode示意一个组件
  3. vnode.nodeName是string 即当前vnode示意一个对平常html元素的js示意

平常我们写react运用,最外层有一个相似<App>的组件,衬着时ReactDOM.render(<App/>>,root),这时候diff走的就是第二步,依据vnode.nodeName==='function'来构建组件,实行buildComponentFromVNode(),实例化组件,子组件等等

第三种状况平常出如今组件的定义是以平常标签包裹的,组件内部状况发作转变了或许初次实例化时,要render组件了,此时,要将当前组件现有的dom与实行compoent.render()要领获得的新的vnode举行Diff,来决议当前组件要怎样更新DOM

class Comp1 extends Component{

    render(){
        return <div>
                {
                    list.map(x=>{
                        return <p key={x.id}>{x.txt}</p>
                    })
                }
            <Comp2></Comp2>
        </div>
    }
    //而不是
    //render(){
    //    return <Comp2></Comp2>
    //}

}

平常标签元素及子节点的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 (
      <div>
        <button onClick={this.change.bind(this)}>change</button>
        {data.map((x, index) => {
          if (index == 2 && this.state.change) {
            return <h2 key={index}>{x}</h2>;
          }
          return <p key={index}>{x}</p>;
        })}
        {!change ? <h1>hello world</h1> : null}
      </div>
    );
  }
}

初次衬着

App组件初次挂载后的DOM构造大抵示意为

dom = {
       tageName:"DIV",
       childNodes:[
           <button>change</button>
           <p key="0">1</p>,
           <p key="1">2</p>,
           <p key="2">3</p>,
           <p key="3">4</p>,
           <h1>hello world</h1>
       ]
}

更新

点击一下按钮,触发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)事情流程

  1. 对dom结点下的子节点遍历,依据是不是有key,放入两个数组keyed和children(那些没有key放到这个里)
  2. 遍历vchildren,为当前的vchild找一个相对应的dom下的子节点child,比方,key一样的,假如vchild没有key,就从children数组中找标署名一样的
  3. child=idiff(child, vchild); 递归diff,依据vchild来获得处置惩罚后的child,将child运用到当前父元素dom下

接着看上面的例子

  1. dom子节点遍历 获得两个数组
keyed=[
    <p key="0">1</p>,
       <p key="1">2</p>,
       <p key="2">3</p>,
       <p key="3">4</p>
]
children=[
    <button>change</button>,
    <h1>hello world</h1>
]
  1. 迭代vnode的children数组

存在key相称的

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

存在标署名转变的

vchild={ nodeName:"h2", attributes:{key:"2"}, children:[33]},
child=keyed[2]=<p key="2">3</p>,

存在标署名相称的

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

然后对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=<p key="0">11</p>
//文本值更新了

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

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

至此,diff算是说了一半了,另一半是vnode示意一个组件的状况,举行组件衬着或更新diff

组件的衬着、diff与更新

和组件的衬着,diff相干的要领重要有三个,顺次挪用关联

buildComponentFromVNode

  1. 组件之前没有实例化过,实例化组件,为组件运用props,setComponentProps()
  2. 组件已实例化过,属于更新阶段,setComponentProps()

setComponentProps

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

  1. 依据当前组件实例是初次实例化照样更新属性来挪用组件的componentWillMount或许componentWillReceiveProps
  2. 推断是不是时强迫衬着,renderComponent()或许把组件入衬着行列,异步衬着

renderComponent

renderComponent内会做这些事:

  1. 推断组件是不是更新,更新的话实行componentWillUpdate(),
  2. 推断shouldComponentUpdate()的效果,决议是不是跳过实行组件的render要领
  3. 须要render,实行组件render(),返回一个vnode,diff当前组件示意的页面构造上的实在DOM和返回的这个vnode,运用更新.(像上面申明的谁人例子一样)

依旧从例子入手,假定如今有如许一个组件

class Welcom extends Component{

    render(props){
        return <p>{props.text}</p>
    }

}

class App extends Component {

    constructor(props){
        super(props) 
        this.state={
            text:"hello world"
        }
    }

    change(){
        this.setState({
            text:"now changed"
        })
    }

    render(props){

        return <div>
                <button onClick={this.change.bind(this)}>change</button>
                <h1>preact</h1>
                <Welcom text={this.state.text} />
            </div>

    }

}

render(<App></App>,root)

vnode={
    nodeName:App,
}

初次render

render(<App/>,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:[
        <button>change</button>
           <h1>preact</h1>
        <p>hello world</p>
       ]
}

然后为当前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文件,但一篇文章不能掩盖一切点,这里只是记录了一些重要的流程,末了放一张有毒的图

《preact源码剖析,有毒》

github

    原文作者:对对对就是这
    原文地址: https://segmentfault.com/a/1190000013728465
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞