手写简化版Vue(四) _render的实现

153 阅读4分钟

本系列文章会以实现Vue的各个核心功能为标杆(初始化、相应式、编译、虚拟dom、更新、组件原理等等), 不会去纠结于非重点或者非本次学习目标的细节, 从头开始实现简化版Vue, 但是, 即使是简化, 也需要投入一定的时间和精力去学习, 而不可能毫不费力地学习到相对复杂的知识; 所有简化代码都会附上原版源码的路径, 简化版仅仅实现了基本功能, 如需了解更多细节, 可以去根据源码路径去阅读对应的原版源码;

挂载

前面我们已经完成了一个编译器, 严格来讲, 是将一段字符串转为一个render方法, 那么接下来应该怎么做呢? 注意, 我们的最终目的是生成真实的dom, 并挂载上去! 我们在响应式原理那一章节中, 最后在mountComponent方法中, 我们用innerHTML将更新的节点进行了挂载;

// 源码路径 /src/platforms/web/runtime/index.ts
Vue.prototype.$mount = function (el) {
  // 入传入字符传选择器, 则将其转换为节点
  if (typeof el === 'string') {
    el = document.querySelector(el)
  }
  mountComponent(this, el)
}

export default Vue

// 源码路径 /src/core/instance/lifecycle.ts
export function mountComponent (vm, el) {
  const updateComponent = function () {
    // 更新
    el.innerHTML = vm.name
  }
  new Watcher(vm, updateComponent)
}

当然, innerHTML只是一个临时方案, 真实源码中, 会调用render方法得到vdom, 再通过update将vdom转为真实dom, 并挂载!

所以, mountComponent此时应该改为:

export function mountComponent (vm, el) {
  vm.$el = el
  const updateComponent = function () {
  // el.innerHTML = vm.name
    vm._update(vm._render())
  }
  new Watcher(vm, updateComponent)
}

现在, 我们需要完成两件事:

  1. 定义好vm._render和vm._update
  2. 定义好_c, _v等辅助方法, 以免执行render的时候, 会报_c, _v等方法不存在错误

runtime-with-compiler

在解决以上2个问题之前, 我们先来了解下Vue的构建版本

我们前面在日常使用vue的时候, 都知道, vue有多个构建版本(见下图), 但无非就是从是否带自编译器/是否压缩/模块化规范等几个维度来划分:

所谓的完整版其实就是compiler+runtime

我们这里探讨的是Vue的编译过程, 所以必须是完整版, 因此我们定位到完整版的入口文件, 开始我们的分析:

// 源码地址: /src/platforms/web/runtime-with-compiler.ts
import { compileToFunctions } from './compiler/index'
import Vue from './runtime/index'
/**
	Vue.prototype.$mount, 我们上面已经赋值过一次了
  这里将其赋值给mount, 然后再重新定义Vue.prototype.$mount方法
*/
const mount = Vue.prototype.$mount
Vue.prototype.$mount = function (el) {
  const options = this.$options
  const template = options.template
  /*
  	这里获取到了我们上一章得到的render方法
  */
  const { render } = compileToFunctions(template)
  // 赋值给options.render
  options.render = render
  // 执行我们之前定义的$mount方法
  mount.call(this, el)
}

export default Vue

新的$mount方法中, 主要做了两件事:

  1. $mount方法, 在完整版下, 被重新定义了, 在这里加入了编译方法compileToFunctions;
  2. compileToFunctions返回的render方法被赋给了options对象;

vm._render

这里梳理下执行顺序: 在入口文件中, 我们看到了mount方法被重新定义了,原本的mount方法被重新定义了, 原本的mount方法, 在options.render被赋值之后, 才被执行mount.call(this, el); 所以, 前面$mount中的vm._render方法正是最新的options.render方法, 不信, 可以看下它的定义:

// 代码地址: /src/core/instance/render.ts
import { createElement } from "../vdom/create-element"
import {installRenderHelpers} from '../../core/instance/render-helpers/index'
export function renderMixin (Vue) {
  // 初始化_s,_v等方法
  installRenderHelpers(Vue.prototype)
  // 定义_render方法
  Vue.prototype._render = function () {
    /**
      注意, 此时, 我们从options中拿到了render方法, 
      这正是自带的compileToFunctions方法执行的结果, 而在runtime版本中, 
      compileToFunctions方法又可以由vue-loader来提供, 正是这种松耦合的代码组织形式
      使vue的代码可以在不同的构建版本中得到复用! 比如这个_render方法,就能在不同构建版本
      中重复使用, 因为它只负责从options中拿render方法, 至于这个方法是谁提供的,它不在乎!
    */
    const { render } = this.$options
    // 执行render
    const vnode = render.call(this)
    return vnode
  }
}

接下来看下installRenderHelpers的实现逻辑

// 源码地址: /src/core/instance/render-helps/index.ts
import { createTextNode } from '../../vdom/vnode'

export function installRenderHelpers (target) {
  target._v = createTextNode
  target._s = String
}

installRenderHelpers方法就是将这些render中需要用到的辅助方法进行挂载! 这里可以看到一个createTextNode方法, 即 创建一个文本节点, 这里, 就要首次接触了VNode这个对象了

// 源码地址: /src/core/vdom/vnode.ts
export default class VNode {
  /**
   *
   * @param tag 标签
   * @param data 属性
   * @param children 子vdom
   * @param text 文本
   * @param elm 真实节点
   * @param context 上下文, 通常就是Vue实例
   */
  constructor (tag, data, children, text, elm, context) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.context = context
  }
}

export function createTextNode (val) {
  return new VNode(undefined, undefined, undefined, String(val))
}

在render中, 通过调用_v方法, 也就是createTextNode, 可以创建一个文本类型的VNode对象, 这就是所谓的虚拟dom

走完了_v, _s的初始化逻辑, 我们再来看看最重要的_c方法, 它的定义逻辑在initRender方法中

// 代码地址: /src/core/instance/render.ts
import { createElement } from "../vdom/create-element"
// ...省略其他逻辑..
// 初始化render方法
export function initRender (vm) {
  vm._c = (a, b, c, d) => createElement(vm, a, b, c, d)
}

可以看出, vm._c其实就是执行了createElement, 方法, 看名字可以猜得到,就是创建元素, 严格来讲是VNode对象

// 源码地址: /src/core/vdom/create-element.ts
import VNode from "./vnode"
export function createElement (context, tag, data, children) {
  return _createElement(context, tag, data, children)
}

function _createElement (context, tag, data, children) {
  return new VNode(tag, data, children, undefined, undefined, context)
}

介绍完了render的一整套逻辑, 我们来梳理下以上代码的执行顺序

引入阶段, 即 我们执行 import Vue from 'vue'的时候, 将会执行renderMixin方法

// 源码路径src/core/instance/index.ts
import { initMixin } from './init'
// 增加
import { renderMixin } from './render'
export default function Vue (options) {
  this._init(options)
}

initMixin(Vue)
// 增加
renderMixin(Vue)

实例化阶段, 我们 new Vue的时候, 将会执行initRender方法以及vm.$mount方法

// 源码路径src/core/instance/init.ts
import { initRender } from './render'
// 增加
import { initState } from './state'
export function initMixin (Vue) {
  Vue.prototype._init = function (options) {
    const vm = this
    vm.$options = options
    // 增加
    initRender(vm)
    initState(vm)
    // 增加
    if (vm.$options.el) {
      vm.$mount(vm.$options.el)
    }
  }
}

最终, mountComponent方法中的render, 执行后返回的是一个完整的VNode对象:

template:

123

执行结果:

这就是render转VNode的一个过程! 还有一个点, 注意图中的elm此时是undefined; 在后续转为真实节点后, 这里的值会变为一个真实的节点对象!

小节:

  1. 在第一个$mount方法中, 其实是最通用的代码, 即 无论用哪种vue的编译包, 都必然走vm._update(vm._render())这段逻辑, 所以这段不变的部分被单独抽了出来;
  2. 在第二个$mount中, 即 runtime + compiler的入口文件, 给options.render赋值为我们之前得到的compileToFunctions(template)的返回值, 这里的options.render其实就是一个可变的因素, 在这里会被赋值为本编译包的compileToFunctions执行结果, 而在runtime版本中, 又可以使用vue-loader的compileToFunctions为其赋值, 这样保证了代码的灵活性和可维护性;

往期回顾

手写简化版Vue(一) 初始化

手写简化版Vue(二) 响应式原理

手写简化版Vue(三) 编译器原理