vue 2.6.14 初始流程简析

337 阅读2分钟

环境准备

git clone https://github.com/vuejs/vue.git
目前稳定版本是 2.6.14

修改 package.json 加上 --sourcemap
"dev": "rollup -w -c scripts/config.js --environment TARGET:web-full-dev --sourcemap",

下载依赖,注自己设置淘宝镜像源
yarn

npm run dev

终止控制台
dist/vue.js 生成 sourcemap 文件,可以指向源码

vscode 添加 debugger 配置 launch.json 内容如下:
{
    // Use IntelliSense to learn about possible attributes.
    // Hover to view descriptions of existing attributes.
    // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
    "version": "0.2.0",
    "configurations": [
        {
            "type": "pwa-chrome",
            "request": "launch",
            "name": "Launch Chrome against localhost",
            // "url": "http://localhost:8080",
            "file": "${workspaceFolder}/vue/examples/grid/index.html" // 修改为自己调试路径
        }
    ]
}

html script 直接引用 dist/vue.js 调试

调试 html

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <title>Vue.js grid component example</title>
  <!-- <link rel="stylesheet" href="style.css"> -->
  <!-- Delete ".min" for console warnings in development -->
  <script src="../../dist/vue.js"></script>
</head>

<body>
  <div id="app"></div>

  <script>
    let Child = {
      template: `
      <button @click="clickHandler($event)">
        click me
      </button>
      `,
      methods: {
        clickHandler(e) {
          console.log('Button clicked!', e)
          this.$emit('select')
          this.$emit('my')
        }
      }
    }

    let vm = new Vue({
      el: '#app',
      template: `
      <div id="test">
        <child @select="selectHandler" @my="selectHandler" @click.native.prevent="clickHandler"></child>
      </div>
      `,
      methods: {
        clickHandler() {
          console.log('Child clicked!')
        },
        selectHandler() {
          console.log('Child select!')
        }
      },
      components: {
        Child
      }
    })
  </script>


</body>

</html>

初始化流程

简单的文本显示

let vm = new Vue({
  el: '#app',
  template: `
      <div id="test">
       {{msg}}
      </div>
      `,
  data(){
    return {
      msg: 'dxx'
    }
  },
})

流程如下:

image.png

_init: 先简单理解是初始化准备,打印下实例:

11.png

重写的 $mount : 在 compileToFunctions (vue/src/compiler/to-function.js) 返回打个断点,可查看生成的 ast 和 render

image.png

执行 $mount:

流程上看:执行编译阶段生成的 render 函数,创建 vnode,然后将 vnode 生成 DOM ,渲染到页面中。

函数执行栈,先执行的函数进入栈底,先进后出:

image.png

就拿 mountComponent 和 Wathcer 来说:

mountComponent 函数入栈,里面调用 new Watcher,当 Watcher 执行完后,出栈,回到 mountComponent 环境中。

我将 mountComponent 中要等待 Wathcer 标记为等待,即:

function mountComponent (){
	new Watcher();
	等待 Watcher 执行完
	if (vm.$vnode == null) {
    vm._isMounted = true
    callHook(vm, 'mounted')
  }
  return vm
}

其他的类推,需要栈顶完成出栈后再执行,整理了整个过程的等待函数位置:

33.png

render 的 Vnode 如下:

image.png

可以看到 外面是 div vnode,children 里面 是个文本 vnode

patch 函数逻辑

function patch(oldVnode, vnode, hydrating, removeOnly) {
  // vnode 没有
  if (isUndef(vnode)) {
    // oldVnode 有 -> 销毁逻辑
    if (isDef(oldVnode)) invokeDestroyHook(oldVnode)
    return
  }
  // vnode 有
  if (isUndef(oldVnode)) {
    // 加载组件时,根元素
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, null, null, removeOnly)
    } else {
      createElm(
        vnode,
        insertedVnodeQueue,
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )
    }
  }

  invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch)
  return vnode.elm
}
新节点旧节点处理
没有销毁旧 invokeDestroyHook(oldVnode)
没有没有return
非元素且一样 -> diff 算法;其他->创建新的
没有创建新的 createElm(vnode, insertedVnodeQueue)

createElm

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)) {
    vnode.elm = vnode.ns
      ? nodeOps.createElementNS(vnode.ns, tag)
    : nodeOps.createElement(tag, vnode)
    // 子 node 处理 还调 createElm
    createChildren(vnode, children, insertedVnodeQueue)
    if (isDef(data)) {
      invokeCreateHooks(vnode, insertedVnodeQueue)
    }

    insert(parentElm, vnode.elm, refElm)
  } 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)
  }

}

上例中,patch 时,oldVnode 是 div#test 元素,vnode 为 render 创建的,新旧都有且旧是元素,走 createElm(vnode,insertedVnodeQueue,oldElm._leaveCb ? null : parentElm,nodeOps.nextSibling(oldElm))

创建元素,非组件,tag 为 div,vnode.elm 为空 div ,然后处理 children,child vnode.elm 为文本,将它插入到父 DOM(div) 中,子完成后,处理 div 的属性(invokeCreateHooks),将 div 放到 body 中。

createElm 执行完回到 patch,移除旧(div#app),执行(invokeInsertHook),返回 vnode.elm (div#test)

回到 _update,回到 Watcher,回到 mountComponent ,结束。

内容为组件

let App = {
 render: h => h('div','dxx1')
} 
new Vue({
  render: h => h(App)
}).$mount('#app')

执行 _init:同上,因为 options 没有传 el,需要后面手动执行 $mount

$mount

区别:这里自定义 render 函数(而上例由 template 编译生成),省略编译步骤

生成组件 vnode:

image.png

patch 和原来一样,但 createElm 进入组件实例初始挂载(和 Vue 实例差不多),会等组件渲染完后,才执行根挂载。

组件 init vnode.data.hook.init :

1、new VueComponent -> _init

2、$mount

组件实例时注意:

1、vnode.componentOptions.Ctor 是什么:

组件 vnode 创建时 Ctor(VueComponent) = Vue.extend(组件对象)

Vue.extend 是将 VueComponent 原型指向 Vue.prototype,还有 Vue 的函数方法赋给 VueComponent

2、new VueComponent 的入参 options

{
  _isComponent: true,
  _parentVnode: vnode, // 父 vnode 即上图 vue-component-1
  parent // activeInstance Vue 实例
}

组件 $mount

div Vnode

image.png

搞清楚 vm 中 _vnode, $vnode, $el 是什么

改变它们的地方:
_vnode
_update:
vm._vnode = vnode

$vnode
initRender:
vm._vnode = null // the root of the child tree
vm._staticTrees = null // v-once cached trees
const options = vm.$options
const parentVnode = vm.$vnode = options._parentVnode // 组件实例传来的
const renderContext = parentVnode && parentVnode.context

$el
mountComponent
vm.$el = el

_update
vm.$el = vm.__patch__()
if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
  vm.$parent.$el = vm.$el
}

声明:

vnode: <App /> vnode

渲染 vnode 页面真实渲染的: App 组件内容 即<div>dxx</div> 的 div vnode,

它们关系如下:

执行环境方法$vnode 父_vnode 渲染$el
VueinitRender
mountComponentdiv#app
_updateApp Vnode同上;组件执行完 _update 后会改为 div Vnode
VueComponentinitRenderApp Vnode
mountComponent同上
_update同上div Vnode
__patch__同上同上div Vnode

44.png

整个执行过程:

Vue 初始化,$mount('#app'),生成 App Vnode ;

update 中,创建 App DOM 是组件,进入组件初始化;

VueComponent 初始化,$mount(undefined),生成 div Vnode 渲染 vnode;

update 中,创建好 div DOM,将 Vue.$el 改为 创好的 div DOM,注意此时 DOM 还没有插入到 html 中;

组件 mountComponent 执行环境完成后,回到 Vue 上下文,修改初始 App Vnode.elm 为 div,执行 invokeCreateHooks

createComponent 中将组件 div 插入到 body 中;

执行 App Vnode, invokeInsertHook 组件 mounted 生命周期就在此时执行;

执行 Vue 的 mounted;

看一开始的例子

let Child = {
  template: `
      <button @click="clickHandler($event)">
        click me
      </button>
      `,
  methods: {
    clickHandler(e) {
      console.log('Button clicked!', e)
      this.$emit('select')
      this.$emit('my')
    }
  }
}

let vm = new Vue({
  el: '#app',
  template: `
      <div id="test">
        <child @select="selectHandler" @my="selectHandler" @click.native.prevent="clickHandler"></child>
      </div>
      `,
  data(){
    return {
      msg: 'dxx'
    }
  },
  methods: {
    clickHandler() {
      console.log('Child clicked!')
    },
    selectHandler() {
      console.log('Child select!')
    }
  },
  components: {
    Child
  }
})

这个相当于原来的 <App /> 包了一层 div,所以

if (vm.$vnode && vm.$parent && vm.$vnode === vm.$parent._vnode) {
 vm.$parent.$el = vm.$el
}
这里不会执行
等父执行完 div#test 赋给 vm.$el

image.png

总结

本文用简单的例子调试分析 Vue 初始加载的整个流程。就是初始化, render 函数生成 vnode,vnode 生成 DOM,当 vnode 是组件节点,走组件流程,初始创建挂载组件;vnode 是元素、注释、文本,创建相应元素。整个是个深度遍历过程。所有子创建完后,最后挂到 body 中。