前端练习37 迷你MVVM

知识点

  1. 如何获取所有文本节点
  2. definePropertydefineProperties
  3. 将字符串替换为参数变量
  4. 如何标记依赖的元素

题目

做一个小型的MVVM库,可以做到数据和视图之间的自动同步。

你需要做的就是完成一个函数bindViewToData,它接受一个DOM节点和一个对象data作为参数。bindViewToData会分析这个DOM节点下的所有文本节点,并且分析其中被{{}}包裹起来的表达式,然后把其中的内容替换成在data上下文执行该表达式的结果。例如:

<div id='app'>
  <p>
    My name is {{firstName + ' ' + lastName}}, I am {{age}} years old.
  </p>
<div>
const appData = {
  firstName: 'Lucy',
  lastName: 'Green',
  age: 13
}
bindViewToData(document.getElementById('app'), appData)

// div 里面的 p 元素的内容为
// My name is Lucy Green, I am 13 years old.

appData.firstName = 'Jerry'
appData.age = 16

// div 里面的 p 元素的内容自动变为
// My name is Jerry Green, I am 16 years old.

当数据改变的时候,会自动地把相应的表达式的内容重新计算并且插入文本节点。

实现

获取文本节点

对DOM的操作和属性又有点忘记了,获取子节点原来都使用的节点的children属性,但是children里面并没有文本节点,要使用的是childNodes属性。

二者的最主要的区别就是:childNodes会返回文本节点和空节点,而children属性不会返回文本节点和空节点

所以这里我们要使用childNodes属性,并且通过nodeName === '#text'筛选文本节点(也可以使用nodeType === 3),然后通过node.data获取文本

然后通过递归筛选出所有的文本节点,这之前练习过的获取元素标签《前端练习20 DOM标签统计》是类似的,有几个方式

复习一下,这里是将所有文本节点放到一个数组中:

可以在递归调用时多传递一个变量参数来存储结果:

const getAllTextNode = (node, result = []) => {
  if (node.nodeName === '#text') {
    result.push(node.data.trim());
  }
  if (node.childNodes.length > 0) {
    result.concat([...node.childNodes].map(childNode => getAllTextNode(childNode, result)))
  }
  return result
};

也可以直接递归,通过将数组返回值展平来实现:

const getAllTextNode = (node) => {
  if (node.nodeName === '#text') {
    return node.data.trim();
  }
  if (node.childNodes.length > 0) {
    return [].concat(...[...node.childNodes].map(childNode => getAllTextNode(childNode)))
  }
};

当然也可以通过reduce来实现:

const getAllTextNode = (node) => {
  if (node.nodeName === '#text') {
    return node.data.trim();
  }
  if (node.childNodes.length > 0) {
    return [...node.childNodes].reduce((total, current) => {
      return total.concat(getAllTextNode(current))
    }, []).filter(v => v)
  }
};

实际使用的时候,不能只返回一个成员是字符串或者是节点的数组,还需要将节点的初始值存起来,否则的话替换一次之后就没有办法响应式的变化了

// 收集依赖
const collectDep = node => {
  if (node.nodeName === '#text' && node.data.trim()) {
    const str = node.data.trim();
    if (str.includes('{{') && str.includes('}}')) {
      return {
        node: node,
        originText: node.data
      }
    }
  }
  if (node.childNodes.length > 0) {
    return [].concat(...[...node.childNodes].map(childNode => collectDep(childNode)).filter(v => v))
  }
};

上面的方法我们返回值是一个数组,数组成员是对象,里面的originText就是我们存的表达式初始值

使用defineProperty

实现响应式,使用了definePropertydata里面每个属性的settergetter劫持

注意defineProperty和``definePropertiessetter`方法传入的参数是不同的,实现的时候又搞混了

// 劫持data的setter,实现响应式更新
const keys = Object.keys(data);
if (keys.length > 0) {
  keys.forEach(v => {
    let value = data[v];
    Object.defineProperty(data, v, {
      get() {
        return value
      },
      set(newValue) {
        value = newValue;
        update(depends, data);
        return value;
      }
    })
  });
}

将字符串替换为参数变量

这个方法和之前练习过的《前端练习28 执行任意表达式》是一样的,避免使用evalwith,使用new Function来实现

// 替换字符串为变量
const replaceStr = (str, data) => {
  return str.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
    const keys = Object.keys(data);
    const values = keys.map(v => data[v]);
    return new Function(...keys, `return ${p1}`)(...values)
  })
};

标记依赖的元素

在Vue中,每个组件都有一个watch对象,它就是用来观察属性的变化,并将每个属性的依赖关系

我总感觉我写的东西总是没有很好的设计感,总是没有面向对象的感觉,功能能实现,未来扩展困难,不优雅

这就是我现在最大的问题,实际上,我也没有遇到过将来要扩展的情况,写的代码都只是完成功能而已,太缺乏实践。

结果

最终我的实现:

const appData = {
  firstName: 'Lucy',
  lastName: 'Green',
  age: 13
};
// 替换字符串为变量
const replaceStr = (str, data) => {
  return str.replace(/\{\{(.*?)\}\}/g, (match, p1) => {
    const keys = Object.keys(data);
    const values = keys.map(v => data[v]);
    return new Function(...keys, `return ${p1}`)(...values)
  })
};
// 收集依赖
const collectDep = node => {
  if (node.nodeName === '#text' && node.data.trim()) {
    const str = node.data.trim();
    if (str.includes('{{') && str.includes('}}')) {
      return {
        node: node,
        originText: node.data
      }
    }
  }
  if (node.childNodes.length > 0) {
    return [].concat(...[...node.childNodes].map(childNode => collectDep(childNode)).filter(v => v))
  }
};
// 更新节点列表
const update = (nodeList, data) => {
  if (Array.isArray(nodeList) && nodeList.length > 0) {
    nodeList.forEach(v => {
      v.node.data = replaceStr(v.originText, data)
    })
  }
};
const bindViewToData = (el, data) => {
  // 收集依赖
  let depends = collectDep(el);
  // 将依赖中的变量进行替换
  update(depends, data);
  // 劫持data的setter,实现响应式更新
  const keys = Object.keys(data);
  if (keys.length > 0) {
    keys.forEach(v => {
      let value = data[v];
      Object.defineProperty(data, v, {
        get() {
          return value
        },
        set(newValue) {
          value = newValue;
          update(depends, data);
          return value;
        }
      })
    });
  }
};

总结

  1. 好多细节不确定,DOM操作的某些属性和方法也记不清了,估计手写实现够呛,必须一遍调试一遍写
  2. 没有很好的面向对象编程的思维,写的代码不优雅
  3. 多练多写吧

参考

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值