vue:虛擬dom的實現

Vitual DOM是一種虛擬dom技術,本質上是基於javascript實現的,相對於dom對象,javascript對象更簡單,處理速度更快,dom樹的結構,屬性信息均可以很容易的用javascript對象來表示:javascript

let element={
    tagName:'ul',//節點標籤名
    props:{//dom的屬性,用一個對象存儲鍵值對
        id:'list'
    },
    children:[//該節點的子節點
        {tagName:'li',props:{class:'item'},children:['aa']},
        {tagName:'li',props:{class:'item'},children:['bb']},
        {tagName:'li',props:{class:'item'},children:['cc']}
    ]
}
對應的html寫法是:
<ul id='list'>
    <li class='item'>aa</li>
    <li class='item'>aa</li>
    <li class='item'>aa</li>
</ul>

Virtual DOM並無徹底實現DOMVirtual DOM最主要的仍是保留了Element之間的層次關係和一些基本屬性. 你給我一個數據,我根據這個數據生成一個全新的Virtual DOM,而後跟我上一次生成的Virtual DOMdiff,獲得一個Patch,而後把這個Patch打到瀏覽器的DOM上去。html

咱們能夠經過javascript對象表示的樹結構來構建一棵真正的dom樹,當數據狀態發生變化時,能夠直接修改這個javascript對象,接着對比修改後的javascript對象,記錄下須要對頁面作的dom操做,而後將其應用到真正的dom樹,實現視圖的更新,這個過程就是Virtual DOM的核心思想。vue

VNode的數據結構圖:
clipboard.pngjava

clipboard.png

VNode生成最關鍵的點是經過render2種生成方式,第一種是直接在vue對象的option中添加render字段。第二種是寫一個模板或指定一個el根元素,它會首先轉換成模板,通過html語法解析器生成一個ast抽象語法樹,對語法樹作優化,而後把語法樹轉換成代碼片斷,最後經過代碼片斷生成function添加到optionrender字段中。node

ast語法優的過程,主要作了2件事:segmentfault

  • 會檢測出靜態的class名和attributes,這樣它們在初始化渲染後就永遠不會再被比對了。
  • 會檢測出最大的靜態子樹(不須要動態性的子樹)而且從渲染函數中萃取出來。這樣在每次重渲染時,它就會直接重用徹底相同的vnode,同時跳過比對。
src/core/vdom/create-element.js

const SIMPLE_NORMALIZE = 1
const ALWAYS_NORMALIZE = 2

function createElement (context, tag, data, children, normalizationType, alwaysNormalize) {
  // 兼容不傳data的狀況
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 若是alwaysNormalize是true
  // 那麼normalizationType應該設置爲常量ALWAYS_NORMALIZE的值
  if (alwaysNormalize) normalizationType = ALWAYS_NORMALIZE
  // 調用_createElement建立虛擬節點
  return _createElement(context, tag, data, children, normalizationType)
}

function _createElement (context, tag, data, children, normalizationType) {
  /**
   * 若是存在data.__ob__,說明data是被Observer觀察的數據
   * 不能用做虛擬節點的data
   * 須要拋出警告,並返回一個空節點
   * 被監控的data不能被用做vnode渲染的數據的緣由是:
   * data在vnode渲染過程當中可能會被改變,這樣會觸發監控,致使不符合預期的操做
   */
  if (data && data.__ob__) {
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // 當組件的is屬性被設置爲一個falsy的值
  // Vue將不會知道要把這個組件渲染成什麼
  // 因此渲染一個空節點
  if (!tag) {
    return createEmptyVNode()
  }
  // 做用域插槽
  if (Array.isArray(children) &&
      typeof children[0] === 'function') {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 根據normalizationType的值,選擇不一樣的處理方法
  if (normalizationType === ALWAYS_NORMALIZE) {
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // 若是標籤名是字符串類型
  if (typeof tag === 'string') {
    let Ctor
    // 獲取標籤名的命名空間
    ns = config.getTagNamespace(tag)
    // 判斷是否爲保留標籤
    if (config.isReservedTag(tag)) {
      // 若是是保留標籤,就建立一個這樣的vnode
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
      // 若是不是保留標籤,那麼咱們將嘗試從vm的components上查找是否有這個標籤的定義
    } else if ((Ctor = resolveAsset(context.$options, 'components', tag))) {
      // 若是找到了這個標籤的定義,就以此建立虛擬組件節點
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // 兜底方案,正常建立一個vnode
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
    // 當tag不是字符串的時候,咱們認爲tag是組件的構造類
    // 因此直接建立
  } else {
    vnode = createComponent(tag, data, context, children)
  }
  // 若是有vnode
  if (vnode) {
    // 若是有namespace,就應用下namespace,而後返回vnode
    if (ns) applyNS(vnode, ns)
    return vnode
  // 不然,返回一個空節點
  } else {
    return createEmptyVNode()
  }
}

方法的功能是給一個Vnode對象對象添加若干個子Vnode,由於整個Virtual DOM是一種樹狀結構,每一個節點均可能會有若干子節點。而後建立一個VNode對象,若是是一個reserved tag(好比html,head等一些合法的html標籤)則會建立普通的DOM VNode,若是是一個component tag(經過vue註冊的自定義component),則會建立Component VNode對象,它的VnodeComponentOptions不爲Null.
建立好Vnode,下一步就是要把Virtual DOM渲染成真正的DOM,是經過patch來實現的,源碼以下:api

src/core/vdom/patch.js

  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) { // oldVnoe:dom||當前vnode,vnode:vnoder=對象類型,hydration是否直接用服務端渲染的dom元素
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // 空掛載(多是組件),建立新的根元素。
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue, parentElm, refElm)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch 現有的根節點
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
            // 安裝到一個真實的元素。
            // 檢查這是不是服務器渲染的內容,若是咱們能夠執行。
            // 成功的水合做用。
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // 不是服務器呈現,就是水化失敗。建立一個空節點並替換它。
          oldVnode = emptyNodeAt(oldVnode)
        }

        // 替換現有的元素
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)

        // create new node
        createElm(
          vnode,
          insertedVnodeQueue,
          // 極爲罕見的邊緣狀況:若是舊元素在a中,則不要插入。
            // 離開過渡。只有結合過渡+時纔會發生。
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // 遞歸地更新父佔位符節點元素。
        if (isDef(vnode.parent)) {
          let ancestor = vnode.parent
          const patchable = isPatchable(vnode)
          while (ancestor) {
            for (let i = 0; i < cbs.destroy.length; ++i) {
              cbs.destroy[i](ancestor)
            }
            ancestor.elm = vnode.elm
            if (patchable) {
              for (let i = 0; i < cbs.create.length; ++i) {
                cbs.create[i](emptyNode, ancestor)
              }
              // #6513
              // 調用插入鉤子,這些鉤子可能已經被建立鉤子合併了。
                // 例如使用「插入」鉤子的指令。
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
                // 從索引1開始,以免從新調用組件掛起的鉤子。
                for (let i = 1; i < insert.fns.length; i++) {
                  insert.fns[i]()
                }
              }
            } else {
              registerRef(ancestor)
            }
            ancestor = ancestor.parent
          }
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }

    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }

patch支持的3個參數,其中oldVnode是一個真實的DOM或者一個VNode對象,它表示當前的VNode,vnodeVNode對象類型,它表示待替換的VNode,hydrationbool類型,它表示是否直接使用服務器端渲染的DOM元素,下面流程圖表示patch的運行邏輯:瀏覽器

clipboard.png

patch運行邏輯看上去比較複雜,有2個方法createElmpatchVnode是生成dom的關鍵,源碼以下:服務器

/**
 * @param vnode根據vnode的數據結構建立真實的dom節點,若是vnode有children則會遍歷這些子節點,遞歸調用createElm方法,
 * @param insertedVnodeQueue記錄子節點建立順序的隊列,每建立一個dom元素就會往隊列中插入當前的vnode,當整個vnode對象所有轉換成爲真實的dom 樹時,會依次調用這個隊列中vnode hook的insert方法
 */

  let inPre = 0
  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    vnode.isRootInsert = !nested // 過渡進入檢查
    if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
      return
    }

    const data = vnode.data
    const children = vnode.children
    const tag = vnode.tag
    if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          inPre++
        }
        if (
          !inPre &&
          !vnode.ns &&
          !(
            config.ignoredElements.length &&
            config.ignoredElements.some(ignore => {
              return isRegExp(ignore)
                ? ignore.test(tag)
                : ignore === tag
            })
          ) &&
          config.isUnknownElement(tag)
        ) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
      vnode.elm = vnode.ns
        ? nodeOps.createElementNS(vnode.ns, tag)
        : nodeOps.createElement(tag, vnode)
      setScope(vnode)

      /* istanbul ignore if */
      if (__WEEX__) {
        // in Weex, the default insertion order is parent-first.
        // List items can be optimized to use children-first insertion
        // with append="tree".
        const appendAsTree = isDef(data) && isTrue(data.appendAsTree)
        if (!appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
        createChildren(vnode, children, insertedVnodeQueue)
        if (appendAsTree) {
          if (isDef(data)) {
            invokeCreateHooks(vnode, insertedVnodeQueue)
          }
          insert(parentElm, vnode.elm, refElm)
        }
      } else {
        createChildren(vnode, children, insertedVnodeQueue)
        if (isDef(data)) {
          invokeCreateHooks(vnode, insertedVnodeQueue)
        }
        insert(parentElm, vnode.elm, refElm)
      }

      if (process.env.NODE_ENV !== 'production' && data && data.pre) {
        inPre--
      }
    } 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的數據結構建立真實的DOM節點,若是vnodechildren,則會遍歷這些子節點,遞歸調用createElm方法,InsertedVnodeQueue是記錄子節點建立順序的隊列,每建立一個DOM元素就會往這個隊列中插入當前的VNode,當整個VNode對象所有轉換成爲真實的DOM樹時,會依次調用這個隊列中的VNode hookinsert方法。數據結構

/**
     * 比較新舊vnode節點,根據不一樣的狀態對dom作合理的更新操做(添加,移動,刪除)整個過程還會依次調用prepatch,update,postpatch等鉤子函數,在編譯階段生成的一些靜態子樹,在這個過程
     * @param oldVnode 中因爲不會改變而直接跳過比對,動態子樹在比較過程當中比較核心的部分就是當新舊vnode同時存在children,經過updateChildren方法對子節點作更新,
     * @param vnode
     * @param insertedVnodeQueue
     * @param removeOnly
     */
  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    if (oldVnode === vnode) {
      return
    }

    const elm = vnode.elm = oldVnode.elm

    if (isTrue(oldVnode.isAsyncPlaceholder)) {
      if (isDef(vnode.asyncFactory.resolved)) {
        hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
      } else {
        vnode.isAsyncPlaceholder = true
      }
      return
    }

      // 用於靜態樹的重用元素。
        // 注意,若是vnode是克隆的,咱們只作這個。
        // 若是新節點不是克隆的,則表示呈現函數。
        // 由熱重加載api從新設置,咱們須要進行適當的從新渲染。
    if (isTrue(vnode.isStatic) &&
      isTrue(oldVnode.isStatic) &&
      vnode.key === oldVnode.key &&
      (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
    ) {
      vnode.componentInstance = oldVnode.componentInstance
      return
    }

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }
  }

updateChildren方法解析在此:vue:虛擬DOM的patch

相關文章
相關標籤/搜索