乐趣区

关于前端:Vue2源码整体流程浅析

本文基于 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.js
    const mount = Vue.prototype.$mount; // 在原来 $mount()根底上再封装一层逻辑,而后调用原来的 $mount
    Vue.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.js
    export 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.js
    Vue.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.js
    export 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.js
    Vue.prototype.__patch__ = inBrowser ? patch : noop
    // node_modules/vue/src/platforms/web/runtime/patch.js
    export const patch: Function = createPatchFunction({nodeOps, modules})
    // node_modules/vue/src/core/vdom/patch.js
    const 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.js
    function 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.js
    function 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)
    }
})

首次渲染流程图

退出移动版