从 vue 源码看问题 —— vue 中如何进行 patch ?(一)

简介: 从 vue 源码看问题 —— vue 中如何进行 patch ?

image.png

前言

当组件进行更新时,new Watcher(vm, updateComponent, ...) 中的 updateComponent 方法会被执行,而更新时的 updateComponent 方法如下:

// 负责更新组件
updateComponent = () => {
  // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode 
  vm._update(vm._render(), hydrating)
}
复制代码

首先会执行 vm._render() 得到组件的 VNode,接着将 VNode 传递给 vm._update 方法,就正式开始 patch 阶段.

vue1.x 到 vue2.x 的转变

vue1.x

1.x 中并没有 2.x 中对应的 VNodediff 的概念,因为 1.x 的核心是 响应式,即 Object.definePropertyDepWatcher.

  • Object.defineProperty: 负责数据的拦截(本质就是对象属性的劫持),对数据属性 key 设置 gettersetter,在 getter触发时进行依赖收集,在 setter 触发时通过 dep 通知对应的 watcher 进行更新
  • Dep:每个 dep 实例和 data 选项中返回对象的 key一对一 的关系
  • Watcherdata 选项中返回对象的 keywatcher一对多 的关系,模版中每使用一次 key 就会生成一个对应的 watcher

由于 watcherDOM 属于 一对一 的关系,当数据发生更新时,dep 会通知 watcher 直接更新对应的 DOM,即 定向更新; 所以更新的效率是非常高的,因为 watcher 可以明确知道与它对应的 key 在组件模版中的具体位置(即对应的 dom 元素).

但是这种高效的更新并不适用于复杂场景,因为当页面足够复杂时(即包含很多组件),对应的页面会就产生大量的 watcher 与真实 dom 进行强关联,这是非常耗资源.

vue2.x

由于 vue1.x 存在的问题,于是 Vue 2.0 就引入了 VNodediff 算法解决问题.

针对复杂页面 watcher 太多导致性能下降的问题,Vue 2.0 中将 watcher 的粒度放大,即一个组件对应一个 watcher(渲染 watcher),这样 watcher 的维度就属于是组件级别,而不是单个 DOM 级别.

当响应式数据更新时,dep 通知 watcher 去更新组件内容,这对于 vue1.x 来说是很简单的,但是 vue2.0 中的 watcher 是组件级别,因此这个 watcher 并不知道要更新模板中的哪些位置.

于是 vue2.0 中通过引入 VNode 来查找本次组件需要更新的内容

当组件中数据更新时,会通过调用 render 方法为组件生成一个新的 VNode,将新的 VNode 与 旧的 VNode 通过 diff 算法进行比较,查找本次需要更新的内容,接着执行 DOM 操作去更新对应节点.

深入源码

入口

文件位置:src\core\instance\lifecycle.js

export function mountComponent (
  vm: Component,
  el: ?Element,
  hydrating?: boolean
): Component {
  ...
  callHook(vm, 'beforeMount')
  let updateComponent
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    updateComponent = () => {
     ...
    }
  } else {
    // 负责更新组件
    updateComponent = () => {
      // 执行 _update 进入更新阶段,首先会执行 _render,将组件变成 VNode 
      vm._update(vm._render(), hydrating)
    }
  }
  ...
 if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}
复制代码

vm._update()

文件位置:src\core\instance\lifecycle.js

// 组件初始化渲染和更新时的入口
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // prevVnode 不存在,代表是初始化渲染
    if (!prevVnode) {
      // patch 阶段:patch、diff 算法
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
       // prevVnode 存在,代表是后续更新阶段
      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.
  }
复制代码

vm.__patch__()

文件位置:src\platforms\web\runtime\index.jsVue 原型上挂载 __patch__ 方法

// install platform patch function
Vue.prototype.__patch__ = inBrowser ? patch : noop
复制代码

patch()

文件位置:src\platforms\web\runtime\patch.js

通过 createPatchFunction() 工厂函数,为其传入平台特有的一些操作,然后返回一个 patch 函数

export const patch: Function = createPatchFunction({ nodeOps, modules })
复制代码

nodeOps

文件位置:src\platforms\web\runtime\node-ops.js

/**
 * web 平台的 DOM 操作 API
 */
// 创建标签名为 tagName 的元素节点
export function createElement (tagName: string, vnode: VNode): Element {
  // 创建元素
  const elm = document.createElement(tagName)
  // 非 select 元素直接返回
  if (tagName !== 'select') {
    return elm
  }
  // false or null will remove the attribute but undefined will not
  // 如果是 select 元素,则为它设置 multiple 属性
  if (vnode.data && vnode.data.attrs && vnode.data.attrs.multiple !== undefined) {
    elm.setAttribute('multiple', 'multiple')
  }
  return elm
}
// 创建带命名空间的元素节点
export function createElementNS (namespace: string, tagName: string): Element {
  return document.createElementNS(namespaceMap[namespace], tagName)
}
// 创建文本节点
export function createTextNode (text: string): Text {
  return document.createTextNode(text)
}
// 创建注释节点
export function createComment (text: string): Comment {
  return document.createComment(text)
}
// 在指定节点前插入节点
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}
// 移除子节点
export function removeChild (node: Node, child: Node) {
  node.removeChild(child)
}
// 添加子节点
export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
// 返回指定节点的父节点
export function parentNode (node: Node): ?Node {
  return node.parentNode
}
// 返回指定节点的下一个兄弟节点
export function nextSibling (node: Node): ?Node {
  return node.nextSibling
}
// 返回指定节点的标签名 
export function tagName (node: Element): string {
  return node.tagName
}
// 为指定节点设置文本
export function setTextContent (node: Node, text: string) {
  node.textContent = text
}
// 为节点设置指定的 scopeId 属性,属性值为 '',如 <div scopeId></div>
export function setStyleScope (node: Element, scopeId: string) {
  node.setAttribute(scopeId, '')
}
复制代码

modules

文件位置:src\core/vdom/modules/index.js + web/runtime/modules/index.js

平台特有的一些操作,如:attr、class、style、event 等,还有核心的 directiveref,它们会向外暴露一些特有的方法,比如:create、activate、update、remove、destroy,这些方法在 patch 阶段时会被调用,从而做相应的操作,比如创建 attr、指令等.

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)
复制代码

createPatchFunction()

文件位置:src\core\vdom\patch.js

const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
/* 
  工厂函数:
     注入平台特有的一些功能操作,并定义一些方法,然后返回 patch 函数 
*/ 
export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  /*
    modules: { ref, directives, 平台特有的一些操纵,比如 attr、class、style 等 }
    nodeOps: { 对元素的增删改查 API }
  */
  const { modules, nodeOps } = backend
  /*
    hooks = ['create', 'activate', 'update', 'remove', 'destroy']
    遍历这些钩子,然后从 modules 的各个模块中找到相应的方法,
    比如:directives 中的 create、update、destroy 方法
    让这些方法放到 cbs[hook] = [hook 方法] 中,比如: cb.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]])
      }
    }
  }
 ...
  /*
    vm.__patch__
      1、新节点不存在,老节点存在,调用 destroy,销毁老节点
      2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
      3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
  */
 return function patch (oldVnode, vnode, hydrating, removeOnly){
  ...
 }
}
复制代码

createPatchFunction 中返回的 patch()

/*
    vm.__patch__
      1、新节点不存在,老节点存在,调用 destroy,销毁老节点
      2、如果 oldVnode 是真实元素,则表示首次渲染,创建新节点,并插入 body,然后移除老节点
      3、如果 oldVnode 不是真实元素,则表示更新阶段,执行 patchVnode
  */
  function patch(oldVnode, vnode, hydrating, removeOnly) {
    // 新节点不存在,老节点存在,则调用 destroy,销毁老节点
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }
    let isInitialPatch = false
    const insertedVnodeQueue = []
    // 老的 VNode 不存在
    if (isUndef(oldVnode)) {
      /*
        老的 VNode 不存在,新的 VNode 存在,这种情况会在一个组件初次渲染的时候出现,
        比如:<div id="app">
                <comp></comp>
              </div>
        这里的 comp 组件初次渲染时就会走这儿
        empty mount (likely as component), create new root element
      */
      isInitialPatch = true
      createElm(vnode, insertedVnodeQueue)
    } else {
      // 老的 VNode 存在
      // 判断老的 VNode 是否是一个真实的 dom 节点
      const isRealElement = isDef(oldVnode.nodeType)
      /*
        老的 VNode 不是真实元素,但是老节点和新节点是同一个节点,
        则属于更新阶段,需要执行 patch 更新节点
        patch existing root node
      */
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
      } else {
        // 老的 VNode 是真实元素
        if (isRealElement) {
          /* 
            挂载到真实元素以及处理服务端渲染的情况
            oldVnode.nodeType === 1 代表的是 html 元素
          */
          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.'
              )
            }
          }
          /*
           走到这儿说明不是服务端渲染,或者 hydration 失败,
           则根据 oldVnode 创建一个 vnode 节点替换 oldVnode
          */ 
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        // 获取老节点的真实元素
        const oldElm = oldVnode.elm
        // 获取老节点的父元素,即 body 元素
        const parentElm = nodeOps.parentNode(oldElm)
        // 基于新 vnode 创建整棵 DOM 树并插入到 body 元素下
        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
          }
        }
        // 移除老节点
        if (isDef(parentElm)) {
          removeVnodes([oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
复制代码



目录
相关文章
|
1天前
|
存储 JavaScript 前端开发
vue尚品汇商城项目-day05【30.登录与注册静态组件(处理公共图片资源问题)+31.注册的业务+登录业务】
vue尚品汇商城项目-day05【30.登录与注册静态组件(处理公共图片资源问题)+31.注册的业务+登录业务】
10 1
|
1天前
|
存储 JavaScript API
vue尚品汇商城项目-day05【33.token令牌理解+34.用户登录携带token获取用户信息+35.退出登录】
vue尚品汇商城项目-day05【33.token令牌理解+34.用户登录携带token获取用户信息+35.退出登录】
7 0
|
1天前
|
JavaScript API 数据安全/隐私保护
vue尚品汇商城项目-day05【36.导航守卫理解】
vue尚品汇商城项目-day05【36.导航守卫理解】
7 0
|
1天前
|
JavaScript API
vue尚品汇商城项目-day06【37.获取交易数据+38.用户地址信息展示+39.交易信息展示及交易页面完成+40.提交订单+41.支付组件内获取订单号与展示支付信息】
vue尚品汇商城项目-day06【37.获取交易数据+38.用户地址信息展示+39.交易信息展示及交易页面完成+40.提交订单+41.支付组件内获取订单号与展示支付信息】
6 0
|
1天前
|
JavaScript 前端开发
vue尚品汇商城项目-day06【vue插件-42.支付页面中使用ElementUI以及按需引入】
vue尚品汇商城项目-day06【vue插件-42.支付页面中使用ElementUI以及按需引入】
6 0
|
1天前
|
JavaScript API
vue尚品汇商城项目-day07【44.个人中心二级路由搭建+45.我的订单+46.优化登录跳转+47.独享守卫】
vue尚品汇商城项目-day07【44.个人中心二级路由搭建+45.我的订单+46.优化登录跳转+47.独享守卫】
6 0
|
1天前
|
JavaScript
vue尚品汇商城项目-day07【vue插件-48.(了解)图片懒加载插件】
vue尚品汇商城项目-day07【vue插件-48.(了解)图片懒加载插件】
7 0
|
JavaScript 测试技术 容器
Vue2+VueRouter2+webpack 构建项目
1). 安装Node环境和npm包管理工具 检测版本 node -v npm -v 图1.png 2). 安装vue-cli(vue脚手架) npm install -g vue-cli --registry=https://registry.
1039 0
|
10天前
|
JavaScript
vue组件中的插槽
本文介绍了Vue中组件的插槽使用,包括单个插槽和多个具名插槽的定义及在父组件中的使用方法,展示了如何通过插槽将父组件的内容插入到子组件的指定位置。
|
8天前
|
JavaScript
vue消息订阅与发布
vue消息订阅与发布
下一篇
无影云桌面