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

文章详细阐述了Vue.js中如何从render函数生成虚拟节点vnode的过程,包括mountComponent方法的执行,render函数的调用,以及vm._render方法的封装,同时提到了_s、_v和_c方法的作用,最终目的是将虚拟节点转换为真实DOM节点。
摘要由CSDN通过智能技术生成

一,前言

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

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

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


二,前文回顾

前面介绍,html模板最终会被编译为render函数

为了避免重复编译,会将生成好的render函数保存到 opts.render 上:

// src/init.js

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函数,完成渲染操作:

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

二,挂载组件 mountComponent

1,mountComponent 方法

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

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

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

创建生命周期模块:src/lifecycle.js,在mountComponent方法中调用render函数,并抛出mountComponent

// src/lifecycle.js#mountComponent

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

在原型方法$mount中,导入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.jsmountComponent方法中,调用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 () {
  
    // 在 vm 中,包含了全部数据  vm.xxx => vm._data.xxx
    const vm = this;  
    let { render } = vm.$options;
    
    // 内部会调用 _c、_v、_s 三个方法,最终返回虚拟节点
    let vnode = render.call(vm);  
    console.log(vnode)
    
    return vnode;
  }
}

4,代码调试

代码示例:

<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 方法:将对象转成字符串,并返回结果;

// src/render.js#renderMixin

// _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函数:

// src/render.js#renderMixin

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 方法中,只负责渲染逻辑,而具体如何创建虚拟dom,是vdom需要做的事情,所以需要将这两部分逻辑拆分开(渲染逻辑、创建虚拟 dom);

renderMixin方法,只返回虚拟节点,并不关心虚拟节点如何产生,将虚拟节点的生成逻辑封装到vdom模块内部的createElementcreateText方法中;

6,实现 _v 和 _c

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

// src/render.js#renderMixin

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

传入 vm 的作用:确定虚拟节点所属实例;

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

vnode:一个用来描述节点的对象;

// src/vdom/index.js

// 返回虚拟节点
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方法:

// src/vdom/index.js

// 通过函数返回 vnode 对象
// key 标识作用:后边元素根据 key 标识做 diff 算法,取值 data.key;
function vnode(vm, tag, data, children, key, text) {
  return {
    vm,
    tag,
    data,
    children,
    key,
    text
  }
}

重构代码:

// src/vdom/index.js

// 参数:_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 对象
// key 标识作用:后边元素根据 key 标识做 diff 算法,取值 data.key;
function vnode(vm, tag, data, children, key, text) {
  return {
    vm,       // 所属实例
    tag,      // 标签
    data,     // 数据
    children, // 孩子
    key,      // 标识
    text      // 文本
  }
}

输出最终的vnode对象:

image.png

至此,就完成了根据render函数生成虚拟节点vnode;(一个描述 dom 结构的对象)

接下来,再根据“虚拟节点+真实数据”渲染成为真实节点,就完成了全部的初渲染流程;

当数据更新时,通过render函数生成新的虚拟节点,与新的真实数据再生成新的真实节点,就实现了更新渲染;


三,结尾

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

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

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


更新日志

  • 20230128:调整目录结构,优化内容描述、代码注释,添加内容中的代码高亮,添加若干备注;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

BraveWangDev

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值