vue 虚拟dom转换真实dom源码解析

2023-11-13

当不断的通过JS修改DOM时,不经意间会触发到渲染引擎的回流或者重绘,这个性能开销是非常巨大的。因此为了降低开销,我们需要做的是尽可能减少DOM操作,当我们想用JS脚本大批量进行DOM操作时,会优先作用于Virtual DOM这个JS对象,最后通过对比将要改动的部分通知并更新到真实的DOM。尽管最终还是操作真实的DOM,但Virtual DOM可以将多个改动合并成一个批量的操作,从而减少 DOM 重排的次数,进而缩短了生成渲染树和绘制所花的时间。

Vnode构造函数

var VNode = function VNode (tag,data,children,text,elm,context,componentOptions,asyncFactory) {
    this.tag = tag; // 标签
    this.data = data;  // 数据
    this.children = children; // 子节点
    this.text = text;
    ···
    ···
  };

创建Vnode注释节点

// 创建注释vnode节点
var createEmptyVNode = function (text) {
    if ( text === void 0 ) text = '';

    var node = new VNode();
    node.text = text;
    node.isComment = true; // 标记注释节点
    return node
};

创建Vnode文本节点

// 创建文本vnode节点
function createTextVNode (val) {
    return new VNode(undefined, undefined, undefined, String(val))
}

克隆vnode

function cloneVNode (vnode) {
    var cloned = new VNode(
      vnode.tag,
      vnode.data,
      vnode.children && vnode.children.slice(),
      vnode.text,
      vnode.elm,
      vnode.context,
      vnode.componentOptions,
      vnode.asyncFactory
    );
    cloned.ns = vnode.ns;
    cloned.isStatic = vnode.isStatic;
    cloned.key = vnode.key;
    cloned.isComment = vnode.isComment;
    cloned.fnContext = vnode.fnContext;
    cloned.fnOptions = vnode.fnOptions;
    cloned.fnScopeId = vnode.fnScopeId;
    cloned.asyncMeta = vnode.asyncMeta;
    cloned.isCloned = true;
    return cloned
  }

Virtual DOM的创建

  • 如果我们传递的是template模板,模板会先经过编译器的解析,并最终根据不同平台生成对应代码,此时对应的就是将with语句封装好的render函数;如果传递的是render函数,则跳过模板编译过程,直接进入下一个阶段。
  • 下一阶段是拿到render函数,调用vm._render()方法将render函数转化为Virtual DOM,并最终通过vm._update()方法将Virtual DOM渲染为真实的DOM节点
Vue.prototype.$mount = function(el, hydrating) {
    ···
    return mountComponent(this, el)
}
function mountComponent() {
    ···
    updateComponent = function () {
        vm._update(vm._render(), hydrating);
    };
}


renderMinxin

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' && !(this instanceof Vue) ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}
// 定义Vue原型上的init方法(内部方法)
initMixin(Vue);
// 定义原型上跟数据相关的属性方法
stateMixin(Vue);
//定义原型上跟事件相关的属性方法
eventsMixin(Vue);
// 定义原型上跟生命周期相关的方法
lifecycleMixin(Vue);
// 定义原型上渲染相关的函数
renderMixin(Vue); 

_render

// 引入Vue时,执行renderMixin方法,该方法定义了Vue原型上的几个方法,其中一个便是 _render函数
renderMixin();//
function renderMixin() {
    Vue.prototype._render = function() {
        var ref = vm.$options;
        var render = ref.render;
        ···
        try {
        //vm._renderProxy在数据代理分析过,本质上是为了做数据过滤检测
        //vm.$createElement即为createElement生成虚拟dom的方法
            vnode = render.call(vm._renderProxy, vm.$createElement);
        } catch (e) {
            ···
        }
        ···
        return vnode
    }
}

new Vue({
    el: '#app',
    render: function(createElement) {
        return createElement('div', {}, this.message)
    },
    data() {
        return {
            message: 'dom'
        }
    }
})

createElement来源

export function initMixin (Vue: Class<Component>) {
  // 在原型上添加 _init 方法
  Vue.prototype._init = function (options?: Object) {
    // 保存当前实例
    const vm: Component = this
    // 合并配置
    if (options && options._isComponent) {
      // 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$options
      initInternalComponent(vm, options)
    } else {
      // 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
      vm.$options = mergeOptions(
        resolveConstructorOptions(vm.constructor),
        options || {},
        vm
      )
    }
    vm._self = vm
    initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
    initEvents(vm) // 初始化事件:$on, $off, $emit, $once
    initRender(vm) // 初始化渲染: render, mixin
    callHook(vm, 'beforeCreate') // 调用生命周期钩子函数
    initInjections(vm) // 初始化 inject
    initState(vm) // 初始化组件数据:props, data, methods, watch, computed
    initProvide(vm) // 初始化 provide
    callHook(vm, 'created') // 调用生命周期钩子函数

    if (vm.$options.el) {
      // 如果传了 el 就会调用 $mount 进入模板编译和挂载阶段
      // 如果没有传就需要手动执行 $mount 才会进入下一阶段
      vm.$mount(vm.$options.el)
    }
  }
}

initRender

  • 其中 vm._c 是template内部编译成render函数时调用的方法,vm.$createElement是手写render函数时调用的方法。两者的唯一区别仅仅是最后一个参数的不同。通过模板生成的render方法可以保证子节点都是Vnode,而手写的render需要一些检验和转换
function initRender(vm) {
    vm._c = function(a, b, c, d) { return createElement(vm, a, b, c, d, false); }
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };
}

createElement

function createElement (
    context, // vm 实例
    tag, // 标签
    data, // 节点相关数据,属性
    children, // 子节点
    normalizationType,
    alwaysNormalize // 区分内部编译生成的render还是手写render
  ) {
    // 对传入参数做处理,如果没有data,则将第三个参数作为第四个参数使用,往上类推。
    if (Array.isArray(data) || isPrimitive(data)) {
      normalizationType = children;
      children = data;
      data = undefined;
    }
    // 根据是alwaysNormalize 区分是内部编译使用的,还是用户手写render使用的
    if (isTrue(alwaysNormalize)) {
      normalizationType = ALWAYS_NORMALIZE;
    }
    return _createElement(context, tag, data, children, normalizationType) // 真正生成Vnode的方法
  }

_createElement

function _createElement (context,tag,data,children,normalizationType) {
    // 1. 数据对象不能是定义在Vue data属性中的响应式数据。
    if (isDef(data) && isDef((data).__ob__)) {
      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() // 返回注释节点
    }
    if (isDef(data) && isDef(data.is)) {
      tag = data.is;
    }
    if (!tag) {
      // 防止动态组件 :is 属性设置为false时,需要做特殊处理
      return createEmptyVNode()
    }
    // 2. key值只能为string,number这些原始数据类型
    if (isDef(data) && isDef(data.key) && !isPrimitive(data.key)
    ) {
      {
        warn(
          'Avoid using non-primitive value as key, ' +
          'use string/number value instead.',
          context
        );
      }
    }
    ···

	if (normalizationType === ALWAYS_NORMALIZE) {
      // 用户定义render函数
      children = normalizeChildren(children);
    } else if (normalizationType === SIMPLE_NORMALIZE) {
      // 模板编译生成的的render函数
      children = simpleNormalizeChildren(children);
    }
  }

// 处理编译生成的render 函数
function simpleNormalizeChildren (children) {
    for (var i = 0; i < children.length; i++) {
        // 子节点为数组时,进行开平操作,压成一维数组。
        if (Array.isArray(children[i])) {
        return Array.prototype.concat.apply([], children)
        }
    }
    return children
}

// 处理用户定义的render函数
function normalizeChildren (children) {
    // 递归调用,直到子节点是基础类型,则调用创建文本节点Vnode
    return isPrimitive(children)
      ? [createTextVNode(children)]
      : Array.isArray(children)
        ? normalizeArrayChildren(children)
        : undefined
  }

// 判断是否基础类型
function isPrimitive (value) {
    return (
      typeof value === 'string' ||
      typeof value === 'number' ||
      typeof value === 'symbol' ||
      typeof value === 'boolean'
    )
  }

最终转换过程

var vm = new Vue({
  el: '#app',
  template: '<div><span>virtual dom</span></div>'
})

模板编译生成render函数
(function() {
  with(this){
    return _c('div',[_c('span',[_v("virual dom")])])
  }
})

Virtual DOM tree的结果
{
  tag: 'div',
  children: [{
    tag: 'span',
    children: [{
      tag: undefined,
      text: 'virtual dom'
    }]
  }]
}

虚拟Vnode映射成真实DOM

  • 调用Vue原型上_update方法,将虚拟DOM映射成为真实的DOM。从源码上可以知道,_update的调用时机有两个,一个是发生在初次渲染阶段,另一个发生数据更新阶段
updateComponent = function () {
    // render生成虚拟DOM,update渲染真实DOM
    vm._update(vm._render(), hydrating);
};

vm._update

lifecycleMixin()
function lifecycleMixin() {
    Vue.prototype._update = function (vnode, hydrating) {
        var vm = this;
        var prevEl = vm.$el;
        var prevVnode = vm._vnode; // prevVnode为旧vnode节点
        // 通过是否有旧节点判断是初次渲染还是数据更新
        if (!prevVnode) {
            // 初次渲染
            vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false)
        } else {
            // 数据更新
            vm.$el = vm.__patch__(prevVnode, vnode);
        }
}

_patch_

// 浏览器端才有DOM,服务端没有dom,所以patch为一个空函数
  Vue.prototype.__patch__ = inBrowser ? patch : noop;

patch

 var patch = createPatchFunction({ nodeOps: nodeOps, modules: modules });

// 将操作dom对象的方法合集做冻结操作
 var nodeOps = /*#__PURE__*/Object.freeze({
    createElement: createElement$1,
    createElementNS: createElementNS,
    createTextNode: createTextNode,
    createComment: createComment,
    insertBefore: insertBefore,
    removeChild: removeChild,
    appendChild: appendChild,
    parentNode: parentNode,
    nextSibling: nextSibling,
    tagName: tagName,
    setTextContent: setTextContent,
    setStyleScope: setStyleScope
  });

// 定义了模块的钩子函数
  var platformModules = [
    attrs,
    klass,
    events,
    domProps,
    style,
    transition
  ];

var modules = platformModules.concat(baseModules);

  • createPatchFunction函数核心是通过调用createElm方法进行dom操作,创建节点,插入子节点,递归创建一个完整的DOM树并插入到body中。并且在产生真实阶段阶段,会有diff算法来判断前后Vnode的差异,以求最小化改变真实阶段
本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

vue 虚拟dom转换真实dom源码解析 的相关文章

随机推荐

  • LogisticRegression(逻辑回归)

    LogisticRegression定义 logistic回归 是一种广义的线性回归分析模型 常用于数据挖掘 疾病自动诊断 经济预测等领域 例如 探讨引发疾病的危险因素 并根据危险因素预测疾病发生的概率等 以胃癌病情分析为例 选择两组人群
  • SVG转为Png

    1 pom中引入maven依赖
  • 自定义实现OAuth2.0 授权码模式

    文章目录 OAuth2 0 授权码模式 实践 依赖知识 术语 授权码流程 认证服务器 拉起请求用户授权页面 用户手动授权 提交授权 生成code 下发Token 第三方应用 收到code并请求Token 访问受保护的资源 项目结构 Tomc
  • 类EMD的“信号分解方法”及MATLAB实现(第四篇)——VMD

    重头戏来了 在以往的应用经验里 VMD方法在众多模态分解方法中可以说是非常好的 从催更力度上看 这个方法也是格外受关注 笔者决定加快进度快一些写完这个方法 十月份了有些同学要开始做毕设 希望这篇文能帮上忙 1 VMD 变分模态分解 的概念
  • poj1338【丑数·DP】

    我记得这道题以前写过 而且是写出来了 DP吧 然后现在想了好久 没想出来 然后考虑一下递推 mdzz 直接就是让之前的这个每次乘以2 3 5就好了嘛 然后每轮取最小 include
  • jquery form表单.serialize()序列化后中文乱码问题原因及解决decodeURIComponent

    jquery form表单 serialize 序列化后中文乱码问题原因及解决 原因 serialize 自动调用了encodeURIComponent方法将数据编码了 解决方法 调用decodeURIComponent XXX true
  • 列表list转树形结构(python/golang/js/php)

    文章目录 1 原数据 2 利用对象内存共享生成嵌套结构 2 1 算法原理 2 2 算法实现 2 2 1 JS 2 2 2 Python 2 2 3 go 2 2 4 php 2 3 运行结果 3 递归 3 1 算法实现 3 1 1 pyth
  • 基于协同过滤算法的书籍推荐 毕业设计-附源码101555

    摘 要 21世纪的今天 随着社会的不断发展与进步 人们对于信息科学化的认识 已由低层次向高层次发展 由原来的感性认识向理性认识提高 管理工作的重要性已逐渐被人们所认识 科学化的管理 使信息存储达到准确 快速 完善 并能提高工作管理效率 促进
  • 微服务restful风格,用Post在服务之间发送请求接收不到参数接收不到问题(@RequestParam和@RequestBody)

    上代码 发送端 接收端 问题 发送端可以接受从前段传过来的数据 但是请求接收端时 接收端可以接收url请求 但是参数传不到接收端 分析 用get和post传输的数据是截然不同的 用get是追加在url之后 直接放在请求头 但是post请求的
  • 体验css:repeat和grid

    文章目录 一 repeat 1 语法 2 auto fill和auto fit 3 专属尺寸 fr auto max content min content 二 grid 1 设置grid布局 2 设置列宽行高 3 设置间距 4 设置分区
  • 【C++实现】 数据库连接池

    文章目录 涉及知识 为什么要弄连接池 功能介绍 成员变量讲解 代码剖析 Connection h Connection cpp ConnectionPool h ConnectionPool cpp 性能测试 难点 总结 涉及知识 MySQ
  • 解一元二次方程——Java

    解一元二次方程 可以使用下面的公式求元二次方程ax x bx c 0的两个根 b b 4ac称作一元二次方程的判别式 如果它是正值 那么一元二次方程就有两个实数根 如果它为0 方程式就只有一个根 如果它是负值 方程式无实数根 编写程序 提示
  • js中使用websocket

    后端地址是http的 websocket地址 ws开头 后端地址是https的 websocket地址wss开头 对于websocket没有跨域的问题 import MessageBox from element ui let url ws
  • Linux学习笔记--8(文件权限)

    文件权限与归属 Linux不同的字符来区分文件类型 常见如下 普通文件 d 目录文件 l 链接文件 b 块设备文件 c 字符设备文件 p 管道文件 对应目录文件 可读 表示能够读取目录内的文件列表 可写 表示能够在目录内新增 删除 重命名文
  • Oracle : ORA-02290: 违反检查约束条件

    背景 一个oracle表 有个字段开始被设置不为空 后来我想测试 把这个不为空 去掉了 然后保存 就报错 om dtwave meteor connector common exception ConnectorException Writ
  • 感知机原始形式、对偶形式的Python实现

    2019独角兽企业重金招聘Python工程师标准 gt gt gt 感知机学习的目标就是求得一个能够将训练数据集中正负实例完全分开的分类超平面 感知机原始形式 from future import division import rando
  • Rabbitmq延迟队列实现定时任务

    场景 开发中经常需要用到定时任务 对于商城来说 定时任务尤其多 比如优惠券定时过期 订单定时关闭 微信支付2小时未支付关闭订单等等 都需要用到定时任务 但是定时任务本身有一个问题 一般来说我们都是通过定时轮询查询数据库来判断是否有任务需要执
  • 多态数组的简单代码教学

    多态数组的简单代码教学 package com hspedu polrArr public class PloyArray public static void main String args Person persons new Per
  • 基于github搭建自已的个人博客

    昨天搭建了基于github 的个人博客 在此记录搭建过程 1 环境准备 1 1 git 1 2 nodejs 1 3 hexo 1 安装命令 npm install hexo g 2 测试是否安装成功 hexo v 3 安装hexo依赖 n
  • vue 虚拟dom转换真实dom源码解析

    当不断的通过JS修改DOM时 不经意间会触发到渲染引擎的回流或者重绘 这个性能开销是非常巨大的 因此为了降低开销 我们需要做的是尽可能减少DOM操作 当我们想用JS脚本大批量进行DOM操作时 会优先作用于Virtual DOM这个JS对象