读 preact 源码

558 阅读4分钟

h.js

  • wtf-is-jsx

    • function render(vnode) {  
        	// Strings just convert to #text Nodes:
          if (vnode.split) return document.createTextNode(vnode);
      
          // create a DOM element with the nodeName of our VDOM element:
          let n = document.createElement(vnode.nodeName);
      
          // copy attributes onto the new node:
          let a = vnode.attributes || {};
          Object.keys(a).forEach( k => n.setAttribute(k, a[k]) );
      
          // render (build) and then append child nodes:
          (vnode.children || []).forEach( c => n.appendChild(render(c)) );
      
          return n;
      }
      
      var vnode = {
        	nodeName: "div",
        	attributes: {
          	"id": "fei"
        	},
        	children: ["Hello!"]
      }
      
      render(vnode) =======> <div id="fei">Hello!</div>
      
  • Creates a VNode (virtual DOM element). A tree of VNodes can be used as a lightweight representation

    of the structure of a DOM tree. This structure can be realized by recursively comparing it against

    the current actual DOM structure, and applying only the differences.

  • h(nodeName, attributes)

    • 接受 nodeName 和 attributes,剩下参数将 push 到 stack 中

    • const stack = []
      for (i = arguments.length; i-- > 2; ) {
          stack.push(arguments[i]);
      }
      
    • nodeName: html 标签. 如: div , a , span …..

constants.js

  • // render modes
    
    // 不 render
    export const NO_RENDER = 0;
    // 同步 render
    export const SYNC_RENDER = 1;
    // 强制 render
    export const FORCE_RENDER = 2;
    // 异步 render
    export const ASYNC_RENDER = 3;
    
    
    export const ATTR_KEY = '__preactattr_';
    

component.js

  • export function Component(props, context) {
        // 脏数据,在调用 this.setState(), 标记 _dirty = true, 到每一个事件循环结束, 检查所有标记 _dirty 的 component 重新绘制。
    	this._dirty = true;
    	this.context = context;
    	this.props = props;
    	this.state = this.state || {};
    }
    
  • setState(state, cb) : 用来更新组件的 state —> enqueueRender

  • forceUpdate(cb): 立刻同步重新渲染组件 —> renderComponent

  • render: 返回组件的渲染内容的 vNode

dom / index.js

  • createNode: document.createElement(nodeName)
  • removeNode: parentNode.removeChild(node)
  • setAccessor 设置 node 属性
    • className、key、ref、style、dangerouslySetInnerHTML。。。。
    • onXxxx: node.addEventListener(name, eventProxy, useCapture) 钩子函数

vdom

index.js
  • isSameNodeType(node, vnode, hydrating)
  • isNamedNode(node, nodeName)
  • getNodeProps(vnode)
component.js
  • setComponentProps
  • renderComponent
    • component.shouldComponentUpdate
    • component.componentWillUpdate
    • component.render
    • component.componentDidUpdate
    • options.afterUpdate
    • component._renderCallbacks
    • flushMounts()
  • buildComponentFromVNode
    • Apply the Component referenced by a VNode to the DOM.
  • unmountComponent
    • Remove a component from the DOM and recycle it
diff.js
  • Apply differences in a given vnode (and it's deep children) to a real DOM Node.

  • flushMounts()

  • diff(dom, vnode, context, mountAll, parent, componentRoot)

  • // dom: vnode 所对应的之前未更新的真实 dom
    // vnode: 要渲染的虚拟 dom
    // parent: 就是你要将虚拟dom挂载的父节点
    export function diff(dom, vnode, context, mountAll, parent, componentRoot) {
    	// diffLevel having been 0 here indicates initial entry into the diff (not a subdiff)
    	if (!diffLevel++) {
    		// when first starting the diff, check if we're diffing an SVG or within an SVG
    		isSvgMode = parent!=null && parent.ownerSVGElement!==undefined;
    		// hydration is indicated by the existing element to be diffed not having a prop cache
    		hydrating = dom!=null && !(ATTR_KEY in dom);
    	}
    
    	let ret = idiff(dom, vnode, context, mountAll, componentRoot);
    	
        // append the element if its a new parent
    	if (parent && ret.parentNode!==parent) parent.appendChild(ret);
    	
        // diffLevel being reduced to 0 means we're exiting the diff
    	if (!--diffLevel) {
    		hydrating = false;
            // invoke queued componentDidMount lifecycle methods
    		if (!componentRoot) flushMounts();
    	}
    
    	return ret;
    }
    
  • idiff(dom, vnode, context, mountAll, componentRoot)

    • diff 算法的内部实现

    • function idiff(dom, vnode, context, mountAll, componentRoot) {
          // 空的node 渲染空的文本节点
      	if (vnode==null || typeof vnode==='boolean') vnode = '';
          
      	// VNode 为 String、Number 等简单类型类型节点
          if(dom === 'text') {
              // dom 为 text, update if it's already a Text node:
              dom.nodeValue = vnode;
          } else {
                  // dom 不是 text, replace it with one and recycle the old Element
            	document.createTextNode(vnode);
      	    dom.parentNode.replaceChild(out, dom);
          	recollectNodeTree(dom, true);   
          }
          
          // VNode 为 component
          // dom 不存在或者类型错误
          // dom 和 vnode name 不同
          if (!dom || !isNamedNode(dom, vnodeName)) {
      		out = createNode(vnodeName, isSvgMode);
      
      		if (dom) {
      			// move children into the replacement node
      			while (dom.firstChild) out.appendChild(dom.firstChild);
      
      			// if the previous Element was mounted into the DOM, replace it inline
      			if (dom.parentNode) dom.parentNode.replaceChild(out, dom);
      
      			// recycle the old element (skips non-Element node types)
      			recollectNodeTree(dom, true);
      		}
      	}
          
          // dom 存在,并且和 vnode name 相同
       	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;
      	}   
          ...
          // vchildren 只有一个直接赋值
          if(vchildren.length===1) fc.nodeValue = vchildren[0]
          // else 
          innerDiffNode()
      }
      
      // 重点,多看几遍这
      function innerDiffNode(dom, vchildren, context, mountAll, isHydrating) {
      	let originalChildren = dom.childNodes,
      		children = [],
      		keyed = {},
      		keyedLen = 0,
      		min = 0,
      		len = originalChildren.length,
      		childrenLen = 0,
      		vlen = vchildren ? vchildren.length : 0,
      		j, c, f, vchild, child;
      
      	// 创建一个包含 key 的子元素和一个不包含有子元素的 Map
          /* 
      	   1. 遍历父 dom 节点,如果是 preact 所渲染并且有 key,keyed[key] = child;
      	   2. isHydrating 为 true 时表示的是 dom 元素不是 Preact 创建的 child.nodeValue.trim()
             3. dom 由 preact 创建children[childrenLen++] = child
          */
      	if (len!==0) {
      		for (let i=0; i<len; i++) {
      			let child = originalChildren[i],
      				props = child[ATTR_KEY],
      				key = vlen && props ? child._component ? child._component.__key : props.key : null;
      			if (key!=null) {
      				keyedLen++;
      				keyed[key] = child;
      			}
      			else if (props || (child.splitText!==undefined ? (isHydrating ? child.nodeValue.trim() : true) : isHydrating)) {
      				children[childrenLen++] = child;
      			}
      		}
      	}
          
      	// 遍历 vnode
      	if (vlen!==0) {
      		for (let i=0; i<vlen; i++) {
      			vchild = vchildren[i];
      			child = null;
      
      			// 通过 key 去寻找节点
      			let key = vchild.key;
                 	// 有 key
      			if (key!=null) {
      				if (keyedLen && keyed[key]!==undefined) {
                          // 在 keyed 中查找对应的 dom元素,并在keyed将该元素删除
      					child = keyed[key];
      					keyed[key] = undefined;
      					keyedLen--;
      				}
      			}
      			// 从现有的子节点中找出相同类型的节点
      			else if (!child && min<childrenLen) {
      				for (j=min; j<childrenLen; j++) {
                          // isSameNodeType 查找是否和该元素类型相同的节点
      					if (children[j]!==undefined && isSameNodeType(c = children[j], vchild, isHydrating)) {
                              // 有相同类型的节点,则在 children 中删除
      						child = c;
      						children[j] = undefined;
                              // 缩小排查范围
      						if (j===childrenLen-1) childrenLen--;
      						if (j===min) min++;
      						break;
      					}
      				}
      			}
      			
                  // 递归 idiff
      			// morph the matched/found/created DOM child to match vchild (deep)
      			child = idiff(child, vchild, context, mountAll);
      
      			f = originalChildren[i];
                  // 该 dom 与原始 dom 中对应位置的 dom
      			if (child && child!==dom && child!==f) {
      				if (f==null) {
                          // 添加到对应位置之前
      					dom.appendChild(child);
      				}
      				else if (child===f.nextSibling) {
                          // 移除当前的真实 dom
      					removeNode(f);
      				}
      				else {
                          // 添加到父节点
      					dom.insertBefore(child, f);
      				}
      			}
      		}
      	}
      
    • // dom 事件触发
      function diffAttributes(dom, attrs, old) {
      	let name;
      
      	// = undefined,移除不在 vnode 中的属性,这样都行啊
      	for (name in old) {
      		if (!(attrs && attrs[name]!=null) && old[name]!=null) {
      			setAccessor(dom, name, old[name], old[name] = undefined, isSvgMode);
      		}
      	}
      
      	// add new & update changed attributes
      	for (name in attrs) {
      		if (name!=='children' && name!=='innerHTML' && (!(name in old) || attrs[name]!==(name==='value' || name==='checked' ? dom[name] : old[name]))) {
      			setAccessor(dom, name, old[name], old[name] = attrs[name], isSvgMode);
      		}
      	}
      
component-recycler.js
  • const components = {}

    • Retains a pool of Components for re-use, keyed on component name.
  • collectComponent(component)

    • Reclaim a component for later re-use by the recycler
    • component.constructor.name;
  • createComponent(Ctor, props, context)

    • 创建组件实例(PFC's and classful Components)

    • RFC(Pure Function Component) 纯函数组件

    • // 从创建的池中取出同类组件的实例,再取出该实例之前渲染的实例(nextBase)
      // 目的: 只渲染当前 dom,优化渲染
      if (list) {
          for (let i=list.length; i--; ) {
              if (list[i].constructor===Ctor) {
                  // 赋值到我们的新创建组件实例的 nextBase 属性上
                  inst.nextBase = list[i].nextBase;
                  list.splice(i, 1);
                  break;
              }
          }
      }