最近在看《vue.js 设计与实现》,看到了虚拟 DOM 这里,做了个笔记
1、VNode虚拟DOM节点
-
虚拟DOM节点如下:
const vnode = { tag: 'h2', props: { class: 'active', data: 'text', onClick: () => alert('Hello Render!') }, children: 'Click Me!' }
tag
:用来描述标签名称,所以tag: 'h2'
描述的就是一个<h2>
标签。props
:是一个对象,用来描述<h2>
标签的类名、属性、事件等内容。可以看到,我给h2
绑定一个active
类名,一个text
自定义属性,一个click
点击事件。children
:用来描述标签的子节点,在上面的代码中,children
是一个字符串值,意思是h2
标签有一个文本子节点:<h2>Click Me!</h2>
2、render工作原理
- 第一步: 创建元素,把
vnode.tag
作为标签名称来创建DOM元素。 - 第二步: 为元素添加属性和事件,遍历
vnode.props
对象。如果key是class
,说明它是一个类名,将其直接绑定给tag元素;如果key以on
字符串开头,说明它是一个事件,把字符on
截取掉后再调用toLowerCase
函数将事件名称小写化,最终得到合法的事件名称,例如onClick
会变成click
,最后调用addEventListener
绑定事件处理函数。 - 第三步: 处理
children
,如果children
是一个数组,就递归地调用render
继续渲染。注意,此时我们要把刚刚创建的元素作为挂载节点(父节点);如果children
是字符串,则使用createTextNode
函数创建一个文本节点,并将其添加到新的创建的元素内。
3、代码实现
-
自己手写一个简陋的render渲染器,上代码:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .active { color: red; } </style> </head> <body> <!-- 根节点 --> <div id="app"></div> <script> // 获取根节点 const app = document.getElementById("app"); // 要进行的挂载的虚拟 dom 节点 const vnode = { // 节点类型 tag: 'h2', // 节点上面绑定的类型、属性、方法 props: { class: 'active', // 类名 data: 'text', // 自定义属性 onClick: () => alert('Hello Render!') // 方法 }, // 子节点 children: 'Click Me!' } // 封装的渲染器 render 函数 // vnode 是虚拟 dom 节点 // container 是要挂载节点的元素 const render = (vnode, container) => { // 根据 vnode.tag 创建对应的 dom 节点 const el = document.createElement(vnode.tag); // 遍历 vnode.props,给节点绑定类名、属性、事件等 for (const key in vnode.props) { if (key === 'class') { // 如果是类名,则给元素添加类名 el.className += vnode.props[key] } else if (/^on/.test(key)) { // 如果属性是以 on 开头的,那么就绑定对应的事件 el.addEventListener( key.substr(2).toLowerCase(), // 改变事件类型:例如 onClick 变为 click vnode.props[key] // 绑定对应的事件 ) } else { // 如果是自定义属性,给元素绑定对应的属性 el.setAttribute(key, vnode.props[key]) } } // 处理子节点 if (typeof vnode.children === 'string') { // 如果子节点是 string 类型,那么就是本文节点 el.appendChild(document.createTextNode(vnode.children)); } else if (Array.isArray(vnode.children)) { // 如果子节点是 array 类型,那么继续渲染子节点 vnode.children.forEach(child => render(child, el)); } // 将渲染的 vnode 挂载在根节点上 app.appendChild(el); } // 调用 render 函数 render(vnode, app) </script> </body> </html>
-
看一下页面效果:
4、render拓展
在上面进行VNode渲染的过程,我只是讨论了当VNode是一个虚拟DOM节点的情况,那么接下来我将会讨论组件component的渲染,而component组件应该有两种情况:
- component是一个函数;
- component是一个对象;
-
首先直接来看代码的实现:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Document</title> <style> .active1 { color: red; } .active2 { color: blue; } .active3 { color: orange; } </style> </head> <body> <!-- 根节点 --> <div id="app"></div> <script> // 获取根节点 const app = document.getElementById("app"); // 要进行绑定的 component 组件 —— 返回函数 const component1 = function () { return { // 节点类型 tag: 'h2', // 节点上面绑定的类型、属性、方法 props: { class: 'active2', // 类名 data: 'component1', // 自定义属性 onClick: () => alert('Hello Component1!') // 方法 }, // 子节点 children: 'Function Component!' } } // 要进行绑定的 component 组件 —— 返回对象 const component2 = { // 该组件对象的 render 属性 render() { return { // 节点类型 tag: 'h2', // 节点上面绑定的类型、属性、方法 props: { class: 'active3', // 类名 data: 'component2', // 自定义属性 onClick: () => alert('Hello Component2!') // 方法 }, // 子节点 children: 'Object Component!' } } } // 要进行的挂载的虚拟 dom 节点(标签元素) const vnode1 = { // 节点类型 tag: 'h2', // 节点上面绑定的类型、属性、方法 props: { class: 'active1', // 类名 data: 'vnode', // 自定义属性 onClick: () => alert('Hello Render!') // 方法 }, // 子节点 children: 'Click Me!' } // 要进行的挂载的虚拟 dom 节点(组件1) const vnode2 = { tag: component1 } // 要进行的挂载的虚拟 dom 节点(组件2) const vnode3 = { tag: component2 } // 挂载标签元素的方法 const momentElement = (vnode, container) => { // 根据 vnode.tag 创建对应的 dom 节点 const el = document.createElement(vnode.tag) // 遍历 vnode.props,给节点绑定类名、属性、事件等 for (const key in vnode.props) { if (key === 'class') { // 如果是类名,则给元素添加类名 el.className += vnode.props[key] } else if (/^on/.test(key)) { // 如果属性是以 on 开头的,那么就绑定对应的事件 el.addEventListener( key.substr(2).toLowerCase(), // 改变事件类型:例如 onClick 变为 click vnode.props[key] // 绑定对应的事件 ) } else { // 如果是自定义属性,给元素绑定对应的属性 el.setAttribute(key, vnode.props[key]) } } // 处理子节点 if (typeof vnode.children === 'string') { // 如果子节点是 string 类型,那么就是本文节点 el.appendChild(document.createTextNode(vnode.children)) } else if (Array.isArray(vnode.children)) { // 如果子节点是 array 类型,那么继续渲染子节点 vnode.children.forEach(child => render(child, el)) } // 将渲染的 vnode 挂载在根节点上 app.appendChild(el); } // 挂载组件的函数 const momentComponent = (vnode, container) => { // 如果组件返回的是函数:调用组件函数,获取组件要渲染的内容(虚拟 DOM) // 如果组件返回的是对象:vnode.tag 是组件对象,调用它的 render 函数得到组件要渲染的内容(虚拟 DOM) const componentTree = typeof vnode.tag === 'function' ? vnode.tag() : vnode.tag.render() // 递归地调用 render 渲染 componentTree render(componentTree, container) } // 封装的渲染器 render 函数 // vnode 是虚拟 dom 节点 // container 是要挂载节点的元素 const render = (vnode, container) => { if (typeof vnode.tag === 'string') { // 如果 vnode.tag 是 string 类型,则表示该 vnode 是标签元素,调用挂载标签元素的函数 momentElement(vnode, container) } else if (typeof vnode.tag === 'object' || typeof vnode.tag === 'function') { // 如果 vnode.tag 是 object 类型,则表示该 vnode 是组件,调用挂载组件的函数 momentComponent(vnode, container) } } // 调用 render 函数 render(vnode1, app) render(vnode2, app) render(vnode3, app) </script> </body> </html>
-
看一下页面效果:
-
事项说明:
- 组件就是一组DOM元素的封装
- 代码中我的注释写的都挺清楚了,一定要注意component组件的类型分为Object和Function两种类型,这两种不同情况的返回值都是不一样的,一定要注意返回值,不要搞混。