探一探源码/new Vue发生了什么(二)

发布于 / Front End / 0 条评论

一、render

在上一节末尾我们提到update方法中第一个参数是vm._render。那么这个函数的定义实际上是在src\core\instance\render.js里。

我们先把render的代码放上来,然后进行拆解

export function renderMixin (Vue: Class<Component>) {  //在最初initMixin里的rendermixin执行
  // install runtime convenience helpers
  installRenderHelpers(Vue.prototype)

  Vue.prototype.$nextTick = function (fn: Function) {  //挂载vue.$nextTick
    return nextTick(fn, this)
  }

  Vue.prototype._render = function (): VNode { //初始化vm._render
    const vm: Component = this
    const { render, _parentVnode } = vm.$options //拿到render

    if (_parentVnode) {
      vm.$scopedSlots = normalizeScopedSlots(
        _parentVnode.data.scopedSlots,
        vm.$slots,
        vm.$scopedSlots
      )
    }

    // set parent vnode. this allows render functions to have access
    // to the data on the placeholder node.
    vm.$vnode = _parentVnode
    // render self
    let vnode
    try {
      // There's no need to maintain a stack because all render fns are called
      // separately from one another. Nested component's render fns are called
      // when parent component is patched.
      currentRenderingInstance = vm
      vnode = render.call(vm._renderProxy, vm.$createElement)
    } catch (e) {
      handleError(e, vm, `render`)
      // return error render result,
      // or previous vnode to prevent render error causing blank component
      /* istanbul ignore else */
      if (process.env.NODE_ENV !== 'production' && vm.$options.renderError) {
        try {
          vnode = vm.$options.renderError.call(vm._renderProxy, vm.$createElement, e)
        } catch (e) {
          handleError(e, vm, `renderError`)
          vnode = vm._vnode
        }
      } else {
        vnode = vm._vnode
      }
    } finally {
      currentRenderingInstance = null
    }
    // if the returned array contains only a single node, allow it
    if (Array.isArray(vnode) && vnode.length === 1) {
      vnode = vnode[0]
    }
    // return empty vnode in case the render function errored out
    if (!(vnode instanceof VNode)) {
      if (process.env.NODE_ENV !== 'production' && Array.isArray(vnode)) {
        warn(
          'Multiple root nodes returned from render function. Render function ' +
          'should return a single root node.',
          vm
        )
      }
      vnode = createEmptyVNode()
    }
    // set parent
    vnode.parent = _parentVnode
    return vnode
  }
}

首先在最初我们从vm.$options里拿到render函数,全局搜索可知最初该render函数的定义在之前src\core\instance\lifecycle.js的 mountComponent 里

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

所以我们实际上知道如果用户不自己定义render函数,那么Vue会自动给你增加一个空的VNode作为render函数

vnode = render.call(vm._renderProxy, vm.$createElement)

同时我们注意到这行代码,实际上他就是调用了render的call方法把自己的上下文传进去,我们首先看一下第二个参数 vm.$createElement ,首先看看她的定义

  // bind the createElement fn to this instance
  // so that we get proper render context inside it.
  // args order: tag, data, children, normalizationType, alwaysNormalize
  // internal version is used by render functions compiled from templates
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  // normalization is always applied for the public version, used in
  // user-written render functions.
  vm.$createElement = (a, b, c, d) => createElement(vm, a, b, c, d, true)

后续我们继续查看createElement可以看到其实他就是定义一些tag,attrs还有有些children。我们在new Vue的时候其实可以利用这个函数自己写一个render,如下

new Vue({
  el: '#app',
  router,
  render(createElement){
    return createElement('div',{
      attrs:{
        id:'app'
      }
    },this.me1) 
  },
  data(){
    return{
      me1:"hello!",
    }
  }
})

接着我们看到第一个参数 vm._renderProxy ,全局搜索可知当Vue在initMixin的时候实际上就赋值这个renderProxy了。

    if (process.env.NODE_ENV !== 'production') {
      initProxy(vm)
    } else {
      vm._renderProxy = vm
    }

可以看到在生产环境下renderProxy就是vm,也就是this,在开发环境中执行了initProxy,继续看

  initProxy = function initProxy (vm) {
    if (hasProxy) {
      // determine which proxy handler to use
      const options = vm.$options
      const handlers = options.render && options.render._withStripped
        ? getHandler
        : hasHandler
      vm._renderProxy = new Proxy(vm, handlers)
    } else {
      vm._renderProxy = vm
    }
  }

实际上做的就是一件事情,在开发环境中用ES6的Proxy去劫持vm并进行一些Vue预定义的报错。(handlers),举一个最简单的例子,当模板中使用了methods或者data中未定义的数据时的报错就是从这里定义的。

二、 createElement

这里我们重点分析一下这个 createElement ,上述代码可知createElement 实际上是由两个版本的,唯一的区别就是最后一个参数的不同。

查看src\core\vdom\create-element.js可知这个参数实际上叫做alwaysNormalize,他是一个布尔值,当他为true时,normalizationType就为 ALWAYS_NORMALIZE ,否则为 SIMPLE_NORMALIZE ,如果我们是手写render函数,那么调用的实际上就是true这个分支。

// The template compiler attempts to minimize the need for normalization by
// statically analyzing the template at compile time.
//
// For plain HTML markup, normalization can be completely skipped because the
// generated render function is guaranteed to return Array<VNode>. There are
// two cases where extra normalization is needed:

// 1. When the children contains components - because a functional component
// may return an Array instead of a single root. In this case, just a simple
// normalization is needed - if any child is an Array, we flatten the whole
// thing with Array.prototype.concat. It is guaranteed to be only 1-level deep
// because functional components already normalize their own children.
export function simpleNormalizeChildren (children: any) {
  for (let i = 0; i < children.length; i++) {
    if (Array.isArray(children[i])) {
      return Array.prototype.concat.apply([], children)
    }
  }
  return children
}

// 2. When the children contains constructs that always generated nested Arrays,
// e.g. <template>, <slot>, v-for, or when the children is provided by user
// with hand-written render functions / JSX. In such cases a full normalization
// is needed to cater to all possible types of children values.
export function normalizeChildren (children: any): ?Array<VNode> {
  return isPrimitive(children)
    ? [createTextVNode(children)]
    : Array.isArray(children)
      ? normalizeArrayChildren(children)
      : undefined
}

上述这段代码的英文注释讲的很清楚,但是涉及到后续的函数式组件,暂时先放一放,总之这两个函数是有一个flat的功能,一个是一层的拍平,一个是递归的拍平。

三、update

上面有一句核心代码是vm._update(vm._render(), hydrating,我们来讲讲这个vm._update方法。全局搜索Vue.prototype._update可知在 lifecycleMixin 方法里有定义。

export function lifecycleMixin (Vue: Class<Component>) {
  Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) { //入参包含一个VNode
    const vm: Component = this //缓存vm
    const prevEl = vm.$el //缓存当前el
    const prevVnode = vm._vnode //缓存当前vnode
    const restoreActiveInstance = setActiveInstance(vm)
    vm._vnode = 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.
  }

我们看一下Vue.prototype.__patch__,他是在src\core\vdom\patch.js定义的

阅读源码可知这个文件中蕴含着大量的patch算法。

转载原创文章请注明,转载自: 静沐暖阳 » 探一探源码/new Vue发生了什么(二)
Not Comment Found