2-1. 【函数科里化与渲染模型】:生成新的虚拟DOM更新旧的虚拟DOM

140 阅读2分钟

一、vue 渲染模型

1651910832(2).jpg

  1. 对以上进行分析:
  • 页面初始化 的时候,AST(抽象语法树) 结合 数据data 生成 一份 虚拟DOM(VNode) ,内存把这个 虚拟DOM(VNode) 保存下来, 抽象语法树(AST) 也被缓存下来了。

  • 数据变化 的时候,AST 结合 数据 生成一个 新的虚拟DOM(VNode)旧的虚拟DOM新的虚拟DOM 进行比较(这里面比较涉及到 diff 算法),没有变化的模板及数据不进行替换,有变化的模板及数据用 新的虚拟DOM 里面的内容进行替换。然后 旧的虚拟DOM 这时候是更新后的状态,再转化成 真实DOM, 渲染到页面上。

  1. 在真正的 Vue 中使用了二次提交的 设计结构
  • 在页面中 的 DOM 和 虚拟DOM 是一一对应的关系。
    
  • 先用 AST 和 数据 生成 虚拟DOM
    
  • 然后将 旧的 VNode 和 新的 VNode 比较(通过 diff 算法),最后更新页面(update

二、仿 vue 生成虚拟DOM

  1. Vue 中的是 通过 AST 结合 数据 生成一份虚拟DOM。 我们现在是通过 带坑的虚拟DOM(有{{}}) 结合 数据 来生成一份 有数据的虚拟DOM(不带坑的{{}})
<div id="root">
  <div class="box">{{name}}-{{age}}</div>
  <div>{{message}}</div>
</div>
<script>
  // 把 【虚拟 DOM】 转化成【真实 DOM】
  function parseVnode(vnode) {
    let type = vnode.type;
    let _node = null;
    if (type == 3) {
      return document.createTextNode(vnode.value);
    } else if (type == 1) {
      // 创建一个元素节点
      _node = document.createElement(vnode.tag);
      // 给元素节点添加属性
      let attrs = vnode.data;
      Object.keys(attrs).forEach(key => {
        let attrName = key;
        let attrValue = attrs[key];
        _node.setAttribute(attrName, attrValue);
      })
      // 给元素节点添加子元素节点
      let childrenNodes = vnode.children;
      for (var i = 0; i < childrenNodes.length; i++) {
        _node.appendChild(parseVnode(childrenNodes[i]))
      }
    }
    return _node;
  }

  // 转虚拟DOM 的类
  class Vnode {
    /**
     * tag 表示元素节点元素名称nodeName,例如 div; 文本节点 nodeName 值为 undefined
     * data 表示元素节点上的 属性对象,例如{title: '123', id: 'box'}; 文本节点 没有属性
     * value 表示文本节点的 nodeValue; 元素节点的 nodeValue 为 undefined
     * type 表示节点的类型,元素节点的 nodeType 值为 1, 文本节点的 nodeType 值为 3
     */
    constructor(tag, data, value, type) {
      this.tag = tag && tag.toLowerCase();
      this.data = data;
      this.value = value;
      this.type = type;
      this.children = [];
    }
    appendChild(vNode) {
      this.children.push(vNode);
    }
  }

  // 把 HTML DOM 转化成 虚拟DOM(VNode)
  function getVnode(node) {
    let nodeType = node.nodeType;
    let _vnode = null;
    if (nodeType === 1) {
      // 元素节点
      let NodeName = node.nodeName;
      let attrs = node.attributes;
      let attrsObj = {};
      for (var i = 0; i < attrs.length; i++) {
        attrsObj[attrs[i].nodeName] = attrs[i].nodeValue;
      }
      _vnode = new Vnode(NodeName, attrsObj, undefined, nodeType);
      let childNodes = node.childNodes;
      for (var i = 0; i < childNodes.length; i++) {
        _vnode.appendChild(getVnode(childNodes[i]))
      }
    } else if (nodeType == 3) {
      _vnode = new Vnode(undefined, undefined, node.nodeValue, nodeType);
    }
    return _vnode;
  }

  let rKuohao = /{\{(.+?)\}\}/g;
  // 多层次数据返回属性值
  function getValueByPath(obj, path) {
    let paths = path.split('.');
    let res = obj;
    let prop;
    while (prop = paths.shift()) {
      res = res[prop];
    };
    return res;
  }

  // 将带有坑的 VNode 与数据 data 结合,得到填充数据的 VNode, 模拟 Vue 源码中的 AST 转化成 VNode
  function combine(vnode, data) {
    let _type = vnode.type;           // 获取节点类型
    let _value = vnode.value;         // 获取文本节点 value 值
    let _tag = vnode.tag;             // 获取元素节点 标签名
    let _data = vnode.data;           // 获取元素节点 属性数据
    let _children = vnode.children;   // 获取元素节点下的子元素节点
    let _vnode = null;
    if(_type === 3) {
      // 文本节点
      // 对文本进行处理
      _value = _value.replace(rKuohao, (_, g) => {
        return getValueByPath(data, g.trim())
      })
      return new Vnode(_tag, _data, _value, _type);
    } else if(_type == 1) {
      // 表示元素节点
      _vnode = new Vnode(_tag, _data, _value, _type);
      _children.forEach(_subVnode => _vnode.appendChild(combine(_subVnode, data)))
    }
    return _vnode;
  }

  // 创建 JGVue 构造函数
  function JGVue(options) {
    this._data = options.data;
    this._template = document.querySelector(options.el);
    this.mount();     // 进行实例挂载
  }

  JGVue.prototype.mount = function () {
    // 需要提供一个 render 方法, 生成  【虚拟 DOM】
    this.render = this.createRenderFn();
    this.mountComponent();
  }

  JGVue.prototype.mountComponent = function () {
    // 这里执行 mountComponent 函数
    let mount = () => {
      this.updata(this.render());
    }
    mount.call(this)
  }


  // 这里是生成  【虚拟 DOM】  的函数,目的是缓存抽象语法树,我们这里用的是 【虚拟 Dom】 来模拟 vue 源码中的 AST
  JGVue.prototype.createRenderFn = function () {
    let ast = getVnode(this._template);
    console.log(ast, '带坑的虚拟DOM')
    // Vue 源码: 是将 AST(抽象语法树) 结合 数据data 生成一个 虚拟DOM(VNode)
    // 我们这里:带有坑的 虚拟Dom(VNode) 结合 数据data 生成含有数据的 VNode
    return function render() {
      // 将带有坑的 VNode 转换为 带数据的 VNode
      let _tmp = combine(ast, this._data);
      console.log(_tmp, '带数据的虚拟DOM')
      return _tmp;
    }
  }

  // 将 【虚拟 DOM】 渲染到页面当中,这里涉及到 diff 算法
  JGVue.prototype.updata = function () {

  }


  // 创建 vue 实例
  let app = new JGVue({
    el: '#root',
    data: {
      name: '姓名',
      age: '年龄',
      message: '消息',
    }
  })

三、带坑的 虚拟Dom

image.png

四、由带坑的 虚拟Dom 转化成 带数据的 虚拟Dom

image.png