本系列文章会以实现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)
}
现在, 我们需要完成两件事:
- 定义好vm._render和vm._update
- 定义好_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方法中, 主要做了两件事:
- $mount方法, 在完整版下, 被重新定义了, 在这里加入了编译方法compileToFunctions;
- compileToFunctions返回的render方法被赋给了options对象;
vm._render
这里梳理下执行顺序: 在入口文件中, 我们看到了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:
执行结果:
这就是render转VNode的一个过程! 还有一个点, 注意图中的elm此时是undefined; 在后续转为真实节点后, 这里的值会变为一个真实的节点对象!
小节:
- 在第一个$mount方法中, 其实是最通用的代码, 即 无论用哪种vue的编译包, 都必然走vm._update(vm._render())这段逻辑, 所以这段不变的部分被单独抽了出来;
- 在第二个$mount中, 即 runtime + compiler的入口文件, 给options.render赋值为我们之前得到的compileToFunctions(template)的返回值, 这里的options.render其实就是一个可变的因素, 在这里会被赋值为本编译包的compileToFunctions执行结果, 而在runtime版本中, 又可以使用vue-loader的compileToFunctions为其赋值, 这样保证了代码的灵活性和可维护性;
往期回顾