preact源码分析,有毒
最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看 vnode和h()虚拟结点是对真实DOM元素的 h()方法在根据指定结点名称、属性、子节点来创建vnode之前,会对子节点进行处理,包括
例如: h('div',{ id: 'foo',name : 'bar' },[ h('p',null,'test1'),'hello',null 'world',h('p','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,false); diffdiff主要做三件事
重点看idiff idiff(dom,vnode)处理vnode的三种情况
一般我们写react应用,最外层有一个类似<App>的组件,渲染时 第三种情况一般出现在 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我们以一个真实的组件的渲染过程来对照着走一下 假设现在有这样一个组件 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]},attributes:{key:"1"},children:[22]},{ nodeName:"h2",attributes:{key:"2"},children:[33]},attributes:{key:"3"},children:[44]},] } //少了最后的h1元素,第三个p元素变成了h2 然后在renderComponent方法内diff上面的dom和vnode 首先dom和vnode标签名是一样的,都是div(如果不一样,要通过vnode.nodeName来创建一个新元素,并把dom子节点复制到这个新元素下),并且vnode有多个children,所以直接进入innerDiffNode(dom,vnode.children)函数 innerDiffNode(dom,vchildren)工作流程
接着看上面的例子
keyed=[ <p key="0">1</p>,<p key="3">4</p> ] children=[ <button>change</button>,<h1>hello world</h1> ]
存在key相等的 vchild={ nodeName:"p",child=keyed[0]=<p key="0">1</p> 存在标签名改变的 vchild={ nodeName:"h2",child=keyed[2]=<p key="2">3</p>, 存在标签名相等的 vchild={ nodeName:"button",child=<button>change</button>, 然后对vchild和child进行diff child=idff(child,vchild) 看一组子元素的更新 看上面那组 但这里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=<p key="0">11</p> //文本值更新了 然后将这个child放入当前dom下的合适位置,一个子元素的更新就完成了 如果vchild.children数组有多个元素,又会进行vchild的子元素的迭代diff 至此,diff算是说了一半了,另一半是vnode表示一个组件的情况,进行组件渲染或更新diff 组件的渲染、diff与更新和组件的渲染,diff相关的方法主要有三个,依次调用关系 buildComponentFromVNode
setComponentProps 在setComponentProps(compInst)内部进行两件事
renderComponent renderComponent内会做这些事:
依然从例子入手,假设现在有这样一个组件 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( 程序首次执行,页面还没有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,SYNC_RENDER,context,mountAll); dom = c.base; 在setComponentProps中,执行component.componentWillMount(),组件入异步渲染队列,在一段时间后,组件渲染,执行 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属性,所以此时实例属于更新阶段 这时候根据新的props,state rendered = component.render(props,context); rendered={ nodeName:"div",attributes:{ text:'now changed' //这里变化 } } ] } 然后,像第一次render一样, 总结preact src下只有15个js文件,但一篇文章不能覆盖所有点,这里只是记录了一些主要的流程,最后放一张有毒的图
github (编辑:李大同) 【声明】本站内容均来自网络,其相关言论仅代表作者个人观点,不代表本站立场。若无意侵犯到您的权利,请及时与联系站长删除相关内容! |