当学习成为了习惯,知识也就变成了常识。 感谢各位的 关注、点赞、收藏和评论。
新视频和文章会第一时间在微信公众号发送,欢迎关注:李永宁lyn
文章已收录到 github 仓库 liyongning/blog,欢迎 Watch 和 Star。
前言
上一篇文章 手写 Vue2 系列 之 编译器 中完成了从模版字符串到 render 函数的工作。当我们得到 render 函数之后,接下来就该进入到真正的挂载阶段了:
挂载 -> 实例化渲染 Watcher -> 执行 updateComponent 方法 -> 执行 render 函数生成 VNode -> 执行 patch 进行首次渲染 -> 递归遍历 VNode 创建各个节点并处理节点上的普通属性和指令 -> 如果节点是自定义组件则创建组件实例 -> 进行组件的初始化、挂载 -> 最终所有 VNode 变成真实的 DOM 节点并替换掉页面上的模版内容 -> 完成初始渲染
目标
所以,本篇文章目标就是实现上面描述的整个过成,完成初始渲染。整个过程中涉及如下知识点:
-
render helper
-
VNode
-
patch 初始渲染
-
指令(v-model、v-bind、v-on)的处理
-
实例化子组件
-
插槽的处理
实现
接下来就正式进入代码实现过程,一步步实现上述所有内容,完成页面的初始渲染。
mount
/src/compiler/index.js
/**
* 编译器
*/
export default function mount(vm) {
if (!vm.$options.render) {
// 没有提供 render 选项,则编译生成 render 函数
// ...
}
mountComponent(vm)
}
mountComponent
/src/compiler/mountComponent.js
/**
* @param {*} vm Vue 实例
*/
export default function mountComponent(vm) {
// 更新组件的的函数
const updateComponent = () => {
vm._update(vm._render())
}
// 实例化一个渲染 Watcher,当响应式数据更新时,这个更新函数会被执行
new Watcher(updateComponent)
}
vm._render
/src/compiler/mountComponent.js
/**
* 负责执行 vm.$options.render 函数
*/
Vue.prototype._render = function () {
// 给 render 函数绑定 this 上下文为 Vue 实例
return this.$options.render.apply(this)
}
render helper
/src/compiler/renderHelper.js
/**
* 在 Vue 实例上安装运行时的渲染帮助函数,比如 _c、_v,这些函数会生成 Vnode
* @param {VueContructor} target Vue 实例
*/
export default function renderHelper(target) {
target._c = createElement
target._v = createTextNode
}
createElement
/src/compiler/renderHelper.js
/**
* 根据标签信息创建 Vnode
* @param {string} tag 标签名
* @param {Map} attr 标签的属性 Map 对象
* @param {Array<Render>} children 所有的子节点的渲染函数
*/
function createElement(tag, attr, children) {
return VNode(tag, attr, children, this)
}
createTextNode
/src/compiler/renderHelper.js
/**
* 生成文本节点的 VNode
* @param {*} textAst 文本节点的 AST 对象
*/
function createTextNode(textAst) {
return VNode(null, null, null, this, textAst)
}
VNode
/src/compiler/vnode.js
/**
* VNode
* @param {*} tag 标签名
* @param {*} attr 属性 Map 对象
* @param {*} children 子节点组成的 VNode
* @param {*} text 文本节点的 ast 对象
* @param {*} context Vue 实例
* @returns VNode
*/
export default function VNode(tag, attr, children, context, text = null) {
return {
// 标签
tag,
// 属性 Map 对象
attr,
// 父节点
parent: null,
// 子节点组成的 Vnode 数组
children,
// 文本节点的 Ast 对象
text,
// Vnode 的真实节点
elm: null,
// Vue 实例
context
}
}
vm._update
/src/compiler/mountComponent.js
Vue.prototype._update = function (vnode) {
// 老的 VNode
const prevVNode = this._vnode
// 新的 VNode
this._vnode = vnode
if (!prevVNode) {
// 老的 VNode 不存在,则说明时首次渲染根组件
this.$el = this.__patch__(this.$el, vnode)
} else {
// 后续更新组件或者首次渲染子组件,都会走这里
this.$el = this.__patch__(prevVNode, vnode)
}
}
安装 __patch__、render helper
/src/index.js
/**
* 初始化配置对象
* @param {*} options
*/
Vue.prototype._init = function (options) {
// ...
initData(this)
// 安装运行时的渲染工具函数
renderHelper(this)
// 在实例上安装 patch 函数
this.__patch__ = patch
// 如果存在 el 配置项,则调用 $mount 方法编译模版
if (this.$options.el) {
this.$mount()
}
}
patch
/src/compiler/patch.js
/**
* 初始渲染和后续更新的入口
* @param {VNode} oldVnode 老的 VNode
* @param {VNode} vnode 新的 VNode
* @returns VNode 的真实 DOM 节点
*/