在 《抛开 vue,React,jQuery 这类第三方 JS, 我们该怎么写代码?》 https://tech.gtxlab.com/web-components.html 文章中提到了使用原生的 Web components 技术来开发项目. 开发中碰到了一些有挑战性的问题, 这篇文章就来记录一下在封装 DOM 操作时碰到的问题以及解决方式.
主流框架与数据绑
关于 DOM 操作就不得不提到一个 JS 库 --jQuery.jQuery 是成也 DOM(强大的选择器, 链式操作方式) 败也 DOM(数据绑定取代了 DOM 操作).
业务代码中嵌入大量的 DOM 操作会带来一些问题:
1. 作用域. DOM 操作没有作用域, 也就是说可以被任何代码操作, 这样导致变化不可追溯, 出现问题难以调试. 虽然 shadow DOM 具有一定的作用域, 但其它代码也是可以操作的.
2. 性能. 频繁或大量地操作 DOM 通常容易引起渲染性能问题, 原生操作 DOM 的方式优化起来需要一定的经验和技巧, 所以容易导致不同水平的开发者写出性能不同的代码.
3. 耦合度. JavaScript 逻辑和 DOM 操作混合的代码耦合性很高, 可读性低且难以测试.
所以封装 DOM 操作是必要的, 借鉴现有的主流视图框架思想, 可以采用数据绑定.
即建立一个数据模型, 通过修改数据对象属性来操作视图.
数据绑定的实现形式主要有 3 种:
脏值检测
脏值检测的实现原理是建立一个待检测队列, 在解析视图模板的时候, 将需要进行绑定的数据模型属性放入队列中. 代表框架: AngularJS.
在需要检测的时候遍历队列, 当属性发生变化时修改视图.
那么什么时候进行检测呢?
大致可分为两类
同步操作, 比如组件实例化的时候.
异步操作, 包括 Ajax 请求, 事件监听, setTimeout,setInterval 等.
这种方式缺陷很明显
需要对所有的可能引起数据变化的操作进行封装, 而且在编写业务代码的时候必须使用封装后的函数.
每次检测会遍历整个队列, 随着绑定属性增多, 性能会受到影响.
状态提交
数据模型修改时 (后), 调用函数来触发视图修改. 代表框架: React.
这种方式在进行批量操作的时候非常有优势, 这就和 SQL 数据库中使用事务来提交批量操作有些类似.
缺陷也比较明显, 就是每次修改数据都要进行提交, 代码写起来略嫌麻烦.
数据劫持
数据劫持就监听数据模型属性的变动, 然后触发对应的视图修改. 代表框架: Vue.
可以通过 Object.defineProperty 或者在不考虑兼容的情况下使用 Proxy .
但是在处理数组数据的时候有一些问题: 调用数组函数如 push , pop 等不会触发属性监听事件.
所以需要一些 hack 手段将这些函数进行封装.
实现数据绑定
选择
个人的编程习惯比较偏向于 "onDemand", 在编写代码的时候的体现为按需编写和调用代码, 在编译后的代码中喜欢按需加载代码.
既然如此, AngularJS 那种监听属性全部遍历的粗放做法肯定不是我的首选.
然后手动提交更新的方式一来会增加代码来进行提交操作, 另一方面也容易忘记提交导致视图不更新产生 bug, 所以最后的选择只剩下数据劫持了.
思路
如果按照 Vue 的实现过程, 需要解析视图模板, 然后建立 vdom 树, 同时对于需要绑定的数据进行监听, 然后通过操作 vdom 树来更新视图.
鉴于项目本身并不复杂, 而且也没有必要完全照搬其实现思路, 所以精简一下实现思路:
"解析" 视图模板.
对需要绑定的数据进行监听.
在监听函数中执行对应的 DOM 操作.
"解析" 模板
一般来说 "解析" 这种操作是会将原有的代码或数据进行转化, 比如 "词法解析" 就会把源码转化成一个一个的 token.
而这里 "解析" 模板的目的只是为了识别字符串中的需要数据绑定的语法 (我们暂且称之 "指令"). 所以可以在实例化之后直接使用选择器来进行操作.
比如要进行文本属性的绑定, 使用了 x-bind 指令, 那么我们可以直接在 shadowDOM 中进行查找
this.shadowRoot.querySelectorAll('[x-bind]')
找到这些 DOM 元素之后, 可以通过 getAttribute('x-bind') 来获取需要绑定的属性.
数据监听
在建立数据监听之前我们需要建立一个数据模型, 用来和视图建立映射关系, 即当我们修改这个数据模型的时候能同步到视图上.
假设我们的数据模型变量名为 state . 然后通过 Object.defineProperty(this.state, 'xxx', ...) 对 state 变量的指定属性进行监听.
这时候需要注意的是, 一个属性可能和多个视图元素进行绑定, 但是我们监听数据属性只能编写一次, 所以需要对监听属性建立一个队列, 当数据模型数据发生变化时, 遍历队列中的执行函数并调用.
操作 DOM
在执行函数中我们传入其绑定的 DOM, 然后执行函数根据各个指令的功能来操作 DOM 了.
比如 x-bind 指令的执行逻辑会是这样:
this.textContent = undefined === value ? '' : value;
当然到这一步还只能算完成了一半, 因为只实现了 数据 ==> 视图 的操作, 视图 ==> 数据 还没有完成. 因此我们需要进行事件绑定.
事件绑定是不是也可以用指令的方式呢? 比如绑定单击事件:
<button x-click="click">click me</button>
这样能满足一部分业务场景, 但是更多的时候我们不仅要触发事件, 而且还要 传入参数 . 而被传入的参数有可能是变量名, 也有可能是常量. 比如:
<button x-click="click(name, true)">click me</button>
name 为数据模型上的属性名, 而 true 为一个布尔值常量. 所以需要对事件绑定进行简单的语法解析, 并在调用对应函数的时候传入正确的参数.
优化
数据绑定
基于上面的实现, 还可以将表单元素的事件绑定和数据绑定封装一下, 实现双向数据绑定, 这样能进一步减少业务代码.
假定这个指令的名称为 x-model . 那么在 "解析" 模板的时候要编写一个执行函数来同步 DOM 的 value 值和模型数据属性. 同时建立事件监听来将数据模型属性同步到 DOM 中.
变化检测
因为数据监听是在数据被赋值的时候就会触发, 为了减少更新 DOM, 可以在调用 DOM 更新的执行函数时进行判断: 只有属性发生变化时才触发 DOM 更新.
来源: http://www.tuicool.com/articles/7FrAby2