作者: 刀哥 (朱建)
前言: mvvm 模式即 model-view-viewmodel 模式简称, 单项 / 双向数据绑定的实现, 让前端开发者们从繁杂的 dom 事件中解脱出来, 很方便的处理数据和 ui 之间的联动. 本文将从 vue 的双向数据绑定入手, 剖析 mvvm 库设计的核心代码与思路.
1, 需求整理与分析
需求:
数据一旦改变则更新数据对应的 ui
ui 改变则触发事件改变 ui 对应的数据
分析:
通过 dom 节点的指令获取刷新函数, 用来刷新指定的 ui.
实现一个桥接的方法, 让刷新函数和需要的数据关联起来.
监听数据变化, 数据改变后通过桥接方法调用刷新函数.
ui 改变触发对应的 dom 事件在改变特定的数据.
2, 实现思路
实现 observer, 重新定义 data, 为 data 上每个属性增加 setter,getter 以监听数据的变化.
实现 compile, 扫描模版 template, 提取每个 dom 节点中的指令信息.
实现 directive, 通过指令信息是实例化对应的 directive 实例, 不同类型的 directive 拥有不同的刷新函数 update.
实现 watcher, 让 observer 的属性监听函数与 directive 的 update 函数做一一对应, 以实现数据变化后更新视图.
3, 模块划分
MVVM 目前划分为 observer,compile,directive,watcher 四个模块.
4, 数据监听模块 observer
通过 es5 规范中的 object.defineProperty 方式实现对数据的监听.
5, 实现思路
递归遍历 data, 将 data 下面所有属性都加上 set,get 方法, 以实现对所有属性的拦截.
注意: 对象可能含有数组属性, 数组的内置有 push,pop,splice 等方法改变内部数据.
此时做法是改变数组的原型链, 在原型链中增加一层自定义的 push,pop,splice 方法做拦截, 这些方法里面加上我们自己的回调函数, 然后在调用原生的 push,pop,splice 等方法.
- export function defineProperty(obj, prop, val) {
- if (prop == '__observe__') {
- return;
- }
- val = val || obj[prop];
- var dep = new Dep();
- obj.__observe__ = dep;
- var childDep = addObserve(val);
- Object.defineProperty(obj, prop, {
- get: function() {
- var target = Dep.target;
- if (target) {
- dep.addSub(target);
- if (childDep) {
- childDep.addSub(target);
- }
- }
- return val;
- },
- set: function(newVal) {
- if(newVal!=val){
- val = newVal;
- dep.notify();
- }
- }
- });
- }
6, 编译模块 compiler
实现思路:
将模版 template 上的 dom 遍历一遍, 将其存入文档碎片 frag
遍历 frag, 通过 attributes 获取节点的属性信息, 在通过正则表达式过滤属性信息, 进而拿到元素节点和文档节点的指令信息
- var complieTemplate = function (nodes, model) {
- if ((nodes.nodeType == 1 || nodes.nodeType == 11) && !isScript(nodes)) {
- paserNode(model, nodes);
- if (nodes.hasChildNodes()) {
- nodes.childNodes.forEach(node=> {
- complieTemplate(node, model);
- })
- }
- }
- };
7, 指令模块 directive
指令信息如: v-text,v-for,v-model 等.
每种指令信息需要的初始化动作以及指令的刷新函数 update 都可能不一样, 所以我们把它抽象出来单独做一个模块. 当然也有公用的如公共属性, 统一的 watcher 实例化, unbind.
update 函数则具体定义所属指令如何渲染 ui, 如简单的 vtext 指令的 update 函数如下:
- vt.update = function (textContent) {
- this.el.textContent = textContent;
- };
9, 结构图
9, 数据订阅模块 watcher
watcher 的功能是让 directive 和 observer 模块关联起来. 初始化的时候做两件事:
将 directive 模块的 update 函数当参数传入, 并将其存入自身 update 属性中.
调用 getValue, 从而获取对象 data 的特定属性值, 进而触发一次之前在 observer 定义的属性函数的 getter 方法.
由于在 defineProperty 函数中定义的 dep 变量在 setter 和 getter 函数里有引用, 使 dep 变量处于闭包状态没有释放, 此时在 getter 方法中通过判断 Depend.target 的存在, 来获取订阅者 watcher, 通过发布者 dep 储存起来. 数据的每个属性都有一个唯一的的 dep 变量, 记录着所有订阅者 watcher 的信息, 一旦属性有变化, 调用 setter 函数的时候触发 dep.notify(), 通知所有已订阅的 watcher, 进而执行所有与该属性关联的刷新函数, 最后更新指定的 ui.
watcher 初始化部分代码:
- Depend.target = this;
- this.value = this.getValue();
- Depend.target = null;
observer.JS 属性定义代码:
- export function defineProperty(obj, prop, val) {
- if (prop == '__observe__') {
- return;
- }
- val = val || obj[prop];
- var dep = new Dep();
- obj.__observe__ = dep;
- var childDep = addObserve(val);
- Object.defineProperty(obj, prop, {
- get: function() {
- var target = Dep.target;
- if (target) {
- dep.addSub(target);
- if (childDep) {
- childDep.addSub(target);
- }
- }
- return val;
- },
- set: function(newVal) {
- if(newVal!=val){
- val = newVal;
- dep.notify();
- }
- }
- });
- }
10, 流程图
11, 总结
文基本对 mvvm 库的需求整理, 拆分, 以及对拆分模块的逐一实现来达到整体双向绑定功能的实现, 当然目前市场上的 mvvm 库功能绝不止于此, 本文只是略举个人认为的核心代码. 如果思路和实现上的问题, 也请各位斧正, 谢谢阅读!
原代码: https://github.com/laughing-pic-zhu/mvvm
想要深入了解的同学可以访问数澜社区, 和大家一起讨论学习~
来源: https://yq.aliyun.com/articles/717042