浅曦Vue源码-13-挂载阶段-$mount(1)

998 阅读4分钟

「这是我参与2022首次更文挑战的第14天,活动详情查看:2022首次更文挑战」。

一、前情回顾 & 背景

从这个系列开篇至今,我们其实都在说一个方法:Vue.prototype._init 方法,包括合并 Vue.options 和我们传入选项对象的 mergeOptions,初始化 Vue 响应式逻辑的 initState 方法等,从本篇起讨论 _init 方法的最后一个部分——挂载。

为什么先说挂载?

严格意义上来说,真正的数据响应式的全过程是没有讲完的,因为我们只讲了怎么将数据定义成响应式:defineReactive() 方法借助 Object.defineProperty() 方法设置 gettersetter 用于数据访问和设置的拦截。但是要明确的是:gettersetter 定义完并不会马上就被触发,而是等到被访问到才会触发 getter,被设置才会触发 setter

基于上面的这种情况,我并不打算把依赖收集和派发更新的流程混入到响应式数据的初始化中。我采用的方式仍然是顺着代码的执行取到挂载阶段。

挂载阶段就会对模板进行编译得到 render 函数,编译的时候就会对模板语法、bind 等指令进行处理,变成用于从 vm 上取值的函数调用,这玩意儿就是我们说的渲染函数。然后执行渲染函数,此时就会触发前面响应式数据的读取过程,即 getter 被触发。

二、_init 中的 $mount 调用

export function initMixin (Vue: Class<Component>) {

  Vue.prototype._init = function (options?: Object) {
    // ... 各种 init 以及 mergeOptions
    
    // $mount 被调用
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

从代码中可以看出,当我们在 options 中传入了 el 属性时才会启用自动挂载,否则就手续要我们手动挂载;在我们前面的 test.html。在我们的 demoel 属性值就是 '#app' 这个字符串;

此外,可以看出 $mountVue.prototype 上的方法,先来看看这个方法是在哪里注册的。

三、$mount 方法

因为我们使用的是带有编译器的 Vue.js这个版本的和不带编译器的略有不同。所以我们先看这个方法是在哪里定义的;

看 Vue 源码的时候如果一时间找不到这个方法定义,全局搜索吧,搜 ue.prototype.$mount 后发现编译器的版本中的入口文件中:ntry-runtime-with-compiler.js

方法位置:rc/platforms/web/entry-runtime-with-compiler.js -> Vue.prototype.$mount

方法作用:通过运行时的编译器编译组件模板得到 render 函数 和 staticRenderFns,然后通过执行挂载的时候调用前面的 render 函数得到 VNode,然后把 VNode 变为真实 DOM 插入到页面中,最终完成渲染。主要通过以下具体步骤实现:

  1. 根据 el 选项获取 el 对应的 DOM 节点,即 div#app
  2. 如果检测到 vm.$options 上没有 render 函数选项时,则获取模板选项 template,然后处理不同类型模板,最终处理成字符串形式的
  3. 调用编译,将模板编译成渲染函数
  4. 将两个渲染函数放到 vm.$options 上,即 this.$options.renderthis.$options.staticRenderFns
  5. 调用挂载方法
const mount = Vue.prototype.$mount // 缓存通用的 $mount 方法,这个方法不包含编译器逻辑
Vue.prototype.$mount = function (
  el?: string | Element,
  hydrating?: boolean
): Component {
  // 挂载点
  el = el && query(el)

  // 配置项
  const options = this.$options
 
  if (!options.render) {
    let template = options.template
    if (template) {
      // 处理 template 选项
      if (typeof template === 'string') {
        // { template: '#app' } 
        // template 是一个 id 选择器时,获取该元素的 innerHtml 作为 template
        if (template.charAt(0) === '#') {
          template = idToTemplate(template)
      
        }
      } else if (template.nodeType) {
        // template 是一个 DOM 节点对象,nodeType 标识 HTML 节点类型的字段,
        // 不同的 HTML 节点有不同类型,同样获取其 innerHTML
        template = template.innerHTML
      } else {
    
        return this
      }
    } else if (el) {
      // 如果设置了 el 选项,则获取 el 选择器的 outerHTML 作为模板
      // new Vue 时要研究的就属于这个,new Vue({ el: '#app' })
      template = getOuterHTML(el)
    }

    if (template) {

      // 编译模板,得到动态渲染函数和静态渲染函数?渲染函数,还动态?静态?
      const { render, staticRenderFns } = compileToFunctions(template, {
        outputSourceRange: process.env.NODE_ENV !== 'production',
        shouldDecodeNewlines,
        shouldDecodeNewlinesForHref,
        delimiters: options.delimiters, // 界定符:默认 {{}}
        comments: options.comments // 是否保留注释
      }, this)

      // 将两个渲染函数放到 vm.$options 上,即 this.$options,
      // 给 Vue.prototype._render 方法用
      options.render = render
      options.staticRenderFns = staticRenderFns
    }
  }
  // 执行挂载
  return mount.call(this, el, hydrating)
}

3.1 准备模板

image.png

从上图中可以看出,template 就是我们写在 test.htmldiv#app 这部分的 html 字符串:

image.png

3.2 compileToFunctions 进入编译阶段

compileToFunctionscreateCompiler 方法的返回值,而 createCompiler 又是 createCompilerCreator 的返回值,这个地方有点复杂,和前面的方法直接讲述位置、参数作用的路数不太一样,所以大致上 compileToFunctions 等效于 createCompilerCreator()(),所以我们从后向前看,这样看起来能清晰一些;

3.2.1 createCompilerCreator 方法

方法位置:src/compiler/create-compiler.js -> createCompilerCreator

方法参数:baseCompile,函数数据类型,从前面可以知道是获取 createCompiler 时调用 createCompilerCreator 时传入的函数;

export function createCompilerCreator (baseCompile: Function): Function {
  // 这个就是 createCompiler 函数了
  // baseCompile 方法将会在这个 return 出去的 createCompiler 中调用
  return function createCompiler (baseOptions: CompilerOptions) {
  }
}

所以 createCompilerCreator 的作用就是接收 baseCompile 函数,返回一个工厂函数,这个作为返回值的工厂函数就是 creatCompiler 函数;

3.2.2 createCompiler 方法

方法位置:是前面 createCompilerCreator 方法的返回值;

方法参数:baseOptions 创建编译器所需的配置

export function createCompilerCreator (baseCompile: Function): Function {
  return function createCompiler (baseOptions: CompilerOptions) {
    function compile (template: string, options?: CompilerOptions): CompiledResult {
      //....
      return compiled
    }

    // compile 是处理编译的,返回编译结果
    return {
      compile,
      compileToFunctions: createCompileToFunctionFn(compile) // 这个就是 compileToFunctions 函数
    }
  }
}

所以,调用过 createCompiler 就得到这个对象:

return {
  compile,
  compileToFunctions: createCompileToFunctionFn(compile) 
}

在 $mount 中需要的就是 compileToFunctions 方法,模板也是传递给这个函数,而这个函数又是 createToFunctionFn 的返回值,接着看看 createToFunctionFn 方法;

3.2.3 createToFunctionFn 方法

方法位置:src/compiler/to-function.js -> function createCompileToFunctionFn return compileToFunctions

方法参数:

  1. template:模板字符串
  2. options:编译器选项
  3. vmVue 实例

经过上面一系列的操作后,你就发现,最后执行的就是这个函数,我们看看这里面发生了什么。

方法作用:

  1. 接收传递进来的编译选项 options
  2. 检查 CSPCSP 将影响编译器工作
  3. 检查缓存,如果缓存有编译结果就返回
  4. 执行 createCompileToFunctionFn 接收到的 compile 函数编译,templateoptions 都是传递给 compile
  5. compile 得到的字符串变成函数,所谓渲染函数,当然得是函数了
// 注意这个 export function.. 外面这个不是重点,重点是他的返回值
export function createCompileToFunctionFn (compile: Function): Function {
  const cache = Object.create(null)
  // 返回值是个重点
  return function compileToFunctions (
    template: string,
    options?: CompilerOptions,
    vm?: Component
  ): CompiledFunctionResult {
    // 传递进来的编译选项
    options = extend({}, options)
    const warn = options.warn || baseWarn
    delete options.warn

   
    // .... 检测 CSP 限制,CSP 是内容安全策略,限制使用 new Function 和 eval,
    // 没有这个编译器无法工作
 
    // 如果有缓存,直接从缓存中获取上次编译结果
    const key = options.delimiters
      ? String(options.delimiters) + template
      : template
    if (cache[key]) {
      return cache[key]
    }

    // 执行 compile 函数,得到编译结果
    const compiled = compile(template, options) // 这个 compile 是 createCompilerCreator 调用 createCompileToFunctionsFn 方法时传入的回调,在 src/compiler/create-compiler.js

  
    // 检查编译期间产生的 error 和 tips 输出到 console
    if (process.env.NODE_ENV !== 'production') {
      // ...
    }


    // 转换编译得到的字符串代码为函数,通过 new Function(code) 实现
    const res = {}
    const fnGenErrors = []
    res.render = createFunction(compiled.render, fnGenErrors)
    res.staticRenderFns = compiled.staticRenderFns.map(code => {
      return createFunction(code, fnGenErrors)
    })

    // ....

    // 缓存编译结果并返回 res
    return (cache[key] = res)
  }
}

这个操作俄罗斯套娃看了落泪,老虎机看了惊呼复杂。不过也得确实承认,我还没看出来这个设计的好处在哪,工厂套工厂的模式,目前可以确保每次得到的都是一个新的函数,彼此间的渲染函数不同,互相独立。

四、总结

本篇小作文主要讨论了以下几个问题:

  1. $mount 方法的注册时机和位置;
  2. template 模板的准备过程,以及不同类型的模板处理
  3. 编译模板的 compileToFunctions 方法的获取路径,套娃的既视感