【手写 Vue2.x 源码】第十九篇 - 根据 vnode 创建真实节点

320 阅读4分钟

一,前言

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

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

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


二,根据 vnode 创建真实节点

1,前文回顾

前面,通过一系列操作,生成了render函数:html模板 -> AST语法树 -> render函数;

之后,又创建了 mountComponent方法,目标是做以下两件事,从而完成组件的挂载操作: 1,使用 render 函数 + 真实数据创建虚拟节点 vnode; 2,根据虚拟节点 vnode创建出真实节点,并将组件挂载到vm.$el上;

上一篇,已经搞好了第一件事,在mountComponent方法中,通过调用vm实例上的原型方法vm._render生成了vnode,主要思路如下:

  • vm._render方法:在Vue初始化时,通过调用renderMixin_render扩展到Vue原型上;
  • 通过调用原型方法vm._render,执行了render函数;(render 的生成:1,拼接 code;2,with + new Function;)
  • render函数执行过程中,会调用_s_v_c方法,最终生成 vnode
// src/lifecycle.js

export function mountComponent(vm) {
  // 调用 render 函数,内部触发_s,_v,_c...
  vm._render();
}

// src/render.js
export function renderMixin(Vue) {
  Vue.prototype._render = function () {
    const vm = this;
    let { render } = vm.$options;
    // 内部将会调用 _c ,_v ,_s 方法
    let vnode = render.call(vm); 
    
    return vnode; // 返回虚拟节点
  }
  Vue.prototype._c = function () {/* 创建元素虚拟节点 */}
  Vue.prototype._v = function () {/* 创建文本虚拟节点 */}
  Vue.prototype._s = function () {/* JSON.stringify */}
}

这样,在mountComponent方法中通过调用vm._render方法,就得到了虚拟节点vnode

接下来继续,根据vnode创建真实节点并完成挂载操作;

这里,就需要一个更新方法,完成将虚拟节点更新到页面上的操作;

2,vm._update 方法

// src/lifecycle.js

export function mountComponent(vm) {
  // 1,vm._render():调用 render 方法
  // 2,vm._update(vnode):将虚拟节点更新到页面上
  vm._update(vm._render());  
}

故技重施,在Vue原型上继续扩展_update方法:

// src/lifecycle.js

export function mountComponent(vm) {
  vm._update(vm._render());  
}

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    console.log("_update-vnode", vnode)
  }
}

Vue初始化时,通过调用lifeCycleMixin方法,完成_updateVue原型上的扩展:

// src/index.js

import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";

function Vue(options){
    this._init(options);
}

initMixin(Vue)
renderMixin(Vue)
lifeCycleMixin(Vue)	// 在 Vue 原型上扩展 _update 方法

export default Vue;

示例:

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

vm._render方法返回的虚拟节点vnode,将被传入vm._update中继续处理,打印日志看一下:

image.png

TODO:_update 为什么选择在 Vue 原型上进行扩展?

下一步,根据虚拟节点创建出真实节点...

vnode是一个描述了节点关系的对象

3,patch 方法

问题:如何根据虚拟节点创建出真实节点呢?

方案:递归处理vnode对象,生成真实节点;(采用先序深度遍历创建节点)

注意:由于Vue的更新机制是组件级别,所以,此处的递归理论上并不会产生性能问题;

因此,在实际开发中,合理的组件化拆分,可以有效的避免由于递归所产生的性能问题;

patch方法:将虚拟节点转化为真实节点,并插入到元素中;

patch方法所属vdom模块,创建src/vdom/patch.js

备注:后续的diff算法,也会在patch方法中进行;

// src/vdom/patch.js

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} el    当前真实元素 id#app
 * @param {*} vnode 虚拟节点
 * @returns         新的真实元素
 */
export function patch(el, vnode) {
  console.log(el, vnode)
  // 根据虚拟节点创造真实节点,替换为真实元素并返回
}

vm._update中,调用patch方法,返回新的真实元素:


// src/lifeCycle.js#lifeCycleMixin

export function lifeCycleMixin(Vue){
  Vue.prototype._update = function (vnode) {
    const vm = this;
    // 传入当前真实元素vm.$el,虚拟节点vnode,返回新的真实元素
    vm.$el = patch(vm.$el, vnode);
  }
}

测试:

image.png

此部分,在Vue官方文档中有对应说明:

image.png

如图描述:会创建出一个新的$el,并且使用它替换掉了原来的el;

所以,还需要继续完成以下两步操作:

  1. 根据控制台输出中右侧的vnode,创建出真实节点;
  2. 使用真实节点替换掉控制台输出中左侧的老节点,即id#app

4,createElm 根据虚拟节点创建真实节点

createElm方法:根据虚拟节点创建真实节点;

// src/vdom/patch.js

// 将虚拟节点转为真实节点后插入到元素中
export function patch(el, vnode) {

  // 1,根据虚拟节点创建真实节点
  const elm = createElm(vnode);
  
  // 2,使用真实节点替换老节点
  
  return elm;
}

createElm方法中,递归处理vnode中的节点信息,转化为真实节点:

// src/vdom/patch.js

function createElm(vnode) {
  let el;
  let{tag, data, children, text, vm} = vnode;
  
  // 通过 tag 判断当前节点是元素 or 文本,判断逻辑:文本 tag 是 undefined
  if(typeof tag === 'string'){
    el = document.createElement(tag)    // 创建元素的真实节点
    // 继续处理元素的儿子:递归创建真实节点并添加到对应的父亲上
    children.forEach(child => { // 若不存在儿子,children为空数组,循环终止
      el.appendChild(createElm(child))
    });
  } else {
    el = document.createTextNode(text)  // 创建文本的真实节点
  }
  
  return el;
}

测试结果:

image.png

注意:当前生成的真实节点中,缺少了id#app

5,处理 data 属性

在生成元素时,如果有data属性,需要将data设置到元素上,否则就会丢失id#app

// src/vdom/patch.js

function createElm(vnode) {
  let{tag, data, children, text, vm} = vnode;
  if(typeof tag === 'string'){
    vnode.el = document.createElement(tag)
    // 处理 data 属性
    updateProperties(vnode.el, data)
    children.forEach(child => {
      vnode.el.appendChild(createElm(child))
    });
  } else {
    vnode.el = document.createTextNode(text)
  }
  return vnode.el;
}

// 循环 data 添加到 el 的属性上
function updateProperties(el, props = {} ) {
  // todo 当前实现没有考虑样式属性
  for(let key in props){
    el.setAttribute(key, props[key])
  }
}

测试结果:

image.png

至此,完成了对虚拟节点的递归处理,创建出了真实节点;

下一步,使用真实节点替换原始节点;


三,结尾

本篇,根据 vnode 虚拟节点创建真实节点,主要涉及以下几点:

  • vnode 渲染真实节点的步骤
  • Vue 原型方法 _update 的扩展
  • patch 方法中的两个步骤:1,创建真实节点;2,替换掉老节点
  • createElm实现:根据虚拟节点创建真实节点

下一篇,使用真实节点替换原始节点


更新记录

  • 20230130:调整了部分内容描述和措辞,添加了必要的代码注释和备注,调整了文章格式;