赞
踩
我们传统的前端开发中,我们是编写自己的HTML,最终被渲染到浏览器上的,那么它是什么样的过程呢?
目前框架都会引入虚拟DOM来对真实的DOM进行抽象,这样做有很多的好处:
首先是可以对真实的元素节点进行抽象,抽象成VNode(虚拟节点),这样方便后续对其进行各种操作:
其次是方便实现跨平台,包括你可以将VNode节点渲染成任意你想要的节点
事实上Vue的源码包含三大核心:
三个系统之间如何协同工作呢:
这里我们实现一个简洁版的Mini-Vue框架,该Vue包括三个模块:
渲染系统,该模块主要包含三个功能:
h函数的实现:直接返回一个VNode对象即可
// 定义 h 函数 /** * * @param {*} tag 元素名 * @param {*} props 属性 * @param {*} children 子元素 */ const h = (tag, props, children) => { // vnode -> js对象 -> {} return { tag, props, children, // vnode -> el -> 记录一份真实dom // el } }
mount函数的实现:
第一步:根据tag,创建HTML元素,并且存储 到vnode的el中;
第二步:处理props属性
第三步:处理子节点
// 挂载功能 /** * * @param {*} vnode 虚拟dom * @param {*} container 挂载的容器 */ const mount = (vnode, container) => { // vnode -> element // 1. 创建出真实的元素,并在vnode上保留一份 el const el = vnode.el = document.createElement(vnode.tag); // 2. 处理props if (vnode.props) { for (const key in vnode.props) { const value = vnode.props[key]; // 以on开头 做函数的事件监听 if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), value); } else { // 设置属性 el.setAttribute(key, value); } } } // 3. 处理children if (vnode.children) { if (typeof vnode.children === "string") { // 字符串 el.innerHTML = vnode.children; } else if (vnode.children instanceof Array) {// 数组进行类型检测 拿到的是object // 数组 vnode.children.forEach(childVnode => { mount(childVnode, el); }) } } if (!container) { // 没有容器 throw new Error('container is must be exist'); } // 将el 挂载到container容器上 else if (typeof container === "string") { // 是字符串,则传进来的是选择器 document.querySelector(container).appendChild(el); } else { // 是已经选中的元素 container.appendChild(el); } }
patch函数的实现,分为两种情况:
n1和n2是不同类型的节点:
n1和n2节点是相同的节点:
// patch 函数 /** * * @param {*} n1 vnode1 * @param {*} n2 vnode2 */ const patch = (n1, n2) => { // 先判断类型(元素)是否相同 if (n1.tag !== n2.tag) { // 类型不同 肯定不同了 // 移除vnode1,换成vnode2 通过父节点移除 const parent = n1.el.parentElement; parent.removeChild(n1.el); // 创建新的vnode2节点,并挂载到父节点上 mount(n2, parent); } else { // 类型相同 // 1. 取出元素(element)对象,并且在n2中也进行保存el const el = n2.el = n1.el; // 2. 处理props // 如果props为null 则默认为空对象 const oldProps = n1.props || {}; const newProps = n2.props || {}; // 2.1 获取所有的newProps 添加到el的属性上 for (const key in newProps) { // 看是否有相同的属性且值相同 const oldValue = oldProps[key]; const newValue = newProps[key]; if (oldValue !== newValue) { // 事件处理 on开头 if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), newValue); } else { // 不相同才需要进行设置 el.setAttribute(key, newValue); } } } // 2.2 删除旧的props for (const key in oldProps) { // 在新的属性里面不存在的旧属性都移除 if (!(key in newProps)) { // 移除事件 if (key.startsWith("on")) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]); } else { // 移除属性 el.removeAttribute(key); } } } // 3. 处理children const oldChildren = n1.children || []; const newChildren = n2.children || []; if (typeof newChildren === "string") { // 新子节点是字符串类型数据 且内容不同 直接完成内容替换 if (typeof oldChildren === "string") { if (newChildren !== oldChildren) // 有需要的话 可以自行加更多的边界判断 el.textContent = newChildren; } else { // oldChildren不是字符串类型 el.innerHTML = newChildren; } } // 不是字符串类型 则在这里就是数组类型 对象形式我们不考虑 else { if (typeof oldChildren === "string") { // string vs array el.innerHTML = ""; // 遍历newChildren 挂载 子节点 newChildren.forEach(child => { mount(child, el); }) } else { // array vs array 都是数组 // old: [v1,v2,v3,v5,v7] // new: [v3,v5,v6,v8,v9,v11] // diff 算法 进行比对(这里不考虑key) // 拿到新旧child数组较短的那个 const commonLength = Math.min(oldChildren.length, newChildren.length); // 1. 遍历 将共有长度的那些节点进行patch for (let i = 0; i < commonLength; i++) { // 循环进行patch操作 patch(oldChildren[i], newChildren[i]); } // 2. 旧child数组的长度更长,则删除多余的元素 if (oldChildren.length > commonLength) { oldChildren.slice(commonLength).forEach(child => { // 移除原来挂载的子元素 el.removeChild(child.el); }) } else if (newChildren > commonLength) { // 3. 新的child的数组长度更长 则做新增操作 // 截取数组后面多出来的部分进行遍历,挂载 newChildren.slice(commonLength).forEach(child => { // 挂载 mount(child, el); }) } } } } }
代码如下:
<!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> </head> <body> <div id="app"></div> <script src="./renderer.js"></script> <script> const vnode = h("div", { class: 'mao', name: '111' }, [ h("h2", {}, "你好!!!!"), h("h2", null, "你好!当前计数:100"), h("button", { onClick: function () { } }, "+1") ]); // 通过mount函数 将vnode 挂载到div#app上 mount(vnode, "#app") // 创建新的vnode 这个作为更新数据后的vnode const vnode2 = h("div", { class: 'codermao', data: "mao" }, [ h("h2", {}, "你好!!!!"), h("h2", { style: "color:red" }, "哈哈哈") ]); // 新的vnode和旧的vnode进行diff算法的比对,找出不同 // patch函数进行比对 setTimeout(() => { patch(vnode, vnode2); }, 3000); </script> </body> </html>
// 定义 h 函数 /** * * @param {*} tag 元素名 * @param {*} props 属性 * @param {*} children 子元素 */ const h = (tag, props, children) => { // vnode -> js对象 -> {} return { tag, props, children, // vnode -> el -> 记录一份真实dom // el } } // 挂载功能 /** * * @param {*} vnode 虚拟dom * @param {*} container 挂载的容器 */ const mount = (vnode, container) => { // vnode -> element // 1. 创建出真实的元素,并在vnode上保留一份 el const el = vnode.el = document.createElement(vnode.tag); // 2. 处理props if (vnode.props) { for (const key in vnode.props) { const value = vnode.props[key]; // 以on开头 做函数的事件监听 if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), value); } else { // 设置属性 el.setAttribute(key, value); } } } // 3. 处理children if (vnode.children) { if (typeof vnode.children === "string") { // 字符串 el.innerHTML = vnode.children; } else if (vnode.children instanceof Array) {// 数组进行类型检测 拿到的是object // 数组 vnode.children.forEach(childVnode => { mount(childVnode, el); }) } } if (!container) { // 没有容器 throw new Error('container is must be exist'); } // 将el 挂载到container容器上 else if (typeof container === "string") { // 是字符串,则传进来的是选择器 document.querySelector(container).appendChild(el); } else { // 是已经选中的元素 container.appendChild(el); } } // patch 函数 /** * * @param {*} n1 vnode1 * @param {*} n2 vnode2 */ const patch = (n1, n2) => { // 先判断类型(元素)是否相同 if (n1.tag !== n2.tag) { // 类型不同 肯定不同了 // 移除vnode1,换成vnode2 通过父节点移除 const parent = n1.el.parentElement; parent.removeChild(n1.el); // 创建新的vnode2节点,并挂载到父节点上 mount(n2, parent); } else { // 类型相同 // 1. 取出元素(element)对象,并且在n2中也进行保存el const el = n2.el = n1.el; // 2. 处理props // 如果props为null 则默认为空对象 const oldProps = n1.props || {}; const newProps = n2.props || {}; // 2.1 获取所有的newProps 添加到el的属性上 for (const key in newProps) { // 看是否有相同的属性且值相同 const oldValue = oldProps[key]; const newValue = newProps[key]; if (oldValue !== newValue) { // 事件处理 on开头 if (key.startsWith("on")) { el.addEventListener(key.slice(2).toLowerCase(), newValue); } else { // 不相同才需要进行设置 el.setAttribute(key, newValue); } } } // 2.2 删除旧的props for (const key in oldProps) { // 在新的属性里面不存在的旧属性都移除 if (!(key in newProps)) { // 移除事件 if (key.startsWith("on")) { el.removeEventListener(key.slice(2).toLowerCase(), oldProps[key]); } else { // 移除属性 el.removeAttribute(key); } } } // 3. 处理children const oldChildren = n1.children || []; const newChildren = n2.children || []; if (typeof newChildren === "string") { // 新子节点是字符串类型数据 且内容不同 直接完成内容替换 if (typeof oldChildren === "string") { if (newChildren !== oldChildren) // 有需要的话 可以自行加更多的边界判断 el.textContent = newChildren; } else { // oldChildren不是字符串类型 el.innerHTML = newChildren; } } // 不是字符串类型 则在这里就是数组类型 对象形式我们不考虑 else { if (typeof oldChildren === "string") { // string vs array el.innerHTML = ""; // 遍历newChildren 挂载 子节点 newChildren.forEach(child => { mount(child, el); }) } else { // array vs array 都是数组 // old: [v1,v2,v3,v5,v7] // new: [v3,v5,v6,v8,v9,v11] // diff 算法 进行比对(这里不考虑key) // 拿到新旧child数组较短的那个 const commonLength = Math.min(oldChildren.length, newChildren.length); // 1. 遍历 将共有长度的那些节点进行patch for (let i = 0; i < commonLength; i++) { // 循环进行patch操作 patch(oldChildren[i], newChildren[i]); } // 2. 旧child数组的长度更长,则删除多余的元素 if (oldChildren.length > commonLength) { oldChildren.slice(commonLength).forEach(child => { // 移除原来挂载的子元素 el.removeChild(child.el); }) } else if (newChildren > commonLength) { // 3. 新的child的数组长度更长 则做新增操作 // 截取数组后面多出来的部分进行遍历,挂载 newChildren.slice(commonLength).forEach(child => { // 挂载 mount(child, el); }) } } } } }
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。