补充概念 参考链接-浏览器的工作原理:新式网络浏览器幕后揭秘
Dom树 解析器的输出“解析树”是由 DOM 元素和属性节点构成的树结构。DOM 是文档对象模型 (Document Object Model) 的缩写。它是 HTML 文档的对象表示,同时也是外部内容(例如 JavaScript)与 HTML 元素之间的接口。 解析树的根节点是“Document”对象
比如这样的HTML,Dom和标记是一一对应的关系:
1 2 3 4 5 6 7 8 <html > <body > <p > Hello World </p > <div > <img src ="example.png" /> </div > </body > </html >
它的Dom树如下:
呈现引擎的工作流程 webkit主流程:
Mozilla 的 Gecko主流程:
Vue基本原理 Vue内部流程
Vue.js异步更新及nextTick https://blog.csdn.net/sinat_17775997/article/details/82183435
https://github.com/tuzilingdang/learnVue/blob/master/docs/Vue.js%E5%BC%82%E6%AD%A5%E6%9B%B4%E6%96%B0DOM%E7%AD%96%E7%95%A5%E5%8F%8AnextTick.MarkDown
VNode结点 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 export default class VNode { tag: string | void ; data: VNodeData | void ; children: ?Array <VNode>; text: string | void ; elm: Node | void ; ns: string | void ; context: Component | void ; functionalContext: Component | void ; key: string | number | void ; componentOptions: VNodeComponentOptions | void ; componentInstance: Component | void ; parent: VNode | void ; raw: boolean; isStatic: boolean; isRootInsert: boolean; isComment: boolean; isCloned: boolean; isOnce: boolean; constructor ( tag?: string, data?: VNodeData, children?: ?Array<VNode>, text?: string, elm?: Node, context?: Component, componentOptions?: VNodeComponentOptions ) { this .tag = tag this .data = data this .children = children this .text = text this .elm = elm this .ns = undefined this .context = context this .functionalContext = undefined this .key = data && data.key this .componentOptions = componentOptions this .componentInstance = undefined this .parent = undefined this .raw = false this .isStatic = false this .isRootInsert = true this .isComment = false this .isCloned = false this .isOnce = false } get child (): Component | void { return this .componentInstance } }
简单的Vnode树
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 { tag : 'div' data: { class: 'test' }, children: [ { tag : 'span' , data: { class: 'demo' } text: 'hello,VNode' } ] }
对应的HTML
1 2 3 <div class ="test" > <span class ="demo" > hello,VNode</span > </div >
Vue的Diff算法 1. 新老节点的比较 patch比较过程
相同节点 – sameVNode 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 function sameVnode (a, b ) { return ( a.key === b.key && a.tag === b.tag && a.isComment === b.isComment && isDef(a.data) === isDef(b.data) && sameInputType(a, b) ) }
2. 相同节点进行 patchVNode比较
如果新旧VNode都是静态的,同时它们的key相同(代表同一节点),并且新的VNode是clone或者是标记了once(标记v-once属性,只渲染一次),那么只需要替换elm以及componentInstance即可。
新老节点均有children子节点,则对子节点进行diff操作,调用updateChildren,这个updateChildren也是diff的核心。
如果老节点没有子节点而新节点存在子节点,先清空老节点DOM的文本内容,然后为当前DOM节点加入子节点。
当新节点没有子节点而老节点有子节点的时候,则移除该DOM节点的所有子节点。
当新老节点都无子节点的时候,只是文本的替换。
3. Diff的核心内容 —— updateChildren 循环 (1) 四个指针向内靠拢, 两两比对共4次
通过createKeyToOldIdx会得到一个oldKeyToIdx,里面存放了一个key为旧的VNode,value为对应index序列的哈希表。从这个哈希表中可以找到是否有与newStartVnode一致key旧的VNode节点,如果同时满足sameVnode,patchVnode的同时会将这个真实DOM(elmToMove)移动到oldStartVnode对应的真实DOM的前面
newStartVnode在旧的VNode节点找不到一致的key,或者是即便key相同却不是sameVnode,这个时候会调用createElm创建一个新的DOM节点
循环结束
小游戏的性能问题 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 left(matrix) { if (this .pos.y - 1 < 0 ) return false for (let i = this .pos.x; i < this .pos.x + this .shape.length; i++) { if (matrix[i][this .pos.y - 1 ] == 1 ) return false } this .pos.y--; for (let i = 0 ; i < this .shape.length; i++) { matrix[this .pos.x + i] && matrix[this .pos.x + i].splice(this .pos.y + this .shape[0 ].length, 1 , 0 ) } for (let i = 0 ; i < this .shape.length; i++) { for (let j = 0 ; j < this .shape[i].length; j++) { matrix[this .pos.x + i] && matrix[this .pos.x + i].splice(this .pos.y + j, 1 , this .shape[i][j]) } } return true }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 down(matrix, accRowsList, clearRows) { const shapeHeight = this .shape.length const shapeWidth = this .shape[0 ].length const shape = this .shape const posY = this .pos.y const matLength = matrix[0 ].length let shapeAcc = [] let crashType = '' for (let j = 0 ; j < shapeWidth; j++) { const attatchBottom = (this .pos.x + this .shape.length) >= matrix.length const blockCrash = matrix[this .pos.x + this .shape.length] && matrix[this .pos.x + this .shape.length][this .pos.y + j] && this .shape[shapeHeight - 1 ][j] if (attatchBottom || blockCrash) { for (let i = 0 ; i < shapeHeight; i++) { if (this .pos.x + i > -1 ) { let checkLine = matrix[this .pos.x + i] && matrix[this .pos.x + i].filter(value => { return value == 1 }) if (checkLine.length == matLength) clearRows.push(this .pos.x + i) } } crashType = blockCrash ? 'blockCrash' : 'attatchBottom' } } if (crashType) { for (let j = 0 ; j < shapeWidth; j++) { for (let i = 0 ; i < shapeHeight; i++) { if (!shapeAcc[j] && shape[i][j]) shapeAcc[j] = shapeHeight - i } accRowsList[posY + j] = crashType == 'blockCrash' ? matrix.length - this .pos.x - shapeHeight + shapeAcc[j] : accRowsList[posY + j] + shapeAcc[j] } return false } this .pos.x++; if (this .pos.x - 1 >= 0 ) { for (let i = 0 ; i < this .shape[0 ].length; i++) matrix[this .pos.x - 1 ].splice(this .pos.y + i, 1 , 0 ) } for (let i = 0 ; i < this .shape.length; i++) { for (let j = 0 ; j < this .shape[i].length; j++) { const newVal = i == this .shape.length - 1 ? this .shape[i][j] || matrix[this .pos.x + i][this .pos.y + j] : this .shape[i][j] matrix[this .pos.x + i] && matrix[this .pos.x + i].splice(this .pos.y + j, 1 , newVal) } } return true }
1 2 3 4 5 6 <div class ="screen-grid-area" :class ="!isGameOn ? 'hidden': ''" > <div class ="square" :class ="!gameOver && matrix[parseInt((n-1)/columnNum)][(n-1)%columnNum ] ? 'black':''" v-for ="n in columnNum*rowNum" :id ="`${parseInt((n-1)/columnNum)}-${(n-1)%columnNum }`" v-bind:key ="n" > <div class ="square-inner" > </div > </div > </div >
结论 每次掉落都会触发一次diff, 每秒 diff 的最大复杂度 O(12 * 22 ), 一个周期 22 * O(12 * 22 )
合理划分组件后: 一个周期触发一次diff O(12 * 22 )