最近读了读preact源码,记录点笔记,这里采用例子的形式,把代码的执行过程带到源码里走一遍,顺便说明一些重要的点,建议对着preact源码看
虚拟结点是对真实DOM元素的一个js对象表示
,由h()建立html
h()方法在根据指定结点名称、属性、子节点来建立vnode以前,会对子节点进行处理,包括node
例如:react
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()就是react中的ReactDOM.render(vnode,parent,merge),将一个vnode转换成真实DOM,插入到parent中,只有一句话,重点在diff中git
return diff(merge, vnode, {}, false, parent, false);
diff主要作三件事github
重点看idiff数组
idiff(dom,vnode)处理vnode的三种状况dom
通常咱们写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> //} }
咱们以一个真实的组件的渲染过程来对照着走一下表示普通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)工做流程
child
,例如,key同样的,若是vchild没有key,就从children数组中找标签名同样的 接着看上面的例子
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> ]
存在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相关的方法主要有三个,依次调用关系
buildComponentFromVNode
setComponentProps
在setComponentProps(compInst)内部进行两件事
renderComponent
renderComponent内会作这些事:
当前组件表示的页面结构上的真实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文件,但一篇文章不能覆盖全部点,这里只是记录了一些主要的流程,最后放一张有毒的图