vuejs 设计与实现 - 渲染器 - 挂载与更新

渲染器的核心功能:挂载与更新

挂载子节点和元素的属性

挂载子节点 (vnode.children)

vnode.children可以是字符串类型的,也可以是数组类型的,如下:

const vnode ={
	type: 'div',
	children: [
		{
			type: 'p',
			children: 'hello'
		}
	]
}	

可以看到,vnode.children 是一个数组,它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构,即虚拟DOM 树。
为了完成子节点的渲染,我们需要修改 mountElement 函数,如下面的代码所示:

 function mountElement(vnode, container) {
      // 创建dom元素
      const el = createElement(vnode.type)
      console.log(vnode.children)


+      // 处理子元素
      if (typeof vnode.children === 'string') {
          setElementText(el, vnode.children)
      } else if (Array.isArray(vnode.children)) {
		 // 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它
          vnode.children.forEach(child => {
              patch(null, child, el)
          });
      }
	
	 insert(el, container)
}

在上面这段代码中,我们增加了新的判断分支。使用 Array.isArray 函数判断vnode.children 是否是数组,如果是 数组,则循环遍历它,并调 patch 函数挂载数组中的虚拟节点。在挂 载子节点时,需要注意以下两点:

  • 传递给 patch 函数的第一个参数是 null。因为是挂载阶段,没 有旧 vnode,所以只需要传递 null 即可。这样,当 patch 函数 执行时,就会递归地调用 mountElement 函数完成挂载。
  • 传递给 patch 函数的第三个参数是挂载点。由于我们正在挂载的 子元素是 div 标签的子节点,所以需要把刚刚创建的 div 元素作 为挂载点,这样才能保证这些子节点挂载到正确位置。

元素的属性(vnode.props)

我们知道,HTML 标签有很多属 性,其中有些属性是通用的,例如 id、class 等,而有些属性是特定 元素才有的,例如 form 元素的 action 属性。实际上,渲染一个元 素的属性比想象中要复杂,不过我们仍然秉承一切从简的原则,先来 看看最基本的属性处理。

为了描述元素的属性,我们需要为虚拟 DOM 定义新的 vnode.props 字段,如下面的代码所示:

const vnode ={
	type: 'div',
	
	// 使用 props 描述一个元素的属性
	props: {
		id: 'foo'
	},
	children: [
		{
			type: 'p',
			children: 'hello'
		}
	]
}

vnode.props 是一个对象,它的键代表元素的属性名称,它的值 代表对应属性的值。这样,我们就可以通过遍历 props 对象的方式, 把这些属性渲染到对应的元素上,如下面的代码所示:

function mountElement(vnode, container) {
      // 创建dom元素
      const el = createElement(vnode.type)
      console.log(vnode.children)


      // 处理子元素
      if (typeof vnode.children === 'string') {
          setElementText(el, vnode.children)
      } else if (Array.isArray(vnode.children)) {
		 // 如果 children 是数组,则遍历每一个子节点,并调用 patch 函数挂载它
          vnode.children.forEach(child => {
              patch(null, child, el)
          });
      }
+	 // 处理元素属性
	
	
	
	 insert(el, container)
}

HTML Attributes DOM Properties

元素的属性分为2种:

  • 1.HTML Attributes
  • 2.DOM Properties

如何区分:

key in el 返回值为true则是:DOM Properties,返回false则是HTML Attributes

如何争取的设置到元素上:

  • DOM Properties
el[key] = value
  • HTML Attributes
el.setAttribute(key, value)

正确的设置元素的属性

思路:

  • 1.我们知道元素的属性分为2种,而且这2种的设置方式不一样。因此,我们要特殊处理。

  • 2.处理特殊情况:例如button按钮,它的vnode节点如下:

const button = {
	type: 'button',
	props: {
		disabled: ''
	}
}

但是在解析的时候,会出现问题,用户的本意是“不禁用”按钮,但如果渲染器仍然使用 setAttribute 函数设置属性值,则会产生意外的效果,即按钮被禁 用了.那么应该怎么办呢?一个很自然的思路是,我们可以优先设置 DOM Properties,例如:

el.disabled = false
  • 3.处理特殊情况2: form表单的一些只读属性:
<form id="form1"></form>
<input form="form1" />

在这段代码中,我们为 标签设置了 form 属性 (HTML Attributes)。它对应的 DOM Properties 是 el.form,但 el.form 是只读的,因此我们只能够通过 setAttribute 函数来设 置它。

function shouldSetAsProps(el, key, value) {
	// 特殊处理
	if(key === 'form' && el.tagName === 'INPUT') return false
	//兜底
	return key in el
}

function mountElement(vnode, container) {
	const el = createElement(vnode.type)
	// 省略 children 的处理
	
	 // 处理元素的属性
+ 	 if (vnode.props) {
		for (const key in vnode.props) {
			const value = vnode.props[key]
			// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
			if (shouldSetAsProps(el, key, vaue)) {
				if (type === 'boolean' && value === '') {
					el[key] = true
				} else {
					el[key] = false
				}
			} else {
				// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
                // HTML Attributes
                el.setAttribute(key, vnode.props[key])
			}
		}
	}
}


class的处理

class有三种不同的vnode表示方式,

  • 方式1:
const vnode = {
	type: 'p',
	props: {
		class: { foor: true, bar: false }
	}
}
  • 方式2
const vnode = {
	type: 'p',
	props: {
		class: 'foo bar'
	}
}
  • 方式3
const vnode = {
	type: 'p',
	props: {
		class: [ 'foo bar', { bar: true }]
	}
}

因此我们需要一个normalizeClass函数来将不同类型的class值正常化为字符串。

const vnode = {
	type: 'p',
	props: {
		class: normalizeClass([ 'foo bar', { baz: true }])
	}
}

处理之后:

const vnode = {
	type: 'p',
	props: {
		class: 'foo bar baz'
	}
}

处理之后,设置class的方式也有三种1.className, 2.setAttribute, 3.classList 但是这三种设置的方式不同,性能也是不一样,经过调查发现className的性能是最优的,因此我们使用className设置元素的class。

function mountElement(vnode, container) {
	const el = createElement(vnode.type)
	// 省略 children 的处理
	
	 // 处理元素的属性
 	 if (vnode.props) {
		for (const key in vnode.props) {
			const value = vnode.props[key]
	+		if (key === 'class') {
	+			el.className = value || ''
	+		} else if (shouldSetAsProps(el, key, vaue)) {
				// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
				if (type === 'boolean' && value === '') {
					el[key] = true
				} else {
					el[key] = false
				}
			} else {
				// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
                // HTML Attributes
                el.setAttribute(key, vnode.props[key])
			}
		}
	}
}

简化上面的操作,我们可以把处理元素属性的逻辑放在一个函数(patchProps)里面:

function patchProps(el, key, prevValue, nextValue){
	// 对class 特殊处理
	if (key === 'class') {
		el.className = value || ''
	} else if (shouldSetAsProps(el, key, vaue)) {
		// 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties设置
		if (type === 'boolean' && value === '') {
			el[key] = true
		} else {
			el[key] = false
		}
	} else {
		// 如果要设置的属性没有对应的 DOM Properties,则使用setAttribute 函数设置属性
        // HTML Attributes
        el.setAttribute(key, vnode.props[key])
	}
}

在mountElement函数中调用

function mountElement(vnode, container) {
	const el = createElement(vnode.type)
	// 省略 children 的处理
	
	 // 处理元素的属性
 	 if (vnode.props) {
		for (const key in vnode.props) {
			const value = vnode.props[key]
+			patchProps(el, key, null, vnode.props[key])
		}
	}
}

卸载操作(unmount)

卸载操作的时机: 旧 vnode 存在,且新 vnode 不存在,说明是卸载(unmount)操作

我们不能简单地使用 innerHTML 来完成卸 载操作。正确的卸载方式是,根据 vnode 对象获取与其相关联的真实 DOM 元素,然后使用原生 DOM 操作方法将该 DOM 元素移除。

由于卸载操作是比较常见且基本的操作,所以我们应该将它封装 到 unmount 函数中,以便后续代码可以复用它,如下面的代码所示:

// 卸载
unmount(vnode) {
	// 获取 el 的父元素
     const parent = vnode.el.parentNode
	
	// 调用 removeChild 移除元素
     parent && parent.removeChild(vnode.el)
 }

简化render函数

function render(vnode, container) {
	if(vnode) {
		patch(container._vnode, vnode, container)
	} else {
		if(container._vnode) {
			// 调用 unmount 函数卸载 vnode
+			unmount(container._vnode)
		} 
	}
	container._vnode = vnode
}

将卸载操作封装到 unmount 中,还能够带来两点额外的好处:

  • 在 unmount 函数内,我们有机会调用绑定在 DOM 元素上的指令钩子函数,例如 beforeUnmount、unmounted 等。
  • 当 unmount 函数执行时,我们有机会检测虚拟节点 vnode 的类型。如果该虚拟节点描述的是组件,则我们有机会调用组件相的生命周期函数。

区分vnode类型

了解事件的处理

事件冒泡与更新时机问题

更新子节点

文本节点和注释节点

Fragment

你可能感兴趣的:(vue.js,前端,javascript)