Índice
prefácio
Eu tenho usado o Vue por um tempo, li recentemente o código-fonte do Vue e queria resumir e compartilhar as coisas novas que aprendi.
Se você acha que olhar diretamente para o código-fonte é muito chato, pode combinar os artigos ou vídeos resumidos pelos predecessores, acredito que obterá o dobro do resultado com metade do esforço.
Para o código-fonte, você deve ler mais e pensar mais.Se você quer ser proficiente, definitivamente não é suficiente fazê-lo uma ou duas vezes. Às vezes, olhando para um problema, você pode descobrir outro problema que já viu antes, mas não entendeu.
Pretendo publicar uma série de artigos sobre código-fonte Vue, que podem ser considerados como meu processo de aprendizado pessoal de código-fonte.
Primeiro, encontre o endereço github do projeto Vue: vue2.x source code link , git clone xxx
baixe o código-fonte.
1. Crie uma instância Vue
Crie um novo arquivo html para importar vue.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./vue-2.7.14/dist/vue.js"></script>
</script>
<script>
new Vue({
el:'#app',
})
</script>
</body>
</html>
A inicialização do Vue começa aqui.
2. Encontre o construtor Vue
// src/core/instance/index.ts
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'
import type {
GlobalAPI } from 'types/global-api'
// Vue构造函数的声明
function Vue(options) {
if (__DEV__ && !(this instanceof Vue)) {
warn('Vue is a constructor and should be called with the `new` keyword')
}
// 初始化方法
this._init(options)
}
// 从文件中可以看出 上面的 _init() 是从下面的混入中获得的,那么具体从哪个中得到的需要分析一下
// 初始化混入
initMixin(Vue)
// state的混入
stateMixin(Vue)
// events的混入
eventsMixin(Vue)
// 生命周期的混入
lifecycleMixin(Vue)
// 渲染函数的混入
renderMixin(Vue)
// 上面的这些混入其实就是初始化实例的方法和属性
// 其实通过名字不难发现, _init() 方法肯定是在初始化的混入中:initMixin()
export default Vue as unknown as GlobalAPI
Na verdade, não é difícil descobrir pelo nome, _init()
o método deve estar no mix de inicialização: initMixin()
, então continue olhando para initMixin()
o arquivo onde está localizado.
3. Análise do código-fonte - Vue.prototype._init
// src/core/instance/init.ts
export function initMixin(Vue: typeof Component) {
// 负责 Vue 的初始化过程;接收用户传进来的选项:options
Vue.prototype._init = function (options?: Record<string, any>) {
// vue的实例
const vm: Component = this
// 每个 vue 实例都有一个 _uid,并且是依次递增的
vm._uid = uid++
let startTag, endTag
if (__DEV__ && config.performance && mark) {
startTag = `vue-perf-start:${
vm._uid}`
endTag = `vue-perf-end:${
vm._uid}`
mark(startTag)
}
// vue标志, 避免被 Observe 观察
vm._isVue = true
vm.__v_skip = true
vm._scope = new EffectScope(true)
vm._scope._vm = true
// 选项合并:用户选项和系统默认的选项需要合并
// 处理组件的配置内容,将传入的options与构造函数本身的options进行合并(插件的策略都是默认配置和传入配置进行合并)
if (options && options._isComponent) {
// 子组件:优化内部组件(子组件)实例化,且动态的options合并相当慢,这里只有需要处理一些特殊的参数属性。减少原型链的动态查找,提高执行效率
initInternalComponent(vm, options as any)
} else {
// 根组件: 将全局配置选项合并到根组件的配置上,其实就是一个选项合并
vm.$options = mergeOptions(
// 获取当前构造函数的基本options
resolveConstructorOptions(vm.constructor as any),
options || {
},
vm
)
}
if (__DEV__) {
initProxy(vm)
} else {
vm._renderProxy = vm
}
vm._self = vm
// 下面的方法才是整个初始化最重要的核心代码
initLifecycle(vm) // 初始化实例的属性、数据:$parent, $children, $refs, $root, _watcher...等
initEvents(vm) //初始化事件:$on, $off, $emit, $once
initRender(vm) // 初始化render渲染所需的slots、渲染函数等。其实就两件事1、插槽的处理、2、$createElm 也就是 render 函数中的 h 的声明
callHook(vm, 'beforeCreate', undefined, false /* setContext */) // 调用生命周期的钩子函数,在这里就能看出一个组件在创建之前和之后分别做了哪些初始化
// provide/inject 隔代传参
// provide:在祖辈中可以直接提供一个数据
// inject:在后代中可以通过inject注入后直接使用
initInjections(vm) // 在 data/props之前执行;隔代传参时 先inject。作为一个组件,在要给后辈组件提供数据之前,需要先把祖辈传下来的数据注入进来
initState(vm) // 数据响应式的重点,处理 props、methods、data、computed、watch初始化
initProvide(vm) // 在 data/props之后执行;在把祖辈传下来的数据注入进来以后 再provide
// 总而言之,上面的三个初始化其实就是:对组件的数据和状态的初始化
callHook(vm, 'created') // created 初始化完成,可以执行挂载了
if (__DEV__ && config.performance && mark) {
vm._name = formatComponentName(vm, false)
mark(endTag)
measure(`vue ${
vm._name} init`, startTag, endTag)
}
// 如果发现配置项上有 el 选项,则自动调用 $mount 方法,也就是说有了 el 选项,就不需要再手动调用 $mount,反之,没有 el 则必须手动调用 $mount
if (vm.$options.el) {
// 调用 $mount 方法,进入挂载阶段
vm.$mount(vm.$options.el)
}
}
}
4. Análise do código-fonte - chame o método $mount para entrar na fase de montagem
Abra-o $mount
e veja o que ele faz. Simplifique algum código redundante.
// src/platforms/web/runtime-with-compiler.ts
import config from 'core/config'
import {
warn, cached } from 'core/util/index'
import {
mark, measure } from 'core/util/perf'
import Vue from './runtime/index'
import {
query } from './util/index'
import {
compileToFunctions } from './compiler/index'
import {
shouldDecodeNewlines,
shouldDecodeNewlinesForHref
} from './util/compat'
import type {
Component } from 'types/component'
import type {
GlobalAPI } from 'types/global-api'
// 获取宿主元素的方法
const idToTemplate = cached(id => {
const el = query(id)
return el && el.innerHTML
})
// 扩展 $mount 方法
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && query(el)
// 获取选项 $options
const options = this.$options
/**
* 编译权重:
* 优先看有没有render函数,如果有直接用
* 如果没有render函数就看有没有template模板
* 如果都没有就直接获取el的outerHTML作为渲染模板
*/
// 如果 render 选项不存在
if (!options.render) {
// 则查找 template
let template = options.template
// 如果 template 存在
if (template) {
// 则判断一下 template 的写法
if (typeof template === 'string') {
// 如果是字符串模板 例如:"<div> template </div>"
if (template.charAt(0) === '#') {
// 如果是宿主元素的选择器,例如:"#app"
// 则调用上面的 idToTemplate() 方法查找
template = idToTemplate(template)
if (__DEV__ && !template) {
warn(
`Template element not found or is empty: ${
options.template}`,
this
)
}
}
// 如果是一个dom元素
} else if (template.nodeType) {
// 则使用它的 innerHTML
template = template.innerHTML
} else {
if (__DEV__) {
warn('invalid template option:' + template, this)
}
return this
}
// 如果设置了 el
} else if (el) {
// 则以 el 的 outerHTML 作为 template
template = getOuterHTML(el)
}
// 如果存在 template 选项,则编译它获取 render 函数
if (template) {
// 编译的过程:把 template 变为 render 函数
const {
render, staticRenderFns } = compileToFunctions(
template,
{
outputSourceRange: __DEV__,
shouldDecodeNewlines,
shouldDecodeNewlinesForHref,
delimiters: options.delimiters,
comments: options.comments
},
this
)
// 最终获得的 render 函数将赋值给 选项 options
options.render = render
options.staticRenderFns = staticRenderFns
// 执行默认的挂载
return mount.call(this, el, hydrating)
}
/**
* 总结一下:
* new Vue({
* el: "#app",
* template: "<div> template </div>",
* template: "#app",
* render(h){ return h("div", "render")},
* data: {}
* })
* 在用户同时设置了 el、template、render的时候,优先级的判断为:render > template > el
*/
// 获取 outerHTML 的方法
function getOuterHTML(el: Element): string {
if (el.outerHTML) {
return el.outerHTML
} else {
const container = document.createElement('div')
container.appendChild(el.cloneNode(true))
return container.innerHTML
}
}
Vue.compile = compileToFunctions
export default Vue as GlobalAPI
O código acima implementa principalmente uma etapa muito importante no processo de renderização do vue, obtendo render
a função .
Se usarmos para template
escrever código HTML, o Vue compilará internamente o modelo em uma função que o Vue possa reconhecer render
e, se houver renderização, o processo de compilação poderá ser omitido. (Escrever diretamente a função de renderização será mais eficiente para compilação vue)
O Vue no arquivo entry-runtime-with-compiler.js acima vem de './runtime/index' , então nós mesmos analisamos o arquivo './runtime/index' .
// src/platforms/web/runtime/index.ts
// 能看到 Vue也不是在这里定义的,一样是导入的,那么这个文件主要做了什么呢?
import Vue from 'core/index'
import config from 'core/config'
import {
extend, noop } from 'shared/util'
import {
mountComponent } from 'core/instance/lifecycle'
import {
devtools, inBrowser } from 'core/util/index'
import {
query,
mustUseProp,
isReservedTag,
isReservedAttr,
getTagNamespace,
isUnknownElement
} from 'web/util/index'
import {
patch } from './patch'
import platformDirectives from './directives/index'
import platformComponents from './components/index'
import type {
Component } from 'types/component'
//...
// 安装了一个 patch 函数,也可以叫补丁函数或者更新函数。主要的作用就是把:虚拟dom 转化为真实的dom(vdom => dom)
Vue.prototype.__patch__ = inBrowser ? patch : noop
// 实现了 $mount 方法:其实就只调用了一个mountComponent()方法
// $mount的最终目的就是:把虚拟dom 转化为真实的dom,并且追加到宿主元素中去(vdom => dom => append)
Vue.prototype.$mount = function (
el?: string | Element,
hydrating?: boolean
): Component {
el = el && inBrowser ? query(el) : undefined
return mountComponent(this, el, hydrating)
}
export default Vue
Abra o código-fonte src/core/instance/lifecycle.js para encontrar o método mountComponent
// src/core/instance/lifecycle.ts
export function mountComponent(...): Component {
// 调用生命周期钩子函数
callHook(vm, 'beforeMount')
let updateComponent
// 创建一个更新渲染函数; 调用 _update 对 render 返回的虚拟 DOM 进行 patch(也就是 Diff )到真实DOM,这里是首次渲染
updateComponent = () => {
vm._update(vm._render(), hydrating)
}
// 当触发更新的时候,会在更新之前调用
const watcherOptions: WatcherOptions = {
before() {
// 判断 DOM 是否是挂载状态,就是说首次渲染和卸载的时候不会执行
if (vm._isMounted && !vm._isDestroyed) {
// 调用生命周期钩子函数
callHook(vm, 'beforeUpdate')
}
}
}
//生成一个渲染 watcher 每次页面依赖的数据更新后会调用 updateComponent 进行渲染
new Watcher(
vm,
updateComponent,
noop,
watcherOptions,
true
)
// 没有老的 vnode,说明是首次渲染
if (vm.$vnode == null) {
vm._isMounted = true
// 渲染真实 dom 结束后调用 mounted 生命周期
callHook(vm, 'mounted')
}
return vm
}
V. Resumo
Neste ponto, toda a inicialização do Vue está completa. O código detalhado específico não é mostrado aqui. O principal é o código para marcação. Vamos fazer um resumo aqui.
A partir das funções acima, o que o novo Vue faz se desdobra como um fluxograma, que são
选项合并
, processar o conteúdo de configuração do componente e mesclar as opções de entrada com as opções do próprio construtor (mesclar opções do usuário e opções padrão do sistema)- Inicialização
vue实例生命周期
Propriedades relacionadas, inicialização de propriedades de relacionamento do componente, definir como$parent
,$children
,$root
,$refs
etc. - Initialize
事件
, se houver um evento de ouvinte pai, adicione-o à instância. - Inicialize
render渲染
os slots necessários, funções de renderização, etc. Na verdade, existem duas coisas: o processamento do slot e a declaração de $createElm, que é a declaração da função h na função render. - Chame
beforeCreate
a função de gancho e aqui você pode ver quais inicializações um componente fez antes e depois da criação. - Inicialize os dados de injeção e injete primeiro ao passar parâmetros de geração em geração. Como um componente, antes de fornecer dados aos componentes descendentes, os dados transmitidos pelos ancestrais precisam ser injetados nele.
- Inicialize
props
,methods
,data
,computed
,watch
incluindo o processamento responsivo. - Em seguida, injete os dados transmitidos pelos ancestrais e inicialize o provide.
- Chame
created
a função de gancho, a inicialização está completa e a montagem pode ser executada. - Anexado ao
DOM
elemento correspondente. Se o construtor do componente definir a opção el, ele será montado automaticamente, portanto não há necessidade de chamar manualmente$mount
para montar.
Você pode consultar:
Vue source code series (2): O que o Vue inicializa?
Análise de leitura de código-fonte Vue (super detalhada)