Aprendizagem do código-fonte Vue - o que a nova inicialização do Vue faz?

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 xxxbaixe 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 $mounte 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 rendera função .

Se usarmos para templateescrever código HTML, o Vue compilará internamente o modelo em uma função que o Vue possa reconhecer rendere, 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

  1. 选项合并, 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)
  2. Inicialização vue实例生命周期Propriedades relacionadas, inicialização de propriedades de relacionamento do componente, definir como $parent, $children, $root, $refsetc.
  3. Initialize 事件, se houver um evento de ouvinte pai, adicione-o à instância.
  4. 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.
  5. Chame beforeCreatea função de gancho e aqui você pode ver quais inicializações um componente fez antes e depois da criação.
  6. 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.
  7. Inicialize props, methods, data, computed, watchincluindo o processamento responsivo.
  8. Em seguida, injete os dados transmitidos pelos ancestrais e inicialize o provide.
  9. Chame createda função de gancho, a inicialização está completa e a montagem pode ser executada.
  10. Anexado ao DOMelemento correspondente. Se o construtor do componente definir a opção el, ele será montado automaticamente, portanto não há necessidade de chamar manualmente $mountpara 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)

おすすめ

転載: blog.csdn.net/weixin_45811256/article/details/131708328