手把手教你写前端框架(三):数据更新

本系列文章是分享我自己一步一步编写整个框架的过程,有兴趣的xdm可以参考源代码阅读。git仓库:github.com/sullay/art-…

数据更新

通过前面的努力我们的框架已经可以完成页面渲染的工作了,那数据更新其实只需要把数据有变更的组件更新一下就可以了。

回顾一下之前代码,对于一个vNode对象,如果没有$dom属性就会重新生成dom。

getDom() {
    if (!this.$dom) this.createDom(); 
    return this.$dom; 
}
复制代码

所以我们只需要在数据更新以后将原来的$dom置空,便可以再后续的渲染中重新生成。这里我们约定Component组件的data属性用来存放所有支持动态更新到属性。

// 传入需要更新的数据,触发渲染
setData(data) {
  for (const key in data) {
    this.data[key] = data[key];
  }
  let oldDom = this.$vNode.$dom;
  // 自定义组件node的$dom指向子节点的$dom,此处赋值为null是为了触发createDom
  this.$vNode.$dom = null;
  // 传入oldDom用作替换
  renderDomTree(this.$vNode, this.$vNode.$parentNode, oldDom);
}
复制代码

通过简单的$dom置空完成了数据更新,但是这里面其实存在一些问题:

  • 自定义组件的所有dom都需要重新创建
  • 子组件被初始化状态没有保留

$dom、$instance复用

为了解决上述两个问题,我们就需要在createDom时尽可能使用之前创造的dom,针对vComponentNode还需要用旧node节点的$instance替换掉新node节点的$instance属性。

// vComponentNode类
createDom() {
    let preChildren = this.$children;
    let child = this.$instance.render();
    this.$children = [child];
    // 每次创建时通过diff复用之前的dom或者子组件
    vNode.diffDom(this.$children, preChildren);
    this.$dom = child.getDom();
  }
  
  // 旧的node中的dom根据新node重新赋值
  static updateDom(newNode, preNode) {
    let dom = preNode.$dom;
    for (let key in preNode.$props) dom[key] = null;
    for (let event in preNode.$events) dom.removeEventListener(event, preNode.$events[event]);
    // 去掉子节点,调用renderDomTree方法时再组织dom结构
    dom.innerHTML = null;
    // 设置属性
    for (let key in newNode.$props) dom[key] = newNode.$props[key];
    // 监听事件
    for (let event in newNode.$events) dom.addEventListener(event, newNode.$events[event]);
    newNode.$dom = dom;
  }

  // 新旧节点对比,尽可能复用已有dom或者自定义组件
  static diffDom(newNodes = [], preNodes = []) {
    if (!newNodes.length || !preNodes.length) return;
    // 所有旧node
    let preMap = new Map();
    for (const node of preNodes) {
      if (!preMap.has(node.$type)) preMap.set(node.$type, []);
      if (node.$dom) preMap.get(node.$type).push(node);
    }
    // 遍历新node,查找是否存在可以复用的node
    for (const node of newNodes) {
      if (preMap.has(node.$type) && preMap.get(node.$type).length) {
        let preNode = preMap.get(node.$type).shift();
        if (vComponentNode.isVNode(node)) {
          let preChildren = preNode.$children;
          // 自定义组件,复用旧的组件实例
          node.$instance = preNode.$instance;
          // 组件实例$vNode指向新的node
          node.$instance.$vNode = node;
          // 更新自定义组件虚拟dom树
          let child = node.$instance.render();
          node.$children = [child];
          vNode.diffDom(node.$children, preChildren);
          // 自定义组件$dom指向子组件的$dom
          node.$dom = child.$dom;
        } else {
          // 普通node节点,更新后继续diff子组件
          vNode.updateDom(node, preNode);
          vNode.diffDom(node.$children, preNode.$children);
        }
      }
    }
  }
复制代码

上述代码目前只支持同一层级的复用,不同层级复用建议使用指定key值来实现。

props、events更新

此时我们已经可以复用之前的dom以及组件实例,但其实还存在一个隐藏的bug。先回顾一下上级篇文章的代码。

export class vComponentNode extends vNode {
  constructor(type = '', allProps = {}, slots = []) {
    ...
    this.$instance = new type(this.$props, this.$events, slots);
    this.$instance.$vNode = this;
  }
}

复制代码

假设我们存在父子组件,我们希望子组件的props属性可以根据父组件的data属性变更。但是子组件的props属性是在构造组件对象的时候传入的,我们数据更新时复用了原来的$instance属性,因此props属性便无法完成更新。

我们可以考虑使用类似于react的做法,通过一个额外的钩子函数来触发props的更新,这里我使用了另一种做法。


export class vComponentNode extends vNode {
  constructor(type = '', allProps = {}, slots = []) {
    super(type, allProps);
    // 标识自定义组件
    this.$isComponent = true;
    // 插槽
    this.$slots = slots;
    // 创建组件实例
    this.$instance = new type(this);
  }
}

// 自定义组件类
export class Component {
  constructor(node) {
    // 绑定对应node节点
    this.$vNode = node;
    this.data = {}
  }
  // 所有属性都去node上面拿,复用原组件时不需要初始化可以更新数据与事件。
  get props() {
    return this.$vNode.$props;
  }
  get events() {
    return this.$vNode.$events;
  }
  get slots() {
    return this.$vNode.$slots;
  }
}

复制代码

我们不把props属性传给组件类,而是把vNode对象传过去,这样props属性还是绑定在vNode对象上。组件更新时,$instance虽然被复用,但是props属性依然可以被更新。

到此为止我们已经完成了数据更新的工作,下一章我们将开始编写框架的任务调度,也是此框架最核心的功能。

xdm觉得这系列文章对你有帮助的点赞支持啊!!!

猜你喜欢

转载自juejin.im/post/7083312666550206500