【手写 Vue2.x 源码】第十八篇 - 根据 render 函数,生成 vnode

news2024/11/23 22:49:31

一,前言

上篇,介绍了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#mountComponent

export function mountComponent(vm) {
  render();// 调用 render 方法
}

引入 mountComponent 并调用:

// src/init.js

import { mountComponent } from "./lifecycle"; // 引入 mountComponent

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 元素上
  mountComponent(vm);
}

3,封装 vm._render

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

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

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

创建src/render.js

// src/render.js#renderMixin

export function renderMixin(Vue) {
  // 在 vue 上进行方法扩展
  Vue.prototype._render = function () {
    // todo...
  }
}

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

// src/index.js

import { 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#renderMixin

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

4,代码调试

demo 示例:

<body>
  <div id="app">aaa {{name}} bbb {{age}} ccc</div>
  <script src="./vue.js"></script>
  <script>
    let vm = new Vue({
      el: '#app',
      data() {
        return { name:  "Brave" , age : 123}
      }
    }); 
  </script>
</body>

设置断点并进行调试:

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.js

export 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, // 文本没有 tag
    children,
    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 虚拟节点渲染真实节点

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/158276.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

linux系统中QT里面的视频播放器的实现方法

大家好&#xff0c;今天主要和大家聊一聊&#xff0c;如何使用QT中视频播放器的方法。 目录 第一&#xff1a;视频播放器基本简介 第二&#xff1a;视频播放器头文件说明 第三&#xff1a;源文件的具体实现方法 第四&#xff1a;运行效果显示 第一&#xff1a;视频播放器基本…

ADS振铃仿真

目录 无振铃时的原理图 无振铃时的Vout和VL输出波形 ​LineCalc对微带线阻抗的计算结果 将微带线线宽Width统一由116改为130 将微带线线宽Width统一由116改为80 将微带线TL9线宽由116改为300 将微带线TL9线宽由116改为50 本文介绍了微带线线宽变化时100MHz信号的反射现象…

2023 年 15 大测试自动化趋势

在过去&#xff0c;软件测试只是为了发现软件产品中的错误。目标是——提高软件质量。但如今&#xff0c;软件测试的范围已经扩大。在软件测试方面&#xff0c;自动化测试一直走在前列。按照最新的测试自动化趋势&#xff0c;软件测试行业有望比过去十年发展得更快。 根据 Mar…

Java面向对象综合训练

Java面向对象综合训练一、文字版格斗游戏Role类测试类输出结果二、对象数组练习对象数组1商品类测试类输出结果对象数组2汽车类测试类输出结果对象数组3手机类测试类输出结果对象数组4女朋友类测试类输出结果对象数组5学生类测试类输出结果一、文字版格斗游戏 Role类 import j…

去掉 域名后面的 /#/ vue-router 和 hbuilder发布 web项目和h5项目

1. vue-router vue-router默认的路由模式是hash&#xff0c;我们要去掉url中的#需要将路由模式切换为history const router new VueRouter({base: test, // 如果项目项目在 域名 根目录下&#xff0c;则去掉这行mode: history, // 路由模式... })这样子&#xff0c;url中的#…

为什么ERP和项目管理的集成是必要的?

在一个企业中&#xff0c;传统的责任分工意味着会计人员看管资金和维持财务标准&#xff0c;而职能经理分配人力资源和维持技术标准。项目经理指导分配的资金和其他资源&#xff0c;同时努力实现项目目标。每个学科都有自己的业务规则&#xff0c;自己的做法&#xff0c;自己的…

C++ | 左值、右值、将亡值和引用的概念 | 聊聊我对它们的深入理解

文章目录前言左右值的辨析一个特殊的问题将亡值引用的深刻理解前言 这篇文章是我在探究完美转发这个语法点时&#xff0c;引发的相关问题思考&#xff0c;为了使自己的理解更深刻&#xff0c;故写下这篇博客 左右值的辨析 首先需要明白两个概念&#xff1a;类型&#xff08;…

1577_AURIX_TC275_MTU中检测控制相关寄存器

全部学习汇总&#xff1a; GreyZhang/g_TC275: happy hacking for TC275! (github.com) 开篇介绍的功能室之前看过很多次的一个握手的功能。快速行以及快速列模式的测试中&#xff0c;这个行列其实是对应的存储的bit阵列信息。一个对应相应的字&#xff0c;另一个则对应bit序列…

【Linux】进程创建、终止、等待、替换、shell派生子进程的理解…

柴犬&#xff1a; 你好啊&#xff0c;屏幕前的大帅哥or大美女&#xff0c;和我一起享受美好的今天叭&#x1f603;&#x1f603;&#x1f603; 文章目录一、进程创建1.调用fork之后&#xff0c;内核都做了什么&#xff1f;2.如何理解fork函数有两个返回值&#xff1f;3.如何理…

(短信服务)java SpringBoot 阿里云短信功能实现发送手机验证码

一.阿里云准备工作 1.阿里云短信服务-注册账号 阿里云官网: https://www.aliyun.com/ 点击官网首页注册按钮。 2.阿里云短信服务-设置短信签名&#xff08;阿里云提供测试的签名&#xff0c;暂时可以跳过&#xff09; 注册成功后&#xff0c;点击登录按钮进行登录。登录后…

简单方式调用WebService服务

好久没有进行过WebService开发了&#xff0c;由于项目需要&#xff0c;重拾WebService&#xff0c;记录一下简单的服务调用方法。拿到需求&#xff0c;仅半页word&#xff0c;其他的就没有了&#xff0c;为了快速开发&#xff0c;尝试过使用插件逆向生成调用的一大堆类&#xf…

AWVS安装与激活

AWVS安装与激活 1.AWVS简介 AWVS&#xff08;Acunetix Web Vulnerability Scanner&#xff09;是一款知名的网络漏洞扫描工具&#xff0c;通过网络爬虫测试网站安全&#xff0c;检测流行的Web应用攻击&#xff0c;如跨站脚本、sql 注入等。据统计&#xff0c;75% 的互联网攻击…

pmp备考全攻略

我这里分享一下我备考的经验&#xff0c;如何对大家有帮助也可以稍微给点支持&#xff0c;让更多人了解&#xff01; 一&#xff0c;我的pmp备考经验 1.一阶段&#xff1a;铺底&#xff0c;花费时间1.5周左右 主要是熟悉考试框架和内容&#xff0c;通过看网盘资料里的章节重…

vue3+ts实现自定义按钮导航

效果图 点击对应按钮&#xff0c;相应按钮被激活&#xff0c;背景平移至激活按钮&#xff0c;字体高亮&#xff0c;其余按钮重置&#xff0c;由于ele没有类似tab&#xff0c;就简单记录下。 实现 <template><div class"tab_wrapper"><spanv-for&q…

这些技巧你值得学会

技巧一&#xff1a;多图合并为PDF文件 处理合并多份PDF文件外&#xff0c;使用PS的【PDF演示文稿】工具&#xff0c;也能一同将多张图片合并成PDF文档&#xff01;通过合并的方式&#xff0c;不但能够批量归纳汇总图片&#xff0c;而且还能根据自身需求&#xff0c;将图片与PD…

Visual studio C++程序内使用Sqlite3

Visual studio C程序内使用Sqlite3 前言 本篇讲解了如何在Visual studio开发的C桌面应用程序内使用Sqlite数据库&#xff0c;Sqlite的语法和Mysql是一样的&#xff0c;所以本篇文章不对数据库语法做过多介绍&#xff0c;介绍一些常用Sqlite的API ★提高阅读体验★ &…

二叉树常见题目

目录 一、判断一棵树是否为另一棵树的子树 二、判断是否对称二叉树 三、翻转二叉树 四、二叉树构建及遍历 五、根据二叉树创建字符串 六、二叉树的最近公共祖先 七、根据前序遍历和中序遍历构造二叉树 八、根据后序遍历和中序遍历构造二叉树 九、二叉树前序非递归…

MySQL——“order by”是如何工作的

假设目前有这么一个表 CREATE TABLE t (id int(11) NOT NULL,city varchar(16) NOT NULL,name varchar(16) NOT NULL,age int(11) NOT NULL,addr varchar(128) DEFAULT NULL,PRIMARY KEY (id),KEY city (city) ) ENGINEInnoDB; 业务要求是要查询城市是“杭州”的所有人名字&a…

1.2计算机系统的层次结构

文章目录&#xff08;1&#xff09;微指令&#xff08;2&#xff09;汇编语言&#xff08;3&#xff09;高级语言&#xff08;4&#xff09;操作系统&#xff08;5&#xff09;编译程序与解释程序&#xff08;6&#xff09;总结请先食用这一篇 计算机工作过程&#xff08;1&…

考研《数据结构》线性表—顺序表练习题

2.设计一个高效算法&#xff0c;将顺序表L的所有元素逆置&#xff0c;要求算法的空间复杂度为0&#xff08;1&#xff09;。 思路&#xff1a;扫描顺序表L的前半部分元素&#xff0c;对于元素L.data[i]与L.data[len-1-i]对换。 #include<iostream> using namespace std…