vue虚拟dom原理与实现

真实dom

webkit引擎的处理流程

浏览器渲染引擎工作流程:
创建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 = tagName
		this.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 = tagName
		this.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
	}
	
	//通过render映射成真实dom
	Element.prototype.render = function(){
		//创建节点
		const el = document.createElement(this.tagName)
		const props = this.props

		//循环添加属性
		for (let proName in props) {
			setAttr(el, proName, props[proName])
		}
		
		//遍历子节点  对子节点执行render
		this.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']),					//REPLACE
		Element('div', {}, ['after update']),				//TEXT
		Element('ul', { class: 'ul'}, [						//PROPS
			Element('li', { class: 'item'}, ['item 1']),
			//Element('li', { class: 'item'}, ['item 2']),	//REORDER remove
			Element('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.props
		const newProps = newNode.props

		let key
		const propsPatches = {}
		let isSame = true

		//找出不同的属性
		for (key in oldProps) {
			if (newProps[key] !== oldProps[key]) {
				isSame = false
				propsPatches[key] = newProps[key]
			}
		}

		//找出新增的属性
		for (key in newProps) {
			if (!oldProps.hasOwnProperty(key)) {
				isSame = false
				propsPatches[key] = newProps[key]
				
			}
		}
		//返回patch对象  不过我这跟官方有点区别
		return isSame ? null : propsPatches
	}
//根据Diff更新DOM
	//node 节点
	//walker 可查询变化的序数
	//patches 变化细节
	function dfsWalk(node, walker, patches){
		//查询变化类型
		const currentPatches = patches[walker.index]
		//对子节点执行dfsWalk
		const len = node.childNode ? node.childNode.length : 0
		for (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中。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值