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



目录
相关文章
|
19天前
|
人工智能 JavaScript 算法
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
Vue 中 key 属性的深入解析:改变 key 导致组件销毁与重建
129 0
|
20天前
|
JavaScript UED
用组件懒加载优化Vue应用性能
用组件懒加载优化Vue应用性能
|
1月前
|
JavaScript 前端开发 开发者
Vue 自定义进度条组件封装及使用方法详解
这是一篇关于自定义进度条组件的使用指南和开发文档。文章详细介绍了如何在Vue项目中引入、注册并使用该组件,包括基础与高级示例。组件支持分段配置(如颜色、文本)、动画效果及超出进度提示等功能。同时提供了完整的代码实现,支持全局注册,并提出了优化建议,如主题支持、响应式设计等,帮助开发者更灵活地集成和定制进度条组件。资源链接已提供,适合前端开发者参考学习。
135 17
|
1月前
|
JavaScript 前端开发 UED
Vue 表情包输入组件实现代码及详细开发流程解析
这是一篇关于 Vue 表情包输入组件的使用方法与封装指南的文章。通过安装依赖、全局注册和局部使用,可以快速集成表情包功能到 Vue 项目中。文章还详细介绍了组件的封装实现、高级配置(如自定义表情列表、主题定制、动画效果和懒加载)以及完整集成示例。开发者可根据需求扩展功能,例如 GIF 搜索或自定义表情上传,提升用户体验。资源链接提供进一步学习材料。
72 1
|
1月前
|
JavaScript API 开发者
Vue框架中常见指令的应用概述。
通过以上的详细解析,你应该已经初窥Vue.js的指令的威力了。它们是Vue声明式编程模型的核心之一,无论是构建简单的静态网站还是复杂的单页面应用,你都会经常用到。记住,尽管Vue提供了大量预定义的指令,你还可以创建自定义指令以满足特定的需求。为你的Vue应用程序加上这些功能增强器,让编码变得更轻松、更愉快吧!
34 1
|
1月前
|
存储 JavaScript 前端开发
如何高效实现 vue 文件批量下载及相关操作技巧
在Vue项目中,实现文件批量下载是常见需求。例如文档管理系统或图片库应用中,用户可能需要一次性下载多个文件。本文介绍了三种技术方案:1) 使用`file-saver`和`jszip`插件在前端打包文件为ZIP并下载;2) 借助后端接口完成文件压缩与传输;3) 使用`StreamSaver`解决大文件下载问题。同时,通过在线教育平台的实例详细说明了前后端的具体实现步骤,帮助开发者根据项目需求选择合适方案。
87 0
|
1月前
|
JavaScript 前端开发 UED
Vue 项目中如何自定义实用的进度条组件
本文介绍了如何使用Vue.js创建一个灵活多样的自定义进度条组件。该组件可接受进度段数据数组作为输入,动态渲染进度段,支持动画效果和内容展示。当进度超出总长时,超出部分将以红色填充。文章详细描述了组件的设计目标、实现步骤(包括props定义、宽度计算、模板渲染、动画处理及超出部分的显示),并提供了使用示例。通过此组件,开发者可根据项目需求灵活展示进度情况,优化用户体验。资源地址:[https://pan.quark.cn/s/35324205c62b](https://pan.quark.cn/s/35324205c62b)。
42 0
|
3月前
|
JavaScript
vue实现任务周期cron表达式选择组件
vue实现任务周期cron表达式选择组件
336 4
|
2月前
|
JavaScript 数据可视化 前端开发
基于 Vue 与 D3 的可拖拽拓扑图技术方案及应用案例解析
本文介绍了基于Vue和D3实现可拖拽拓扑图的技术方案与应用实例。通过Vue构建用户界面和交互逻辑,结合D3强大的数据可视化能力,实现了力导向布局、节点拖拽、交互事件等功能。文章详细讲解了数据模型设计、拖拽功能实现、组件封装及高级扩展(如节点类型定制、连接样式优化等),并提供了性能优化方案以应对大数据量场景。最终,展示了基础网络拓扑、实时更新拓扑等应用实例,为开发者提供了一套完整的实现思路和实践经验。
220 77
|
1月前
|
监控 JavaScript 前端开发
Vue 文件批量下载组件封装完整使用方法及优化方案解析
本文详细介绍了批量下载功能的技术实现与组件封装方案。主要包括两种实现方式:**前端打包方案(基于file-saver和jszip)** 和 **后端打包方案**。前者通过前端直接将文件打包为ZIP下载,适合小文件场景;后者由后端生成ZIP文件流返回,适用于大文件或大量文件下载。同时,提供了可复用的Vue组件`BatchDownload`,支持进度条、失败提示等功能。此外,还扩展了下载进度监控和断点续传等高级功能,并针对跨域、性能优化及用户体验改进提出了建议。可根据实际需求选择合适方案并快速集成到项目中。
152 17