【手写 Vue2.x 源码】第二十篇 - 使用真实节点替换原始节点

174 阅读5分钟

一,前言

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

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

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


二,新老节点的替换更新

到目前为止,都是围绕根节点进行的实现,还尚未涉及到组件的更新;

1,前情回顾

前面,通过createElm方法,完成了根据虚拟节点生成真实节点,详细步骤如下:

  1. 通过vnode中的tag,判断节点是元素还是文本:文本的tagundefined
  2. 若当前节点是文本,创建文本真实节点:document.createTextNode(text)
  3. 若当前节点是元素,创建元素真实节点:document.createElement(tag)
  • 当前节点是元素且存在儿子,需要递归处理创建出儿子的真实节点,并添加到对应的父亲中:children.forEach(child => {el.appendChild(createElm(child))});
  1. 返回生成的真实节点;

下面继续,使用真实节点替换原始节点,那么,如何进行新老节点的替换呢?

2,新老节点的更新方案

1,新老节点的替换方案分析

若使用replace方法进行dom替换,就需要找到父节点,还需要指定用谁来替换谁,使用起来不方便;

2,Vue的实现方案

  1. 找到老节点;
  2. 将新节点插入到老节点之后,使新老节点成为兄弟节点;
  3. 删除老节点;

3,Vue实现方案的优势

能够确保在新老节点完成更新后,文档的顺序不变;

3,虚拟节点与真实节点映射

1,为什么要做真实节点与虚拟节点的映射关系?

当后续虚拟节点更新时,便于跟踪并找到与vnode对应的真实节点el,快速完成真实节点的更新操作;

2,代码实现

代码实现:将真实节点绑定到vnode的扩展属性el上:

// src/vdom/patch.js

function createElm(vnode) {

  let{tag, data, children, text, vm} = vnode;
  if(typeof tag === 'string'){ // 标签
  
    // vnode.el 赋值操作:绑定真实节点与虚拟节点的映射关系,便于后续的节点更新操作
    vnode.el = document.createElement(tag)    // 创建元素的真实节点
    
    // 处理当前元素节点的儿子:递归创建儿子的真实节点,并添加到对应的父亲中
    children.forEach(child => { // 若不存在儿子,children为空数组
      vnode.el.appendChild(createElm(child))
    });
    
  } else { // 文本:文本中 tag 是 undefined
    vnode.el = document.createTextNode(text)  // 创建文本的真实节点
  }
  
  console.log(" ====== 输出 vnode ====== " + ", tag = " + tag)
  console.log(vnode)
  return vnode.el;
}

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} el    当前真实元素 id#app
 * @param {*} vnode 虚拟节点
 * @returns         新的真实元素
 */
export function patch(el, vnode) {
  console.log(el, vnode)
  
  // 根据虚拟节点创建真实节点
  const elm = createElm(vnode);
  console.log("createElm", elm);

  return elm;
}

控制台输出映射后的vnode对象:

image.png

每一个虚拟节点都对应着一个真实节点vnode.el

控制台输出createElm,查看页面渲染结果:

image.png

后续使用此真实节点去更新页面即可

注意:当前真实节点缺少了id=app部分,样式也没有进行处理;

id=app丢失的原因是由于 data 没有被添加到真实节点上导致的,所以,还需要一个处理 data 属性的方法 updateProperties

2,处理 data 属性 updateProperties

updateProperties方法:在处理元素时调用,将data中的属性值渲染到真实节点上;

// src/vdom/patch.js

// 遍历 data 属性,添加到 el 真实节点的属性上(暂不考虑样式)
function updateProperties(el, props = {} ) { 
  for(let key in props){
    el.setAttribute(key, props[key])
  }
}

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;
}

再次输出真实节点

image.png

现在,真实节点已经完成了;

下一步,使用新节点替换到老节点就可以了;

暂时没有考虑样式,后续在 diff 算法时会继续完善;

4,实现新老节点的替换

1,新老节点的更新方案

  1. 找到元素的父亲节点;
  2. 找到老节点的下一个兄弟节点;
  3. 将新节点插入到老节点的下一个兄弟节点之前;
  4. 删除老节点;

2,代码实现

/**
 * 将虚拟节点转为真实节点后插入到元素中
 * @param {*} el    当前真实元素 id#app
 * @param {*} vnode 虚拟节点
 * @returns         新的真实元素
 */
export function patch(el, vnode) {
  console.log(el, vnode)
  
  // 1,根据虚拟节点创建真实节点
  const elm = createElm(vnode);
  console.log("createElm", elm);

  // 2,使用真实节点替换掉老节点
  // 1)找到元素的父亲节点
  const parentNode = el.parentNode;
  // 2)找到老节点的下一个兄弟节点(nextSibling 若不存在将返回 null)
  const nextSibling = el.nextSibling;
  // 3)将新节点 elm 插入到老节点 el 的下一个兄弟节点nextSibling的前面
  // 备注:若 nextSibling 为 null,则 insertBefore 等价于 appendChild
  parentNode.insertBefore(elm, nextSibling); 
  // 4)删除老节点 el
  parentNode.removeChild(el);

  return elm;
}

查看页面渲染结果:

image.png

至此,就完成Vue文档中的“Create vm.$el and replace 'el' with it”,即完成了Vue的初始化流程;

后续,将进入Vue的更新流程:当时数据更新时,Vue通过diff算法进行组件级别的更新,这就需要进行依赖收集;


三,结尾

本篇,使用真实节点替换原始节点,主要涉及以下几点:

  • 新老节点的更新方案;
  • 虚拟节点与真实节点映射;
  • 实现新老节点的替换;

下一篇,依赖收集的过程分析;


更新记录

  • 20230131:调整目录结构,添加内容中的代码高亮,对部分内容描述进行微调,添加必要的代码注释,添加 todo;
  • 20230201:
    • 完成 todo:添加图片-控制台打印虚拟节点与真实节点映射后的vnode对象并添加说明;
    • 将 updateProperties 处理 data 单独拆分出来,提出问题->解决问题;
    • 添加初渲染完成后的截图;