vue源码之虚拟dom

237 阅读6分钟

什么是虚拟dom

用js对象来描述dom结构,而不是真实dom,称之为虚拟dom

虚拟dom的优点

  1. 跨平台
  2. 复杂视图的更新更快
  3. 避免直接dom操作,提高开发效率

render函数的参数h函数

h函数对应的是源码中的createElement函数
作用: 创建一个虚拟节点Vnode

h(tag, data, children)一个可以传入四个参数

  • 第一个参数可以是标签名称,一个字符串'h1',或者组件对象
  • 第二个参数data 是用来描述tag的内容的,可以设置标签的属性或者对应dom元素的属性,注册事件等
  • 第三个参数可以是字符串或者是数组,字符串的话设置的是标签里的内容,数组的话设置的是标签内的子节点

vue中的h函数支持组件和插槽

Vnode的核心属性

  • tag
  • data
  • children
  • text
  • elm 记录真实dom,当Vnode转化成真实dom的时候,会把真实dom记录到这个属性里面来
  • key dom复用

虚拟dom创建的整体过程

  • vm._init 初始化vue中的实例成员,最后调用了 vm.$mount()
  • vm.$mount() 中调用了 mountCompoent()
  • mountCompoent() 中创建了 Watcher对象
  • Watcher 中调用了 updateCompoent()
  • updateCompoent()
    • vm._update(vm._render(), hydrating)
  • vm._render()
    • vnode = render.call(vm.renderProxy, vm.$createElement)通过call来调用的,第一个参数就是改变内部的this的指向,第二个就是render函数的参数,就是h 函数
    • vm.$createElement()
      • h函数,render中调用,通过代码延时
      • createElement(vm,a,b,c, true)
      • _createElement(context, tag, data, children, normalizationType)
    • vm._createElement()
      • vnode = new VNode(config.parsePlatformTagName(tag), data, chidlren, undefind, undefind, context)
      • vm._render() 结束,返回vnode
  • vm._update()
    • 负责把虚拟dom, 渲染成真实dom
    • 首次执行 vm.__patch__(vm.$el, vnode, hydrating, false)
    • 数据更新 vm.__patch__(preVnode, vnode)
  • vm.patch()
  • patchVnode()
  • updateChildren
vm._render() 之 _createElement()

render方法中调用了用户传过来的render函数(这里调用的是vm.$createElement())或者模板编译生成的render函数(这里调用的是vm._c()),两者差异在于参数,最终调用了_createElement()

vm.$createElement()函数对于第二个参数data和第三个参数children有没有传做出一些处理,然后把参数传入_createElement()做出真正的创建虚拟dom的处理

1. 先判断data是否传入并且data.__ob__是否存在,是的话创建空节点并且发出警告,不需要响应式的data
2. 确认data中是否有is属性,是的话,确认为自定义组件,赋值给tagtag = data.is

  1. 在用户传过来的render和模板转化过来的render 用不同的方法把传进来的children参数铺平
  2. 判断tag的类型
    • tag是string的时候,不同情况下通过不同的方法创建vnode
      • 是否是保留标签
      • 是否是自定义组件
      • 是普通字符串
    • 否则就是组件 通过createComponent创建组件对应的VNode对象
  3. 根据得到的vnode判断,是直接返回vnode还是处理过后返回vnode,或者创建一个空的vnode返回

vm._update()

执行完_createElement()之后,返回一个VNode,通过vm._render()返回

vm._update(vm._render(), hydrating) // _render生成虚拟dom _update调用patch 对比两个虚拟dom的差异并更新

被vm._update()当做参数接收,而vm._update()的作用就是把虚拟dom转换成真实dom,这个函数在首次渲染和数据更新的时候都会被调用,这个函数的核心就是调用vm.__patch__ 这个方法

每次调用vm._update()的时候,都会在vm._vnode中存储最新的 vnode,当每次调用update函数的时候,会更具里面是否有值,来判断是否是首次渲染

vm.patch()

createElm

createElm()作用就是

  1. 把vnode转换成dom元素,并且插入到dom树上,
  2. 把虚拟节点的children,转换成真实dom,并且插入到dom树上
  3. 触发一些相应的钩子函数(create,insert加入insertedVnodeQueue队列)
patchVnode

patchVnode对比新旧vnode,以及新旧节点的子节点,找到差异,更新到真实dom,也就是执行diff算法

  • 新节点没有文本节点的时候
    • 新老节点的子节点是否都存在,如果都存在并且不相等的话调用updateChildren
    • 新的有子节点,老的没有子节点
      • 检查是否有重复的key
      • 老节点有文本节点的话,清空老节点的文本节点
      • 为当前dom 节点加入子节点
    • 新的没有子节点,老的有子节点,删除老节点中的子节点,并且触发remove 和destory 钩子函数
    • 老的有文本节点的话,置空
  • 新老节点都有文本节点 修改文本
updateChildren

当新老节点都有子节点,并且新老节点是sameVnode的时候,会调用updateChildren

作用:对比新老子节点,找到差异,更新到dom树,如果没有发生变化的话会重用该节点

  • 老的开始节点和新的开始节点是sameVnode的时候
    • 直接将该vnode进行patchVnode
    • 获取下一组新老开始节点,就是他们的index值++ 并且重新给新老节点赋值
  • 老的结束节点和新的结束节点是sameVnode的时候
    • 直接将该vnode进行patchVnode
    • 获取下一组新老结束(end)节点,就是他们的index值-- 并且重新给新老节点赋值
  • 老的开始节点和新的结束节点是sameVnode的时候(处理数组翻转的情况)
    • 直接将该vnode进行patchVnode
    • 把老的开始节点移动到老的结束节点之后,就是移动到最后
    • 获取下一组节点(老的++ 新的--)
  • 老的结束节点和新的开始节点是sameVnode的时候(处理数组翻转的情况)
    • 直接将该vnode进行patchVnode
    • 把老的结束节点移动到老的开始节点之前,就是移动到最前
    • 获取下一组节点(老的-- 新的++)
  • 如果情况都不满足,newStartVnode 依次和旧的节点比较
    • 拿新的开始节点的key,去老节点数组中依次找相同key的老节点
      • 优化了找的过程 先把老节点数组中的key和索引存储到了一个对象 oldKeyToIdx 中
      • 如果新的开始节点有key属性,就去oldKeyToIdx中查找老节点的索引
      • 如果新的开始节点没有key属性,就要去老节点数组中依次遍历找到相同老节点对应的索引
      • 有key的话会快一点
    • 如果没有找到的话 ,说明没有相同节点
      • 创建新开始节点对应的dom对象,并插入到老的开始节点对应的dom元素前面
    • 如果找到老节点, 取出要移动的老节点
      • 如果找到的老节点和新的开始节点是sameVnode,执行patchVnode,并且将找到的旧节点移动到旧的开始节点之前
      • 如果不是sameVnode,就是key相同,但是tag不同,是不同的元素,那么创建新元素,并插入到老的开始节点对应的dom元素前面

结束之后index++ newStartVnode = newCh[++newStartIdx]

当一次节点对比结束时

  • 当结束时 oldStartIdx > oldEndIdx, 旧节点遍历完,但是新节点还没有,说明新节点比老节点多,把剩下的新节点插入到老节点的后面
  • 当结束时 newStartIdx > newEndIdx, 新节点遍历完,老节点数组还没有,删除老节点中的剩余节点