Vue源码剖析-虚拟DOM

news/2024/7/10 2:02:33 标签: vue

Vue源码剖析-虚拟DOM

  • 课程回顾
  • 虚拟DOM概念回顾
  • 代码演示
    • h函数用法
    • h函数的返回结果-vnode
  • 整体分析过程
  • VNode的创建过程-createElement-上
  • VNode的创建过程-createElement-下
  • VNode的处理过程-update
    • patch函数初始化
    • patch函数的执行过程
      • cbs对象
      • patch函数内部的实现
        • createElm函数
        • patchVnode

课程回顾

  • 虚拟DOM库-Snabbdom
  • Vue.js响应式原理模拟实现
  • Vue.js源码-响应式原理

虚拟DOM概念回顾

什么是虚拟DOM

  • 虚拟DOM(Virtual DOM)是使用JavaScript对象描述真实的DOM
  • Vue.js中的虚拟DOM借鉴Snabbdom,并添加了Vue.js的特性
    • 例如:指令和组件机制

为什么要使用虚拟DOM

  • 避免直接操作DOM,提高开发效率
  • 作为一个中间层可以跨平台(服务端渲染)
  • 虚拟DOM不一定可以提高性能
    • 首次渲染的时候会增加开销
    • 复杂视图情况下提升渲染性能

代码演示

  • Vue中的h函数,相对于snanbbdom中的h函数,支持传入插槽和组件
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Document</title>
  </head>
  <body>
    <div id="app">
      <p ref="P1">{{msg}}</p>
    </div>
    <!-- 完整版本 -->
    <script src="../../dist/vue.js"></script>
    <script>
      const vm = new Vue({
        el: "#app",
        render(h) {
          const vnode = h("h1", { attrs: { id: "title" } }, this.msg);
          console.log(vnode);
          return vnode;
        },
        data: {
          msg: "Hello Vue",
        },
      });
    </script>
  </body>
</html>

在这里插入图片描述

h函数用法

  • vm.$creteElement(tag,data,children,normalizeChildren)
    • tag:标签名或组件名称
    • data:描述tag,可以设置DOM的属性或者标签的属性
    • children:tag中的文本内容或者子节点

h函数的返回结果-vnode

  • Vnode的核心属性
    • tag
    • data
    • children
    • text
    • elm :记录vnode转换成的真实dom
    • key: 复用当前的元素

整体分析过程

在这里插入图片描述

VNode的创建过程-createElement-上

  • render函数内部的h函数就是createElement,有两种情况
    • 把template模板转换成render函数时,render函数内部会使用_c
    • 当直接写的render函数(不是template转变过来的),render函数内部使用$createElement
// 当把template模板转换成render函数时,render函数内部会使用_c
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  // 当直接写的render函数(不是template转变过来的),render函数内部使用$createElement
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)
  • createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数,而真正创建Vnode是在_createElement函数中完成
// createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数
// 而真正创建Vnode是在_createElement函数中完成
export function createElement (
  context: Component, // vm实例
  tag: any, // 标签
  data: any, // 描述标签的数据
  children: any, // 子节点
  normalizationType: any,
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果data是数组或者原始值,实际上data就是children
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 用户传入的render函数,alwaysNormalize为true,模板编译转换成的render函数alwaysNormalize为false
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE // 常量,值为2
  }
  return _createElement(context, tag, data, children, normalizationType)
}

VNode的创建过程-createElement-下

  • _createElement函数分析
// createElement函数的作用是处理参数,,因为我们调用h参数的时候既可以传递两个参数,也可以传递三个参数
// 而真正创建Vnode是在_createElement函数中完成
// 正常情况下h函数只需要传tag/data/children这三个参数,normalizationType和alwaysNormalize是createElement内部的变量
export function createElement (
  context: Component, // vm实例
  tag: any, // 标签
  data: any, // 描述标签的数据
  children: any, // 子节点
  normalizationType: any, // normalizationType不同,处理children的函数不同
  alwaysNormalize: boolean
): VNode | Array<VNode> {
  // 如果data是数组或者原始值,实际上data就是children
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  // 用户传入的render函数,alwaysNormalize为true,模板编译转换成的render函数alwaysNormalize为false
  if (isTrue(alwaysNormalize)) {
    normalizationType = ALWAYS_NORMALIZE // 常量,值为2
  }
  return _createElement(context, tag, data, children, normalizationType)
}
// _createElement函数的作用是生成vnode
export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
  // data存在,且data._ob_属性(observer对象)存在,说明data是响应式的数据
  if (isDef(data) && isDef((data: any).__ob__)) {
    // 在开发环境下警告,避免使用响应式的数据
    process.env.NODE_ENV !== 'production' && warn(
      `Avoid using observed data object as vnode data: ${JSON.stringify(data)}\n` +
      'Always create fresh vnode data objects in each render!',
      context
    )
    return createEmptyVNode()
  }
  // object syntax in v-bind
  // <component :is="currentTabComponent"></component>
  if (isDef(data) && isDef(data.is)) {
    // 如果data中有is属性,会记录到tag中来
    // is属性的作用:动态组件
    tag = data.is
  }
  if (!tag) {
    // in case of component :is set to falsy value
    // 如果tag是false,相当于把is指令设置为false,返回一个空的虚拟节点
    return createEmptyVNode()
  }
  // warn against non-primitive key
  if (process.env.NODE_ENV !== 'production' &&
  // 如果data中有key属性,且不是原始值,此时报一个警告,key避免使用非原始值,应该适应字符串或者number类型值
    isDef(data) && isDef(data.key) && !isPrimitive(data.key)
  ) {
    if (!__WEEX__ || !('@binding' in data.key)) {
      warn(
        'Avoid using non-primitive value as key, ' +
        'use string/number value instead.',
        context
      )
    }
  }
  // support single function children as default scoped slot
  // 处理作用域插槽
  if (Array.isArray(children) &&
    typeof children[0] === 'function'
  ) {
    data = data || {}
    data.scopedSlots = { default: children[0] }
    children.length = 0
  }
  // 下面的代码就是将children拍平,将多维数组转换成的一维数组
  if (normalizationType === ALWAYS_NORMALIZE) {
    // 当normalizationType的值为ALWAYS_NORMALIZE的时候,说明执行的是有用户传过来的render函数
    // 将多维数组转换成一维数组
    children = normalizeChildren(children)
  } else if (normalizationType === SIMPLE_NORMALIZE) {
    // 当normalizationType的值为SIMPLE_NORMALIZE的时候,说明执行的是tempalte编译转换成的render函数
    // 将二维数组(如果children的元素存在是函数式组件的时候)转换成一维数组
    children = simpleNormalizeChildren(children)
  }
  let vnode, ns
  // tag是字符串
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    // tag是html中的保留标签
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      if (process.env.NODE_ENV !== 'production' && isDef(data) && isDef(data.nativeOn)) {
        warn(
          `The .native modifier for v-on is only valid on components but it was used on <${tag}>.`,
          context
        )
      }
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      // 判断是否是 自定义组件
      // 查找自定义组件构造函数的声明
      // 根据Ctor创建组件的VNode
      vnode = createComponent(Ctor, data, context, children, tag)
    } else {
      // unknown or unlisted namespaced elements
      // check at runtime because it may get assigned a namespace when its
      // parent normalizes children
      // 不是保留标签的话,就是自定义标签,创建对应的VNode对象
      vnode = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    // 如果不是字符串,这说明是组件,创建组件对应的VNode对象
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    // vnode是数组,直接返回
    return vnode
  } else if (isDef(vnode)) {
    // 如果vnode已经初始化好了
    if (isDef(ns)) applyNS(vnode, ns) // 处理vnode的命名空间
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    // 都不满足,返回一个空的注释节点
    return createEmptyVNode()
  }
}

VNode的处理过程-update

  • vm._update(vm._render(), hydrating)=>vm._render()只是创建的VNode,将创建好的VNode交给update函数处理
  • patch函数的作用:判断是否是首次渲染,调用vm._patch_()方法
 Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode // vm实例的_vnode存储的是之前处理过的VNode对象,如果不存在该属性,说明是首次渲染
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    // 在src\platforms\web\runtime\index.js中注册了Vue.prototype.__patch__ 
    if (!prevVnode) {
      // initial render
      // 首次渲染
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      // 数据变化
      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.
  }
  • 首先会判断preVnode(vm._vnode属性中保存)存在吗,如果存在,则说明不是首次渲染,如果存在,则说明是数据更新后的对比渲
  1. 首次渲染
vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  1. 数据更新后对比渲染
 vm.$el = vm.__patch__(prevVnode, vnode)
  • vm.__patch__方法会将虚拟节点转换后真实dom挂载到vm.$el属性上

patch函数初始化

  • src\platforms\web\runtime\index.js
import { patch } from './patch'
Vue.prototype.__patch__ = inBrowser ? patch : noop
  • 在src\platforms\web\runtime\patch.js中
    在这里插入图片描述
  1. nodeOps是操作dom的各种api在这里插入图片描述
  2. baseModules:与平台无关的模块,处理指令的
    在这里插入图片描述
  3. 平台相关的模块attrs/klass/events/style等
  4. createPatchFunction创建patch函数的高阶函数
  • 在src\core\vdom\patch.js中
    在这里插入图片描述

patch函数的执行过程

cbs对象

  • patch函数是执行createPatchFunction函数返回的函数=>柯里化
  • 现返回patch函数之前,定义了cbs=>各种生命周期钩子对应的处理函数
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']
// 定义cbs   
for (i = 0; i < hooks.length; ++i) {
    // cbs['update']=[]
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        // cbs['update']=[updateAttrs,updateClass,update...]
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }

patch函数内部的实现

  • 全局变量insertedVnodeQueue=>新插入节点的队列,存储将来新插入VNode节点,存储这些节点的目的是将来把这些VNode节点对应的DOM元素挂载到DOM树上之后会触发这些VNode的insert钩子函数
  • 如果老节点不是真实dom,且新老虚拟节点不是相同节点=>patchVnode(oldVnode, vnode, insertedVnodeQueue)
  • 如果老节点oldVnode是 真实dom,将oldVnode 转换成虚拟dom=>oldVnode = emptyNodeAt(oldVnode),然后将新虚拟节点转换成真实dom,挂载到父节点上=>createElm(vnode,insertedVnodeQueue,parentElm)

createElm函数

  • 作用;将虚拟dom转换成真实dom,并且挂载到dom树上
    在这里插入图片描述
    在这里插入图片描述

patchVnode

  • 参考snabbdom
    在这里插入图片描述

http://www.niftyadmin.cn/n/1499631.html

相关文章

jstat gcutil

QQA: jstat gcutil 的输出是什么意思 当 Java 程序有性能问题时&#xff0c;尤其是响应时间有突然变化时&#xff0c;最好第一时间查看 GC 的状态。一般用 jstat -gcutil <pid> 1s 来查看&#xff0c;那么它的输出又是什么含义呢&#xff1f; 输出样例 一般会用两种方式调…

网络设备驱动第二课---收发

概述 一些接口函数 框架流程 1.预留空间 准备一个skbuf2.往sybuf中扔数据转载于:https://www.cnblogs.com/xxg1992/p/6636404.html

Linux下使用 xrandr 命令设置屏幕分辨率

最近在Linux下修改屏幕分辨率的时候&#xff0c;发现了一个非常有用的命令&#xff1a;xrandr 使用这个命令&#xff0c;可以方便的设置您显示器的的分辨率。尤其是当你使用了一些需要或者会自动改动您屏幕分辨率的程序以后。 您可以使用如下命令来将屏幕恢复到原来的分辨率&am…

Vue源码剖析-模板编译

Vue源码剖析-模板编译模板编译简介模板编译的结果Vue Template Explorer编译的入口函数createCompilerCreator函数详解模板编译的过程compile函数baseCompile-ASTbaseCompile-parsebaseCompile-optimizebaseCompile-generate 上调试实例模板编译过程思维导图模板编译简介 模板…

自动化部署必备技能—定制化RPM包

回顾下安装软件的三种方式&#xff1a;1、编译安装软件&#xff0c;优点是可以定制化安装目录、按需开启功能等&#xff0c;缺点是需要查找并实验出适合的编译参数&#xff0c;诸如MySQL之类的软件编译耗时过长。 2、yum安装软件&#xff0c;优点是全自动化安装&#xff0c;不需…

1.0总结

GitHub develop分支 团队&#xff1a;盖嘉轩 031602211许郁杨 031602240 1.0主要是描述一下程序本身&#xff0c;也就是代码和编码规范部分&#xff0c;完整的总结会在完善一些细节后再提交。其实原定是应该在13号提交这篇文章的&#xff0c;只是我写的部分出了点问题。。 其实…

组件化

组件化组件化回顾组件注册组件注册方式全局组件局部组件调试组件注册的过程组件的创建过程回顾首次渲染过程组件化回顾 一个Vue组件就是一个拥有预定义选项的一个Vue实例一个组件可以组成页面上一个功能完备的区域,组件可以包含脚本,样式,模板 组件注册 组件注册方式 全局组…

JVM内存设置多大合适?Xmx和Xmn如何设置?

JVM内存设置多大合适&#xff1f;Xmx和Xmn如何设置&#xff1f; 问题:新上线一个java服务&#xff0c;或者是RPC或者是WEB站点&#xff0c; 内存的设置该怎么设置呢&#xff1f;设置成多大比较合适&#xff0c;既不浪费内存&#xff0c;又不影响性能呢&#xff1f; 分析&#x…