作者:Siddharth
译者:前端小智
起源:dev
有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。
本文 GitHub https://github.com/qq449245884/xiaozhi 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。
你可能据说过Virtual DOM(以及Shadow DOM)。甚至可能应用过它(JSX基本上是VDOM的语法糖)。如果你想理解更多,那么就看看明天这篇文章。
什么是虚构DOM?
DOM操作很贵。做一次时,差别可能看起来很小(调配一个属性给一个对象之间大概0.4毫秒的差别),但它会随着工夫的推移而减少。
// 将属性赋值给对象1000次let obj = {};console.time("obj");for (let i = 0; i < 1000; i++) { obj[i] = i;}console.timeEnd("obj");// 操纵dom 1000次console.time("dom");for (let i = 0; i < 1000; i++) { document.querySelector(".some-element").innerHTML += i;}console.timeEnd("dom");
当我运行下面的代码片段时,我发现第一个循环破费了约3ms,而第二个循环破费了约41ms。
咱们举一个更实在的例子。
function generateList(list) { let ul = document.createElement('ul'); document.getElementByClassName('.fruits').appendChild(ul); list.forEach(function (item) { let li = document.createElement('li'); ul.appendChild(li); li.innerHTML += item; }); return ul;}document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Orange"])
到目前为止,所有都好。当初,如果数组扭转,咱们须要从新渲染,咱们这样做:
document.querySelector("ul.some-selector").innerHTML = generateList(["Banana", "Apple", "Mango"])
看看出了什么问题?
即便只须要扭转一个元素,咱们也会扭转整个元素,因为咱们很懒。
这就是为什么创立了虚构DOM的起因。那什么是虚构 Dom?
Virtual DOM是DOM作为对象的示意。 假如咱们有上面的 HTML:
<div class="contents"> <p>Text here</p> <p>Some other <b>Bold</b> content</p></div>
它能够写作以下VDOM对象:
let vdom = { tag: "div", props: { class: 'contents' }, children: [ { tag: "p", children: "Text here" }, { tag: "p", children: ["Some other ", { tag: "b", children: "Bold" }, " content"] } ]}
请留神,理论开发中可能存在更多属性,这是一个简化的版本。
VDOM是一个对象,带有:
- 一个名为tag(有时也称为type)的属性,它示意标签的名称
- 一个名为
props
的属性,蕴含所有 props - 如果内容只是文本,则为字符串
- 如果内容蕴含元素,则vdom数组
咱们这样应用 VDOM:
- 咱们扭转了vdom而不是dom
- 函数查看DOM和VDOM之间的所有差别,只更改变动的局部
- 扭转VDOM被标记为最新的扭转,这样咱们下次比拟VDOM时就能够节俭更多的工夫。
有什么益处?
晓得了什么是 VDOM,咱们来改良一下后面的 generateList
函数。
function generateList(list) { // VDOM 生成过程,待下补上}patch(oldUL, generateList(["Banana", "Apple", "Orange"]));
不要介意patch
函数,它的作用是就将更改的局部附加到DOM中。当前再扭转DOM时:
patch(oldUL, generateList(["Banana", "Apple", "Mango"]));
patch
函数发现只有第三个li
产生了变动,,而不是所有三个元素都产生了变动,所以只会操作第三个 li 元素。
构建 VDOM!
咱们须要做4件事:
- 创立一个虚构节点(vnode)
- 挂载 VDOM
- 卸载 VDOM
- Patch (比拟两个vnode,找出差别,而后挂载)
创立 vnode
function createVNode(tag, props = {}, children = []) { return { tag, props, children}}
在Vue(和许多其余中央)中,此函数称为 h
,hyperscript 的缩写。
挂载 VDOM
通过挂载,将vnode附加到任何容器,如#app
或任何其余应该挂载它的中央。
这个函数将递归遍历所有节点的子节点,并将它们挂载到各自的容器中。
留神,上面的所有代码都放在挂载函数中。
function mount(vnode, container) { ... }
创立DOM元素
const element = (vnode.element = document.createElement(vnode.tag))
你可能会想这个vnode.element
是什么。 它只是一个外部设置的属性,咱们能够依据它晓得哪个元素是vnode
的父元素。
从props
对象设置所有属性。咱们能够对它们进行循环
Object.entries(vnode.props || {}).forEach([key, value] => { element.setAttribute(key, value)})
挂载子元素,有两种状况须要解决:
- children 只是文本
- children 是 vnode 数组
if (typeof vnode.children === 'string') { element.textContent = vnode.children} else { vnode.children.forEach(child => { mount(child, element) // 递归挂载子节点 })}
最初,咱们必须将内容增加到DOM中:
container.appendChild(element)
最终的后果:
function mount(vnode, container) { const element = (vnode.element = document.createElement(vnode.tag)) Object.entries(vnode.props || {}).forEach([key, value] => { element.setAttribute(key, value) }) if (typeof vnode.children === 'string') { element.textContent = vnode.children } else { vnode.children.forEach(child => { mount(child, element) // Recursively mount the children }) } container.appendChild(element)}
卸载 vnode
卸载就像从DOM中删除一个元素一样简略:
function unmount(vnode) { vnode.element.parentNode.removeChild(vnode.element)}
patch vnode.
这是咱们必须编写的(相对而言)最简单的函数。要做的事件就是找出两个vnode之间的区别,只对更改局部进行 patch。
function patch(VNode1, VNode2) { // 指定父级元素 const element = (VNode2.element = VNode1.element); // 当初咱们要查看两个vnode之间的区别 // 如果节点具备不同的标记,则阐明整个内容曾经更改。 if (VNode1.tag !== VNode2.tag) { // 只需卸载旧节点并挂载新节点 mount(VNode2, element.parentNode) unmount(Vnode1) } else { // 节点具备雷同的标签 // 所以咱们要查看两个局部 // - Props // - Children // 这里不打算查看 Props,因为它会减少代码的复杂性,咱们先来看怎么查看 Children 就行啦 // 查看 Children // 如果新节点的 children 是字符串 if (typeof VNode2.children == "string") { // 如果两个孩子齐全不同 if (VNode2.children !== VNode1.children) { element.textContent = VNode2.children; } } else { // 如果新节点的 children 是一个数组 // - children 的长度是一样的 // - 旧节点比新节点有更多的子节点 // - 新节点比旧节点有更多的子节点 // 查看长度 const children1 = VNode1.children; const children2 = VNode2.children; const commonLen = Math.min(children1.length, children2.length) // 递归地调用所有公共子节点的patch for (let i = 0; i < commonLen; i++) { patch(children1[i], children2[i]) } // 如果新节点的children 比旧节点的少 if (children1.length > children2.length) { children1.slice(children2.length).forEach(child => { unmount(child) }) } // 如果新节点的children 比旧节点的多 if (children2.length > children1.length) { children2.slice(children1.length).forEach(child => { mount(child, element) }) } } }}
这是vdom实现的一个根本版本,不便咱们疾速把握这个概念。当然还有一些事件要做,包含查看 props 和一些性能方面的改良。
当初让咱们渲染一个vdom!
回到generateList
例子。对于咱们的vdom实现,咱们能够这样做
function generateList(list) { let children = list.map(child => createVNode("li", null, child)); return createVNode("ul", { class: 'fruits-ul' }, children)}mount(generateList(["apple", "banana", "orange"]), document.querySelector("#app")/* any selector */)
线上示例:https://codepen.io/SiddharthS...
~完,我是小智,SPA 走一波,下期见!
代码部署后可能存在的BUG没法实时晓得,预先为了解决这些BUG,花了大量的工夫进行log 调试,这边顺便给大家举荐一个好用的BUG监控工具 Fundebug。
原文:https://dev.to/siddharthshyni...
交换
有幻想,有干货,微信搜寻 【大迁世界】 关注这个在凌晨还在刷碗的刷碗智。
本文 GitHub https://github.com/qq44924588... 已收录,有一线大厂面试残缺考点、材料以及我的系列文章。