Vue.js源码学习——Vue如何和真实DOM建立联系__Vue.js
发布于 1 个月前 作者 banyungong 300 次浏览 来自 分享
粉丝福利 : 关注VUE中文社区公众号,回复视频领取粉丝福利

在之前mount实例的挂载的学习中,已经知道了mount挂载过程中最后调用了vm._update(vm._render(), hydrating)来将我们的Vue对象变成真实的DOM。

这一行代码中,hydrating先忽略,这个参数和服务器渲染相关。主要的就是renderupdate这两个方法。

render的执行过程

在我们新建一个Vue对象的时候,进行了很多初始化的工作。

src/core/instance/index.js文件中使用renderMixin(Vue)来定义了Vue.prototype._render。该方法的返回参数是一个vnode也就是Virtual DOM。这个vnode怎么来的呢?

在函数定义中可以看到存在这么一句语句:vnode = render.call(vm._renderProxy, vm.$createElement)。

render就是我们在创建Vue对象的时候自己定义的render函数。如下面的例子:

new Vue({
  el: '#app',
  render: function (createElement) {
    return createElement('div', this.message)
  },
  data: {
    message: 'Hello world!'
  }
})

vm._renderProxy在生产环境中就是Vue实例,在非生产环境中,会使用Proxy进行代理,对一些错误进行提示。第二个参数则是在我们自定义render函数时候的一个方法。

在initRender的时候,会初始化以下两行代码:

vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)

vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

第一行是通过模版编译或得到render函数的时候使用,第二个则是我们自定义render函数的时候使用。但本质上都调用了createElement这个方法。

createElement

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

createElement方法做了一下数据的处理:

在传递参数的时候,data可能存在为空的情况,当data满足第一个if中的条件的时候,则说明data为空,通过参数的前移获取真正对应的参数。之后将处理后的参数传递进入_createElement

以上面定义的例子进行单步调试:

这个例子在_createElement调用的时候参数比较有限,运行的分支逻辑也比较简单。主要进行了两步操作:

  1. 将children属性标准化,将数组拍平成为一维数组

    if (normalizationType === ALWAYS_NORMALIZE) {
        children = normalizeChildren(children);
    } else if (normalizationType === SIMPLE_NORMALIZE) {
        children = simpleNormalizeChildren(children);
    }
    
  2. 创建一个vnode

    vnode = new VNode(
    	config.parsePlatformTagName(tag), data, children,
    	undefined, undefined, context
    );
    

最后返回的这个vnode就是render函数返回的vnode。

update的执行过程

update的调用时机分为两个:

  • 初始渲染的时候
  • 数据变更→视图更新

这里先看初始渲染的过程。

src/core/instance/lifecycle.js里面定义Vue.prototype._update。由于是初始渲染的情况,prevNode是不存在的,就调用了vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)这一方法。

Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {
    const vm: Component = this
    const prevEl = vm.$el
    const prevVnode = vm._vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = vnode
    // Vue.prototype.__patch__ is injected in entry points
    // based on the rendering backend used.
    if (!prevVnode) {
      // initial render
      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
    } else {
      // updates
      vm.$el = vm.__patch__(prevVnode, vnode)
    }
    restoreActiveInstance()
    // 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.
  }

通过单步调试来查看一下进入该方法的时候的初始化参数的情况: 参数基本都为undefined,主要就执行了vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)。patch也是在Vue对象初始化的时候引入的。

patch相关的定义在src/platforms/web/runtime/patch.js文件中:

const patch: Function = createPatchFunction({ nodeOps, modules })

该文件中内容相对较少,patch就是createPatchFunction返回的函数。nodeOps是操作真实dom的方法。这里通过函数柯里化的方式在一开始将平台差异抹平,减少后续操作的分支处理。createPatchFunction返回了一个patch方法。该返回的patch方法中和真实dom相关的就是createElm方法。

patch

进入patch的一些参数:

由于本次不使用服务器渲染,第一部分只执行了oldVnode = emptyNodeAt(oldVnode);来将从文档中获取的dom节点转化为vnode。patch方法通过递归构建子元素,然后将子元素插入父元素,最后将创建好的元素插入到真实的DOM树中,在这一步之后,我们可以看到浏览器中的DOM就已经渲染好了。

总结

版权声明:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。 作者: 小馒头儿 原文链接:https://juejin.im/post/6925360511655657480

回到顶部