VNode 是什么? 有什么用?

VNode 表示 虚拟节点 Virtual DOM,为什么叫虚拟节点呢,因为不是真的 DOM 节点,是一个 DOM 副本。

是什么?

一个JavaScript 对象,本质上是轻量的 JavaScript 数据格式来表示真实的 DOM 在特定时间的外观。

  1. {
  2. tag: 'div',
  3. data: {attrs: {}, ...}
  4. children: []
  5. }

有什么用?

  1. 减少DOM 操作。用 JavaScript 直接操作 DOM 的计算成本很高,比在 JavaScript 环境执行 JavaScript 代码更消耗资源

    • 假设有 1000个元素列表,创建 1000个JavaScript 对象是非常节省的,也非常快。但是创建 1000 个实际的 DOM 节点要昂贵得多。

    • 在需要更新列表项时,将更改应用至 JavaScript 副本、虚拟 DOM 中,然后在它们和实际 DOM 之间执行 diff

    • 最后,才对已更改的内容,进行批量处理调用,并一次性更改 DOM,对 UI 进行高效的更新!

  2. 跨平台。

    • 虚拟 DOM 把渲染逻辑,从真实 DOM 中分离出来,所以需要更新时先 计算差异,然后再将这些更改,再应用到 DOM 上。

    • 如果去掉 更新到 DOM 这一步,那么所有的更新逻辑实际上都是在 更改 JavaScript 对象而已,因为它不需要接触 DOM 了。

    • 只要在操作 DOM 做这一层抽象,就可以应用到任何能运行 JavaScript 环境的应用,可以是原生渲染 (例如,IOS或Android),让 虚拟DOM 进行原生渲染 如:WEEX

VNode 怎么生成的?

Vue 源码中,是通过一个类(class)去表示 VNode 的,最终也是 通过实例化 new 创建/生成 一个 VNode 节点。

  1. class VNode {
  2. constructor (
  3. tag?: string,
  4. data?: VNodeData,
  5. children?: ?Array<VNode>,
  6. text?: string,
  7. elm?: Node,
  8. context?: Component,
  9. componentOptions?: VNodeComponentOptions
  10. ) {
  11. /*当前节点的标签名*/
  12. this.tag = tag
  13. /*当前节点对应的对象,包含了具体的一些数据信息,是一个VNodeData类型,可以参考VNodeData类型中的数据信息*/
  14. this.data = data
  15. /*当前节点的子节点,是一个数组*/
  16. this.children = children
  17. /*当前节点的文本*/
  18. this.text = text
  19. /*当前虚拟节点对应的真实dom节点*/
  20. this.elm = elm
  21. /*当前节点的名字空间*/
  22. this.ns = undefined
  23. /*当前节点的编译作用域*/
  24. this.context = context
  25. /*函数化组件作用域*/
  26. this.functionalContext = undefined
  27. /*节点的key属性,被当作节点的标志,用以优化*/
  28. this.key = data && data.key
  29. /*组件的option选项*/
  30. this.componentOptions = componentOptions
  31. /*当前节点对应的组件的实例*/
  32. this.componentInstance = undefined
  33. /*当前节点的父节点*/
  34. this.parent = undefined
  35. /*简而言之就是是否为原生HTML或只是普通文本,innerHTML的时候为true,textContent的时候为false*/
  36. this.raw = false
  37. /*是否为静态节点*/
  38. this.isStatic = false
  39. /*是否作为跟节点插入*/
  40. this.isRootInsert = true
  41. /*是否为注释节点*/
  42. this.isComment = false
  43. /*是否为克隆节点*/
  44. this.isCloned = false
  45. /*是否有v-once指令*/
  46. this.isOnce = false
  47. }

比如 使用 VNode 去描述这样一个 template

Template
image.png
VNode
image.png

VNode 什么时候生成的?

image.png

在初始化完选项,解析完模板之后,就需要挂载 DOM了。此时就需要生成 VNode,才能根据 VNode 生成 DOM 然后挂载,也就是在 beforeMount 之后,在 mounted 之前生成,当 mounted 被触发时已经完成 DOM 挂载。

挂载 DOM 第一步,就是先执行渲染函数,得到整个模板的 VNode

比如 有以下渲染函数,执行会返回 VNode,就是 _c 返回的VNode

  1. function (){
  2. with(this){
  3. return _c('div',{attrs:{"href":"https://baidu.com"}},["1111"]).
  4. }
  5. }
  • 渲染函数执行时 会通过 **with** 绑定执行时上下文

  • _c其实就是vm._c ```javascript vm._c = function(a, b, c, d) {

    return createElement(vm, a, b, c, d, false);

};

  1. ```javascript
  2. function createElement(context, tag, data, children, normalizationType) {
  3. let vnode, ns
  4. if (typeof tag === 'string') {
  5. /* 判断是否是保留的标签 */
  6. if (config.isReservedTag(tag)) {
  7. // 是保留标签 则创建相应的节点的 VNode
  8. vnode = new VNode(
  9. config.parsePlatformTagName(tag), data, children,
  10. undefined, undefined, context
  11. )
  12. } else {
  13. /*
  14. * 从 vm 实例的 option 的 components 中寻找该 tag
  15. * 存在则就是一个组件,创建相应节点,Ctor 为组件的构造类
  16. */
  17. vnode = createComponent(Ctor, data, context, children, tag)
  18. }
  19. } else {
  20. // tag 不是字符串的时候 则是组件
  21. vnode = createComponent(tag, data, context, children)
  22. }
  23. }

从上面看到 普通 HTML 标签 组件标签 都不同流程

1. 普通 HTML 标签

  • Template

    1. <div href="xxxx"></div>
  • render Function

    1. function (){
    2. with(this){
    3. return _c('div',{
    4. attrs:{"href":"xxxx"}},
    5. ["1111"]
    6. )
    7. }
    8. }
  • new VNode

    1. new VNode(tag, data, children, undefined, undefined, context);

    Vue VNode 虚拟DOM - 图4

    2. 组件标签

  • Template

    1. <div>
    2. <test :name="name">hello</test>
    3. </div>
  • render Function

    1. with(this){
    2. return _c('div',[
    3. _c('test',
    4. {attrs:{"name":name}},
    5. ["hello"]
    6. )
    7. ],1)
    8. }
  • create Component

    1. createComponent(Ctor, data, context, children, tag);}

    Vue VNode 虚拟DOM - 图5

    1. function createComponent (Ctor, data, context, children, tag) {
    2. /** 父组件给子组件绑定的props */
    3. const propsData = extractPropsFromVNodeData(data, Ctor, tag)
    4. /** 父组件给子组件绑定的 events 事件 */
    5. const listeners = data.on
    6. const name = Ctor.options.name || tag
    7. const vnode = new VNode(
    8. `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
    9. /** 组件数据 */
    10. data,
    11. undefined, undefined, undefined,
    12. /** 组件实例上下文 */
    13. context,
    14. /** 组件数据对象 */
    15. { Ctor, propsData, listeners, tag, children },
    16. asyncFactory
    17. )
    18. return vnode
    19. }

VNode 存放在哪里?

1. _vnode

vm._vnode 存放表示当前节点的 VNode
什么叫当前,也就是可以通过这个 VNode 直接映射成 当前真实DOM ,也可以说是旧节点,当进行 Diff 比较时所用的 VNode。

赋值

  1. // src\core\instance\lifecycle.js
  2. Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  3. const vm: Component = this
  4. const prevEl = vm.$el
  5. const prevVnode = vm._vnode
  6. // 保存 VNode
  7. vm._vnode = vnode
  8. if (!prevVnode) {
  9. // initial render
  10. // 初始时 渲染
  11. vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  12. } else {
  13. // updates
  14. // 更新时 渲染
  15. vm.$el = vm.__patch__(prevVnode, vnode)
  16. }
  17. }

2. $vnode

vm.$vnode 只有组件实例的才有,存放的是外壳节点,页面实例中内容是不存放在 $vnode 的,例如:

  1. /*
  2. <test :name="name"></test>
  3. */
  4. vm.$vnode = {
  5. data: { class: [name], ...},
  6. tag: "vue-component-1-test"
  7. }

赋值

  1. // src\core\instance\lifecycle.js
  2. function updateChildComponent(
  3. vm, parentVnode
  4. ) {
  5. vm.$options._parentVnode = parentVnode;
  6. vm.$vnode = parentVnode;
  7. if (vm._vnode) {
  8. vm._vnode.parent = parentVnode;
  9. }
  10. }