写点什么

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

用户头像
Brave
关注
发布于: 4 小时前
【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


  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) }}
复制代码



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>
复制代码


设置断点并进行调试:

这里,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.stringifyVue.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);  }
复制代码


观察 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,主要涉及一下几点:

  1. 封装 vm._render

  2. _s,_v,_c 的实现


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

用户头像

Brave

关注

还未添加个人签名 2018.12.13 加入

还未添加个人简介

评论

发布
暂无评论
【Vue2.x 源码学习】第十八篇 - 根据 render 函数,生成 vnode