本文基于Vue 2.6.14进行源码剖析
为了减少可读性,会对源码进行删减、调整程序、扭转的操作,文中所有源码均可视作为伪代码

文章内容

  • 流程图展现Vue2初始化渲染流程
  • 源码(删减、调整程序)剖析无/有Component时的渲染流程
  • 用简略例子,进行整体流程的剖析

整体流程图

流程图代码剖析

_init():初始化逻辑

  1. 初始化生命周期
  2. 初始化event
  3. 初始化createElement等渲染办法
  4. 生命周期beforeCreate调用
  5. 初始化props、methods、data、computed、watch
  6. 生命周期created调用
  7. vm.$mount渲染到实在DOM上

    function Vue (options) {  this._init(options);}Vue.prototype._init = function (options) { const vm = this; // 合并配置 vm.$options = mergeOptions(     resolveConstructorOptions(vm.constructor),     options || {},     vm ); initLifecycle(vm); // 初始化生命周期 initEvents(vm); // 初始化event initRender(vm); // 初始化createElement等渲染办法 callHook(vm, 'beforeCreate'); initInjections(vm); // resolve injections before data/props initState(vm); // 初始化props、methods、data、computed、watch initProvide(vm); // resolve provide after data/props callHook(vm, 'created'); if (vm.$options.el) {     vm.$mount(vm.$options.el); }};

    实例挂载剖析

    Vue.$mount流程

    从上面的代码剖析能够晓得,Vue.$mounte首先会判断是否有render()办法,如果没有手写render()办法,只有<template>,那得先把template转化为render()的模式,最终所有渲染都得转化为render()办法

    // node_modules/vue/src/platforms/web/entry-runtime-with-compiler.jsconst mount = Vue.prototype.$mount; // 在原来$mount()根底上再封装一层逻辑,而后调用原来的$mountVue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && query(el)  const options = this.$options   if (!options.render) { let template = options.template if (template) {   if (typeof template === 'string') {     if (template.charAt(0) === '#') {       template = idToTemplate(template)     }   } else if (template.nodeType) {     template = template.innerHTML   } else {     if (process.env.NODE_ENV !== 'production') {       warn('invalid template option:' + template, this)     }     return this   } } else if (el) {   template = getOuterHTML(el) } if (template) {   const { render, staticRenderFns } = compileToFunctions(template, {     outputSourceRange: process.env.NODE_ENV !== 'production',     shouldDecodeNewlines,     shouldDecodeNewlinesForHref,     delimiters: options.delimiters,     comments: options.comments   }, this)   options.render = render   options.staticRenderFns = staticRenderFns }  }  return mount.call(this, el, hydrating)}

    初始化渲染Watcher

    由上面代码能够晓得,转化render()会进行渲染Watcher的注册,而后调用生命周期mounted调用
    从上面代码剖析也能够晓得,最终渲染触发的办法是vm._update(vm._render(), hydrating)

    Vue.prototype.$mount = function (  el?: string | Element,  hydrating?: boolean): Component {  el = el && inBrowser ? query(el) : undefined  return mountComponent(this, el, hydrating)}// node_modules/vue/src/core/instance/lifecycle.jsexport function mountComponent (  vm: Component,  el: ?Element,  hydrating?: boolean): Component {  vm.$el = el  if (!vm.$options.render) { vm.$options.render = createEmptyVNode  }  callHook(vm, 'beforeMount')  // 删除源码中的if分支  let updateComponent;  updateComponent = () => { vm._update(vm._render(), hydrating)  }  new Watcher(vm, updateComponent, noop, { before () {   if (vm._isMounted && !vm._isDestroyed) {     callHook(vm, 'beforeUpdate')   } }  }, true /* isRenderWatcher */)  hydrating = false  if (vm.$vnode == null) { vm._isMounted = true callHook(vm, 'mounted')  }  return vm}
    首次渲染会触发new Watcher的渲染,因为首次渲染vm._isMounted=false,因而不会调用生命周期beforeUpdate,只有下一次渲染才会触发生命周期beforeUpdate的打印

    vm._render()

    最终通过调用render()办法进行渲染,而后返回VNode数据
    render函数传入vm.$createElement进行渲染
    在下面下面initRender()的剖析中,咱们晓得vm.$createElement=createElement
    createElement实际上会调用_createElement

    // node_modules/vue/src/core/instance/render.jsVue.prototype._render = function (): VNode { // ...   const { render, _parentVnode } = vm.$options vnode = render.call(vm._renderProxy, vm.$createElement) if (Array.isArray(vnode) && vnode.length === 1) {     vnode = vnode[0] } if (!(vnode instanceof VNode)) {     vnode = createEmptyVNode() } // set parent vnode.parent = _parentVnode return vnode}
    _createElement()
  • VNode的children节点进行解决,可能是任意类型,咱们须要解决为标准的length=1VNode数组
  • 依据tag进行VNode的创立,比方Component组件类型,须要调用不同的创立办法
  • 最初返回创立的VNode

    function _createElement(context, tag, data, children, normalizationType) {  // children的整顿和规范化  if (Array.isArray(children) && typeof children[0] === 'function') {      data = data || {};      data.scopedSlots = { default: children[0] };      children.length = 0;  }  if (normalizationType === ALWAYS_NORMALIZE) {      children = normalizeChildren(children);  } else if (normalizationType === SIMPLE_NORMALIZE) {      children = simpleNormalizeChildren(children);  }  // 依据tag做类型判断,是要间接创立createVNode还是createComponent  // 实质都是返回VNode数据  var vnode, ns;  if (typeof tag === 'string') {      var Ctor;      ns = (context.$vnode && context.$vnode.ns) || config.getTagNamespace(tag);      if (config.isReservedTag(tag)) {          vnode = new VNode(              config.parsePlatformTagName(tag), data, children,              undefined, undefined, context          );      } else if ((!data || !data.pre) && isDef(Ctor = resolveAsset(context.$options, 'components', tag))) {          // component          vnode = createComponent(Ctor, data, context, children, tag);      } else {          vnode = new VNode(              tag, data, children,              undefined, undefined, context          );      }  } else {      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()  }}
    createComponent():创立组件类型的VNode
    如果遇到组件类型,_createElement()则调用createComponent()进行组件VNode的创立
  • 继承Vue函数,构建扩大后的Constructor()办法
  • 合并4个钩子到VNodeData.hook中,不便后续逻辑调用
  • 传入下面构建的CtorVNodeData作为参数,实例化VNode
  • 返回VNode

    // node_modules/vue/src/core/vdom/create-component.jsexport function createComponent(...args): VNode | Array<VNode> | void {  // core/global-api/index.js: Vue.options._base = Vue  // 因而baseCtor = Vue  const baseCtor = context.$options._base  if (isObject(Ctor)) {      // Vue.extend = function (extendOptions: Object): Function {      //     const Sub = function VueComponent(options) {      //         this._init(options)      //     }      // }      Ctor = baseCtor.extend(Ctor); // 返回Vue的继承类,继承根底上扩大一些性能  }  // 合并4个钩子函数到VNodeData.hook中,不便后续逻辑调用  installComponentHooks(data)  // 创立vue-component类型的VNode    const name = Ctor.options.name || tag;  const vnode = new VNode(      `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,      data, undefined, undefined, undefined, context,      { Ctor, propsData, listeners, tag, children },      asyncFactory  )  return vnode}const componentVNodeHooks = {  init (...){},  prepatch (...){},  insert (...){},  destroy (...){}}const hooksToMerge = Object.keys(componentVNodeHooks)function installComponentHooks(data: VNodeData) {  const hooks = data.hook || (data.hook = {})  for (let i = 0; i < hooksToMerge.length; i++) {      const key = hooksToMerge[i]      const existing = hooks[key]      const toMerge = componentVNodeHooks[key]      if (existing !== toMerge && !(existing && existing._merged)) {          hooks[key] = existing ? mergeHook(toMerge, existing) : toMerge      }  }}

    vm._update()

  • 作用:获取vm._render()渲染的VNode,进行实在DOM的渲染
  • 流程:分为3种状况进行剖析,外围是调用createElm()办法进行VNode的渲染

    Vue.prototype._update = function (vnode: VNode, hydrating?: boolean) {  // ...  if (!prevVnode) {      // initial render      vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */);  } else {      // updates      vm.$el = vm.__patch__(prevVnode, vnode);  }  // ...}// node_modules/vue/src/platforms/web/runtime/index.jsVue.prototype.__patch__ = inBrowser ? patch : noop// node_modules/vue/src/platforms/web/runtime/patch.jsexport const patch: Function = createPatchFunction({ nodeOps, modules })
    // node_modules/vue/src/core/vdom/patch.jsconst hooks = ['create', 'activate', 'update', 'remove', 'destroy']export function createPatchFunction(backend) {  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]])          }      }  }  return function patch(oldVnode, vnode, hydrating, removeOnly) {      if (isRealElement) {          oldVnode = emptyNodeAt(oldVnode)      }      // 三种情景代码...  }}
    patch情景1: 初始化root/渲染更新-无可复用的VNode
    return function patch(oldVnode, vnode, hydrating, removeOnly) {  if (isRealElement) {      oldVnode = emptyNodeAt(oldVnode)  }  var isRealElement = isDef(oldVnode.nodeType);  if (!isRealElement && sameVnode(oldVnode, vnode)) {      // patch existing root node      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);  } else {      // 初始化root时调用      // index.html的id='app'      const oldElm = oldVnode.elm      // id='app'的<div>的parent,即body      const parentElm = nodeOps.parentNode(oldElm)      // create new node      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)      )      // destroy old node      if (isDef(parentElm)) {          removeVnodes([oldVnode], 0, 0)      } else if (isDef(oldVnode.tag)) {          invokeDestroyHook(oldVnode)      }  }}
  • 初始化root时调用,进行newVNode的创立,而后插入到id=app的旁边,而后删除<div id="app">的DOM,如上面代码所示
  • 渲染更新-无可复用的VNode,监测到sameVnode()=false,阐明以后VNode无奈复用,不是之前那个VNode,间接从新建设一个新的VNode,而后将旧的VNode删除(跟初始化root流程差不多)

    // 初始化root// patch之前的状态<div id='app'></div>// createElm之后的状态<div id='app'></div><div id='app1'></div>// destroy old node<div id='app1'></div>
    patch情景2: 渲染更新-可复用的VNode进行patchVnode

    监测到sameVnode()=true,阐明以后VNode可复用,间接进行数据更新,以及它们的childrendiff比拟,找出children可复用的中央(不可复用的中央得从新创立和销毁)

    var isRealElement = isDef(oldVnode.nodeType);if (!isRealElement && sameVnode(oldVnode, vnode)) {  // patch existing root node  patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly);}function sameVnode(a, b) {  return (      a.key === b.key &&      a.asyncFactory === b.asyncFactory && (          (              a.tag === b.tag &&              a.isComment === b.isComment &&              isDef(a.data) === isDef(b.data) &&              sameInputType(a, b)          ) || (              isTrue(a.isAsyncPlaceholder) &&              isUndef(b.asyncFactory.error)          )      )  )}

    注:当两个VNode的key、tag、isComment、VNodeData、inputType都雷同时,阐明是同一个节点,只是有所扭转,能够进行复用

    patchVnode()流程

    • 因为oldVNode和newVNode是同一个节点(sameVnode=true),尝试将oldVNode转化为新VNode,包含props、listeners、slot等更新
    • 执行update的钩子函数(自定义指令注册)
    • 依据它们各自的children进行分组解决

      • oldVNodeChildren!==newVNodeChildren,进行diff算法比对更新
      • oldVNodeChildren为空,newVNodeChildren不为空,执行newVNodeChildren新建插入操作
      • oldVNodeChildren不为空,newVNodeChildren为空,执行oldVNodeChildren删除操作
      • 如果是文本节点,则更新文本内容
    • 执行postpatch的钩子函数(自定义指令注册)

      具体代码剖析请看下一篇文章 Vue2 双端比拟diff算法-patchVNode流程浅析

    patchVnode()总结概述
    更新两个VNode的数据,并且比对两个VNodechlidren,先进行简略的解决,如果有其中一个不存在,则间接执行create/remove操作,如果两者都存在,才须要调用updateChildren()进行比照和复用

    patch情景3: Component内部结构渲染

    遇到组件渲染时,应用

    if (isUndef(oldVnode)) {    // empty mount (likely as component), create new root element    isInitialPatch = true;    createElm(vnode, insertedVnodeQueue);}
    外围办法createElm()-非component渲染

    流程图

  • 作用:通过VNode创立实在的DOM节点并插入
  • 流程:

    • document.createElement创立vnode.elm
    • 遍历children,进行createElm()的递归调用
    • 调用所有生命周期create的办法
    • 调用Node.appendChild/Node.insertBefore办法将VNode.elm挂载的DOM元素插入到目前parentElm
    如果是初始化,此时的parentElm=<Body></Body>
    // node_modules/vue/src/core/vdom/patch.jsfunction createElm(vnode, insertedVnodeQueue, parentElm, refElm, nested, ownerArray, index) {  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {      return;  }  const data = vnode.data  const children = vnode.children  const tag = vnode.tag  if (isDef(tag)) { // 有标签的内容      // 实质是document.createElement创立实在DOM的元素      vnode.elm = vnode.ns          ? nodeOps.createElementNS(vnode.ns, tag)          : nodeOps.createElement(tag, vnode)      setScope(vnode)      // if (Array.isArray(children)) {      //     for (let i = 0; i < children.length; ++i) {      //       createElm(children[i], insertedVnodeQueue, vnode.elm, null, true, children, i)      //     }      // } else if (isPrimitive(vnode.text)) {      //     nodeOps.appendChild(vnode.elm, nodeOps.createTextNode(String(vnode.text)))      // }      createChildren(vnode, children, insertedVnodeQueue); // 办法内容如下面正文代码      if (isDef(data)) {          invokeCreateHooks(vnode, insertedVnodeQueue); // 办法内容如上面正文代码          // for (let i = 0; i < cbs.create.length; ++i) {          //     cbs.create[i](emptyNode, vnode)          //   }          //   i = vnode.data.hook // Reuse variable          //   if (isDef(i)) {          //     if (isDef(i.create)) i.create(emptyNode, vnode)          //     if (isDef(i.insert)) insertedVnodeQueue.push(vnode)          // }      }      // parentElm = body(初始化时)      insert(parentElm, vnode.elm, refElm); // 办法内容如上面正文代码      // function insert (parent, elm, ref) {      //  if (isDef(parent)) {      //     if (isDef(ref)) {      //       if (nodeOps.parentNode(ref) === parent) {      //         // parent.insertBefore(elm, ref)      //         nodeOps.insertBefore(parent, elm, ref)      //       }      //     } else {      //       // parent.appendChild(elm)      //       nodeOps.appendChild(parent, elm)      //     }      // }}  } else if (isTrue(vnode.isComment)) { // 正文内容      vnode.elm = nodeOps.createComment(vnode.text)      insert(parentElm, vnode.elm, refElm)  } else { // 纯文本      vnode.elm = nodeOps.createTextNode(vnode.text)      insert(parentElm, vnode.elm, refElm)  }}
    外围办法createElm()-有component渲染

    由代码剖析能够晓得,会先调用createComponent()尝试进行Component的创立,如果创立胜利,则不持续往下执行

    // node_modules/vue/src/core/vdom/patch.jsfunction createElm(...args) {  if (createComponent(vnode, insertedVnodeQueue, parentElm, refElm)) {      return  }}
    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {  if (isDef((i = i.hook)) && isDef((i = i.init))) {      // Component外面的内容进行初始化和渲染      i(vnode, false /* hydrating */) // componentVNodeHooks.init()  }  if (isDef(vnode.componentInstance)) {      // 拿到曾经渲染好的Component的DOM树:vnode.componentInstance.$el      initComponent(vnode, insertedVnodeQueue)      // 将曾经渲染好的Component的DOM树插入到parentElm之前占位的<component>局部      insert(parentElm, vnode.elm, refElm)  }}var componentVNodeHooks = {  init: function (vnode, hydrating) {      var child = (vnode.componentInstance = createComponentInstanceForVnode(vnode, activeInstance));      child.$mount(hydrating ? vnode.elm : undefined, hydrating);  }}function initComponent(vnode, insertedVnodeQueue) {  // vnode.componentInstance.$el此时就是曾经渲染的Component造成的DOM树   vnode.elm = vnode.componentInstance.$el}
  • 由下面代码能够晓得,先调用了componentVNodeHooks.init()进行Component的外面内容的渲染:child.$mount
  • Component渲染实现后,将渲染实现的DOM挂载在vnode.componentInstance.$el
  • 而后再进行以后Component所在的占位符的parent的插入children-DOM的操作

    DOM的渲染程序因而是 先子后父

示例剖析-Component渲染

因为createComponet波及的点过多,因而应用例子进行独自剖析,次要是剖析创立Component所经验的流程

例子

具体代码请看github-component调试
<div id='el'></div><script type='text/x-template' id='demo-template'>  <div id='children1'>    <p id='children1_1'>Selected: {{ selected }}</p>    <component-select :options='options' v-model='selected' id='children1_2_component'>    </component-select>  </div></script><script type='text/x-template' id='select2-template'>  <select id='children_component_select'>    <option disabled value='0' id='children_component_select_option'>Select one</option>  </select></script>
<script>  Vue.component('component-select', {    .....  })  var vm = new Vue({    el: '#el',    template: '#demo-template',    data: {      selected: 0,      options: [        { id: 1, text: 'Hello' },        { id: 2, text: 'World' }      ]    }  })</script>

从下面html内容能够晓得,最终是要渲染出<component-select></component-select>组件内容

由一开始的剖析能够晓得,最终<template></template>都会转化为render()函数,下面示例代码最终转化的render()函数是

// _v = createTextVNode;// _c = function (a, b, c, d) { return createElement$1(vm, a, b, c, d, false); };(function anonymous() {    with (this) {        return _c('div', {            attrs: {                "id": "children1"            }        }, [_c('p', {            attrs: {                "id": "children1_1"            }        }, [_v("Selected: " + _s(selected))]), _v(" "), _c('component-select', {            attrs: {                "options": options,                "id": "children1_2_component"            },            model: {                value: (selected),                callback: function($$v) {                    selected = $$v                },                expression: "selected"            }        })], 1)    }})

首次渲染流程图