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

2022-06-29 14:49:32

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

Vnode构造函数

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

创建Vnode注释节点

// 创建注释vnode节点varcreateEmptyVNode=function(text){if( text===void0) text='';var node=newVNode();
    node.text= text;
    node.isComment=true;// 标记注释节点return node};

创建Vnode文本节点

// 创建文本vnode节点functioncreateTextVNode(val){returnnewVNode(undefined,undefined,undefined,String(val))}

克隆vnode

functioncloneVNode(vnode){var cloned=newVNode(
      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){
    ···returnmountComponent(this, el)}functionmountComponent(){
    ···updateComponent=function(){
        vm._update(vm._render(), hydrating);};}

renderMinxin

functionVue(options){if(process.env.NODE_ENV!=='production'&&!(thisinstanceofVue)){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();//functionrenderMixin(){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}}
newVue({
    el:'#app',render:function(createElement){returncreateElement('div',{},this.message)},data(){return{
            message:'dom'}}})

createElement来源

exportfunctioninitMixin(Vue: Class<Component>){// 在原型上添加 _init 方法Vue.prototype._init=function(options?: Object){// 保存当前实例const vm: Component=this// 合并配置if(options&& options._isComponent){// 把子组件依赖父组件的 props、listeners 挂载到 options 上,并指定组件的$optionsinitInternalComponent(vm, options)}else{// 把我们传进来的 options 和当前构造函数和父级的 options 进行合并,并挂载到原型上
      vm.$options=mergeOptions(resolveConstructorOptions(vm.constructor),
        options||{},
        vm)}
    vm._self= vminitLifecycle(vm)// 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等initEvents(vm)// 初始化事件:$on, $off, $emit, $onceinitRender(vm)// 初始化渲染: render, mixincallHook(vm,'beforeCreate')// 调用生命周期钩子函数initInjections(vm)// 初始化 injectinitState(vm)// 初始化组件数据:props, data, methods, watch, computedinitProvide(vm)// 初始化 providecallHook(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需要一些检验和转换
functioninitRender(vm){
    vm._c=function(a, b, c, d){returncreateElement(vm, a, b, c, d,false);}
    vm.$createElement=function(a, b, c, d){returncreateElement(vm, a, b, c, d,true);};}

createElement

functioncreateElement(
    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);returncreateEmptyVNode()// 返回注释节点}if(isDef(data)&&isDef(data.is)){
      tag= data.is;}if(!tag){// 防止动态组件 :is 属性设置为false时,需要做特殊处理returncreateEmptyVNode()}// 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);}elseif(normalizationType===SIMPLE_NORMALIZE){// 模板编译生成的的render函数
      children=simpleNormalizeChildren(children);}}
// 处理编译生成的render 函数functionsimpleNormalizeChildren(children){for(var i=0; i< children.length; i++){// 子节点为数组时,进行开平操作,压成一维数组。if(Array.isArray(children[i])){returnArray.prototype.concat.apply([], children)}}return children}// 处理用户定义的render函数functionnormalizeChildren(children){// 递归调用,直到子节点是基础类型,则调用创建文本节点VnodereturnisPrimitive(children)?[createTextVNode(children)]: Array.isArray(children)?normalizeArrayChildren(children):undefined}// 判断是否基础类型functionisPrimitive(value){return(typeof value==='string'||typeof value==='number'||typeof value==='symbol'||typeof value==='boolean')}

最终转换过程

var vm=newVue({
  el:'#app',
  template:'<div><span>virtual dom</span></div>'})模板编译生成render函数(function(){with(this){return_c('div',[_c('span',[_v("virual dom")])])}})

VirtualDOM 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()functionlifecycleMixin(){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的差异,以求最小化改变真实阶段
  • 作者:神奇大叔
  • 原文链接:https://blog.csdn.net/weixin_43294560/article/details/122434017
    更新时间:2022-06-29 14:49:32