Vue 源码解读(12)—— patch

当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注点赞收藏评论

新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn

文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。

前言

前面我们说到,当组件更新时,实例化渲染 watcher 时传递的 updateComponent 方法会被执行:

const updateComponent = () => {
   
  // 执行 vm._render() 函数,得到 虚拟 VNode,并将 VNode 传递给 vm._update 方法,接下来就该到 patch 阶段了
  vm._update(vm._render(), hydrating)
}

首先会先执行 vm._render() 函数,得到组件的 VNode,并将 VNode 传递给 vm._update 方法,接下来就该进入到 patch 阶段了。今天我们就来深入理解组件更新时 patch 的执行过程。

历史

1.x 版本的 Vue 没有 VNode 和 diff 算法,那个版本的 Vue 的核心只有响应式原理:Object.definePropertyDepWatcher

  • Object.defineProperty: 负责数据的拦截。getter 时进行依赖收集,setter 时让 dep 通知 watcher 去更新

  • Dep:Vue data 选项返回的对象,对象的 key 和 dep 一一对应

  • Watcher:key 和 watcher 时一对多的关系,组件模版中每使用一次 key 就会生成一个 watcher

<template>
  <div class="wrapper">
    
    
    <div class="msg1">{
  { msg }}div>
    
    <div class="msg2">{
  { msg }}div>
  div>
template>

<script>
export default {
    
  data() {
    
    return {
    
      // 和 dep 一一对应,和 watcher 一 对 多
      msg: 'Hello Vue 1.0'
    }
  }
}
script>

当数据更新时,dep 通知 watcher 去直接更新 DOM,因为这个版本的 watcher 和 DOM 时一一对应关系,watcher 可以非常明确的知道这个 key 在组件模版中的位置,因此可以做到定向更新,所以它的更新效率是非常高的。

虽然更新效率高,但随之也产生了严重的问题,无法完成一个企业级应用,理由很简单:当你的页面足够复杂时,会包含很多的组件,在这种架构下就意味这一个页面会产生大量的 watcher,这非常耗资源。

这时就在 Vue 2.0 中通过引入 VNode 和 diff 算法去解决 1.x 中的问题。将 watcher 的粒度放大,变成一个组件一个 watcher(就是我们说的渲染 watcher),这时候你页面再大,watcher 也很少,这就解决了复杂页面 watcher 太多导致性能下降的问题。

当响应式数据更新时,dep 通知 watcher 去更新,这时候问题就来了,Vue 1.x 中 watcher 和 key 一一对应,可以明确知道去更新什么地方,但是 Vue 2.0 中 watcher 对应的是一整个组件,更新的数据在组件的的什么位置,watcher 并不知道。这时候就需要 VNode 出来解决问题。

通过引入 VNode,当组件中数据更新时,会为组件生成一个新的 VNode,通过比对新老两个 VNode,找出不一样的地方,然后执行 DOM 操作更新发生变化的节点,这个过程就是大家熟知的 diff。

以上就是 Vue 2.0 为什么会引入 VNode 和 diff 算法的历史原因了,也是 Vue 1.x 到 2.x 的一个发展历程。

目标

  • 深入理解 Vue 的 patch 阶段,理解其 diff 算法的原理。

源码解读

入口

/src/core/instance/lifecycle.js

const updateComponent = () => {
   
  // 执行 vm._render() 函数,得到 VNode,并将 VNode 传递给 _update 方法,接下来就该到 patch 阶段了
  vm._update(vm._render(), hydrating)
}

vm._update

/src/core/instance/lifecycle.js

/**
 * 页面首次渲染和后续更新的入口位置,也是 patch 的入口位置 
 */
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.
}

vm.__patch__

/src/platforms/web/runtime/index.js

/ 在 Vue 原型链上安装 web 平台的 patch 函数
Vue.prototype.__patch__ = inBrowser ? patch : noop

patch

/src/platforms/web/runtime/patch.js

// patch 工厂函数,为其传入平台特有的一些操作,然后返回一个 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)
  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 属性,属性值为 ''
 */
export function setStyleScope (node: Element, scopeId: string) {
   
  node.setAttribute(scopeId, '')
}

modules

/src/platforms/web/runtime/modules 和 /src/core/vdom/modules

平台特有的一些操作,比如:attr、class、style、event 等,还有核心的 directive 和 ref,它们会向外暴露一些特有的方法,比如:create、activate、update、remove、destroy,这些方法在 patch 阶段时会被调用,从而做相应的操作,比如 创建 attr、指令等。这部分内容太多了,这里就不一一列举了,在阅读 patch 的过程中如有需要可回头深入阅读,比如操作节点的属性的时候,就去读 attr 相关的代码。

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', 'activa

你可能感兴趣的:(精通,Vue,技术栈的源码原理,vue.js,前端,javascript,源码,前端框架)