Vue源码解读

Vue的不同构建版本

对于Vue的不同构建版本,以下图说明
Vue的不同构建版本表

  • 有min后缀的是生产版本,其他是开发版本
  • Full表示完整版,同时包含编译器和运行时
    • 编译器:将模板字符串编译为JavaScript渲染函数的代码,即template => h(),体积大(3000多行),效率低
    • 运行时:用来创建Vue实例,渲染并处理虚拟DOM等代码,不包含编译器,体积小,效率高
  • Runtime-only是运行时版本,不包含编译器
  • UMD:通用模块化版本,支持多种模块方式,可以在浏览器环境下直接运行(挂载到window上)
  • CommonJS:配合browserify或webpack等打包工具使用
  • ES Module:为现代打包工具提供的版本,主要是可以进行静态分析,进行tree-shaking来删除无用代码

vue-cli默认使用vue.runtime.esm.js版本,因为编译的过程实际上放在构建流程中更合适。

  • vue-cli对webpack进行了深度封装,如果需要查看webpack的具体配置,可以使用命令vue inspect > output.js将webpack的配置输出到output.js文件中,便于查看(但该文件内容不是有效的配置文件,不能直接使用)。
  • vue-cli在webpack的resolve选项中添加了alias: { vue$: 'vue/dist/vue.runtime.esm.js' }路径别名,这表明使用import vue from 'vue'时,导入的是vue.runtime.esm.js文件

Vue构造函数及初始化

从UMD标准的完整vue.js版本出发(适用于web开发使用),从该版本的打包入口文件来分析Vue构造函数。
入口文件:src/platforms/web/entry-runtime-with-compiler.js
该入口文件中导入了Vue,循着Vue变量的导入导出轨迹可以得到下面的文件:

src/platforms/web/entry-runtime-with-compiler.js
  • web平台打包入口文件
  • 重写了平台相关的$mount方法,增加了编译能力。每个平台的$mount都不同,需要重写。
  • 注册了Vue.compile静态方法,传递一个HTML字符串,返回render函数
src/platforms/web/runtime/index.js
  • web平台相关
  • 注册和平台相关的全局指令:v-model、v-show
  • 注册和平台相关的全局组件:v-transition、v-transition-group
  • 定义了Vue原型的方法:
    • __patch__:虚拟DOM转换为真实DOM,就是patch函数
    • $mount:挂载方法,内部调用mountComponent
src/core/index.js

core文件夹主要是Vue的初始化global-api文件夹和instance文件夹、响应式处理observer文件夹和虚拟DOM vdom文件夹

  • 与平台无关
  • 初始化一些实例属性和方法
  • 调用initGlobalAPI(Vue),初始化Vue的静态方法
src/core/instance/index.js
  • 与平台无关
  • 定义了Vue构造函数,调用this._init(options)方法
  • 给Vue混入了常用的实例成员$data、$props、 $set、 $delete、 $watch

Vue初始化静态成员

初始化静态成员的操作在src/core/global-api文件夹中。

主要涉及到了以下文件

src/core/global-api/index.js

initGlobalAPI函数:

  • 为Vue挂载一个config属性,指向全局配置信息,config见src/core/config.js
  • 为Vue挂载了Vue.set Vue.delete Vue.nextTick三个方法
  • 为Vue挂载了Vue.observable方法
  • 为Vue挂载了options属性
    • options内定义了components、directives和filters三个属性,分别用来存储全局注册的组件、指令和过滤器
    • options_base存储了Vue构造函数,内部使用
    • options.components初始化了内置组件keep-alive
  • 初始化了Vue.use方法initUse(Vue),插件注册src/core/global-api/use.js
  • 初始化了Vue.mixin方法initMixin(Vue),全局混入src/core/global-api/mixin.js
  • 初始化了Vue.extend方法initExtend(Vue),返回Vue构造函数的子类src/core/global-api/extend.js
  • 初始化了Vue.directIve方法、Vue.component方法和Vue.filter方法initAssetRegisters(Vue),全局注册指令、组件和过滤器。src/core/global-api/assets.js

Vue初始化实例成员

初始化实例成员的操作在src/core/instance文件夹中。

src/core/instance/index.js
  • initMixin(Vue):给Vue的原型挂载一个_init()方法
  • stateMixin(Vue):注册Vue原型的$data、$props、 $set、 $delete、 $watch这些方法
  • eventsMixin(Vue):初始化事件相关方法$on $once $off $emit
  • lifecycleMixin(Vue):初始化生命周期相关的混入方法_update(内部调用了__patch__方法来渲染或更新DOM) $forceUpdate $destroy
  • renderMixin(Vue):混入render $nextTick _render

vm._render()方法调用编译后的render函数或用户传入的render函数,返回虚拟DOM节点
vm._update()方法将虚拟DOM节点转换为真实DOM
Vue中使用了with语句来延长作用域链,在作用域链前端增加了临时对象,如with(this){ return _c('div, {attrs: { "id": "app" }}', [_c('h1', [_v(_s(msg))])]) },msg变量实际应该是this.msg,通过使用with语句省略了this

数据响应式原理

响应式的代码都定义在src/core/observer文件夹内

将待观察对象传入observe函数,在observe函数中创建Observer对象,Observer构造函数内部创建与待观察对象相关的dep对象,然后遍历待观察对象的属性,使用defineReactive函数创建响应式属性。

每一个被观察对象(如传给Vue的data对象)都会创建一个dep对象,每一个被观察对象的属性也会创建一个dep对象,如果是深度监听,则嵌套对象的属性也会创建dep对象

属性 -> dependencies调用notify -> watcher 异步更新 -> vnode -> views
只有调用notify才会引起视图更新,因此数组元素即使用索引值更新了,也不会触发视图更新,就是因为使用索引值更新没有定义notify,只有调用改写过的数组方法才会调用notify。

依赖收集过程:

  • 在getter函数中收集依赖,如果Dep.target存在(即watcher实例化了),则开始收集依赖(将watcher添加到该属性的dep实例的subs属性中)
  • 实例化watcher是在mountComponent()函数中,给Dep.target赋值是在watcher的构造函数中,dep.depend是用来将当前Dep.target(即watcher)添加到dep.subs数组中
  • watcher用来将更新后的数据转换到虚拟DOM中,并更新真实DOM

数组的监测是用额外的方式实现的,将数组的一些会改变值的方法改写,并添加了dep.notify()方法来触发更新,所以只有使用被改写的数组方法操作数组,采用触发响应式更新。
改写的方法包括’push’, ‘pop’, ‘shift’, ‘unshift’, ‘splice’, ‘sort’, ‘reverse’
但是这样的话,只要数组变动,所有依赖数组的watcher都会去更新。
数组的元素也会去收集依赖,但元素更新不会触发响应式,收集依赖的目的是如果元素有变化,则自动更新到新的值

有个问题:为什么数组不能用getter/setter来转换为响应式,毕竟数组也是属性为索引值的对象,而且JavaScript是支持这种响应式写法的

我觉得主要是因为数组的元素数量很多,遍历太多的元素设置响应式会影响性能(内存空间占据大),而且用户一般不会对每个元素都去更改,数组更多的主要是提供遍历,对数组元素内部的嵌套对象只要有监听就行,而不是监听数组的每个元素。
如果要对数组的索引对应的值更新,其实使用改写过的splice方法就可以做到了,splice可以做到删除并插入新元素,实际上就是更新索引对应的值。

Watcher侦听器

Watcher有三种类型,computed watcher(计算属性侦听器)、用户watcher(侦听器属性)、渲染watcher(即实现响应式自动更新视图的watcher)。前两种watcher是在initState方法中初始化。
创建顺序: 计算属性watcher、用户watcher(侦听器)、渲染watcher

用户watcher就是给用户一个接口,用于自定义数据更新时的回调函数,类似Hook机制。

渲染watch的创建:

渲染watcher在mountComponent中创建,mountComponent是在编译模板后调用,只要执行mountComponent挂载到DOM中时,就会创建渲染watcher。在代码中,通常就是创建Vue实例或者组件实例时会创建渲染watcher。

  • 渲染watcher的getter属性是updateComponent函数

计算属性watcher和用户watcher的创建:

vue/src/core/instance/init.js文件中,Vue实例方法_init中调用initState()方法(vue/src/core/instance/state.js),该方法内部初始化了实例状态,即将选项对象的data、props、methods、computed、watch等属性初始化,并创建了计算属性watcher和用户watcher,存入vm._watchers数组中。

  • 计算属性watcher和用户watcher的getter属性分别是计算属性的getter函数和用户watcher侦听的数据的getter函数。

计算属性的初始化与watcher的lazy属性缓存效果

在创建好计算属性watcher后,调用defineComputed(vm, key, userDef)将计算属性挂载到vm上。在挂载的过程中,defineComputed(vm, key, userDef)会调用createComputedGetter(key),为计算属性创建getter。如果计算属性也定义了setter,则同时也会创建setter。

重点是计算属性的getter如何实现缓存
function createComputedGetter (key) {
// 返回了计算属性的getter函数
  return function computedGetter () {
    const watcher = this._computedWatchers && this._computedWatchers[key]
    if (watcher) {
      if (watcher.dirty) {
        watcher.evaluate()
      }
      if (Dep.target) {
        watcher.depend()
      }
      return watcher.value
    }
  }
}

计算属性的watcher.lazy是true, 则watcher.dirty = watcher.lazy = true,就会执行watcher.evaluate(),从而求得watcher.value值作为getter返回的值,并且将watcher.dirty = false。另外,当我们执行this.get()时是会为Dep.target赋值的,所以还会执行watcher.depend(),将计算属性的watcher添加到依赖中去。现在,计算属性的初始化完毕。只要watcher.dirty为false,则计算属性的值就不会重新计算,实现了缓存效果。

除非计算属性的依赖data发生变化,则会引起watcher.update执行,重新对watcher的dirty赋值为true,计算属性重新求值。

update () {
    /* istanbul ignore else */
    if (this.lazy) {
      // 计算属性的watcher,dirty属性赋值true
      this.dirty = true
    } else if (this.sync) {
      this.run()
    } else {
      // 将当前watcher放入队列,异步执行更新DOM的操作
      queueWatcher(this)
    }
  }

watcher的执行过程

  • 由dep.notify()方法触发,遍历dep中的watcher,按顺序依次执行watcher.update
  • watcher.update中,将watcher放入队列中合适的位置(按watcher的id来排序),等待异步调用nextTick(flushSchedulerQueue)
  • 触发beforeUpdate钩子函数
  • watcher调用run方法
  • 执行计算属性watcher和用户watcher
  • 执行渲染watcher,更新Vnode,更新DOM

Vue.set静态方法或vm.$set方法

  • 对于数组的处理,可以使用vm.$set(vm.items, indexOfItem, newValue)使用索引指定值,同样是响应式,原因在于内部使用了splice方法
  • 对于对象的处理,如果对象是响应式的,则将属性也设置为该对象的响应式属性,并且调用notify(表示对象有更新),更新视图。

Vue.delete或vm.$delete方法
与Vue.set类似,使用splice操作数组、使用delete操作符删除对象属性,然后调用notify。

Vue监测不到的那些操作主要是由于这些操作并没有增加notify通知更新视图,但实际上这些操作已经改变了数据,只是没有反映到视图上。

对象内部属性值的更改不会触发对象的更改(即不会触发对象发出更新通知notify),而给对象新增或删除、或者重置该对象的值则会触发对象的更改。
比如使用Vue.$watch()(用户watcher)浅监听对象时,更改对象内部的属性值并不会触发对象的notify。

Vue.nextTick(cb):将回调函数cb放入异步更新的队列中,从而获取到DOM更新后的值。
在watcher更新DOM时,同样在watcher的update方法中使用Vue.nextTick将watcher放入异步队列,然后异步更新DOM。因此,DOM的更新都是异步的。

首次渲染

过程

初始化 -> 编译 -> 挂载
其中挂载又包括:实例化watcher -> 创建Vnode(在此期间收集依赖) -> 渲染Vnode到DOM

Vue() -> this._init():初始化了很多事情,如生命周期、事件、响应式数据、触发beforeCreate钩子函数、依赖注入、状态、触发created钩子函数 -> 调用平台相关的this.$mount():被重写后的$mount -> 编译模板:调用将模板编译为render函数的compileToFunctions -> 调用与平台无关的vm.$mount方法 -> 调用mountComponent():内部定义了updateComponent方法 ->beforeMount钩子函数触发 -> 实例化new Watcher(): 渲染watcher -> updateComponent() -> vm._render() -> 渲染函数render -> createElement(): 创建vnode -> vm._update() -> vm.__patch__():将Vnode转换到真实DOM树

Vue的虚拟DOM

h函数:即vm.$createElement的别名
参数

  • tag
  • data
    • attrs:特性节点
    • props:属性
  • children
  • text
  • elm:虚拟DOM节点转换为真实DOM节点后存储到elm,vnode与DOM节点的映射关系,在将vnode与DOM树操作时要用到
  • key

updateComponent函数在mountComponent中定义,然后传递给Watcher构造函数,在watcher中执行。
updateComponent函数先调用vm._render函数
虚拟DOM的创建流程:
watcher -> updateComponent -> _update -> _render -> render(用户传入或者模板编译得到的render) -> vm.$createElement或vm._c(_c表示模板编译的render) -> createElement -> _createElement:创建并返回Vnode

  • createElement:用来对vm.$createElement传入的参数进行处理,再转交给_createElement。也就是说,createElement函数是用来保证API的一致性。
  • _createElement:创建vnode
    • 不允许vnode的data使用响应式数据,否则调用new Vnode,返回空节点
    • data.is属性:用于创建动态组件
    • data.key属性:用来提高虚拟DOM比对的效率,重用DOM节点
    • Vnode构造函数:类似于snabbdom中的Vnode,但提供了更加丰富的属性
  • _update方法:处理vnode
    • 调用vm.__patch__()方法

      • 首次渲染:将vm.$el转化为vnode,比较差异,挂载到真实DOM
      • 更新渲染:比较前后vnode的差异,更新到真实DOM
    • vm.__patch__()实际上就是patch函数(src/platforms/web/runtime/patch.js),由createPatchFunction({ nodeOps, modules })生成

      • nodeOps是一些DOM节点操作方法的集合
      • modules是platformModules和baseModules的拼接集合
        • platformModules是平台相关的API方法,在web中是attrs, klass, events, domProps, style, transition,操作DOM的其他节点或事件
        • baseModules是与平台无法的API方法,这里是directive和ref
      • patch函数初始化:收集modules中的hooks函数
      • patch函数的执行:
        • 如果只调用vm.$mount(),不传入挂载点元素,则不挂载到DOM,只创建该DOM节点
        • 判断oldVnode.nodeType
          • 如果是1则为真实DOM,说明是首次渲染
            • 将oldVnode转换为Vnode,调用了emptyNodeAt(elm)
            • 找到oldVnode的父元素,将新的Vnode创建为DOM节点并插入到父元素之中。
          • 如果不是1,则开始判断前后两个节点是否相同
            • 如果相同,则开始执行patchVnode,对比两个节点并更新DOM
            • 如果不相同,则移除旧节点,挂载新节点
        • 触发insertedVnodeQueue中所有vnode的insert钩子函数,insert钩子函数定义在vnode.data.hook.insert上
        • createElm函数:将vnode转换为DOM元素节点并插入到DOM树
          • vnode是组件、标签元素、注释节点、文本节点四种情况分别处理
          • createChildren函数
            • checkDuplicateKeys:判断是否有相同的key
            • 通过createElm创建DOM元素节点或创建文本节点
          • 触发create钩子函数
            • 先触发modules中的create钩子函数
            • 再触发data.hook.create钩子函数
          • 判断是否有insert钩子函数,如果有,则将vnode加入到insertedVnodeQueue数组中
          • 执行insert,将DOM元素插入到DOM树中
    • patchVnode函数的执行

      • 执行prepatch钩子函数
      • 获取新旧节点的子节点集合
      • 调用modules中的update钩子函数,更新节点的属性、样式、事件等等
      • 调用用户传入的update钩子函数
      • 开始对比新旧两个节点
        • 新的节点没有text
          • 如果新旧节点都有子节点,且子节点不同,则调用updateChildren
            • updateChildren内部基本上与snabbdom的方式一样,但在使用key的时候有点差别。Vue在使用新节点的key在旧节点中查找(这时候的查找使用的是key-index的映射表)时,如果没有对应的索引,则会遍历旧节点来找到。如果还没找到,才认定新节点需要创建,而不能复用旧节点。
          • 如果新节点有子节点,旧节点没有子节点,则调用checkDuplicate判断key是否有重复,然后情况旧节点的文本内容,调用addVnodes把新节点的子节点添加到DOM树
          • 如果旧节点有子节点,新节点没有子节点,则调用removeVnodes删除旧节点的子节点,触发remove和destroy钩子函数
          • 如果新旧节点都没有子节点,但旧节点有text,清空旧节点的text内容
        • 新旧节点的text不一致,更新text内容
      • 获取postpatch钩子函数并执行

Vue的模板编译

模板编译为render函数
模板编译开始的位置:web平台下的重写之后的Vue.prototype.$mount()中调用

模板编译的结果

  • 对template编译后得到vm._c、vm._m、vm._v等函数,在Vue中使用了with(this)语法来扩展作用域链,从而使得调用_c、_m等不需要在前面添加vm。
  • 这些函数除了_c外,都定义在src/core/instance/render-helpers中,在这个文件中,导出了installRenderHelpers,该函数给vm挂载了很多render辅助方法
    • src/core/instance/render.js中导入了installRenderHelpers,在renderMixin函数中调用installRenderHelpers(Vue.prototype)
    • _m:即renderStatic,处理模板中的静态内容
    • _v:即createTextVnode,创建文本节点new VNode(undefined, undefined, undefined, String(val))。标签中的空格、换行的文本内容也会创建文本节点,而这些文本内容一般是没意义的,所以尽量不要有这些内容。Vue3中的render函数已经为此优化过了。
    • _s:即toString,主要使用JSON.stringify()String()来将参数转换为字符串
  • _c函数在src/core/instance/render.js中,定义为vm._c = (a, b, c, d) => createElement(vm, a, b, c, d, false)
  • 对于组件而言,组件标签并没有进一步展开对组件模板的编译,组件模板的编译开始于调用createElement函数,即render函数调用,将标签转换为vnode,其中调用了createElement函数。
vue-template-explorer

使用vue-template-explorer来将模板编译为render函数

模板编译的过程

  • 在web平台,入口文件src/platforms/web/entry-runtime-with-compiler.js中为vue实例注册了compileToFunctionsVue.compile = compileToFunctions

  • src/platforms/web/compiler/index.js中,调用createCompiler(baseOptions)返回compileToFunctions,其中baseOptions如下:

    // baseOptions定义
    baseOptions: CompilerOptions = {
      expectHTML: true,
      modules,
      directives,
      isPreTag,
      isUnaryTag,
      mustUseProp,
      canBeLeftOpenTag,
      isReservedTag,
      getTagNamespace,
      staticKeys: genStaticKeys(modules)
    }
    
  • createCompiler函数又是在文件src/compiler/index.js中,由createCompilerCreator函数接收参数baseCompile而创建,createCompilerCreator(function baseCompile (template: string, options: CompilerOptions) {}

    • createCompiler函数定义了compile函数,并导出该函数

    • createCompiler函数导出compileToFunctions

      // createCompiler函数的返回值
      return {
        compile,
        compileToFunctions: createCompileToFunctionFn(compile)
      }
      
  • createCompilerCreator函数在src/compiler/create-compiler.js中定义

  • 整个调用流程为createCompilerCreator -> createCompiler -> createCompileToFunctionFn(compile) -> compileToFunctions -> Vue.compile

  • CompileToFunctions函数:

    • 先判断是否有编译缓存

    • 调用compile(template, options),返回编译结果compiled

      • compiled.render内容是JavaScript代码的字符串形式
    • 调用createFunction(compiled.render, fnGenErrors),将字符串形式的JavaScript代码转换为函数,内部很简单,使用了new Function的方式

      res.render = createFunction(compiled.render, fnGenErrors)
      res.staticRenderFns = compiled.staticRenderFns.map(code => {
        return createFunction(code, fnGenErrors)
      })
      
    • 缓存编译结果,以template为key,以res对象为值

编译的核心函数compile
  • compile (template: string, options?: CompilerOptions),如果传入了options,则合并baseOptions与options为finalOptions
  • 调用baseCompile(template.trim(), finalOptions),返回compiled(字符串形式的JavaScript代码),并记录编译的错误信息。
baseCompile模板核心编译函数
  • 模板转换为抽象语法树ast = parse(template.trim(), options)

    • 模板字符串转换成AST后,可以通过AST做优化
    • 标记模板中的静态内容,在patch时可以直接跳过(静态内容不会变化,不用比对和重新渲染)
  • 优化抽象语法树optimize(ast, options)

  • 将抽象语法树转换为字符串形式的JavaScript代码code = generate(ast, options)

  • 返回编译结果对象

    return {
      ast,
      // 渲染函数,不过要注意,这里的render并不是我们最终结果的render函数
      render: code.render,
      // 静态渲染函数,生成静态Vnode树
      staticRenderFns: code.staticRenderFns
    }
    
使用AST explorer网站查看各种语言或工具生成的AST

使用节点属性static来表征节点是否是静态的。

parse函数解析模板字符串生成AST

src/compiler/parser/index.js文件中定义了parse函数以及一系列解析指令、修饰符等的函数

  • 调用parseHTML函数:接收template,返回root(AST),内部会调用createASTElement()创建AST节点
    • 通过options传入了一些解析中需要用到的函数如下
      • start:解析到开始标签时执行,创建AST节点,解析指令
      • end:解析到结束标签时执行
      • chars:解析到文本内容时执行
      • comment:解析到注释内容时执行
    • parseHTML函数在src/compiler/parser/html-parser.js中定义,借鉴了http://erik.eae.net/simplehtmlparser/simplehtmlparser.js开源库
      • 通过预定义一些正则表达式,开始对html解析,每解析完一次,就将html截取未解析的部分后重新复制。
    • 解析指令:通过指令名称,获取指令的值,然后记录到AST节点中
优化AST

optimize(ast, options)在文件src/compiler/optimizer.js中定义

  • markStatic(root)标记root中的静态节点
    • isStatic(node)判断是否静态节点,记录到node.static中
      • 如果有node.pre(由v-pre指令而来)属性,也是静态的
    • 判断是否是保留标签,如果不是,则是组件,需要对判断tag是不是slot以及inline-template
    • 遍历子节点,递归调用markStatic,如果子节点只要不是static,那节点就不是static
    • 判断条件渲染的节点,即node.ifConditions,遍历条件渲染值,标记静态节点。
  • markStaticRoots(root, false)标记root中的静态根节点。静态根节点指的是节点有子节点集合(大于1),且第一个子节点不是文本节点,实际上就是元素嵌套元素时,且元素内容是文本的形式。
    • 判断过程与markStatic类似
生成代码

code = generate(ast, options)在文件src/compiler/codegen/index.js中定义。

  • const state = new CodegenState(options)创建生成代码过程中用到的状态对象

  • code = ast ? genElement(ast, state) : '_c("div")'调用genElement(ast, state)生成代码

  • 返回结果对象

    return {
      render: `with(this){return ${code}}`,
      staticRenderFns: state.staticRenderFns
    }
    
  • genElement的执行:代码全部都是字符串形式

    • 判断节点是否是静态根节点且没有被处理过,如果是,则调用genStatic(el, state)

      • genStatic(el, state)内部再次调用genElement方法,将生成的代码拼接后push到state.staticRenderFns中。因为一个AST中可能会有多个静态子树,所以将这些静态子树以数组形式存储。返回一个字符串形式的代码
      state.staticRenderFns.push(`with(this){return ${
        genElement(el, state)}
      }`)
      // ...中间的代码略过
      // 返回的字符串形式的代码,该代码只记录静态子树渲染函数代码的索引,而实际代码在数组中,通过_m()函数来得到
      return
      \`_m(${
        // 这里实际上就是每个静态子树对应的索引
        state.staticRenderFns.length - 1
      }${
        el.staticInFor? ',true' : ''
      })`
      
      • _m方法:就是renderStatic函数,内部由一个cached缓存,存储的是静态子树索引与对应的Vnode子树(如果缓存没有,则调用静态子树的渲染函数来创建Vnode子树,并调用markStatic()标记该子树的静态属性为true)
      tree = cached[index] = this.$options.staticRenderFns[index].call(
        this._renderProxy,
        null,
        this // for render fns generated for functional component templates
      )
      // 标记静态子树的静态属性为true
      markStatic(tree, `__static__${index}`, false)
      
    • getData函数返回一个字符串形式的data对象,该data对象用于传入_createElement函数来创建Vnode

    • 处理子节点,调用genChildren将子节点转换为数组形式,genChildren中对children调用map方法,对children中的每个元素调用gen方法将其生成一个节点对应的代码

    • 将生成的代码组合起来,从AST节点到子节点的代码

code = `_c('${el.tag}'${
  data ? `,${data}` : '' // data
}${
  children ? `,${children}` : '' // children
})`
createCompileToFunctionFn(compile)将compile函数返回的compiled对象内部的字符串形式JavaScript转换为函数形式的JavaScript代码
  • 调用createFunction将字符串形式的code转换为函数new Function(code)
// 把字符串形式代码转换为函数
res.render = createFunction(compiled.render, fnGenErrors)
res.staticRenderFns = compiled.staticRenderFns.map(code => {
  return createFunction(code, fnGenErrors)
})
模板编译总结

模板字符串 -> AST -> 优化(标记静态节点) -> 生成字符串形式代码 -> new Function(code)匿名函数

Vue的组件

  • 一个组件就是一个拥有预定义选项的Vue实例
  • 一个组件可以组成页面上的一个功能完备的区域,组件可以包含脚本、样式、模板

组件注册

  • 全局注册时,会将传入的选项对象使用Vue.extend(options)转换为Vue的子类构造函数,再记录;如果传入的是函数,则直接记录在Vue.options[‘components’]对象中。

  • 局部注册时,先不会转换为构造函数,而是将组件的选项对象记录在父组件的options[‘components’]属性中。

Vue.extend方法

src/core/global-api/extend.js文件中,Vue.extend方法定义在函数initExtend中

  • 设定构造函数的cid属性(Vue和组件构造函数每个都具有cid),用于缓存
  • 组件构造函数与Vue形成继承关系
  • 使用proxy getters初始化props和computed属性
  • 将Vue的静态成员赋值给组件构造函数

组件创建

在编译模板之后,模板内部的组件对应标签并未被进一步编译,组件的创建开始于模板编译过的render函数被调用,render函数内部调用了createElement函数,组件的创建开始于createElement函数,然后就像父组件或根Vue节点一样,开始编译组件自己的模板。

  • createElement创建占位vnode:在创建组件对应的vnode时,会先去父组件注册的组件里寻找,找不到的时候回退到原型链上寻找(即寻找全局组件)。找到后,再调用createComponent(),根据选项对象生成构造函数,进而创建组件,先生成一个占位用的vnode
  • patch过程中调用init钩子函数生成组件实例:接下来在新老vnode的对比中,调用了init钩子函数创建真正的组件实例与对应的vnode,重新走一遍Vue实例的创建、编译模板、挂载流程。

组件构造函数或组件选项对象传入render的h函数(vm.$createElement)中,在init钩子函数中转换为vnode并返回。
组件的颗粒度不是越小越好,因为组件的创建过程会多次执行,比较耗费性能。
_createElement() -> src/core/vdom/create-component.js文件中的createComponent():创建并返回vnode -> installComponentHooks -> new Vnode() -> 调用patch()函数 -> createElm -> src/core/vdom/patch.js文件中的createComponent -> init -> createComponentInstanceForVnode -> new vnode.componentOptions.Ctor(options) 调用组件构造函数创建实例 -> 调用this._init(),即Vue的实例方法_init -> 调用组件的$mount(),挂载到DOM树上。

createComponent:安装钩子函数,创建占位Vnode
  • 第一个参数Ctor: Class<Component> | Function | Object | void,当传入的是对象时,调用Ctor = baseCtor.extend(Ctor)Vue.extend来将对象转换为构造函数

  • 调用installComponentHooks安装组件钩子函数,将用户传入的钩子函数与componentVNodeHooks中的四个钩子函数(init、prepatch、insert和destroy)做合并。

    • 合并的过程就是将两个同类的钩子函数放在一个函数中,依次调用

      function mergeHook (f1: any, f2: any): Function {
        const merged = (a, b) => {
          // flow complains about extra args which is why we use any
          f1(a, b)
          f2(a, b)
        }
        merged._merged = true
        return merged
      }
      
    • 组件实例的创建过程来自于init钩子函数,init内部调用createComponentInstanceForVnode,而createComponentInstanceForVnode调用new vnode.componentOptions.Ctor(options),其中的Ctor即组件构造函数

  • 调用new Vnode创建Vnode

    const vnode = new VNode(
      `vue-component-${Ctor.cid}${name ? `-${name}` : ''}`,
      data, undefined, undefined, undefined, context,
      { Ctor, propsData, listeners, tag, children },
      asyncFactory
    )
    

patch过程创建真正的组件实例与对应Vnode

init钩子函数创建组件实例
init钩子函数在patch函数中调用
patch函数将组件对应的vnode转换到真实DOM上
patch -> createElm -> src/core/vdom/patch.js文件中的createComponent -> init -> createComponentInstanceForVnode -> 调用组件构造函数 -> 调用this._init(),即Vue的实例方法_init -> 调用组件的$mount(),开始挂载过程(包括创建vnode、依赖收集、patch过程转换为真实DOM)。

// createComponent中获取init钩子函数并调用
if (isDef(i = i.hook) && isDef(i = i.init)) {
        i(vnode, false /* hydrating */)
}

_init方法中对组件执行的方法

  • initInternalComponent
  • initLifecycle,记录组件的父子关系
  • Vue.prototype._update中执行const restoreActiveInstance = setActiveInstance(vm),将组件实例缓存。调用restoreActiveInstance()恢复activeInstance。这些代码为了解决组件嵌套时,记录父子组件的关系。
  • 子组件没有el选项,所以不执行挂载。if (vm.$options.el) {vm.$mount(vm.$options.el)}

组件更新

组件中的data数据或者props变更时,会导致组件的更新。此时,组件对应的watcher会调用getter(即updateComponent函数)开始对组件重新创建vnode并返回。
类组件的更新只是会调用当前组件实例的render方法来重新创建Vnode
函数式组件的更新会卸载当前组件,并重新创建新组件,因为函数式组件没有组件实例,不包括生命周期函数。因此,函数是组件的更新包含了两个过程:卸载–创建。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值