Vue 从render函数到真实dom

1,334 阅读3分钟

前言

前段时间,分别学习了newVue流程、Vue的响应式、Vue的模板编译,为了将所有流程串联起来,这几天参考源码梳理了下从render函数到真实dom挂载到页面的过程。

整体流程

我们再来回忆一下整个流程:

  • 初始化的时候,将数据变成响应式,每个数据的key,get函数中收集依赖(Watcher实例)到dep,set函数中dep通知watcher执行更新函数(参考Vue的响应式
  • $mount时获取render函数,options中存在render函数无需处理,否则将template编译成render函数(参考Vue的模板编译
  • mount时,new Watcher传入updateComponent更新函数,在更新函数中执行render函数,获取到VNode的树,通过patch方法,将VNode转化成真实dom树。因为Watcher实例在执行render函数的过程中,访问数据触发了get收集依赖,所以当数据变化时,就通知updateComponent更新dom。
function $mount(el: any) {
    const options = this.$options;
    const vm = this
    // 获取render函数
    if (!options.render) {
      let template = options.template
      if (typeof template === 'string') {
        const root = parse(template);
        optimize(root, {});
        const code = generate(root)
        options.render = new Function(code.render);
      }
    }
    vm.$el = document.querySelector(el)
    // 更新组件的函数
    let updateComponent = () => {
        // 执行render函数,传入$createElement其实就是手写render函数中的h函数,主要用来创建Vnode
        const vnode = options.render.call(vm, vm.$createElement);
        vm._update(vnode)
     }
     new Watcher(vm, updateComponent, () => {}, {})
    return vm
  }
  

render函数生成Vnode

我们知道render函数有两种方式得到,

  • 1.手写的render函数
  • 2.传入template字符串或写的.vue组件,编译生成的render函数
// 1.
render(h) {
    return h('div', 'Hello World!')
}

// 2.
function() {
    with(this) { 
        return _c( "div", { }, [ _v('Hello World!') ] ) 
    }
}

第1种方式传入的h函数和第2种方式的_c函数,最终执行的都是createElement;在init中会执行initRender函数,

  • 第1种方式是因为with绑定了vue实例,直接可以访问
  • 第2种方式是在执行render的时候,vm.$createElement作为参数传入
initRender(vm: any) {
    vm._c = (a: any, b: any, c: any, d: any) => createElement(vm, a, b, c, d, false);
    vm.$createElement = (a: any, b: any, c: any, d: any) => createElement(vm, a, b, c, d, true)
  }

createElement

对参数进行处理,将传入编译或者传入的对象,转换成vnode,实际就是创建VNode实例。

export function createElement(
  context: any,
  tag: any,
  data: any,
  children: any,
  normalizationType: any,
  alwaysNormalize: boolean
): Array<any> {
  if (Array.isArray(data) || isPrimitive(data)) {
    normalizationType = children
    children = data
    data = undefined
  }
  return _createElement(context, tag, data, children, normalizationType)
}

export function _createElement(
  context: any,
  tag?: any,
  data?: any,
  children?: any,
  normalizationType?: number
): any | Array<any> {
  // ...
  let vnode
  if (typeof tag === 'string') {
    vnode = new VNode(
      tag, data, children,
      undefined, undefined, context
    )
  }
  return vnode
}

VNode是什么

在Vue.js中存在一个VNode类,使用它可以实例化不同类型的vnode实例,而不同类型的vnode实例各自表示不同类型的DOM元素。简单的说,vnode可以理解成节点描述对象,它描述了应该怎样去创建真实的dom节点。

class VNode {
  constructor(
    tag?: string,
    data?: any,
    children?: Array<VNode>,
    text?: string,
    elm?: Node,
    context?: any,
    componentOptions?: any,
    asyncFactory?: Function
  ) {
    this.tag = tag // 标签名,元素节点或组件该值会存在
    this.data = data
    this.children = children // 子元素
    this.text = text // 文本或者注释内容
    this.elm = elm // 对应的真实dom
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false // 是否是注释节点
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }
}

patch函数

执行完render函数获取vnode后,我们将vnode传入,执行update函数,核心执行的patch函数,将vnode转化成真实dom。下面步骤是忽略了diff的简化过程。

  • 传入旧的vnode和新的node
  • 根据旧的vnode找到元素的原本位置,将新的vnode创建成dom
  • 最后将新生成的dom插入对应位置,移除页面旧的dom
function _update(vnode: any) {
    const vm: any = this;
    const prevVnode = vm._vnode;
    vm._vnode = vnode
    if (!prevVnode) {
      vm.$el = vm.__patch__(vm.$el, vnode)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
  }
  
// patch方法
function patch(oldVnode: any, vnode: any) {
    // 第一次传入真实dom,创建一个vnode
    if (isDef(oldVnode.nodeType)) {
      oldVnode = new VNode(oldVnode.tagName.toLowerCase(), {}, [], undefined, oldVnode)
    }
    // 创建元素
    createElm(
      vnode,
      oldElm.parentNode,
      oldElm.nextSibling
    )
    removeNode(oldVnode.elm); // 移除旧的dom
    return vnode.elm
  }

// 创建元素
  function createElm(vnode: any, parentElm?: any, refElm?: any, ownerArray?: any, index?: number) {
    const children = vnode.children
    const tag = vnode.tag
    // 创建元素节点
    if (tag != null) {
      vnode.elm = nodeOps.createElement(tag, vnode)
      // 递归创建dom树
      createChildren(vnode, children)
    } else if (vnode.isComment) { // 创建注释节点
      vnode.elm = nodeOps.createComment(vnode.text)
    } else { // 创建文本节点
      vnode.elm = nodeOps.createTextNode(vnode.text)
    }
    // 元素插入到对应位置
    insert(parentElm, vnode.elm, refElm)
  }
  
  // 创建子元素
  function createChildren() {
    if (Array.isArray(children)) {
      for (let i = 0; i < children.length; ++i) {
        createElm(children[i], vnode.elm, null, children, i)
      }
    } else if (tyeof vnode.text === "string" || tyeof vnode.text === "number") {
      nodeOps.appendChild(vnode.elm, document.createTextNode(String(vnode.text)))
    }
  }

最后

2021年过的很快,这一年总结起来,感觉看的东西也比较乱,没有一个系统化的过程,但是知识是还是靠一点点积累的,虽然写的文章目前还是比较简单的,但是也是对自己知识的一种梳理,即是分享又是总结。走过路过的朋友,多多点赞关注,祝大家2022年加油,新年快乐~

代码地址:github.com/zhuye1993/m…