Skip to content

VueJS 的编译阶段到挂载节点 #92

@coconilu

Description

@coconilu

概述

为了实现响应式模式,Vue用render函数来生成vnode,并使用diff算法对比新旧vnode,最后更新到真实DOM上。

由于是在编译阶段而不是在监听阶段,所以vnode没有对比的对象,直接通过vnode生成真实DOM。

Vnode是Vdom上的一个节点,是对真实DOM的抽象,在Vue中,我们可以通过对比新旧Vnode和Vdom来得到需要更新真实DOM的操作,并通过Vue框架来执行这些操作。于是我们可以把更多的精力投放到业务逻辑上。

编译阶段

该阶段会解析template,把template转化为render函数会经过三个过程:

  1. parse,将 template 模板中进行字符串解析,得到指令、class、style等数据,形成 AST
  2. optimize,这个阶段用于优化patch阶段,标记节点的 static 属性是否是静态的
  3. generate,将 AST 转化成 render funtion 字符串,最终得到 render 的字符串以及 staticRenderFns 字符串,这些字符串将来会在render函数里被执行——通过使用eval(string)

如果使用vue-cli工具的话,借助webpack可以在打包过程中把template转化为render函数和staticRenderFns函数

render 的字符串与render 函数的关系

render函数内部包含render字符串:

function render(vm) {
  with(vm) {
    eval(render_string)
  }
}

挂载节点

Vue实例化的最后一步就是挂载节点。该阶段会分为两步:

  1. 通过render函数获得vnode
  2. 通过传入vnode给patch函数(vm._update()函数中的vm.__patch__()),生成真实DOM并挂载到页面上

render函数被执行时机

那么render函数在什么时候会被再次执行呢?

在解释VueJS 响应式原理的时候有提到过,Render-Watcher实例的getter就是执行render函数的:

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

new Watcher(vm, updateComponent, noop, {
  before () {
    if (vm._isMounted) {
      callHook(vm, 'beforeUpdate')
    }
  }
}, true /* isRenderWatcher */)

所以,render函数会被执行的时机有:

  1. Vue初始化的时候,会执行一次
  2. 当template(模板)中需要观察的数据对象更新值的时候,也会触发render函数(render-watcher)执行

render函数的关键是_createElement ,负责返回VNode,它会根据标签名是否存在已注册的组件中,返回普通VNode或是组件VNode:

export function _createElement (
  context: Component,
  tag?: string | Class<Component> | Function | Object,
  data?: VNodeData,
  children?: any,
  normalizationType?: number
): VNode | Array<VNode> {
    // .......
  let vnode, ns
  if (typeof tag === 'string') {
    let Ctor
    ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag)
    if (config.isReservedTag(tag)) {
      // platform built-in elements
      vnode = new VNode(
        config.parsePlatformTagName(tag), data, children,
        undefined, undefined, context
      )
    } else if (isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {
      // component
      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 = new VNode(
        tag, data, children,
        undefined, undefined, context
      )
    }
  } else {
    // direct component options / constructor
    vnode = createComponent(tag, data, context, children)
  }
  if (Array.isArray(vnode)) {
    return vnode
  } else if (isDef(vnode)) {
    if (isDef(ns)) applyNS(vnode, ns)
    if (isDef(data)) registerDeepBindings(data)
    return vnode
  } else {
    return createEmptyVNode()
  }
}

patch函数执行时机

和render函数的一样,因为patch函数就在vm._update(vm._render(), hydrating)中的_update里。

  1. 在Vue初始化的时候,会生成真实DOM并挂载到document上
  2. 当template(模板)中需要观察的数据对象更新值的时候,会对比新旧vnode,并返回新vnode对应的真实DOM
Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
  const vm: Component = this
  const prevEl = vm.$el
  const prevVnode = vm._vnode
  const prevActiveInstance = activeInstance
  activeInstance = vm
  vm._vnode = vnode
  // Vue.prototype.__patch__ is injected in entry points
  // based on the rendering backend used.
  if (!prevVnode) {
    // 初始化渲染
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // 更新渲染
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  activeInstance = prevActiveInstance
  // 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.
}

更新的patch函数的核心是diff算法,类似git的diff指令,大致逻辑如下:

通过对比新旧vnode,找到更新真实DOM需要的所有操作,比如新增、删除、替换节点的操作。然后通过Vue框架来执行这些更新DOM的操作,最后返回更新的DOM。

参考

template 模板是怎样通过 Compile 编译的
Vue.js 技术揭秘

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions