真实dom
浏览器渲染引擎工作流程:
创建DOM树——创建StyleRules——创建Render树——布局Layout——绘制Painting
详细点就是:
第一步,用HTML分析器,分析HTML元素,构建一颗DOM树(标记化和树构建)。
第二步,用CSS分析器,分析CSS文件和元素上的inline样式,生成页面的样式表。
第三步,将DOM树和样式表,关联起来,构建一颗Render树(这一过程又称为Attachment)。每个DOM节点都有attach方法,接受样式信息,返回一个render对象(又名renderer)。这些render对象最终会被构建成一颗Render树。
第四步,有了Render树,浏览器开始布局,为每个Render树上的节点确定一个在显示屏上出现的精确坐标。
第五步,Render树和节点显示坐标都有了,就调用每个节点paint方法,把它们绘制出来。
注意: dom分为可视节点(div等)与非可视节点(script等)
render 树就是根据 可视化节点 和 css 样式表 结合诞生出来的树
display: none 的元素会出现在 DOM树 中,但不会出现在 render 树中;
构建DOM数是一个渐进过程,为达到更好用户体验,渲染引擎会尽快将内容显示在屏幕上。它不必等到整个HTML文档解析完毕之后才开始构建render树和布局。
Render树是DOM树和CSSOM树构建的,这三个过程在实际进行的时候又不是完全独立,而是会有交叉。会造成一边加载,一遍解析,一遍渲染的工作现象。
CSS的解析
我们知道render树是dom树关联cssom树构建的,因此需要根据选择器提供的信息对dom树进行遍历,才能将样式成功附着到对应的dom元素上。
.main/ \.div1 .div2/ \ / \h1 .content .div21 a/ /p p//我们定义一个这样的css.main .desc p {};//从左往右 //.mian .div1 h1 回溯 你需要像这样两次//从右往左//先找出p的所有节点//向上遍历 p -> .content -> .div1 ->.main发现不对换一个p//p->.div21 -> .div2 -> .main//当dom树比较复杂的时候,可以发现从右到左解析能够有效减少回溯次数提升性能。//找出p的所有节点,远小于回溯的性能消耗
DOM树的解析
主要存在两种类型的解析-自上而下或自下而上。直观一点来说就是自上而下是先从高层级语法开始匹配,自下而上则是先开始匹配基础语法,只有在基础级语法验证成功后才开始过渡到高层级语法。
JS操作真实DOM
传统的开发模式,原生JS或JQ操作DOM时,浏览器会从构建DOM树开始从头到尾执行一遍流程。
在一次操作中,我需要更新10个DOM节点,浏览器收到第一个DOM请求后并不知道还有9次更新操作,因此会马上执行流程,最终执行10次。例如,第一次计算完,紧接着下一个DOM更新请求,这个节点的坐标值就变了,前一次计算为无用功。计算DOM节点坐标值等都是白白浪费的性能。
虚拟DOM
虚拟DOM就是为了解决浏览器性能问题而被设计出来的。如前,若一次操作中有10次更新DOM的动作,虚拟DOM不会立即操作DOM,而是将这10次更新的diff内容保存到本地一个JS对象中,最终将这个JS对象一次性attch到DOM树上,再进行后续操作,避免大量无谓的计算量。
所以,用JS对象模拟DOM节点的好处是,页面的更新可以先全部反映在JS对象(虚拟DOM)上,操作内存中的JS对象的速度显然要更快,等更新完成后,再将最终的JS对象映射成真实的DOM,交由浏览器去绘制。
js模拟虚拟DOM
const tree = Element('div', {id: 'vitual-container'},[Element('p', {}, ['virtual DOM']),Element('div', {}, ['before update']),Element('ul', {}, [Element('li', { class: 'item'}, ['item 1']),Element('li', { class: 'item'}, ['item 2']),Element('li', { class: 'item'}, ['item 3']),]),])root = tree.render()document.getElementById('app').appendChild(root)//tagName 节点名//props 节点属性//children 子节点//key 保证同一父元素的所有子元素有不同的key属性//count 子节点数function Element(tagName, props, children){if (!(this instanceof Element)) {return new Element(tagName, props, children)}this.tagName = tagNamethis.props = props || {}this.children = children || {}this.key = props ? props.key : undefined,let count = 0; this.children.forEach((child) => {if(child instanceof Element){count += child.count}count++})this.count = count}
关于key
使用v-for进行列表渲染的时候,如果不使用key属性,Vue会产生警告
1.列表渲染时使用key属性
当 Vue.js 用v-for正在更新已渲染过的元素列表时,它默认用“就地复用”策略。如果数据项的顺序被改变,Vue 将不会移动 DOM
元素来匹配数据项的顺序, 而是简单复用此处每个元素,并且确保它在特定索引下显示已被渲染过的每个元素
官方文档中说:
我这里用index变量,根据列表渲染的规则,它实际上对应了数组中每个元素的索引,这样做的好处是它可以使得每个元素的key值都不同,这是很重要的,如果我们要利用key属性的优点,必须保证同一父元素的所有子元素有不同的key属性。
在有了key属性之后,Vue会记住元素们的顺序,并根据这个顺序在适当的位置插入/删除元素来完成更新,这种方法比没有key属性时的就地复用策略效率更高。
总体来说,当使用列表渲染时,永远添加key属性,这样可以提高列表渲染的效率,提高了页面的性能。
2.key属性强制替换元素
key属性还有另外一种使用方法,即强制替换元素,从而可以触发组件的生命周期钩子或者触发过渡。因为当key改变时,Vue认为一个新的元素产生了,从而会新插入一个元素来替换掉原有的元素。
那么当text改变时,Vue会复用元素,只改变元素的内容,而不会有新的元素被添加进来,也不会有旧的元素被删除。
key属性被用在组件上时,当key改变时会引起新组件的创建和原有组件的删除,此时组件的生命周期钩子就会被触发。
映射成真实DOM
//通过Element解析js创建的虚拟dom//tagName 节点名//props 节点属性//children 子节点//key 保证同一父元素的所有子元素有不同的key属性//count 子节点数function Element(tagName, props, children){if (!(this instanceof Element)) {return new Element(tagName, props, children)}this.tagName = tagNamethis.props = props || {}this.children = children || {}this.key = props ? props.key : undefinedlet count = 0this.children.forEach((child) => {if(child instanceof Element){count += child.count}count++})this.count = count}//通过render映射成真实domElement.prototype.render = function(){//创建节点const el = document.createElement(this.tagName)const props = this.props//循环添加属性for (let proName in props) {setAttr(el, proName, props[proName])}//遍历子节点 对子节点执行renderthis.children.forEach((child) => {const childEl = (child instanceof Element) ? child.render() : document.createTextNode(child);el.appendChild(childEl)})return el}//属性赋值函数function setAttr(el, proName, value){el.setAttribute(proName, value)}const tree = Element('div', {id: 'vitual-container'},[Element('p', {}, ['virtual DOM']),Element('div', {}, ['before update']),Element('ul', {}, [Element('li', { class: 'item'}, ['item 1']),Element('li', { class: 'item'}, ['item 2']),Element('li', { class: 'item'}, ['item 3']),]),]) const root = tree.render()document.getElementById('app').appendChild(root)
diff算法
新旧两棵树进行一个深度的遍历,每个节点都会有一个标记。每遍历到一个节点就把该节点和新的树进行对比,如果有差异就记录到一个对象中。
const tree = Element('div', {id: 'vitual-container'},[Element('p', {}, ['virtual DOM']),Element('div', {}, ['before update']),Element('ul', {}, [Element('li', { class: 'item'}, ['item 1']),Element('li', { class: 'item'}, ['item 2']),Element('li', { class: 'item'}, ['item 3']),]),])const newTree = Element('div', {id: 'vitual-container'},[Element('h1', {}, ['virtual DOM']), //REPLACEElement('div', {}, ['after update']), //TEXTElement('ul', { class: 'ul'}, [ //PROPSElement('li', { class: 'item'}, ['item 1']),//Element('li', { class: 'item'}, ['item 2']), //REORDER removeElement('li', { class: 'item'}, ['item 3']),]),])
同层比较的四种情况
1.节点类型变化:REPLACE
直接将旧节点卸载并装载新节点,旧节点包括下面的子节点都将被卸载。
2、节点类型一样,属性或属性值变化:PROPS
此时不会触发节点卸载和装载,而是节点更新。
3.文本变化:TEXT
直接修改文字内容
4.移动/增加/删除 子节点:REORDER
为数组或枚举型元素增加上key后,它能够根据key,直接找到具体位置进行操作,效率比较高
最后返回一个patch对象用来应用到实际的DOM tree更新,它的结构是这样的:
// index记录是哪一层的改变,type表示是哪种变化,第二个属性对应着变化存储相应的内容
patches = {index:[{type: utils.REMOVE/utils.TEXT/utils.ATTRS/utils.REPLACE, index/content/attrs/node: }, ...], ...}
//查找不同属性方法function diffProps(oldNode, newNode){const oldProps = oldNode.propsconst newProps = newNode.propslet keyconst propsPatches = {}let isSame = true//找出不同的属性for (key in oldProps) {if (newProps[key] !== oldProps[key]) {isSame = falsepropsPatches[key] = newProps[key]}}//找出新增的属性for (key in newProps) {if (!oldProps.hasOwnProperty(key)) {isSame = falsepropsPatches[key] = newProps[key]}}//返回patch对象 不过我这跟官方有点区别return isSame ? null : propsPatches}
//根据Diff更新DOM//node 节点//walker 可查询变化的序数//patches 变化细节function dfsWalk(node, walker, patches){//查询变化类型const currentPatches = patches[walker.index]//对子节点执行dfsWalkconst len = node.childNode ? node.childNode.length : 0for (let i = 0; i < len; i++) {walker.index++dfsWalk(node.childNode[i], walker, patches)}//如果发生变化就执行更新渲染函数if (currentPatches) {applyPatches(node, currentPatches)}}//根据变化类型更新渲染函数function applyPatches(node, currentPatches){currentPatches.forEach((currentPatch) => {switch (currentPatch.type){//节点类型变化case REPLACE: {//判断新节点是何类型const newNode = (typeof currentPatch.node === 'string')? document.createTextNode(currentPatch.node): currentPatch.node.render()//新节点替换旧节点node.parentNode.replaceChild(newNode, node)break}//移动/增加/删除 子节点case REORDER: {reorderChildren(node, currentPatch.moves)break}//属性或属性值变化case PROPS: {setProps(node, currentPatch.props)break}//文本变化case TEXT: {if(node.textContent){node.textContent = currentPatch.content}else{node.nodeValue = currentPatch.content}break}default:throw new Error(`Unknown patch type ${currentPatch.type}`)}})}
我们会有两个虚拟DOM(js对象,new/old进行比较diff),用户交互我们操作数据变化new虚拟DOM,old虚拟DOM会映射成实际DOM(js对象生成的DOM文档)通过DOM fragment操作给浏览器渲染。当修改new虚拟DOM,会把newDOM和oldDOM通过diff算法比较,得出diff结果数据表(用4种变换情况表示)。再把diff结果表通过DOM fragment更新到浏览器DOM中。