【手写 Vue2.x 源码】第十八篇 - 根据 render 函数,生成 vnode
创始人
2024-05-10 23:19:24

一,前言

上篇,介绍了render 函数的生成,主要涉及以下两点:

  • 使用 with 对生成的 code 进行一次包装
  • 将包装后的完整 code 字符串,通过 new Function 输出为 render 函数

本篇,根据 render 函数,生成虚拟节点 vnode


二,挂载组件 mountComponent

1,前情回顾

Vue.prototype.$mount = function (el) {const vm = this;const opts = vm.$options;el = document.querySelector(el);vm.$el = el;if (!opts.render) {let template = opts.template;if (!template) template = el.outerHTML;let render = compileToFunction(template);opts.render = render;}// 将当前 render 渲染到 el 元素上:// 1,根据 render 函数生成虚拟节点// 2,根据虚拟节点加真实数据,生成真实节点
}

前面,生成了 render 函数,并放到 opts.render 上备用

接下来,使用 render 函数进行渲染:

  • 根据 render 函数生成虚拟节点
  • 根据虚拟节点加真实数据,生成真实节点

2,mountComponent

  1. render 函数执行后,最终会生成一个虚拟节点 vnode
  2. 虚拟节点 vnode + 真实数据 => 真实节点

所以,下一个步骤就是进行组件渲染并完成挂

mountComponent 方法:将组件挂载到 vm.$el 上

创建 src/lifecycle.js

// src/lifecycle.js#mountComponentexport function mountComponent(vm) {render();// 调用 render 方法
}

引入 mountComponent 并调用:

// src/init.jsimport { mountComponent } from "./lifecycle"; // 引入 mountComponentVue.prototype.$mount = function (el) {const vm = this;const opts = vm.$options;el = document.querySelector(el);vm.$el = el;	// 真实节点if (!opts.render) {let template = opts.template;if (!template) template = el.outerHTML;let render = compileToFunction(template);opts.render = render;}// 将当前 render 渲染到 el 元素上mountComponent(vm);
}

3,封装 vm._render

mountComponent 方法:主要完成组件的挂载工作

而 render 渲染只是其一,还有其他工作需要处理;

继续考虑 render 方法的复用性;需要将渲染方法 render 进行独立封装

创建src/render.js

// src/render.js#renderMixinexport function renderMixin(Vue) {// 在 vue 上进行方法扩展Vue.prototype._render = function () {// todo...}
}

src/index.js 入口,调用 renderMixin 做 render 方法的混合:

// src/index.jsimport { initMixin } from "./init";
import { renderMixin } from "./render";function Vue(options){this._init(options);
}initMixin(Vue)
renderMixin(Vue)   // 混合 render 方法export default Vue;

src/lifecycle.js 中 mountComponent 调用 render 函数的方式发生改变:

export function mountComponent(vm) {// render();vm._render();
}

当 vm.render 被调用时,内部将会调用 _c,_v,_s 三个方法

所以这三个方法都是和 render 相关的,可以封装到一起;

所以,vm._render 方法中需要做以下几件事:

  • 调用 render 函数
  • 提供_c,_v,_s 三个方法
// src/render.js#renderMixinexport function renderMixin(Vue) {Vue.prototype._c = function () {  // createElement 创建元素型的节点console.log(arguments)}Vue.prototype._v = function () {  // 创建文本的虚拟节点console.log(arguments)}Vue.prototype._s = function () {  // JSON.stringifyconsole.log(arguments)}Vue.prototype._render = function () {const vm = this;  // vm 中有所有数据  vm.xxx => vm._data.xxxlet { render } = vm.$options;let vnode = render.call(vm);  // 此时内部会调用_c,_v,_s方法,执行完成返回虚拟节点console.log(vnode)return vnode; // 返回虚拟节点}
}

4,代码调试

demo 示例:

aaa {{name}} bbb {{age}} ccc

设置断点并进行调试:

image.png

这里,mountComponent 方法入参 vm,包含了 render 函数及所有数据

继续,调用 vm.render 方法:

image.png

vm.render方法中,会调用 render 方法:

image.png

当 render 方法被调用时,将执行:

image.png

由于函数会从内向外执行,即执行顺序为_s(name),_s(age),_v(),_c();

执行 _s(name):先从 _data 取 name 值当进入 _s 时,传入 name 的值

image.png

取值代理

image.png

数据劫持

image.png

进入 _s(name):

image.png

image.png

同理,进入_s(age):(略)

    先从 _data 取 age 值当进入 _s 时,传入 age 的值

继续,进入 _v:

image.png

由于当前的 _s 没有返回值,所以字符串拼接结果中包含 2 个 undefined;

继续,进入 _c:

image.png

参数包含:标签名,属性,孩子

5,实现 _s

_s 方法:将对象转成字符串,并返回

// _s 相当于 JSON.stringify
Vue.prototype._s = function (val) {  if(isObject(val)){  // 是对象,转成字符串return JSON.stringify(val)} else {  					// 不是对象,直接返回return val}
}

调试:

在 _v 中设置断点,查看 _s 处理后返回的字符串

先调用两个 _s,并将拼接结果传递给 _v :

image.png

打印 render 函数:

Vue.prototype._render = function () {const vm = this;let { render } = vm.$options;console.log(render.toString());	// 打印 render 函数结果let vnode = render.call(vm);return vnode;
}

image.png

观察render 函数:

  • 两个 _s 执行后,将拼接后的字符串传递给了 _v,
  • _v 接收文本 text,文本创建完成将结果传递给 _c

所以,需要先创造文本的虚拟节点,再创造元素的虚拟节点

创建目录:src/vdom

包含两个方法:创建元素虚拟节点,创建文本虚拟节点

备注:_v,_c两个方法都与虚拟节点有关,所以将两个方法放到虚拟dom包中

// src/vdom/index.jsexport function createElement() { // 返回元素虚拟节点}
export function createText() {  // 返回文本虚拟节点}

renderMixin 只负责渲染逻辑,而具体如何创建 vdom,需要 vdom 考虑,所以这两部分逻辑需要拆分开

renderMixin 只返回虚拟节点,但不关心虚拟节点如何产生

6,实现 _v 和 _c

_v 方法:创建并返回文本的虚拟节点

Vue.prototype._v = function (text) {  // 创建文本的虚拟节点const vm = this;return createText(vm, text);// vm作用:确定虚拟节点所属实例
}

vm作用:确定虚拟节点所属实例

如何创建文本虚拟节点,交给 createText 来完成

createText 生成 vnode:vnode 是一个用来描述节点的对象

export function createElement(vm, tag, data={}, ...children) { // 返回虚拟节点// _c('标签', {属性}, ...儿子)return {vm,       // 是谁的虚拟节点tag,      // 标签children, // 儿子data,     // 数字// ...    // 其他}
}
export function createText(vm, text) {  // 返回虚拟节点return {vm,tag: undefined, // 文本没有 tagchildren,data,// ...}
}

提取 vnode 方法:通过函数返回对象

// 通过函数返回vnode对象
// 后续元素需要做 diff 算法,需要 key 标识
function vnode(vm, tag, data, children, key, text) {return {vm,tag,data,children,key,text}
}

重构代码:

// 参数:_c('标签', {属性}, ...儿子)
export function createElement(vm, tag, data={}, ...children) {// 返回元素的虚拟节点(元素是没有文本的)return vnode(vm, tag, data, children, data.key, undefined);
}
export function createText(vm, text) {// 返回文本的虚拟节点(文本没有标签、数据、儿子、key)return vnode(vm, undefined, undefined, undefined, undefined, text);
}// 通过函数返回vnode对象
// 后续元素需要做 diff 算法,需要 key 标识
function vnode(vm, tag, data, children, key, text) {return {vm,       // 谁的实例tag,      // 标签data,     // 数据children, // 儿子key,      // 标识text      // 文本}
}

测试:

image.png

这样就完成了根据 render 函数,生成了虚拟节点 vnode

接下来,再根据虚拟节点渲染成为真实节点

当更新时,通过调用 render 生成虚拟节点,并完成真实节点的更新


三,结尾

本篇,根据 render 函数,生成 vnode,主要涉及一下几点:

  • 封装 vm._render 返回虚拟节点
  • _s,_v,_c的实现

下一篇,根据 vnode 虚拟节点渲染真实节点

相关内容

热门资讯

阿西吧是什么意思 阿西吧相当于... 即使你没有受到过任何外语培训,你也懂四国语言。汉语:你好英语:Shit韩语:阿西吧(아,씨발! )日...
猫咪吃了塑料袋怎么办 猫咪误食... 你知道吗?塑料袋放久了会长猫哦!要说猫咪对塑料袋的喜爱程度完完全全可以媲美纸箱家里只要一有塑料袋的响...
demo什么意思 demo版本... 618快到了,各位的小金库大概也在准备开闸放水了吧。没有小金库的,也该向老婆撒娇卖萌服个软了,一切只...
苗族的传统节日 贵州苗族节日有... 【岜沙苗族芦笙节】岜沙,苗语叫“分送”,距从江县城7.5公里,是世界上最崇拜树木并以树为神的枪手部落...