vue源码之从new Vue到DOM生成

初始化VUE

首先我们先来看一下vue2中源码目录结构,有助于我们理解分析。

src
├── compiler    # 编译相关(将模板解析成 AST 语法树,代码生成)
├── core        # 核心代码(内置组件、全局 API 封装,Vue 实例化、Obsever、Virtual DOM、工具函数 Util)
├── platforms   # 不同平台的支持(生产 WEBWEEX 平台支持的js)
├── server      # 服务端渲染(服务端渲染相关的逻辑)
├── sfc         # .vue 文件解析(把.vue 文件内容解析成一个 JavaScript 的对象)
├── shared      # 共享代码(定义浏览器端的 Vue.js 和服务端的 Vue.js 所共享的函数)

筒子们,我们都知道在开始创建vue项目的时候,需要我们 new Vue 来挂载虚拟dom,首先我们来看看new Vue 都执行了什么函数。
我们先找到 vue/src/core/instance/index.js 文件,当校验通过,会执行初始化(混入、存储、事件、生命周期、启动项)方法。

function Vue (options) {
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  this._init(options)
}

initMixin(Vue)
stateMixin(Vue)
eventsMixin(Vue)
lifecycleMixin(Vue)
renderMixin(Vue)

Vue的本质上就是一个用Function实现的Class类,然后它的原型prototypey以及它本身都扩展了一系列的方法和属性(initMixin、stateMixin、eventsMixin、lifecycleMixin、renderMixin方法中实现)。

new Vue的时候,会调用 this._init 方法, 通过该方法,合并配置、初始化生命周期、初始化事件中心、初始化渲染、初始化data 、props、computed、watcher、event、state等等。初始化最后,检测到如果有el 属性,则调用 vm.$mount 方法挂载vm, 把模板渲染成最终的DOM ,源码文件如下:

Vue.prototype._init = function (options?: Object) {
  const vm: Component = this
  // a uid
  vm._uid = uid++

  let startTag, endTag
  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    startTag = `vue-perf-start:${vm._uid}`
    endTag = `vue-perf-end:${vm._uid}`
    mark(startTag)
  }

  // a flag to avoid this being observed
  vm._isVue = true
  // merge options
  if (options && options._isComponent) {
    // optimize internal component instantiation
    // since dynamic options merging is pretty slow, and none of the
    // internal component options needs special treatment.
    initInternalComponent(vm, options)
  } else {
    vm.$options = mergeOptions(
      resolveConstructorOptions(vm.constructor),
      options || {},
      vm
    )
  }
  /* istanbul ignore else */
  if (process.env.NODE_ENV !== 'production') {
    initProxy(vm)
  } else {
    vm._renderProxy = vm
  }
  // expose real self
  vm._self = vm
  initLifecycle(vm)
  initEvents(vm)
  initRender(vm)
  callHook(vm, 'beforeCreate')
  initInjections(vm) // resolve injections before data/props
  initState(vm)
  initProvide(vm) // resolve provide after data/props
  callHook(vm, 'created')

  /* istanbul ignore if */
  if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
    vm._name = formatComponentName(vm, false)
    mark(endTag)
    measure(`vue ${vm._name} init`, startTag, endTag)
  }

  if (vm.$options.el) {
    vm.$mount(vm.$options.el)
  }
}

vue创建实例中的data为什么不能是对象呢?

实际上,它首先需要创建一个组件构造器,然后注册组件。注册组件的本质其实就是建立一个组件构造器的引用。使用组件才是真正创建一个组件实例。所以,注册组件其实并不产生新的组件类,但会产生一个可以用来实例化的新方式。理解这点之后,再理解js的原型链,如果两个实例同时引用一个对象,那么当你修改其中一个属性的时候,另外一个实例也会跟着改。这怎么可以,两个实例应该有自己各自的域才对,所以vue中_init方法的initState中对data类型进行了校验,vue创建的实例中,data应是方法并且返回对象,如下所示:

export function initState (vm: Component) {
  vm._watchers = []
  const opts = vm.$options
  if (opts.props) initProps(vm, opts.props)
  if (opts.methods) initMethods(vm, opts.methods)
  if (opts.data) {
    initData(vm)
  } else {
    observe(vm._data = {}, true /* asRootData */)
  }
  if (opts.computed) initComputed(vm, opts.computed)
  if (opts.watch && opts.watch !== nativeWatch) {
    initWatch(vm, opts.watch)
  }
}
function initData (vm: Component) {
  let data = vm.$options.data
  // 这里判断data是不是一个function
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : data || {}
  if (!isPlainObject(data)) {
    data = {}
    // 会报错给我们我们data未初始换成一个对象的错误
    process.env.NODE_ENV !== 'production' && warn(
      'data functions should return an object:\n' +
      'https://vuejs.org/v2/guide/components.html#data-Must-Be-a-Function',
      vm
    )
  }

我们看到vue的_init最后挂载到$mount中,我们来研究一下,这个方法中都做了什么。

如下所示,Vue不能挂载到body或html这样的根节点上,一般都用div嵌套包括起来,会被覆盖,若el挂载到body或者html上会报警告:Do not mount Vue to <html> or <body> - mount to normal elements instead.,如下所示:

// 原型上添加$mount方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  el = el && query(el)

  /* istanbul ignore if */
  // 若el挂载到body或者html上会报如下警告
  if (el === document.body || el === document.documentElement) {
    process.env.NODE_ENV !== 'production' && warn(
      `Do not mount Vue to <html> or <body> - mount to normal elements instead.`
    )
    return this
  }
 const options = this.$options
  // resolve template/el and convert to render function
   // 如果是已经render()的话,不必再compile()
  if (!options.render) {
    let template = options.template
    if (template) {
        .....
    }
  }
 // 如果是template模板,需要进行compile解析
 if (template) {
      /* istanbul ignore if */
      if (process.env.NODE_ENV !== 'production' && config.performance && mark) {
        mark('compile')
      }
  }
// 最后会创建DOM元素,在这里内容进行覆盖,这也是为什么外层一般要有一个父级div包裹它,而不是写在body或html上,实际上template会走一个compileToFunctions的过程
function getOuterHTML (el: Element): string {
  if (el.outerHTML) {
    return el.outerHTML
  } else {
    const container = document.createElement('div')
    container.appendChild(el.cloneNode(true))
    return container.innerHTML
  }
}

Vue.compile = compileToFunctions

Vue2.0版本中,所有的vue组件不论写的是el或者template属性进行渲染最终都需要render方法,_render():Vue实例的一个私有方法,它用来把实例渲染成一个虚拟Node,用一个原生的JS对象去描述一个DOM节点,会比创建一个DOM的代价要小很多,这里和react的思想是一样的

onstructor (
    tag?: string, // vNode的标签,例如div、p等标签
    data?: VNodeData,         // vNode上的的data值,包括其所有的class、attribute属性、style属性已经绑定的时间
    children?: ?Array<VNode>, // vNode上的子节点
    text?: string,        // 文本
    elm?: Node,           // vNode上对应的真实dom元素
    context?: Component,  //vdom的上下文
    componentOptions?: VNodeComponentOptions
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.functionalContext = 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
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

上面是VNode的初始化,然后Vue是通过createElement方法创建的VNode,如下所示:

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
  }
  // _createElement()是它的私有方法,创建成一个VNode,每个 VNode 有 children,children 每个元素也是一个 VNode,这样就形成了一个 VNode Tree
  return _createElement(context, tag, data, children, normalizationType)
}

Vue.prototype._update中都做了什么呢?

我们来看一下,为了把vNode转换为真实的DOM,_update会再首次渲染和数据更新的时候去调用,核心方法其实是其中的_patch()方法

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
  // 创建一个新的vNode
  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
    // 和之前的vNode,进行diff,将需要更新的dom操作和已经patch的vNode大道需要更新的vNode,完成真实的dom操作
    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里都做了什么,如下:

// 定义了生命周期,这些钩子函数
const hooks = ['create', 'activate', 'update', 'remove', 'destroy']

export function createPatchFunction (backend) {
  let i, j
  const cbs = {}
  const { modules, nodeOps } = backend
  for (i = 0; i < hooks.length; ++i) {
    cbs[hooks[i]] = []
    for (j = 0; j < modules.length; ++j) {
      if (isDef(modules[j][hooks[i]])) {
        cbs[hooks[i]].push(modules[j][hooks[i]])
      }
    }
  }
  // ...
  // oldVnode:旧的VNode节点or DOM对象
  // vnode: 执行了_render()之后范湖的VNode的节点
  // hydrating:是否是服务端渲染,因为patch是和平台相关的,在Web和Weex环境下,把VNode映射到平台DOM的方法也是不同(有它自己的nodeOps和modules)
  // removeOnly: 给transition-group用的
  return function patch (oldVnode, vnode, hydrating, removeOnly) {
    if (isUndef(vnode)) {
      if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
      return
    }

    let isInitialPatch = false
    const insertedVnodeQueue = []

    if (isUndef(oldVnode)) {
      // empty mount (likely as component), create new root element
      isInitialPatch = true
      // 创建新的节点
      createElm(vnode, insertedVnodeQueue)
    } else {
      const isRealElement = isDef(oldVnode.nodeType)
      if (!isRealElement && sameVnode(oldVnode, vnode)) {
        // patch existing root node
        // oldVNode和vnode进行diff,并对oldVnode打patch
        patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
      } else {
        if (isRealElement) {
          // mounting to a real element
          // check if this is server-rendered content and if we can perform
          // a successful hydration.
          if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
            oldVnode.removeAttribute(SSR_ATTR)
            hydrating = true
          }
          if (isTrue(hydrating)) {
            if (hydrate(oldVnode, vnode, insertedVnodeQueue)) {
              invokeInsertHook(vnode, insertedVnodeQueue, true)
              return oldVnode
            } else if (process.env.NODE_ENV !== 'production') {
              warn(
                'The client-side rendered virtual DOM tree is not matching ' +
                'server-rendered content. This is likely caused by incorrect ' +
                'HTML markup, for example nesting block-level elements inside ' +
                '<p>, or missing <tbody>. Bailing hydration and performing ' +
                'full client-side render.'
              )
            }
          }
          // either not server-rendered, or hydration failed.
          // create an empty node and replace it
          oldVnode = emptyNodeAt(oldVnode)
        }
        // replacing existing element
        const oldElm = oldVnode.elm
        const parentElm = nodeOps.parentNode(oldElm)
        // create new node
        // createElm的作用:通过传入的VNode去创建真是的DOM元素,并插图到它的父节点中,
        createElm(
          vnode,
          insertedVnodeQueue,
          // extremely rare edge case: do not insert if old element is in a
          // leaving transition. Only happens when combining transition +
          // keep-alive + HOCs. (#4590)
          oldElm._leaveCb ? null : parentElm,
          nodeOps.nextSibling(oldElm)
        )

        // update parent placeholder node element, recursively
        if (isDef(vnode.parent)) {
         ...
        }

        // destroy old node
        if (isDef(parentElm)) {
          removeVnodes(parentElm, [oldVnode], 0, 0)
        } else if (isDef(oldVnode.tag)) {
          invokeDestroyHook(oldVnode)
        }
      }
    }
    //执行所有created的钩子并把vnodepush到insertedVnodeQueue 中
    invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
    return vnode.elm
  }
}

其中对 oldVNode 和 vnode 类型判断中有一个sameVnode方法(在上面的 return 中),这个方法是 oldVNode 和 vnode 需要进行 diff 和 patch 的前提,把 sameVnode 拿出来,如下:

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

由于insert()方法把DOM插入到父节点中,进行了递归调用,子元素会优先调用 insert,所以整个 vnode 树节点的插入顺序是先子后父,如下:

insert(parentElm, vnode.elm, refElm)

function insert (parent, elm, ref) {
  if (isDef(parent)) {
    if (isDef(ref)) {
      if (ref.parentNode === parent) {
        nodeOps.insertBefore(parent, elm, ref)
      }
    } else {
      nodeOps.appendChild(parent, elm)
    }
  }
}
export function insertBefore (parentNode: Node, newNode: Node, referenceNode: Node) {
  parentNode.insertBefore(newNode, referenceNode)
}

export function appendChild (node: Node, child: Node) {
  node.appendChild(child)
}
...
...
...
 if (isDef(tag)) {
      if (process.env.NODE_ENV !== 'production') {
        if (data && data.pre) {
          creatingElmInVPre++
        }
        // 忘记注册组件的时候,会经常遇到如下报错,这个刚开始的时候遇到的情况很多
        if (isUnknownElement(vnode, creatingElmInVPre)) {
          warn(
            'Unknown custom element: <' + tag + '> - did you ' +
            'register the component correctly? For recursive components, ' +
            'make sure to provide the "name" option.',
            vnode.context
          )
        }
      }
    .....
}

可以看到最终返回的是一个patch()方法,赋值给vm.patch()方法
在createElm过程中,可以看到如果vnode节点不包含tag的话,它有可能是一个注释或者纯文本节点,可以直接插入到父元素中,递归创建一个完整的DOM并插入到body中。

最后来个我们来总结个渲染流程:
在这里插入图片描述

  • 5
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值