Vue 2.6 源码剖析-响应式原理学习 - 2.初始化

本文深入解析Vue的初始化过程,涵盖静态成员与实例成员的初始化,以及Vue构造函数内部的_init方法工作原理。详细阐述了Vue如何设置响应式数据,渲染视图,包括编译template到render函数,以及mountComponent的核心作用。

Vue 初始化

初始化的过程

platforms 目录下是和平台相关的代码。

先查看 src/platforms/web 下打包 Vue 的入口文件(entry-*.js):

  • entry-runtime.js 打包运行时版本的入口文件
  • entry-runtime-with-compiler.js 打包完整版的入口文件
// entry-runtime.js 源码只有两行

// 导入Vue的构造函数
import Vue from './runtime/index'
// 导出
export default Vue
/src/platforms/web/entry-runtime-with-compiler.js
/* @flow */

// ...

// 导入 Vue 构造函数
import Vue from './runtime/index'
// ...

// 获取 $mount 初始定义
const mount = Vue.prototype.$mount
// 重写 $mount 方法,增加功能
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
   
   
  // ...
  // 判断是否定义了 render 选项
  if (!options.render) {
   
   
    // 如果没有定义 render 函数,获取tempalte,编译成render函数
    // 这里是 $mount 方法核心的部分
    // ...
  }
  // 调用 mount 方法,渲染DOM
  return mount.call(this, el, hydrating)
}

// 辅助方法
function getOuterHTML (el: Element): string {
   
   
  // ...
}

// 定义Vue静态方法 compiler:把html字符串编译成render函数
Vue.compile = compileToFunctions

// 导出构造函数
export default Vue

文件中的主要内容是:

  • 导入 Vue 构造函数
  • 重写Vue实例的$mount方法
    • 内部将template转化成render函数
  • 导出 Vue 构造函数
  • 它没有定义 Vue 构造函数

继续寻找定义 Vue 构造函数的地方。

/src/platforms/web/runtime/index
/* @flow */

// 导入模块
import Vue from 'core/index'
// ...

// install platform specific utils
// 向Vue.config注册了一些 和平台相关的特定的通用的 方法
// 这些方法在 Vue 内部使用,外部很少使用
Vue.config.mustUseProp = mustUseProp
Vue.config.isReservedTag = isReservedTag
Vue.config.isReservedAttr = isReservedAttr
Vue.config.getTagNamespace = getTagNamespace
Vue.config.isUnknownElement = isUnknownElement

// install platform runtime directives & components
// 通过extend方法,注册了一些全局的指令和组件
// extend 方法 用于复制对象成员。将第二个参数对象中的所有成员,拷贝到第一个参数对象中
// 通过这段代码可以发现,全局的指令和组件,都存储在 Vue.options 中

// 注册组件 transition transition-group
extend(Vue.options.directives, platformDirectives)
// 注册指令 v-model v-show
extend(Vue.options.components, platformComponents)

// install platform patch function
// 在 Vue 的原型上注册 __patch__ 函数
// 类似 Snabbdom 的 patch 函数:把虚拟DOM转化成真实DOM
// inBrowser:判断是否是浏览器环境;noop:空函数
Vue.prototype.__patch__ = inBrowser ? patch : noop

// public mount method
// 在 Vue 的原型上注册 $mount 方法
// 也就是在注册 Vue 实例上的方法
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
   
   
  el = el && inBrowser ? query(el) : undefined
  // 调用 mountComponent:渲染DOM
  return mountComponent(this, el, hydrating)
}

// devtools global hook
/* istanbul ignore next */
// 和调试相关的代码(暂不关心)
if (inBrowser) {
   
   
  // ...
}

// 导出 Vue 构造函数
export default Vue

  • 注册了和平台相关的指令和组件
  • 注册了 __patch__$mount 两个方法
  • 依然没有定义 Vue 的构造函数
/src/core/index

源码中使用 core/index的方式加载这个文件,语法看上去在加载一个模块。

flow的配置文件.flowconfigname_mapper匹配了名为 core 的模块,并将地址指向了/src/core别名,所以core/index指向/src/core/index

// core/index.js
import Vue from './instance/index'
import {
   
    initGlobalAPI } from './global-api/index'
import {
   
    isServerRendering } from 'core/util/env'
import {
   
    FunctionalRenderContext } from 'core/vdom/create-functional-component'

// 给 Vue 挂载静态方法(这是这个文件的核心部分)
initGlobalAPI(Vue)

// 调用 Object.defineProperty 给 Vue 原型增加一些成员
// 这些成员都和 SSR(服务端渲染) 相关(暂时不关心)

Object.defineProperty(Vue.prototype, '$isServer', {
   
   
  get: isServerRendering
})

Object.defineProperty(Vue.prototype, '$ssrContext', {
   
   
  get () {
   
   
    /* istanbul ignore next */
    return this.$vnode && this.$vnode.ssrContext
  }
})

// expose FunctionalRenderContext for ssr runtime helper installation
Object.defineProperty(Vue, 'FunctionalRenderContext', {
   
   
  value: FunctionalRenderContext
})

// Vue的版本
Vue.version = '__VERSION__'

// 导出
export default Vue

// core/global-api/index.js
/* @flow */

import config from '../config'
import {
   
    initUse } from './use'
import {
   
    initMixin } from './mixin'
import {
   
    initExtend } from './extend'
import {
   
    initAssetRegisters } from './assets'
import {
   
    set, del } from '../observer/index'
import {
   
    ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import {
   
    observe } from 'core/observer/index'

import {
   
   
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
   
   
  // config
  const configDef = {
   
   }
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
   
   
    configDef.set = () => {
   
   
      warn(
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.

  // 定义 Vue.util:一些公用的方法
  // 上面英文翻译:这些工具方法不视作全局 API 的一部分,除非你已经意识到某些风险,否则不要去依赖它们
  // 也就是调用这个方法可能会发生意外,不建议外部使用
  Vue.util = {
   
   
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义一些外部常用的静态方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 定义 observable 方法:让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
   
   
    observe(obj)
    return obj
  }

  // 初始化 Vue.options 对象,并设置一些属性
  // components/directives/filters
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
   
   
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  Vue.options._base = Vue

  // 设置 keep-alive 组件
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(Vue)
}

  • core/index 与平台无关
  • 主要部分就是 给 Vue 挂载静态方法
  • 内部没有定义 Vue 的构造函数。
core/instance/index
// ./instance/index.js
import {
   
    initMixin } from './init'
import {
   
    stateMixin } from './state'
import {
   
    renderMixin } from './render'
import {
   
    eventsMixin } from './events'
import {
   
    lifecycleMixin } from './lifecycle'
import {
   
    warn } from '../util/index'

// Vue 的构造函数
// 此处不用 class 的原因是为了方便后续给 Vue 实例混入实例成员
function Vue (options) {
   
   
  // 判断是否是开发环境
  // 通过this判断,是否通过 new 使用 Vue 构造函数
  // 如果当作普通函数调用 -> Vue() 就发出警告
  if (process.env.NODE_ENV !== 'production' &&
    !(this instanceof Vue)
  ) {
   
   
    warn('Vue is a constructor and should be called with the `new` keyword')
  }
  // 调用 _init() 方法
  this._init(options)
}

// 注册 vm 的 _init() 方法:初始化 vm
initMixin(Vue)
// 继续混入(注册) vm 的成员:$data/$props/$set/$delete/$watch
stateMixin(Vue)
// 初始化事件相关方法
// $on/$once/$off/$emit
eventsMixin(Vue)
// 初始化生命周期相关的混入方法
// _update/$forceUpdate/$destroy
lifecycleMixin(Vue)
// 混入 render
// $nextTick/_render
renderMixin(Vue)

export default Vue

  • 与平台无关
  • 创建了 Vue 的构造函数
  • 设置 Vue 实例的成员
为何 Vue 不用 ES6 的 Class 去实现

从源码中看到后面有很多 xxxMixin 的函数调用,并把Vue作为参数传入。

它们的功能都是给 Vue 的prototype 上扩展一些方法,也就是向 Vue 实例混入成员。

Vue 按功能把这些扩展分散到多个模块中去实现,而不是在一个模块里实现所有。

这么做的好处是方便代码的维护和管理。

这种方式用 Class (的标准用法)难以实现。

Class 创建的类也可以通过prototype添加成员,这在写法上违背了它的语法糖。

Class的标准用法:

  • 一个类就是一个模块
  • 所有成员都应该在类内部定义,或继承自父类,而不是使用 prototype 定义成员
  • prototype 不应该和 Class 搭配使用

所以这里相对于它的标准用法,function要方便。

// 非标准用法
class Vue {
   
   }
Vue.prototype.$set = () => {
   
   }

// 标准用法1:继承
class GlobalApi {
   
   
  $set() {
   
   }
}
class Vue extends GlobalApi {
   
   }

// 标准用法2:直接定义
class Vue {
   
   
  $set() {
   
   }
}
总结:四个导出 Vue 的模块
  • src/platforms/web/entry-runtime-with-compiler.js
    • web 平台相关的入口,核心就是增加了编译的功能
      • 重写了平台相关的 $mount 方法
        • $mount方法内部编译模板,将template 转换成 render函数
      • 注册了 Vue.compile() 方法
        • 传递一个 HTML 字符串 返回 render 函数
  • src/platforms/web/runtime/index.js
    • 与 web 平台相关
    • 注册和平台相关的全局指令,挂载到 Vue.options.directives
      • v-model
      • v-show
    • 注册和平台相关的全局组件,挂载到 Vue.options.components
      • transition
      • transition-group
    • 定义了两个全局方法:
      • __patch__ :把虚拟DOM转换成真实DOM
      • $mount:挂载方法,把DOM渲染到界面中
        • 该文件中定义 $mount
        • 在入口文件中重写了 $mount,增加了编译的能力
  • src/core/index.js
    • 与平台无关
    • 核心作用:
      • 内部直接设置了一些静态方法
      • 还调用 initGlobalAPI(Vue) 定义了一些静态方法
  • src/core/instance/index.js
    • 与平台无关,与实例(instance)相关
    • 内部定义了Vue的构造函数,构造函数内部调用了 this._init(options) 方法
      • _init 相当于整个程序的入口
    • 给Vue中混入了常用的实例成员

之后会详细查看具体注册了哪些静态成员和实例成员。

两个问题

语言检查

vscode中查看源码中的 Flow 语法时(例如声明类型 Vue: GlobalAPI)会有红色波浪线,提示:"types" 只能在 .ts 文件中使用

vscode 和 TypeScript 都是微软开发的,所以 vscode 默认支持 TS 语法。

TypeScript 与 Flow 语法类似,而 vscode 不支持 Flow,所以这里 vscode 将其识别为 TS 语法。

TS的语法只能在.ts文件中使用,当前是 .js 文件,所以判定为校验错误。

解决方法:在 vscode 配置中关闭 JS 语法校验:

// settings.json
// 设置不检查 js 的语法问题,防止 flow 报错
"javascript.validate.enable": false
泛型影响后面代码高亮

在查看src\core\global-api\index.js源码时,有一段代码使用了泛型。之后的代码:

  • 没有高亮显示了
  • 几乎无法使用 转到定义Ctrl + 左键 的功能

在这里插入图片描述

因为vscode在识别到泛型的时候会有问题。

解决高亮的方法:安装插件 Babel JavaScript

在这里插入图片描述

转到定义Ctrl + 左键 暂时没有办法解决。

在这里插入图片描述

静态成员

src\core\index.js文件中调用的 initGlobalAPI方法中,初始化了 Vue 的大部分静态成员。

initGlobalAPI

注册全局API。

查看 src\core\global-api\index.js 中定义的这个方法:

  • 初始化了 Vue.config
    • 之后在src\platforms\web\runtime\index.js中向Vue.config 挂载了一些方法

在这里插入图片描述

  • 设置了 Vue.util,挂载了一些API
    • 这些 API 用于内部使用,官方文档未提供,也不推荐使用。(不用关心)
  • 定义了常用的方法:
    • set
    • delete
    • nextTick
    • observable
  • 初始化了 Vue.options
    • 先使用Object.create(null)初始化这个对象,表示这个对象不需要原型,可以提高它的性能
    • 然后遍历ASSET_TYPES,获取属性名并初始化为不需要原型的空对象
      • 由于泛型使用,此处无法通过 转到定义 查看ASSET_TYPES,需要手动查找。
    • ASSET_TYPES 初始化的3个成员:components/directives/filters
      • 用于存储全局的组件、指令、过滤器
      • 通过 ``Vue.componetVue.directiveVue.filter注册的全局的组件、指令、过滤器,都会存储到Vue.options.[components/directives/filters]`中去。
  • Vue.options._base中记录 Vue 的构造函数
    • weex 场景中使用
  • 使用 extend 注册内置组件 keep-alive
    • extend方法:将对象的所有属性(参数1),(浅)拷贝到另一个对象中(参数2)
    • extend方法定义位置:src\shared\util.js
      • 这个文件中定义了很多通用的方法
  • 最后调用几个函数,分别为Vue注册了几个静态方法
    • Vue.use
    • Vue.mixin
    • Vue.extend
    • Vue.component、Vue.directive、Vue.filter
// src\core\global-api\index.js
/* @flow */

import config from '../config'
import {
   
    initUse } from './use'
import {
   
    initMixin } from './mixin'
import {
   
    initExtend } from './extend'
import {
   
    initAssetRegisters } from './assets'
import {
   
    set, del } from '../observer/index'
import {
   
    ASSET_TYPES } from 'shared/constants'
import builtInComponents from '../components/index'
import {
   
    observe } from 'core/observer/index'

import {
   
   
  warn,
  extend,
  nextTick,
  mergeOptions,
  defineReactive
} from '../util/index'

export function initGlobalAPI (Vue: GlobalAPI) {
   
   
  // config
  // 首先定义 config 属性
  const configDef = {
   
   }
  // 定义 config 的属性描述符
  configDef.get = () => config
  if (process.env.NODE_ENV !== 'production') {
   
   
    // 开发环境增加一个 set 方法,禁止给 config 属性直接赋值
    // 之后在`src\platforms\web\runtime\index.js`中向Vue.config 挂在了一些方法
    configDef.set = () => {
   
   
      warn(
        // 警告:不要对Vue.config重新赋值,可以挂载或修改对象中的属性。
        'Do not replace the Vue.config object, set individual fields instead.'
      )
    }
  }
  // 初始化 Vue.config 对象
  Object.defineProperty(Vue, 'config', configDef)

  // exposed util methods.
  // NOTE: these are not considered part of the public API - avoid relying on
  // them unless you are aware of the risk.

  // 定义 Vue.util:一些公用的方法
  // 上面英文翻译:这些工具方法不视作全局 API 的一部分,除非你已经意识到某些风险,否则不要去依赖它们
  // 也就是调用这个方法可能会发生意外,不建议外部使用
  Vue.util = {
   
   
    warn,
    extend,
    mergeOptions,
    defineReactive
  }

  // 定义一些外部常用的静态方法
  Vue.set = set
  Vue.delete = del
  Vue.nextTick = nextTick

  // 2.6 explicit observable API
  // 定义 observable 方法:让一个对象可响应
  Vue.observable = <T>(obj: T): T => {
   
   
    observe(obj)
    return obj
  }

  // 初始化 Vue.options 对象,挂载3个成员,并初始化为空对象
  // components/directives/filters
  // 3个成员用于存储全局的组件、指令、过滤器

  // Object.create(null) 没有原型,可以提高性能
  Vue.options = Object.create(null)
  ASSET_TYPES.forEach(type => {
   
   
    Vue.options[type + 's'] = Object.create(null)
  })

  // this is used to identify the "base" constructor to extend all plain-object
  // components with in Weex's multi-instance scenarios.
  // 记录 Vue 构造函数到 _base
  Vue.options._base = Vue

  // 注册内置组件 keep-alive
  extend(Vue.options.components, builtInComponents)

  // 注册 Vue.use() 用来注册插件
  initUse(Vue)
  // 注册 Vue.mixin() 实现混入
  initMixin(Vue)
  // 注册 Vue.extend() 基于传入的options返回一个组件的构造函数
  initExtend(Vue)
  // 注册 Vue.directive()、Vue.component()、Vue.filter()
  initAssetRegisters(
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值