接上文: 一套代码小程序 & web&Native 运行的探索 03
对应 Git 代码地址请见:
参考:
之前我们完成了简陋的从模板到虚拟 DOM 从虚拟 DOM 到 HTML 的代码, 我们这里图简单没有对属性和样式做特殊处理, 还是按照一般的模板方式进行的解析, 后续看看这块怎么处理吧, 今天我们的任务是完成 setData 时候同步更新我们的 HTML 的操作, 这里首先我们来看看一般的 MVVM 中数据变化更新是怎么完成的, 在这个基础上进行后续的代码可能各位看得更清晰.
一般的 MVVM 双向绑定
一般来说, 我们数据变化的时候都是一个发布订阅模式, 我们调用 setData 的时候会执行类似这样的代码:
- function setData(data) {
- // 做下数据变更
- //......
- // 会通知对应数据对象数据发生变化了, 这个数据对应的所有 dom 节点都会发生改变
- this.notifyAll();
- }
而在 vue 中我们是直接做这种操作, dom 就发生了变化:
this.name = '叶小钗';
这个是因为, 他使用了访问器属性:
- var obj = { };
- // 为 obj 定义一个名为 name 的访问器属性
- Object.defineProperty(obj, "name", {
- get: function () {
- console.log('get', arguments);
- },
- set: function (val) {
- console.log('set', arguments);
- }
- })
- obj.name = '叶小钗'
- console.log(obj, obj.name)
- /*
- set Arguments ["叶小钗", callee: ƒ, Symbol(Symbol.iterator): ƒ]
- get Arguments [callee: ƒ, Symbol(Symbol.iterator): ƒ]
- */
如果这里写这样的代码:
- <div id="a">
- </div>
- <input type="text" id="b">
- <script type="text/JavaScript">
- function setData(data) {
- // 做下数据变更
- //......
- // 会通知对应数据对象数据发生变化了, 这个数据对应的所有 dom 节点都会发生改变
- this.notifyAll();
- }
- function getElById(id) {
- return document.getElementById(id);
- }
- var obj = {};
- // 为 obj 定义一个名为 name 的访问器属性
- Object.defineProperty(obj, "name", {
- set: function (val) {
- getElById('a').innerHTML = val;
- getElById('b').value = val;
- }
- })
- getElById('b').addEventListener('input', function(e) {
- obj.name = e.target.value;
- });
- </script>
文本框中的字符串和 div 的便会同步更新, 这个便是最简化的双向绑定代码了, 真实情况下我们的代码可能是这样的:
1 将 data 中的数据 (这里是 name 属性), 与两个 dom 对象进行映射一个是 input 另一个是空字符串 (可以想象为 span)
2 当 data 中 name 字段发生变化, 或者 view 中导致 name 发生变化 (控制台或者事件监听)
3 data 数据变化时, 文本节点同步发生变化 (不管是控制台 JS 脚本导致还是输入变化)
PS: 我们这里与小程序保持一致, 真正做更新时候采用 setData 方法进行
这里便开始引入编译过程:
- <div id="App">
- <input type="text" v-model="name">
- {{name}}
- </div>
- <script type="text/JavaScript">
- function getElById(id) {
- return document.getElementById(id);
- }
- // 这块代码仅做功能说明, 不用当真
- function compile(node, vm) {
- let reg = /\{\{(.*)\}\}/;
- // 节点类型
- if(node.nodeType === 1) {
- let attrs = node.attributes;
- // 解析属性
- for(let i = 0, l = attrs.length; i <l; i++) {
- if(attrs[i].nodeName === 'v-model') {
- let name = attrs[i].nodeValue;
- node.value = vm.data[name] || '';
- // 此处不做太多判断, 直接绑定事件
- node.addEventListener('input', function (e) {
- // 赋值操作
- let newObj = {};
- newObj[name] = e.target.value;
- vm.setData(newObj);
- });
- break;
- }
- }
- } else if(node.nodeType === 3) {
- if(reg.test(node.nodeValue)) {
- let name = RegExp.$1; // 获取匹配到的 name
- name = name.trim();
- node.nodeValue = vm.data[name] || '';
- }
- }
- }
- // 获取节点
- function nodeToFragment(node, vm) {
- let flag = document.createDocumentFragment();
- let child;
- while (child = node.firstChild) {
- compile(child, vm);
- flag.appendChild(child);
- }
- return flag;
- }
- function MVVM(options) {
- this.data = options.data;
- let el = getElById(options.el);
- this.$dom = nodeToFragment(el, this)
- this.$el = el.appendChild(this.$dom);
- // this.$bindEvent();
- }
- MVVM.prototype.setData = function (data) {
- for(let k in data) {
- this.data[k] = data[k];
- }
- // 执行更新逻辑
- }
- let mvvm = new MVVM({
- el: 'App',
- data: {
- name: '叶小钗'
- }
- })
- </script>
这个时候 input 输入更改, 对应属性也会发生变化, 但是我们属性发生变化并没有引起所有的 dom 发生变化, 这个是不对的, 这里我们便需要劫持所有的数据对象, 这里引入发布订阅模式:
- <div id="App">
- <input type="text" v-model="name">
- {{name}}
- </div>
- <script type="text/JavaScript">
- function getElById(id) {
- return document.getElementById(id);
- }
- // 主体对象, 存储所有的订阅者
- function Dep () {
- this.subs = [];
- }
- // 通知所有订阅者数据变化
- Dep.prototype.notify = function () {
- for(let i = 0, l = this.subs.length; i <l; i++) {
- this.subs[i].update();
- }
- }
- // 添加订阅者
- Dep.prototype.addSub = function (sub) {
- this.subs.push(sub);
- }
- let globalDataDep = new Dep();
- // 观察者, 框架会接触 data 的每一个与 node 相关的属性,
- // 如果 data 没有与任何节点产生关联, 则不予理睬
- // 实际的订阅者对象
- // 注意, 只要一个数据对象对应了一个 node 对象就会生成一个订阅者, 所以真实通知的时候应该需要做到通知到对应数据的 dom, 这里不予关注
- function Watcher(vm, node, name) {
- this.name = name;
- this.node = node;
- this.vm = vm;
- if(node.nodeType === 1) {
- this.node.value = this.vm.data[name];
- } else if(node.nodeType === 3) {
- this.node.nodeValue = this.vm.data[name] || '';
- }
- globalDataDep.addSub(this);
- }
- Watcher.prototype.update = function () {
- if(this.node.nodeType === 1) {
- this.node.value = this.vm.data[this.name ];
- } else if(this.node.nodeType === 3) {
- this.node.nodeValue = this.vm.data[this.name ] || '';
- }
- }
- // 这块代码仅做功能说明, 不用当真
- function compile(node, vm) {
- let reg = /\{\{(.*)\}\}/;
- // 节点类型
- if(node.nodeType === 1) {
- let attrs = node.attributes;
- // 解析属性
- for(let i = 0, l = attrs.length; i < l; i++) {
- if(attrs[i].nodeName === 'v-model') {
- let name = attrs[i].nodeValue;
- if(node.value === vm.data[name]) break;
- // node.value = vm.data[name] || '';
- new Watcher(vm, node, name)
- // 此处不做太多判断, 直接绑定事件
- node.addEventListener('input', function (e) {
- // 赋值操作
- let newObj = {};
- newObj[name] = e.target.value;
- vm.setData(newObj, true);
- });
- break;
- }
- }
- } else if(node.nodeType === 3) {
- if(reg.test(node.nodeValue)) {
- let name = RegExp.$1; // 获取匹配到的 name
- name = name.trim();
- // node.nodeValue = vm.data[name] || '';
- new Watcher(vm, node, name)
- }
- }
- }
- // 获取节点
- function nodeToFragment(node, vm) {
- let flag = document.createDocumentFragment();
- let child;
- while (child = node.firstChild) {
- compile(child, vm);
- flag.appendChild(child);
- }
- return flag;
- }
- function MVVM(options) {
- this.data = options.data;
- let el = getElById(options.el);
- this.$dom = nodeToFragment(el, this)
- this.$el = el.appendChild(this.$dom);
- // this.$bindEvent();
- }
- MVVM.prototype.setData = function (data, noNotify) {
- for(let k in data) {
- this.data[k] = data[k];
- }
- // 执行更新逻辑
- // if(noNotify) return;
- globalDataDep.notify();
- }
- let mvvm = new MVVM({
- el: 'App',
- data: {
- name: '叶小钗'
- }
- })
- </script>
mvvm.setData({name: 'hello world'})
这段短短的代码, 基本将数据变化如何引起的 dom 变化说的比较清楚了, 几个关键流程是:
1 设置全局的发布订阅模式
2 在模板编译的时候, 一旦碰到数据节点与 dom 节点发生关系时, 则新增一个订阅者, 我们这里的发布者没有状态概念, 真实的情况应该是以 data 为一个集合的分组, 这样可以做到安 data 进行更新
3 数据变化时候执行 setData, 底层调用发布者除非对应订阅者更新数据, 这里只是简单的属性 & 文本更新, 真实情况会复杂的多, 我们这里为保持小程序逻辑, 没有实现访问器属性部分代码
有了以上代码的理解, 我们再回到我们昨天的代码继续完成这个流程便会清晰的多
完成 setData 代码
根据之前的学习, 我们知道添加订阅者一定是发生在编译时期, data 跟 node 产生关联的时候, 但是我们这里需要发布订阅者相关代码, 由于我们这里的诉求还要简单一些并不想去考虑属性样式这些特殊性, 所以我们对 TextParser 做点改造, 先实现之:
注意这里的核心是, 每次数据改变的时候都会触发观察者的 update, 这样会引起重新生成虚拟树 (vnode), 但是到底要不要重新渲染, 怎么渲染后面会直接由 snabbdom 接手, 我们只是将这种关系完成, 代码比较分散大家可以到 GitHub 上面看:
然后今天的学习到此为止, 我们明天开始处理事件部分的代码, 感觉代码逐渐有些慢了, 等组件部分完成后我们画点流程图重新梳理下逻辑
来源: https://www.cnblogs.com/yexiaochai/p/9713519.html