Bootstrap

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

一,前言

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

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

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 示例:


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

设置断点并进行调试:

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

继续,调用 vm.render 方法:

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

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

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

执行 _s(name):

先从 _data 取 name 值

当进入 _s 时,传入 name 的值

取值代理

数据劫持

进入 _s(name):

同理,进入_s(age):

先从 _data 取 age 值

当进入 _s 时,传入 age 的值

(略)

继续,进入 _v:

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

继续,进入 _c:

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

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 :

打印 render 函数:

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

观察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      // 文本
  }
}

测试:

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

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

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

三,结尾

又成功水了一篇,还剩 3 篇

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

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