vue 虚拟DOM快速入门

2022-04-15 0 1,100
目录
  • 虚拟 DOM
    • 什么是虚拟 dom
  • 虚拟 dom 的作用
    • vue 中的虚拟 dom
      • vNode
        • 什么是 vNode
        • vNode 的类型
        • patch
        • 新增节点
        • 删除节点
        • 更新节点
        • 新增节点 – 源码分析
        • 删除节点 – 源码分析

      虚拟 DOM

      什么是虚拟 dom

      dom 是文档对象模型,以节点树的形式来表现文档。

      虚拟 dom 不是真正意义上的 dom。而是一个 javascript 对象。

      正常的 dom 节点在 html 中是这样表示:

      <div class='testId'>
          <p>你好</p>
          <p>欢迎光临</p>
      </div>

      而在虚拟 dom 中大概是这样:

      {
          tag: 'div',
          attributes:{
              class: ['testId']
          },
          children:[
              // p 元素
              // p 元素
          ]
      }
      

      我们可以将虚拟 dom 拆分成两部分进行理解:虚拟 + dom。

      • 虚拟: 表示虚拟 dom 不是真正意义上的 dom,而是一个 javascript 对象;
      • dom: 表示虚拟 dom 能以类似节点树的形式表示文档。

      虚拟 dom 的作用

      现在主流的框架都是声明式操作 dom 的框架。我们只需要描述状态与 dom 之间的映射关系即可,状态到视图(真实的 dom)的转换,框架会帮我们做。

      最粗暴的做法是将状态渲染成视图,每次更新状态,都重新更新整个视图。

      这种做法的性能可想而知。比较好的想法是:状态改变,只更新与状态相关的 dom 节点。虚拟 dom 只是实现这个想法的其中一种方法而已。

      具体做法:

      • 状态 -> 真实 dom(最初)
      • 状态 -> 虚拟 dom -> 真实 dom(使用虚拟 dom)

      状态改变,重新生成一份虚拟 dom,将上一份和这一份虚拟 dom 进行对比,找出需要更新的部分,更新真实 dom。

      vue 中的虚拟 dom

      真实的 dom 是由 节点(Node)组成,虚拟 dom 则是由虚拟节点(vNode)组成。

      虚拟 dom 在 vue 中主要做两件事:

      • 提供与真实节点(Node)对应的虚拟节点(vNode)
      • 将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图

      “虚拟 DOM”是我们对由 Vue 组件树建立起来的整个 VNode 树的称呼 —— vue 官网

      vNode

      什么是 vNode

      上文提到,vNode(虚拟节点)对应的是真实节点(Node)。

      vNode 可以理解成节点描述对象。描述了如何创建真实的 dom 节点。

      vue.js 中有一个 vNode 类。可以使用它创建不同类型的 vNode 实例,不同类型的 vNode 对应着不同类型的 dom 元素。代码如下:

      export default class VNode {
         constructor (
          tag?: string,
          data?: VNodeData,
          children?: ?Array<VNode>,
          text?: string,
          elm?: Node,
          context?: Component,
          componentOptions?: VNodeComponentOptions,
          asyncFactory?: Function
        ) {
          this.tag = tag
          this.data = data
          this.children = children
          this.text = text
          this.elm = elm
          this.ns = undefined
          this.context = context
          this.fnContext = undefined
          this.fnOptions = undefined
          this.fnScopeId = undefined
          this.key = data && data.key
          this.componentOptions = componentOptions
          this.componentInstance = undefined
          this.parent = undefined
          this.raw = false
          this.isStatic = false
          this.isRootInsert = true
          this.isComment = false
          this.isCloned = false
          this.isOnce = false
          this.asyncFactory = asyncFactory
          this.asyncMeta = undefined
          this.isAsyncPlaceholder = false
        }
      
        get child (): Component | void {
          return this.componentInstance
        }
      }

      从代码不难看出 vNode 类创建的实例,本质上就是一个普通的 javascript 对象。

      vNode 的类型

      前面我们已经介绍通过 vNode 类可以创建不同类型的 vNode。而不同类型的 vNode 是由有效属性区分。例如 isComment = true 表示注释节点;isCloned = true 表示克隆节点等等。

      vNode 类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。

      以下是注释节点、文本节点和克隆节点的代码:

      /*
      注释节点
      有效属性:{isComment: true, text: '注释节点'}
      */
      export const createEmptyVNode = (text: string = '') => {
        const node = new VNode()
        node.text = text
        // 注释
        node.isComment = true
        return node
      }
      /*
      文本节点
      有效属性:{text: '文本节点'}
      */
      export function createTextVNode (val: string | number) {
        return new VNode(undefined, undefined, undefined, String(val))
      }
      
      // optimized shallow clone
      // used for static nodes and slot nodes because they may be reused across
      // 用于静态节点和插槽节点
      // multiple renders, cloning them avoids errors when DOM manipulations rely
      // on their elm reference.
      // 克隆节点
      export function cloneVNode (vnode: VNode): VNode {
        const cloned = new VNode(
          vnode.tag,
          vnode.data,
          // #7975
          // clone children array to avoid mutating original in case of cloning
          // a child.
          vnode.children && vnode.children.slice(),
          vnode.text,
          vnode.elm,
          vnode.context,
          vnode.componentOptions,
          vnode.asyncFactory
        )
        cloned.ns = vnode.ns
        cloned.isStatic = vnode.isStatic
        cloned.key = vnode.key
        cloned.isComment = vnode.isComment
        cloned.fnContext = vnode.fnContext
        cloned.fnOptions = vnode.fnOptions
        cloned.fnScopeId = vnode.fnScopeId
        cloned.asyncMeta = vnode.asyncMeta
        // 标记是克隆节点
        cloned.isCloned = true
        return cloned
      }

      克隆节点其实就是将现有节点的所有属性赋值到新节点中,最后用 cloned.isCloned = true 标记自身是克隆节点。

      元素节点通常有以下 4 个属性:

      • tag:节点名称。例如 div、p
      • data:节点上的数据。例如 class、style
      • children:子节点
      • context:在组件内呈现

      组件节点与元素节点类似,包含两个独有的属性:

      • componentOptions:组件节点的选项参数,例如propsData、listeners、children、tag
      • componentInstance:组件的实例

      patch

      前面已经介绍了虚拟 dom 在 vue 中做的第一件事:提供与真实节点(Node)对应的虚拟节点(vNode);接下来介绍第二件事:将新的虚拟节点与旧的虚拟节点进行对比,找出需要差异,然后更新视图。

      第二件事在 vue 中的实现叫做 patch,即打补丁、修补的意思。通过对比新旧 vNode,找出差异,然后在现有 dom 的基础上进行修补,从而实现视图更新。

      对比 vNode 找差异是手段,更新视图才是目的。

      而更新视图无非就是新增节点、删除节点和更新节点。接下来我们逐一分析什么时候新增节点、在哪里新增;什么时候删除节点,删除哪个;什么时候更新节点,更新哪个;

      注:当 vNode 与 oldVNode 不相同的时候,以 vNode 为准。

      新增节点

      一种情况是:vNode 存在而 oldVNode 不存在时,需要新增节点。最典型的是初次渲染,因为 odlVNode 是不存在的。

      另一种情况是 vNode 与 oldVNode 完全不是同一个节点。这时就需要使用 vNode 生成真实的 dom 节点并插入到 oldVNode 指向的真实 dom 节点旁边。oldVNode 则是一个被废弃的节点。例如下面这种情况:

      <div>
        <p v-if="type === 'A'">
          我是节点A
        </p>
        <span v-else-if="type === 'B'">
          我是与A完全不同的节点B
        </span>
      </div>

      当 type 由 A 变为 B,节点就会从 p 变成 span,由于 vNode 与 oldVNode 完全不是同一个节点,所以需要新增节点。

      删除节点

      当节点只在 oldVNode 中存在时,直接将其删除即可。

      更新节点

      前面介绍了新增节点和删除节点的场景,发现它们有一个共同点:vNode 与 oldVNode 完全不相同。

      但更常见的场景是 vNode 与 oldVNode 是同一个节点。然后我们需要对它们(vNode 与 oldVNode)进行一个更细致的对比,再对 oldVNode 对应的真实节点进行更新。

      对于文本节点,逻辑自然简单。首先对比新旧 vNode,发现是同一个节点,然后将 oldVNode 对应的 dom 节点的文本改成 vNode 中的文本即可。但对于复杂的 vNode,比如界面中的一颗树组件,这个过程就会变得复杂。

      新增节点 – 源码分析

      思考一下:前面说到 vNode 的类型有:注释节点、文本节点、克隆节点、元素节点、组件节点。请问这几种类型都会被创建并插入到 dom 中吗?

      答:只有注释节点、文本节点、元素节点。因为 html 只认识这几种。

      由于只有上面三种节点类型,根据类型做响应的创建,然后插入对应的位置即可。

      以元素节点为例,如果 vNode 有 tag 属性,则说明是元素节点。则调用 createElement 方法创建对应的节点,接下来就通过 appendChild 方法插入到指定父节点中。如果父元素已经在视图中,那么把元素插入到它下面将会自动渲染出来;如果 vNode 的 isComment 属性是 true,则表示注释节点;都不是则是文本节点;

      通常元素里面会有子节点,所以这里涉及一个递归的过程,也就是将 vNode 中的 children 依次遍历,创建节点,然后插入到父节点(父节点也就是刚刚创建出的 dom 节点)中,一层一层的递归进行。

      请看源码:

      // 创建元素
      function createElm (
        vnode,
        insertedVnodeQueue,
        parentElm,
        refElm,
        nested,
        ownerArray,
        index
      ) {
        if (isDef(vnode.elm) && isDef(ownerArray)) {
          // This vnode was used in a previous render!
          // now it's used as a new node, overwriting its elm would cause
          // potential patch errors down the road when it's used as an insertion
          // reference node. Instead, we clone the node on-demand before creating
          // associated DOM element for it.
          vnode = ownerArray[index] = cloneVNode(vnode);
        }
      
        vnode.isRootInsert = !nested; // for transition enter check
        if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
          return
        }
      
        var data = vnode.data;
        var children = vnode.children;
        var tag = vnode.tag;
        // 有 tag 属性,表示是元素节点
        if (isDef(tag)) {
          vnode.elm = vnode.ns
            ? nodeOps.createElementNS(vnode.ns, tag)
            // 创建元素。nodeOps 涉及到跨平台
            : nodeOps.createElement(tag, vnode);
          setScope(vnode);
      
          /* istanbul ignore if */
          {
            // 递归创建子节点,并将子节点插入到父节点上
            createChildren(vnode, children, insertedVnodeQueue);
            if (isDef(data)) {
              invokeCreateHooks(vnode, insertedVnodeQueue);
            }
            // 将 vnode 对应的元素插入到父元素中
            insert(parentElm, vnode.elm, refElm);
          }
      
        // isComment 属性表示注释节点
        } else if (isTrue(vnode.isComment)) {
          vnode.elm = nodeOps.createComment(vnode.text);
          // 插入父节点
          insert(parentElm, vnode.elm, refElm);
        // 否则就是子节点
        } else {
          vnode.elm = nodeOps.createTextNode(vnode.text);
          // 插入父节点
          insert(parentElm, vnode.elm, refElm);
        }
      }
      
      // 递归创建子节点,并将子节点插入到父节点上。vnode 表示父节点
      function createChildren (vnode, children, insertedVnodeQueue) {
        if (Array.isArray(children)) {
          if (process.env.NODE_ENV !== 'production') {
            checkDuplicateKeys(children);
          }
          // 依次创建子节点,并将子节点插入到父节点中
          for (var i = 0; i < children.length; ++i) {
            createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i);
          }
        } else if (isPrimitive(vnode.text)) {
          nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)));
        }
      }

      删除节点 – 源码分析

      删除节点非常简单。直接看源码:

      // 删除一组指定节点
      function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
        for (; startIdx <= endIdx; ++startIdx) {
          var ch = vnodes[startIdx];
          if (isDef(ch)) {
            if (isDef(ch.tag)) {
              removeAndInvokeRemoveHook(ch);
              invokeDestroyHook(ch);
            } else { // Text node
              // 删除个节点
              removeNode(ch.elm);
            }
          }
        }
      }
      
      // 删除单个节点
      function removeNode (el) {
        var parent = nodeOps.parentNode(el);
        // element may have already been removed due to v-html / v-text
        if (isDef(parent)) {
          // nodeOps里封装了跨平台的方法
          nodeOps.removeChild(parent, el);
        }
      }

      以上就是vue 虚拟DOM快速入门的详细内容,更多关于vue 虚拟DOM的资料请关注NICE源码其它相关文章!

      免责声明:
      1、本网站所有发布的源码、软件和资料均为收集各大资源网站整理而来;仅限用于学习和研究目的,您必须在下载后的24个小时之内,从您的电脑中彻底删除上述内容。 不得使用于非法商业用途,不得违反国家法律。否则后果自负!

      2、本站信息来自网络,版权争议与本站无关。一切关于该资源商业行为与www.niceym.com无关。
      如果您喜欢该程序,请支持正版源码、软件,购买注册,得到更好的正版服务。
      如有侵犯你版权的,请邮件与我们联系处理(邮箱:skknet@qq.com),本站将立即改正。

      NICE源码网 JavaScript vue 虚拟DOM快速入门 https://www.niceym.com/33633.html