当前位置: 首页 > news >正文

企业网站建设预算表石景山网站建设有哪些公司

企业网站建设预算表,石景山网站建设有哪些公司,vip 支付wordpress,建网站企业讲解渲染器的核心功能#xff1a;挂载与更新。 1、挂载子节点和元素的属性 当 vnode.children 的值是字符串类型时#xff0c;会把它设置为元素的文本内容。一个元素除了具有文本子节点外#xff0c;还可以包含其他元素子节点#xff0c;并且子节点可以是很多个。为了描述…讲解渲染器的核心功能挂载与更新。 1、挂载子节点和元素的属性 当 vnode.children 的值是字符串类型时会把它设置为元素的文本内容。一个元素除了具有文本子节点外还可以包含其他元素子节点并且子节点可以是很多个。为了描述元素的子节点我们需要将 vnode.children 定义为数组 01 const vnode { 02 type: div, 03 children: [ 04 { 05 type: p, 06 children: hello 07 } 08 ] 09 }上面这段代码描述的是“一个 div 标签具有一个子节点且子节点是 p 标签”。可以看到vnode.children 是一个数组它的每一个元素都是一个独立的虚拟节点对象。这样就形成了树型结构即虚拟 DOM 树。 为了完成子节点的渲染我们需要修改 mountElement 函数如下面的代码所示 01 function mountElement(vnode, container) { 02 const el createElement(vnode.type) 03 if (typeof vnode.children string) { 04 setElementText(el, vnode.children) 05 } else if (Array.isArray(vnode.children)) { 06 // 如果 children 是数组则遍历每一个子节点并调用 patch 函数挂载它们 07 vnode.children.forEach(child { 08 patch(null, child, el) 09 }) 10 } 11 insert(el, container) 12 }在上面这段代码中我们增加了新的判断分支。使用Array.isArray 函数判断 vnode.children 是否是数组如果是数组则循环遍历它并调 patch 函数挂载数组中的虚拟节点。在挂载子节点时需要注意以下两点 传递给 patch 函数的第一个参数是 null。因为是挂载阶段没有旧 vnode所以只需要传递 null 即可。这样当 patch 函数执行时就会递归地调用 mountElement 函数完成挂载。传递给 patch 函数的第三个参数是挂载点。由于我们正在挂载的子元素是 div 标签的子节点所以需要把刚刚创建的 div 元素作为挂载点这样才能保证这些子节点挂载到正确位置。 完成了子节点的挂载后我们再来看看如何用 vnode 描述一个标签的属性以及如何渲染这些属性。我们知道HTML 标签有很多属性其中有些属性是通用的例如 id、class 等而有些属性是特定元素才有的例如 form 元素的 action 属性。实际上渲染一个元素的属性比想象中要复杂不过我们仍然秉承一切从简的原则先来看看最基本的属性处理。 为了描述元素的属性我们需要为虚拟 DOM 定义新的vnode.props 字段如下面的代码所示 01 const vnode { 02 type: div, 03 // 使用 props 描述一个元素的属性 04 props: { 05 id: foo 06 }, 07 children: [ 08 { 09 type: p, 10 children: hello 11 } 12 ] 13 }vnode.props 是一个对象它的键代表元素的属性名称它的值代表对应属性的值。这样我们就可以通过遍历 props 对象的方式把这些属性渲染到对应的元素上如下面的代码所示 01 function mountElement(vnode, container) { 02 const el createElement(vnode.type) 03 // 省略 children 的处理 04 05 // 如果 vnode.props 存在才处理它 06 if (vnode.props) { 07 // 遍历 vnode.props 08 for (const key in vnode.props) { 09 // 调用 setAttribute 将属性设置到元素上 10 el.setAttribute(key, vnode.props[key]) 11 } 12 } 13 14 insert(el, container) 15 }在这段代码中我们首先检查了 vnode.props 字段是否存在如果存在则遍历它并调用 setAttribute 函数将属性设置到元素上。实际上除了使用 setAttribute 函数为元素设置属性之外还可以通过 DOM 对象直接设置 01 function mountElement(vnode, container) { 02 const el createElement(vnode.type) 03 // 省略 children 的处理 04 05 if (vnode.props) { 06 for (const key in vnode.props) { 07 // 直接设置 08 el[key] vnode.props[key] 09 } 10 } 11 12 insert(el, container) 13 }在这段代码中我们没有选择使用 setAttribute 函数而是直接将属性设置在 DOM 对象上即 el[key] vnode.props[key]。实际上无论是使用 setAttribute 函数还是直接操作 DOM 对象都存在缺陷。如前所述为元素设置属性比想象中要复杂得多。不过在讨论具体有哪些缺陷之前我们有必要先搞清楚两个重要的概念HTML Attributes和 DOM Properties。 2、HTML Attributes 与 DOM Properties 理解 HTML Attributes 和 DOM Properties 之间的差异和关联非常重要这能够帮助我们合理地设计虚拟节点的结构更是正确地为元素设置属性的关键。 我们从最基本的 HTML 说起。给出如下 HTML 代码 01 input idmy-input typetext valuefoo /HTML Attributes 指的就是定义在 HTML 标签上的属性这里指的就是 id“my-input”、type“text” 和 value“foo”。当浏览器解析这段 HTML 代码后会创建一个与之相符的 DOM 元素对象我们可以通过 JavaScript 代码来读取该 DOM 对象 01 const el document.querySelector(#my-input)这个 DOM 对象会包含很多属性properties如下图所示 这些属性就是所谓的 DOM Properties。很多 HTML Attributes 在 DOM 对象上有与之同名的 DOM Properties例如 id“my-input” 对应 el.idtype“text” 对应 el.typevalue“foo” 对应 el.value 等。但 DOM Properties 与 HTML Attributes 的名字不总是一模一样的例如 01 div classfoo/divclass“foo” 对应的 DOM Properties 则是 el.className。另外并不是所有 HTML Attributes 都有与之对应的 DOM Properties例如 01 div aria-valuenow75/divaria-* 类的 HTML Attributes 就没有与之对应的 DOM Properties。 类似地也不是所有 DOM Properties 都有与之对应的 HTML Attributes例如可以用 el.textContent 来设置元素的文本内容但并没有与之对应的 HTML Attributes 来完成同样的工作。 HTML Attributes 的值与 DOM Properties 的值之间是有关联的例如下面的 HTML 片段 01 div idfoo/div这个片段描述了一个具有 id 属性的 div 标签。其中idfoo对应的 DOM Properties 是 el.id并且值为字符串 ‘foo’。我们把这种 HTML Attributes 与 DOM Properties 具有相同名称即 id的属性看作直接映射。但并不是所有 HTML Attributes 与 DOM Properties 之间都是直接映射的关系例如 01 input valuefoo /这是一个具有 value 属性的 input 标签。如果用户没有修改文本框的内容那么通过 el.value 读取对应的 DOM Properties 的值就是字符串 ‘foo’。而如果用户修改了文本框的值那么el.value 的值就是当前文本框的值。例如用户将文本框的内容修改为 ‘bar’那么 01 console.log(el.value) // bar但如果运行下面的代码会发生“奇怪”的现象 01 console.log(el.getAttribute(value)) // 仍然是 foo 02 console.log(el.value) // bar可以发现用户对文本框内容的修改并不会影响el.getAttribute(‘value’) 的返回值这个现象蕴含着 HTML Attributes 所代表的意义。实际上HTML Attributes 的作用是设置与之对应的 DOM Properties 的初始值。一旦值改变那么 DOM Properties 始终存储着当前值而通过getAttribute 函数得到的仍然是初始值。 但我们仍然可以通过 el.defaultValue 来访问初始值如下面的代码所示 01 el.getAttribute(value) // 仍然是 foo 02 el.value // bar 03 el.defaultValue // foo这说明一个 HTML Attributes 可能关联多个 DOM Properties。例如在上例中value“foo” 与 el.value 和el.defaultValue 都有关联。 虽然我们可以认为 HTML Attributes 是用来设置与之对应的DOM Properties 的初始值的但有些值是受限制的就好像浏览器内部做了默认值校验。如果你通过 HTML Attributes 提供的默认值不合法那么浏览器会使用内建的合法值作为对应DOM Properties 的默认值例如 01 input typefoo /我们知道为 标签的 type 属性指定字符串 ‘foo’ 是不合法的因此浏览器会矫正这个不合法的值。所以当我们尝试读取 el.type 时得到的其实是矫正后的值即字符串’text’而非字符串 ‘foo’ 01 console.log(el.type) // text从上述分析来看HTML Attributes 与 DOM Properties 之间的关系很复杂但其实我们只需要记住一个核心原则即可HTML Attributes 的作用是设置与之对应的 DOM Properties 的初始值。 3、正确地设置元素属性 上一节我们详细讨论了 HTML Attributes 和 DOM Properties 相关的内容因为 HTML Attributes 和 DOM Properties 会影响 DOM 属性的添加方式。对于普通的 HTML 文件来说当浏览器解析 HTML 代码后会自动分析 HTML Attributes 并设置合适的 DOM Properties。但用户编写在 Vue.js 的单文件组件中的模板不会被浏览器解析这意味着原本需要浏览器来完成的工作现在需要框架来完成。 我们以禁用的按钮为例如下面的 HTML 代码所示 01 button disabledButton/button浏览器在解析这段 HTML 代码时发现这个按钮存在一个叫作disabled 的 HTML Attributes于是浏览器会将该按钮设置为禁用状态并将它的 el.disabled 这个 DOM Properties 的值设置为 true这一切都是浏览器帮我们处理好的。但同样的代码如果出现在 Vue.js 的模板中则情况会有所不同。首先这个 HTML 模板会被编译成 vnode它等价于 01 const button { 02 type: button, 03 props: { 04 disabled: 05 } 06 }注意这里的 props.disabled 的值是空字符串如果在渲染器中调用 setAttribute 函数设置属性则相当于 01 el.setAttribute(disabled, )这么做的确没问题浏览器会将按钮禁用。但考虑如下模板 01 button :disabledfalseButton/button它对应的 vnode 为 01 const button { 02 type: button, 03 props: { 04 disabled: false 05 } 06 }用户的本意是“不禁用”按钮但如果渲染器仍然使用setAttribute 函数设置属性值则会产生意外的效果即按钮被禁用了 01 el.setAttribute(disabled, false)在浏览器中运行上面这句代码我们发现浏览器仍然将按钮禁用了。这是因为使用 setAttribute 函数设置的值总是会被字符串化所以上面这句代码等价于 01 el.setAttribute(disabled, false)对于按钮来说它的 el.disabled 属性值是布尔类型的并且它不关心具体的 HTML Attributes 的值是什么只要 disabled 属性存在按钮就会被禁用。所以我们发现渲染器不应该总是使用 setAttribute 函数将 vnode.props 对象中的属性设置到元素上。那么应该怎么办呢一个很自然的思路是我们可以优先设置 DOM Properties例如 01 el.disabled false这样是可以正确工作的但又带来了新的问题。还是以上面给出的模板为例 01 button disabledButton/button这段模板对应的 vnode 是 01 const button { 02 type: button, 03 props: { 04 disabled: 05 } 06 }我们注意到在模板经过编译后得到的 vnode 对象中props.disabled 的值是一个空字符串。如果直接用它设置元素的 DOM Properties那么相当于 01 el.disabled 由于 el.disabled 是布尔类型的值所以当我们尝试将它设置为空字符串时浏览器会将它的值矫正为布尔类型的值即false。所以上面这句代码的执行结果等价于 01 el.disabled false这违背了用户的本意因为用户希望禁用按钮而 el.disabled false 则是不禁用的意思。 这么看来无论是使用 setAttribute 函数还是直接设置元素的 DOM Properties都存在缺陷。要彻底解决这个问题我们只能做特殊处理即优先设置元素的 DOM Properties但当值为空字符串时要手动将值矫正为 true。只有这样才能保证代码的行为符合预期。下面的 mountElement 函数给出了具体的实现 01 function mountElement(vnode, container) { 02 const el createElement(vnode.type) 03 // 省略 children 的处理 04 05 if (vnode.props) { 06 for (const key in vnode.props) { 07 // 用 in 操作符判断 key 是否存在对应的 DOM Properties 08 if (key in el) { 09 // 获取该 DOM Properties 的类型 10 const type typeof el[key] 11 const value vnode.props[key] 12 // 如果是布尔类型并且 value 是空字符串则将值矫正为 true 13 if (type boolean value ) { 14 el[key] true 15 } else { 16 el[key] value 17 } 18 } else { 19 // 如果要设置的属性没有对应的 DOM Properties则使用 setAttribute 函数设置属性 20 el.setAttribute(key, vnode.props[key]) 21 } 22 } 23 } 24 25 insert(el, container) 26 }如上面的代码所示我们检查每一个 vnode.props 中的属性看看是否存在对应的 DOM Properties如果存在则优先设置 DOM Properties。同时我们对布尔类型的 DOM Properties 做了值的矫正即当要设置的值为空字符串时将其矫正为布尔值 true。当然如果 vnode.props 中的属性不具有对应的 DOM Properties则仍然使用 setAttribute 函数完成属性的设置。 但上面给出的实现仍然存在问题因为有一些 DOM Properties 是只读的如以下代码所示 01 form idform1/form 02 input formform1 /在这段代码中我们为 input/ 标签设置了 form 属性HTML Attributes。它对应的 DOM Properties 是el.form但 el.form 是只读的因此我们只能够通过setAttribute 函数来设置它。这就需要我们修改现有的逻辑 01 function shouldSetAsProps(el, key, value) { 02 // 特殊处理 03 if (key form el.tagName INPUT) return false 04 // 兜底 05 return key in el 06 } 07 08 function mountElement(vnode, container) { 09 const el createElement(vnode.type) 10 // 省略 children 的处理 11 12 if (vnode.props) { 13 for (const key in vnode.props) { 14 const value vnode.props[key] 15 // 使用 shouldSetAsProps 函数判断是否应该作为 DOM Properties 设置 16 if (shouldSetAsProps(el, key, value)) { 17 const type typeof el[key] 18 if (type boolean value ) { 19 el[key] true 20 } else { 21 el[key] value 22 } 23 } else { 24 el.setAttribute(key, value) 25 } 26 } 27 } 28 29 insert(el, container) 30 }如上面的代码所示为了代码的可读性我们提取了一个shouldSetAsProps 函数。该函数会返回一个布尔值代表属性是否应该作为 DOM Properties 被设置。如果返回 true则代表应该作为 DOM Properties 被设置否则应该使用setAttribute 函数来设置。在 shouldSetAsProps 函数内我们对 input formxxx / 进行特殊处理即 input/ 标签的 form 属性必须使用 setAttribute 函数来设置。实际上不仅仅是 input/ 标签所有表单元素都具有 form 属性它们都应该作为 HTML Attributes 被设置。 当然input formxxx/ 是一个特殊的例子还有一些其他类似于这种需要特殊处理的情况。我们不会列举所有情况并一一讲解因为掌握处理问题的思路更加重要。另外我们也不可能把所有需要特殊处理的地方都记住更何况有时我们根本不知道在什么情况下才需要特殊处理。所以上述解决方案本质上是经验之谈。不要惧怕写出不完美的代码只要在后续迭代过程中“见招拆招“代码就会变得越来越完善框架也会变得越来越健壮。 最后我们需要把属性的设置也变成与平台无关因此需要把属性设置相关操作也提取到渲染器选项中如下面的代码所示 01 const renderer createRenderer({ 02 createElement(tag) { 03 return document.createElement(tag) 04 }, 05 setElementText(el, text) { 06 el.textContent text 07 }, 08 insert(el, parent, anchor null) { 09 parent.insertBefore(el, anchor) 10 }, 11 // 将属性设置相关操作封装到 patchProps 函数中并作为渲染器选项传递 12 patchProps(el, key, prevValue, nextValue) { 13 if (shouldSetAsProps(el, key, nextValue)) { 14 const type typeof el[key] 15 if (type boolean nextValue ) { 16 el[key] true 17 } else { 18 el[key] nextValue 19 } 20 } else { 21 el.setAttribute(key, nextValue) 22 } 23 } 24 })而在 mountElement 函数中只需要调用 patchProps 函数并为其传递相关参数即可 01 function mountElement(vnode, container) { 02 const el createElement(vnode.type) 03 if (typeof vnode.children string) { 04 setElementText(el, vnode.children) 05 } else if (Array.isArray(vnode.children)) { 06 vnode.children.forEach(child { 07 patch(null, child, el) 08 }) 09 } 10 11 if (vnode.props) { 12 for (const key in vnode.props) { 13 // 调用 patchProps 函数即可 14 patchProps(el, key, null, vnode.props[key]) 15 } 16 } 17 18 insert(el, container) 19 }这样我们就把属性相关的渲染逻辑从渲染器的核心中抽离了出来。 4、class 的处理 在上一节中我们讲解了如何正确地把 vnode.props 中定义的属性设置到 DOM 元素上。但在 Vue.js 中仍然有一些属性需要特殊处理比如 class 属性。为什么需要对 class 属性进行特殊处理呢这是因为 Vue.js 对 calss 属性做了增强。在 Vue.js 中为元素设置类名有以下几种方式 方式一指定 class 为一个字符串值 01 p classfoo bar/p这段模板对应的 vnode 是 01 const vnode { 02 type: p, 03 props: { 04 class: foo bar 05 } 06 }方式二指定 class 为一个对象值 01 p :classcls/p假设对象 cls 的内容如下 01 const cls { foo: true, bar: false }那么这段模板对应的 vnode 是 01 const vnode { 02 type: p, 03 props: { 04 class: { foo: true, bar: false } 05 } 06 }方式三class 是包含上述两种类型的数组 01 p :classarr/p这个数组可以是字符串值与对象值的组合 01 const arr [ 02 // 字符串 03 foo bar, 04 // 对象 05 { 06 baz: true 07 } 08 ]那么这段模板对应的 vnode 是 01 const vnode { 02 type: p, 03 props: { 04 class: [ 05 foo bar, 06 { baz: true } 07 ] 08 } 09 }可以看到因为 class 的值可以是多种类型所以我们必须在设置元素的 class 之前将值归一化为统一的字符串形式再把该字符串作为元素的 class 值去设置。因此我们需要封装normalizeClass 函数用它来将不同类型的 class 值正常化为字符串例如 01 const vnode { 02 type: p, 03 props: { 04 // 使用 normalizeClass 函数对值进行序列化 05 class: normalizeClass([ 06 foo bar, 07 { baz: true } 08 ]) 09 } 10 }最后的结果等价于 01 const vnode { 02 type: p, 03 props: { 04 // 序列化后的结果 05 class: foo bar baz 06 } 07 }至于 normalizeClass 函数的实现这里我们不会做详细讲解因为它本质上就是一个数据结构转换的小算法实现起来并不复杂。 假设现在我们已经能够对 class 值进行正常化了。接下来我们将讨论如何将正常化后的 class 值设置到元素上。其实我们目前实现的渲染器已经能够完成 class 的渲染了。观察前文中函数的代码由于 class 属性对应的 DOM Properties 是el.className所以表达式 ‘class’ in el 的值将会是 false因此patchProps 函数会使用 setAttribute 函数来完成 class 的设置。但是我们知道在浏览器中为一个元素设置 class 有三种方式即使用 setAttribute、el.className 或 el.classList。那么哪一种方法的性能更好呢下图对比了这三种方式为元素设置 1000 次 class 的性能 可以看到el.className 的性能最优。因此我们需要调整patchProps 函数的实现如下面的代码所示 01 const renderer createRenderer({ 02 // 省略其他选项 03 04 patchProps(el, key, prevValue, nextValue) { 05 // 对 class 进行特殊处理 06 if (key class) { 07 el.className nextValue || 08 } else if (shouldSetAsProps(el, key, nextValue)) { 09 const type typeof el[key] 10 if (type boolean nextValue ) { 11 el[key] true 12 } else { 13 el[key] nextValue 14 } 15 } else { 16 el.setAttribute(key, nextValue) 17 } 18 } 19 })从上面的代码中可以看到我们对 class 进行了特殊处理即使用 el.className 代替 setAttribute 函数。其实除了 class 属性之外Vue.js 对 style 属性也做了增强所以我们也需要对style 做类似的处理。 通过对 class 的处理我们能够意识到vnode.props 对象中定义的属性值的类型并不总是与 DOM 元素属性的数据结构保持一致这取决于上层 API 的设计。Vue.js 允许对象类型的值作为 class 是为了方便开发者在底层的实现上必然需要对值进行正常化后再使用。另外正常化值的过程是有代价的如果需要进行大量的正常化操作则会消耗更多性能。 5、卸载操作 前文主要讨论了挂载操作。接下来我们将会讨论卸载操作。卸载操作发生在更新阶段更新指的是在初次挂载完成之后后续渲染会触发更新如下面的代码所示 01 // 初次挂载 02 renderer.render(vnode, document.querySelector(#app)) 03 // 再次挂载新 vnode将触发更新 04 renderer.render(newVNode, document.querySelector(#app))更新的情况有几种我们逐个来看。当后续调用 render 函数渲染空内容即 null时如下面的代码所示 01 // 初次挂载 02 renderer.render(vnode, document.querySelector(#app)) 03 // 新 vnode 为 null意味着卸载之前渲染的内容 04 renderer.render(null, document.querySelector(#app))首次挂载完成后后续渲染时如果传递了 null 作为新 vnode则意味着什么都不渲染这时我们需要卸载之前渲染的内容。回顾前文实现的 render 函数如下 01 function render(vnode, container) { 02 if (vnode) { 03 patch(container._vnode, vnode, container) 04 } else { 05 if (container._vnode) { 06 // 卸载清空容器 07 container.innerHTML 08 } 09 } 10 container._vnode vnode 11 }可以看到当 vnode 为 null并且容器元素的container._vnode 属性存在时我们直接通过 innerHTML 清空容器。但这么做是不严谨的原因有三点 容器的内容可能是由某个或多个组件渲染的当卸载操作发生时应该正确地调用这些组件的 beforeUnmount、unmounted 等生命周期函数。即使内容不是由组件渲染的有的元素存在自定义指令我们应该在卸载操作发生时正确执行对应的指令钩子函数。使用 innerHTML 清空容器元素内容的另一个缺陷是它不会移除绑定在 DOM 元素上的事件处理函数。 正如上述三点原因我们不能简单地使用 innerHTML 来完成卸载操作。正确的卸载方式是根据 vnode 对象获取与其相关联的真实 DOM 元素然后使用原生 DOM 操作方法将该 DOM 元素移除。为此我们需要在 vnode 与真实 DOM 元素之间建立联系修改 mountElement 函数如下面的代码所示 01 function mountElement(vnode, container) { 02 // 让 vnode.el 引用真实 DOM 元素 03 const el vnode.el createElement(vnode.type) 04 if (typeof vnode.children string) { 05 setElementText(el, vnode.children) 06 } else if (Array.isArray(vnode.children)) { 07 vnode.children.forEach(child { 08 patch(null, child, el) 09 }) 10 } 11 12 if (vnode.props) { 13 for (const key in vnode.props) { 14 patchProps(el, key, null, vnode.props[key]) 15 } 16 } 17 18 insert(el, container) 19 }可以看到当我们调用 createElement 函数创建真实 DOM 元素时会把真实 DOM 元素赋值给 vnode.el 属性。这样在vnode 与真实 DOM 元素之间就建立了联系我们可以通过vnode.el 来获取该虚拟节点对应的真实 DOM 元素。有了这些当卸载操作发生的时候只需要根据虚拟节点对象vnode.el 取得真实 DOM 元素再将其从父元素中移除即可 01 function render(vnode, container) { 02 if (vnode) { 03 patch(container._vnode, vnode, container) 04 } else { 05 if (container._vnode) { 06 // 根据 vnode 获取要卸载的真实 DOM 元素 07 const el container._vnode.el 08 // 获取 el 的父元素 09 const parent el.parentNode 10 // 调用 removeChild 移除元素 11 if (parent) parent.removeChild(el) 12 } 13 } 14 container._vnode vnode 15 }如上面的代码所示其中 container._vnode 代表旧 vnode即要被卸载的 vnode。然后通过 container._vnode.el 取得真实 DOM 元素并调用 removeChild 函数将其从父元素中移除即可。 由于卸载操作是比较常见且基本的操作所以我们应该将它封装到 unmount 函数中以便后续代码可以复用它如下面的代码所示 01 function unmount(vnode) { 02 const parent vnode.el.parentNode 03 if (parent) { 04 parent.removeChild(vnode.el) 05 } 06 }unmount 函数接收一个虚拟节点作为参数并将该虚拟节点对应的真实 DOM 元素从父元素中移除。现在 unmount 函数的代码还非常简单后续我们会慢慢充实它让它变得更加完善。有了 unmount 函数后就可以直接在 render 函数中调用它来完成卸载任务了 01 function render(vnode, container) { 02 if (vnode) { 03 patch(container._vnode, vnode, container) 04 } else { 05 if (container._vnode) { 06 // 调用 unmount 函数卸载 vnode 07 unmount(container._vnode) 08 } 09 } 10 container._vnode vnode 11 }最后将卸载操作封装到 unmount 中还能够带来两点额外的好处 在 unmount 函数内我们有机会调用绑定在 DOM 元素上的指令钩子函数例如 beforeUnmount、unmounted 等。当 unmount 函数执行时我们有机会检测虚拟节点 vnode 的类型。如果该虚拟节点描述的是组件则我们有机会调用组件相关的生命周期函数。 6、区分 vnode 的类型 在上一节中我们了解到当后续调用 render 函数渲染空内容即 null时会执行卸载操作。如果在后续渲染时为render 函数传递了新的 vnode则不会进行卸载操作而是会把新旧 vnode 都传递给 patch 函数进行打补丁操作。回顾前文实现的 patch 函数如下面的代码所示 01 function patch(n1, n2, container) { 02 if (!n1) { 03 mountElement(n2, container) 04 } else { 05 // 更新 06 } 07 }其中patch 函数的两个参数 n1 和 n2 分别代表旧 vnode 与新 vnode。如果旧 vnode 存在则需要在新旧 vnode 之间打补丁。但在具体执行打补丁操作之前我们需要保证新旧vnode 所描述的内容相同。这是什么意思呢举个例子假设初次渲染的 vnode 是一个 p 元素 01 const vnode { 02 type: p 03 } 04 renderer.render(vnode, document.querySelector(#app))后续又渲染了一个 input 元素 01 const vnode { 02 type: input 03 } 04 renderer.render(vnode, document.querySelector(#app))这就会造成新旧 vnode 所描述的内容不同即 vnode.type 属性的值不同。对于上例来说p 元素和 input 元素之间不存在打补丁的意义因为对于不同的元素来说每个元素都有特有的属性例如 01 p idfoo / 02 !-- type 属性是 input 标签特有的p 标签则没有该属性 -- 03 input typesubmit /在这种情况下正确的更新操作是先将 p 元素卸载再将input 元素挂载到容器中。因此我们需要调整 patch 函数的代码 01 function patch(n1, n2, container) { 02 // 如果 n1 存在则对比 n1 和 n2 的类型 03 if (n1 n1.type ! n2.type) { 04 // 如果新旧 vnode 的类型不同则直接将旧 vnode 卸载 05 unmount(n1) 06 n1 null 07 } 08 09 if (!n1) { 10 mountElement(n2, container) 11 } else { 12 // 更新 13 } 14 }如上面的代码所示在真正执行更新操作之前我们优先检查新旧 vnode 所描述的内容是否相同如果不同则直接调用unmount 函数将旧 vnode 卸载。这里需要注意的是卸载完成后我们应该将参数 n1 的值重置为 null这样才能保证后续挂载操作正确执行。 即使新旧 vnode 描述的内容相同我们仍然需要进一步确认它们的类型是否相同。我们知道一个 vnode 可以用来描述普通标签也可以用来描述组件还可以用来描述 Fragment 等。对于不同类型的 vnode我们需要提供不同的挂载或打补丁的处理方式。所以我们需要继续修改 patch 函数的代码以满足需求如下面的代码所示 01 function patch(n1, n2, container) { 02 if (n1 n1.type ! n2.type) { 03 unmount(n1) 04 n1 null 05 } 06 // 代码运行到这里证明 n1 和 n2 所描述的内容相同 07 const { type } n2 08 // 如果 n2.type 的值是字符串类型则它描述的是普通标签元素 09 if (typeof type string) { 10 if (!n1) { 11 mountElement(n2, container) 12 } else { 13 patchElement(n1, n2) 14 } 15 } else if (typeof type object) { 16 // 如果 n2.type 的值的类型是对象则它描述的是组件 17 } else if (type xxx) { 18 // 处理其他类型的 vnode 19 } 20 }实际上在前文的讲解中我们一直假设 vnode 的类型是普通标签元素。但严谨的做法是根据 vnode.type 进一步确认它们的类型是什么从而使用相应的处理函数进行处理。例如如果 vnode.type 的值是字符串类型则它描述的是普通标签元素这时我们会调用 mountElement 或 patchElement 完成挂载和更新操作如果 vnode.type 的值的类型是对象则它描述的是组件这时我们会调用与组件相关的挂载和更新方法。 7、事件的处理 本节我们将讨论如何处理事件包括如何在虚拟节点中描述事件如何把事件添加到 DOM 元素上以及如何更新事件。 我们先来解决第一个问题即如何在虚拟节点中描述事件。事件可以视作一种特殊的属性因此我们可以约定在vnode.props 对象中凡是以字符串 on 开头的属性都视作事件。例如 01 const vnode { 02 type: p, 03 props: { 04 // 使用 onXxx 描述事件 05 onClick: () { 06 alert(clicked) 07 } 08 }, 09 children: text 10 }解决了事件在虚拟节点层面的描述问题后我们再来看看如何将事件添加到 DOM 元素上。这非常简单只需要在patchProps 中调用 addEventListener 函数来绑定事件即可如下面的代码所示 01 patchProps(el, key, prevValue, nextValue) { 02 // 匹配以 on 开头的属性视其为事件 03 if (/^on/.test(key)) { 04 // 根据属性名称得到对应的事件名称例如 onClick --- click 05 const name key.slice(2).toLowerCase() 06 // 绑定事件nextValue 为事件处理函数 07 el.addEventListener(name, nextValue) 08 } else if (key class) { 09 // 省略部分代码 10 } else if (shouldSetAsProps(el, key, nextValue)) { 11 // 省略部分代码 12 } else { 13 // 省略部分代码 14 } 15 }那么更新事件要如何处理呢按照一般的思路我们需要先移除之前添加的事件处理函数然后再将新的事件处理函数绑定到 DOM 元素上如下面的代码所示 01 patchProps(el, key, prevValue, nextValue) { 02 if (/^on/.test(key)) { 03 const name key.slice(2).toLowerCase() 04 // 移除上一次绑定的事件处理函数 05 prevValue el.removeEventListener(name, prevValue) 06 // 绑定新的事件处理函数 07 el.addEventListener(name, nextValue) 08 } else if (key class) { 09 // 省略部分代码 10 } else if (shouldSetAsProps(el, key, nextValue)) { 11 // 省略部分代码 12 } else { 13 // 省略部分代码 14 } 15 }这么做代码能够按照预期工作但其实还有一种性能更优的方式来完成事件更新。在绑定事件时我们可以绑定一个伪造的事件处理函数 invoker然后把真正的事件处理函数设置为invoker.value 属性的值。这样当更新事件的时候我们将不再需要调用 removeEventListener 函数来移除上一次绑定的事件只需要更新 invoker.value 的值即可如下面的代码所示 01 patchProps(el, key, prevValue, nextValue) { 02 if (/^on/.test(key)) { 03 // 获取为该元素伪造的事件处理函数 invoker 04 let invoker el._vei 05 const name key.slice(2).toLowerCase() 06 if (nextValue) { 07 if (!invoker) { 08 // 如果没有 invoker则将一个伪造的 invoker 缓存到 el._vei 中 09 // vei 是 vue event invoker 的首字母缩写 10 invoker el._vei (e) { 11 // 当伪造的事件处理函数执行时会执行真正的事件处理函数 12 invoker.value(e) 13 } 14 // 将真正的事件处理函数赋值给 invoker.value 15 invoker.value nextValue 16 // 绑定 invoker 作为事件处理函数 17 el.addEventListener(name, invoker) 18 } else { 19 // 如果 invoker 存在意味着更新并且只需要更新 invoker.value 的值即可 20 invoker.value nextValue 21 } 22 } else if (invoker) { 23 // 新的事件绑定函数不存在且之前绑定的 invoker 存在则移除绑定 24 el.removeEventListener(name, invoker) 25 } 26 } else if (key class) { 27 // 省略部分代码 28 } else if (shouldSetAsProps(el, key, nextValue)) { 29 // 省略部分代码 30 } else { 31 // 省略部分代码 32 } 33 }观察上面的代码事件绑定主要分为两个步骤 先从 el._vei 中读取对应的 invoker如果 invoker 不存在则将伪造的 invoker 作为事件处理函数并将它缓存到 el._vei 属性中。把真正的事件处理函数赋值给 invoker.value 属性然后把伪造的 invoker 函数作为事件处理函数绑定到元素上。可以看到当事件触发时实际上执行的是伪造的事件处理函数在其内部间接执行了真正的事件处理函数 invoker.value(e)。 当更新事件时由于 el._vei 已经存在了所以我们只需要将invoker.value 的值修改为新的事件处理函数即可。这样在更新事件时可以避免一次 removeEventListener 函数的调用从而提升了性能。实际上伪造的事件处理函数的作用不止于此它还能解决事件冒泡与事件更新之间相互影响的问题下文会详细讲解。 但目前的实现仍然存在问题。现在我们将事件处理函数缓存在el._vei 属性中问题是在同一时刻只能缓存一个事件处理函数。这意味着如果一个元素同时绑定了多种事件将会出现事件覆盖的现象。例如同时给元素绑定 click 和 contextmenu 事件 01 const vnode { 02 type: p, 03 props: { 04 onClick: () { 05 alert(clicked) 06 }, 07 onContextmenu: () { 08 alert(contextmenu) 09 } 10 }, 11 children: text 12 } 13 renderer.render(vnode, document.querySelector(#app))当渲染器尝试渲染这上面代码中给出的 vnode 时会先绑定click 事件然后再绑定 contextmenu 事件。后绑定的contextmenu 事件的处理函数将覆盖先绑定的 click 事件的处理函数。为了解决事件覆盖的问题我们需要重新设计 el._vei 的数据结构。我们应该将 el._vei 设计为一个对象它的键是事件名称它的值则是对应的事件处理函数这样就不会发生事件覆盖的现象了如下面的代码所示 01 patchProps(el, key, prevValue, nextValue) { 02 if (/^on/.test(key)) { 03 // 定义 el._vei 为一个对象存在事件名称到事件处理函数的映射 04 const invokers el._vei || (el._vei {}) 05 //根据事件名称获取 invoker 06 let invoker invokers[key] 07 const name key.slice(2).toLowerCase() 08 if (nextValue) { 09 if (!invoker) { 10 // 将事件处理函数缓存到 el._vei[key] 下避免覆盖 11 invoker el._vei[key] (e) { 12 invoker.value(e) 13 } 14 invoker.value nextValue 15 el.addEventListener(name, invoker) 16 } else { 17 invoker.value nextValue 18 } 19 } else if (invoker) { 20 el.removeEventListener(name, invoker) 21 } 22 } else if (key class) { 23 // 省略部分代码 24 } else if (shouldSetAsProps(el, key, nextValue)) { 25 // 省略部分代码 26 } else { 27 // 省略部分代码 28 } 29 }另外一个元素不仅可以绑定多种类型的事件对于同一类型的事件而言还可以绑定多个事件处理函数。我们知道在原生 DOM 编程中当多次调用 addEventListener 函数为元素绑定同一类型的事件时多个事件处理函数可以共存例如 01 el.addEventListener(click, fn1) 02 el.addEventListener(click, fn2)当点击元素时事件处理函数 fn1 和 fn2 都会执行。因此为了描述同一个事件的多个事件处理函数我们需要调整vnode.props 对象中事件的数据结构如下面的代码所示 01 const vnode { 02 type: p, 03 props: { 04 onClick: [ 05 // 第一个事件处理函数 06 () { 07 alert(clicked 1) 08 }, 09 // 第二个事件处理函数 10 () { 11 alert(clicked 2) 12 } 13 ] 14 }, 15 children: text 16 } 17 renderer.render(vnode, document.querySelector(#app))在上面这段代码中我们使用一个数组来描述事件数组中的每个元素都是一个独立的事件处理函数并且这些事件处理函数都能够正确地绑定到对应元素上。为了实现此功能我们需要修改 patchProps 函数中事件处理相关的代码如下面的代码所示 01 patchProps(el, key, prevValue, nextValue) { 02 if (/^on/.test(key)) { 03 const invokers el._vei || (el._vei {}) 04 let invoker invokers[key] 05 const name key.slice(2).toLowerCase() 06 if (nextValue) { 07 if (!invoker) { 08 invoker el._vei[key] (e) { 09 // 如果 invoker.value 是数组则遍历它并逐个调用事件处理函数 10 if (Array.isArray(invoker.value)) { 11 invoker.value.forEach(fn fn(e)) 12 } else { 13 // 否则直接作为函数调用 14 invoker.value(e) 15 } 16 } 17 invoker.value nextValue 18 el.addEventListener(name, invoker) 19 } else { 20 invoker.value nextValue 21 } 22 } else if (invoker) { 23 el.removeEventListener(name, invoker) 24 } 25 } else if (key class) { 26 // 省略部分代码 27 } else if (shouldSetAsProps(el, key, nextValue)) { 28 // 省略部分代码 29 } else { 30 // 省略部分代码 31 } 32 }在这段代码中我们修改了 invoker 函数的实现。当 invoker 函数执行时在调用真正的事件处理函数之前要先检查invoker.value 的数据结构是否是数组如果是数组则遍历它并逐个调用定义在数组中的事件处理函数。 8、事件冒泡与更新时机问题 在上一节中我们介绍了基本的事件处理。本节我们将讨论事件冒泡与更新时机相结合所导致的问题。为了更清晰地描述问题我们需要构造一个小例子 01 const { effect, ref } VueReactivity 02 03 const bol ref(false) 04 05 effect(() { 06 // 创建 vnode 07 const vnode { 08 type: div, 09 props: bol.value ? { 10 onClick: () { 11 alert(父元素 clicked) 12 } 13 } : {}, 14 children: [ 15 { 16 type: p, 17 props: { 18 onClick: () { 19 bol.value true 20 } 21 }, 22 children: text 23 } 24 ] 25 } 26 // 渲染 vnode 27 renderer.render(vnode, document.querySelector(#app)) 28 })
http://www.zqtcl.cn/news/715188/

相关文章:

  • 电脑做网站怎么解析域名河南郑州静默管理
  • 项目网站制作冯提莫斗鱼前在哪个网站做直播
  • 网站建设 思路wordpress 访问记录插件
  • 网站建设diyseo课程培训班费用
  • 舞蹈培训东莞网站建设做直播网站
  • app建设网站公司网站制作预算
  • 移动端网站如何开发市辖区郑州网站建设
  • 山东省双体系建设网站wordpress 帮助 主题
  • 手机怎么做三个视频网站网站建设协议一百互联
  • 创建一个网站一般步骤有哪些安徽软件定制开发
  • 网站建设平台协议书模板下载佳木斯建网站的
  • 部队网站建设招标二级域名注册平台
  • 做网站怎么调用栏目织梦搞笑图片网站源码
  • 开个小网站要怎么做南宁seo外包服务商
  • 济宁做网站的企业app网站开发学习
  • 哪个网站可以做危险化学品供求html静态网站作品
  • 豪圣建设项目管理网站创建网站的视频
  • 网站做接口自己做的网站只能用谷歌浏览器打开
  • 建设网站具体步骤python 做 网站
  • 网站源代码怎么上传wordpress标题字体大小
  • 营销型网站哪家好网页设计一张多少钱
  • 怎么搭建购物网站山东德州网站建设
  • 网站 404 错误页面是否自动跳转太原网站建设王道下拉惠
  • 美仑-专门做服装的网站淘宝详情页制作
  • 网站商城制作策划公司组织结构图
  • 商务网站建设教程企网
  • 北京做网站推广多少钱丽水网站建设公司排名
  • 淄博网站关键词优化安丘网站建设公司
  • 教育建设网站wordpress 创建模板文件
  • 门户网站开发视频教学百度关键词怎么刷上去