Vue源码解读(六):update和patch

Vue 的 _update 是实例上的一个私有方法,主要的作用就是把 VNode 渲染成真实的 DOM ,它在首次渲染和数据更新的时候被调用。在数据更新的时候会发生新 VNode 和 旧 VNode 对比,获取差异更新视图,我们常说的 diff 就是发生在此过程中。

_update

// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
   
  const vm: Component = this
  // 页面的挂载点,真实的元素
  const prevEl = vm.$el
  // 老 VNode
  const prevVnode = vm._vnode
  const restoreActiveInstance = setActiveInstance(vm)
  // 新 VNode
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
   
    // 老 VNode 不存在,表示首次渲染,即初始化页面时走这里
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
   
    // 响应式数据更新时,即更新页面时走这里
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  restoreActiveInstance()
  // update __vue__ reference
  if (prevEl) {
   
    prevEl.__vue__ = null
  }
  if (vm.$el) {
   
    vm.$el.__vue__ = vm
  }
  // if parent is an HOC, update its $el as well
  if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
   
    vm.$parent.$el = vm.$el
  }
  // updated hook is called by the scheduler to ensure that children are
  // updated in a parent's updated hook.
}

__patch__

// src/platforms/web/runtime/index.js
//  web 平台的 patch 函数,不同平台的定义不相同。
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch

// src/platforms/web/runtime/patch.js
import * as nodeOps from 'web/runtime/node-ops'
import {
    createPatchFunction } from 'core/vdom/patch'
import baseModules from 'core/vdom/modules/index'
import platformModules from 'web/runtime/modules/index'

// the directive module should be applied last, after all
// built-in modules have been applied.
const modules = platformModules.concat(baseModules)

// 传入平台特有的一些操作,然后返回一个 patch 函数
export const patch: Function = createPatchFunction({
    nodeOps, modules })

patchcreatePatchFunction 的返回值,这里传入了一个对象,nodeOps 封装了一系列相关平台 DOM的一些操作方法,modules 表示平台特有的一些操作,比如:attr、class、style、event 等,还有核心的 directive 和 ref,它们会向外暴露一些特有的方法。这里不做详细介绍了,有兴趣的老铁可以去了解了解。

createPatchFunction

// src/core/vdom/patch.js
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
/**
 * 传入相关平台一些功能操作,最后返回 patch 函数
 */
export function createPatchFunction (backend) {
   
  let i, j
  const cbs = {
   }

  const {
    modules, nodeOps } = backend
  /**
   * hooks = ['create', 'activate', 'update', 'remove', 'destroy']
   * 遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,比如:directives 中的 create、update、destroy 方法
   *  cbs[hook] = [hook 方法],比如: cbs.create = [fn1, fn2, ...]
   */
  for (i = 0; i < hooks.length; ++i) {
   
    // 比如 cbs.create = []
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
   
      if (isDef(modules[j][hooks[i]])) {
   
        // 遍历各个 modules,找出各个 module 中的 create 方法,然后添加到 cbs.create 数组中
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  
  /**
   * ...这里定义了一大堆辅助函数,下面用到了捡重点看一下,不一一描述了。
   */
  
  /**
   * @param oldVnode  旧节点,可以不存在或是一个 DOM 对象
   * @param vnode  新节点,_render 返回的节点
   * @param hydrating  是否是服务端渲染
   * @param removeOnly  是给 transition-group 用的
   */
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
   
    // 新节点不存在,旧节点存在,表示移除,销毁旧节点。
    if (isUndef(vnode)) {
   
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    if (isUndef(oldVnode)) {
   
      // 新节点存在,旧节点不存在,表示新增。
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
   
      // 新旧节点都存在
      
      // 旧节点是否为真实的元素
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
   
        // patch existing root node
        // 都存在,旧节点不是真实元素且新旧节点是同一节点,表示修改 去做对比
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
   
         // 都存在,旧节点不是真实元素或新旧节点不是同一节点
        if (isRealElement) {
   
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          // 挂载到真实元素和处理服务端渲染
          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.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          // 不是服务端渲染或服务端渲染失败,把 oldVnode 转换成 VNode 对象.
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        // 旧节点的真实元素
        const oldElm = oldVnode.elm
        // 旧节点的父元素
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        // 通过虚拟节点创建真实的元素并插入到它的父节点中
        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)
        )
        // update parent placeholder node element, recursively
        // 递归更新父占位符节点元素(异步组件)
        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
              // invoke insert hooks that may have been merged by create hooks.
              // e.g. for directives that uses the "inserted" hook.
              const insert = ancestor.data.hook.insert
              if (insert.merged) {
   
                // start at index 1 to avoid re-invoking component mounted hook
                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([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
   
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

createPatchFunction 返回的 patch 方法来看,主要分为这么几种情况做处理:

  • 新节点不存在,旧节点存在,表示移除
  • 新节点存在,旧节点不存在,表示新增
  • 新旧节点都存在,旧节点不是真实的元素且新旧节点是同一节点,表示 修改(更新)
  • 新旧节点都存在,旧节点是真实的元素,一般是初始化渲染,旧节点的真实 DOM 也就是传入进来的 vm.$el 对应的元素,比如 <div id="app">;这里还有一个情况就是当 vnode.parent 存在,又不是同一节点,表示 替换(更新),比如异步组件。

移除节点

invokeDestroyHook

// src/core/vdom/patch.js
/**
 * 销毁节点:
 *   执行组件的 destroy 钩子,即执行 $destroy 方法 
 *   执行组件各个模块(style、class、directive 等)的 destroy 方法
 *   如果 vnode 还存在子节点,则递归调用 invokeDestroyHook
 */
function invokeDestroyHook (vnode) {
   
  let i, j
  const data = vnode.data
  if (isDef(data)) {
   
    if (isDef(i = data.hook) && isDef(i = i.destroy)) i(vnode)
    for (i = 0; i < cbs.destroy.length; ++i) cbs.destroy[i](vnode)
  
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
好的,让我来回答你的问题。 Vue 组件 object_vue 码系列一是关于 Object.defineProperty 的。Object.defineProperty 是 JavaScript 中的一个方法,可以用来定义对象的属性。这个方法可以让我们定义一个新的属性或者修改一个已经存在的属性。这个方法的语法如下: ```javascript Object.defineProperty(obj, prop, descriptor) ``` 其中,obj 是要定义属性的对象,prop 是要定义或修改的属性名,descriptor 是属性的描述符,它是一个对象,可以包含以下属性: - value:属性的值,默认为 undefined。 - writable:属性是否可写,默认为 false。 - enumerable:属性是否可枚举,默认为 false。 - configurable:属性是否可配置,默认为 false。 使用 Object.defineProperty 方法,可以实现一些高级的对象操作,例如: 1. 将一个属性设置为只读,即无法修改。 2. 将一个属性设置为不可枚举,即无法通过 for...in 循环遍历到该属性。 3. 将一个属性设置为不可配置,即无法删除该属性或者修改该属性的描述符。 在 Vue 中,Object.defineProperty 方法被广泛地应用于组件的实现中,例如: 1. 监听数据变化,通过设置 getter 和 setter 方法,实现数据的响应式更新。 2. 实现 computed 计算属性,通过设置 getter 方法,实现计算属性的缓存和响应式更新。 3. 实现 watch 监听器,通过设置 getter 方法,监听数据的变化并触发回调函数。 以上就是我对你提出的问题的回答。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值