【Vue源码】第八节数据驱动之_update方法

今天学习的时候发现关于_update有点太复杂了,这部分的源码得慢慢坑,三言两语讲不清楚啊…

首先先记住两点:

  • _update首次渲染数据更新(即响应式时)会被调用。这次分析只会跟首次渲染有关系;
  • _update是将VNode转换为真实的DOM
    我们调用_update是在$mount的时候
vm._update(vm._render(), hydrating)

先找到_update所在的文件

// src/core/instance/lifecycle.js
// 关于首次渲染主要是这行代码
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
// src/platforms/web/runtime/index.js
// inBrowser 表示是否处于浏览器端,其实就是区分浏览器端渲染和服务端渲染。因为在服务端没有真实的 DOM,它不需要把 VNode 转换成真实的 DOM,因此直接返回 noop(空函数)。
Vue.prototype.__patch__ = inBrowser ? patch : noop
// src/platforms/web/runtime/patch.js
const modules = platformModules.concat(baseModules)
export const patch: Function = createPatchFunction({
    
     nodeOps, modules })

其中,对象的 nodeOps 属性定义在 src/platforms/web/runtime/node-ops.js 中,这个文件里面定义了很多关于原生DOM操作,得去细看一下源码,三言两语讲不完TAT。对象modules 是由 platformModulesbaseModules 合并而来,里面定义了一些模块的钩子函数的实现,用于在虚拟 DOM 转换为真实 DOM 之后给真实 DOM 添加 attrclassstyleDOM 属性。

接下来看看createPatchFunction

export const emptyNode = new VNode('', {
    
    }, [])
// 这个是生命周期的钩子
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

function sameVnode (a, b) {
    
    }

function sameInputType (a, b) {
    
    }

function createKeyToOldIdx (children, beginIdx, endIdx) {
    
    }

export function createPatchFunction (backend) {
    
    
  let i, j
  const cbs = {
    
    }

  const {
    
     modules, nodeOps } = backend
  // 在 patch 的不同时期会调用不同的钩子函数
  for (i = 0; i < hooks.length; ++i) {
    
    
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
    
    
      if (isDef(modules[j][hooks[i]])) {
    
    
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  // 中间定义了一些辅助函数
  function emptyNodeAt (elm) {
    
     }

  function createRmCb (childElm, listeners) {
    
       return remove
  }

  function removeNode (el) {
    
     }

  let inPre = 0
  function createElm (vnode, insertedVnodeQueue, parentElm, refElm, nested) {
    
    }

  function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    
    }

  function initComponent (vnode, insertedVnodeQueue) {
    
    }

  function reactivateComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
    
     }

  function insert (parent, elm, ref) {
    
    }

  function createChildren (vnode, children, insertedVnodeQueue) {
    
     }

  function isPatchable (vnode) {
    
    }

  function invokeCreateHooks (vnode, insertedVnodeQueue) {
    
    }

  function setScope (vnode) {
    
     }

  function addVnodes (parentElm, refElm, vnodes, startIdx, endIdx, insertedVnodeQueue) {
    
    }

  function invokeDestroyHook (vnode) {
    
      }
  }

  function removeVnodes (parentElm, vnodes, startIdx, endIdx) {
    
    }

  function removeAndInvokeRemoveHook (vnode, rm) {
    
    }

  function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
    
    }

  function findIdxInOld (node, oldCh, start, end) {
    
    }

  function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
    
     }

  function invokeInsertHook (vnode, queue, initial) {
    
    }

  let bailed = false
  // list of modules that can skip create hook during hydration because they
  // are already rendered on the client or has no need for initialization
  const isRenderedModule = makeMap('attrs,style,class,staticClass,staticStyle,key')

  // Note: this is a browser-only function so we can assume elms are DOM nodes.
  function hydrate (elm, vnode, insertedVnodeQueue) {
    
    }

  function assertNodeMatch (node, vnode) {
    
    }


  // 初次渲染
  // vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  // 传入的 vm.$el 对应的是例子中 id 为 app 的 DOM 对象,这个也就是我们在 index.html 模板中写的 <div id="app"> 
  // vnode 对应的是调用 render 函数的返回值,
  // hydrating 在非服务端渲染情况下为 false,removeOnly 为 false。
  
  // 更新渲染
  // vm.$el = vm.__patch__(prevVnode, vnode)
  return function patch (oldVnode, vnode, hydrating, removeOnly, parentElm, refElm) {
    
    
    // oldVnode 表示旧的 VNode 节点,它也可以不存在或者是一个 DOM 对象
    // vnode 表示执行 _render 后返回的 VNode 的节点
    // hydrating 表示是否是服务端渲染
    // removeOnly 是给 transition-group 用的
    
    // 判断 vnode 是否存在,我们的例子是返回了一个vnode的所以跳过
    if (isUndef(vnode)) {
    
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
    
    
      // ...
    } else {
    
    
      // 第一次执行 patch 函数时传递的 oldVnode 参数是 vm.$el ,也就是要被替换的 DOM 节点
      // 更新调用 patch 函数时传递的 oldVnode 参数是 prevVnode ,所以是一个虚拟 DOM
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
    
    
        // ...
      } else {
    
    
        if (isRealElement) {
    
    
          // 服务端渲染, 跳过
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
    
     }
          // // 由于传入的 hydrating 为 false ,因此也不执行
          if (isTrue(hydrating)) {
    
    }
            
          // 把 oldVnode 转换成 VNode 对象
          // 在emptyNodeAt中我们将oldVnode传给了oldVnode.elm
          oldVnode = emptyNodeAt(oldVnode)
        }
        // oldElm拿到原来的 oldVnode
        const oldElm = oldVnode.elm
        // 拿到父节点body
        const parentElm = nodeOps.parentNode(oldElm)
        // createElm的作用是通过虚拟节点创建真实的 DOM 并插入到它的父节点中,接下来分析 createElm
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )
        // 这是父占位节点,是和组件相关的,这里不会执行
        if (isDef(vnode.parent)) {
    
    }
        
        // 判断之前定义的 parentElm 是否存在,有则删除掉对应的节点
        // 我们之前在 const parentElm = nodeOps.parentNode(oldElm) 中拿到了id=app 的父元素body
        // 之后删除<div id="app"></div>完成新旧节点替换工作。
        if (isDef(parentElm)) {
    
    
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
    
    
          invokeDestroyHook(oldVnode)
        }
      }
    }
    // 我们根据之前递归 createElm 生成的 vnode 插入顺序队列,执行相关的 insert 钩子函数
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    // 最后将vnode.elm(也就是真实DOM)返回
    return vnode.elm
  }
}
// createElm
// src/core/vdom/patch.js
function createElm (
  vnode,
  insertedVnodeQueue,
  parentElm,
  refElm,
  nested,
  ownerArray,
  index
) {
    
    
  // ownerArray为空,跳过
  if (isDef(vnode.elm) && isDef(ownerArray)) {
    
    
    // ...
  }
 
  vnode.isRootInsert = !nested // for transition enter check
  // 调用了 createComponent 函数,这个函数的功能是创建子组件, 我们这里是返回false
  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {
    
    
    return
  }
  
  const data = vnode.data
  const children = vnode.children
  const tag = vnode.tag
  // 在 tag 存在的情况下判断 tag 标签是否合法
  if (isDef(tag)) {
    
    
    if (process.env.NODE_ENV !== 'production') {
    
    
      if (data && data.pre) {
    
    
        creatingElmInVPre++
      }
      if (isUnknownElement(vnode, creatingElmInVPre)) {
    
    
        warn(
          'Unknown custom element: <' + tag + '> - did you ' +
          'register the component correctly? For recursive components, ' +
          'make sure to provide the "name" option.',
          vnode.context
        )
      }
    }
    // 调用平台 DOM 的操作去创建一个占位符元素
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
      : nodeOps.createElement(tag, vnode)

    if (__WEEX__) {
    
    
      // weex相关
    } else {
    
    
      // 创建子节点,分析createChildren
      createChildren(vnode, children, insertedVnodeQueue)
      if (isDef(data)) {
    
    
        // 然后把 vnode push 到 insertedVnodeQueue 中,分析invokeCreateHooks 
        invokeCreateHooks(vnode, insertedVnodeQueue)
      }
      // 把 vnode.elm 插入到父节点 parentElm 中,分析insert
      insert(parentElm, vnode.elm, refElm)
    }

    if (process.env.NODE_ENV !== 'production' && data && data.pre) {
    
    
      creatingElmInVPre--
    }
  // 创建注释节点的
  } 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)
  }
      
}

// createElm 函数就分析完了,其实基本流程就是先创建当前节点 vnode.elm,然后把 vnode.children 插到 vnode.elm 中,再把 vnode.elm 插到父节点 parentElm 中。
// createChildren
// src/core/vdom/patch.js
// 如果 children 是个数组则遍历数组并递归调用 createElm 把所有虚拟子节点转换为真实 DOM 节点然后插入到父节点 vnode.elm 中;
// 如果是一个文本 VNode 则直接 appendChild 插到 vnode.elm 里面。
function createChildren (vnode, children, insertedVnodeQueue) {
    
    
  if (Array.isArray(children)) {
    
    
    if (process.env.NODE_ENV !== 'production') {
    
    
      checkDuplicateKeys(children)
    }
    for (let 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)))
  }
}
// invokeCreateHooks 
// src/core/vdom/patch.js
// invokeCreateHooks 函数其实就是遍历 cbs.create 中的所有函数,然后把 vnode push 到 insertedVnodeQueue 中
function invokeCreateHooks (vnode, insertedVnodeQueue) {
    
    
  for (let i = 0; i < cbs.create.length; ++i) {
    
    
    cbs.create[i](emptyNode, vnode)
  }
  i = vnode.data.hook // Reuse variable
  if (isDef(i)) {
    
    
    if (isDef(i.create)) i.create(emptyNode, vnode)
    if (isDef(i.insert)) insertedVnodeQueue.push(vnode)
  }
}
// insert
// src/core/vdom/patch.js

function insert (parent, elm, ref) {
    
    
  if (isDef(parent)) {
    
    
    // 有参考节点 ref 就调用 insertBefore 插入到参考节点 ref 前,没有就插到父节点 parent 中。
    if (isDef(ref)) {
    
    
      if (nodeOps.parentNode(ref) === parent) {
    
    
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
    
    
      nodeOps.appendChild(parent, elm)
    }
  }
}

总结

  • _update的作用是将VNode转为真实的DOM,即显示在视图上;
  • _update触发的时机分别是在首次渲染更新数据的时候,本次讨论的时候是在首次渲染时;
  • _update的核心是_patch
  • _patch的作用(在首次渲染时)的主要工作是调用createElm,先创建当前节点vnode.elm,然后把vnode.children 插到 vnode.elm 中,再把 vnode.elm 插到父节点 parentElm 中;
  • 最后插入新的节点,删除页面上的旧节点,并执行对应的钩子函数,并返回当前节点vnode.elm
  • _pacth函数中使用了柯里化函数的思想,提高了函数的复用性,值得我们去细细推敲。

猜你喜欢

转载自blog.csdn.net/qq_34086980/article/details/105814088