嘿, Goodgirl and GoodBoy, 点进来了就看完点个赞再 go.
vue 这个框架就不简单介绍了, 它最大的特性就是数据的双向绑定以及虚拟 dom. 核心就是用数据来驱动视图层的改变. 先看一段代码.
一, 示例
var vm = new Vue({data: { obj: { a: 1} }, created: function () { console.log(this.obj); } });
二, 实现原理
vue 数据双向绑定是通过数据劫持结合发布者 - 订阅者模式的方式来实现的.
1)数据劫持, vue 是通过 Object.defineProperty()来实现数据劫持, 其中会有 getter()和 setter 方法; 当读取属性值时, 就会触发 getter()方法, 在 view 中如果数据发生了变化, 就会通
过 Object.definePr
operty( )对属性设置一个 setter 函数, 当数据改变了就会来触发这个函数;
三, 实现步骤
1, 实现 Observer
ok, 思路已经整理完毕, 也已经比较明确相关逻辑和模块功能了, let's do it
我们知道可以利用 Obeject.defineProperty()来监听属性变动
那么将需要 observe 的数据对象进行递归遍历, 包括子属性对象的属性, 都加上 setter 和 getter
这样的话, 给这个对象的某个值赋值, 就会触发 setter, 那么就能监听到了数据变化.. 相关代码可以是这样:
- var data = {name: 'kindeng'};
- observe(data);
- data.name = 'dmq'; // 哈哈哈, 监听到值变化了 kindeng --> dmq
- function observe(data) {
- if (!data || typeof data !== 'object') {
- return;
- }
- // 取出所有属性遍历
- Object.keys(data).forEach(function(key) {
- defineReactive(data, key, data[key]);
- });
- };
- function defineReactive(data, key, val) {
- observe(val); // 监听子属性
- Object.defineProperty(data, key, {
- enumerable: true, // 可枚举
- configurable: false, // 不能再 define
- get: function() {
- return val;
- },
- set: function(newVal) {
- console.log('哈哈哈, 监听到值变化了', val, '-->', newVal);
- val = newVal;
- }
- });
- }
这样我们已经可以监听每个数据的变化了, 那么监听到变化之后就是怎么通知订阅者了, 所以接下来我们需要实现一个消息订阅器, 很简单, 维护一个数组, 用来收集订阅者, 数据变动触发 notify, 再调用订阅者的 update 方法, 代码改善之后是这样:
- // ... 省略
- function defineReactive(data, key, val) {
- var dep = new Dep();
- observe(val); // 监听子属性
- Object.defineProperty(data, key, {
- // ... 省略
- set: function(newVal) {
- if (val === newVal) return;
- console.log('哈哈哈, 监听到值变化了', val, '-->', newVal);
- val = newVal;
- dep.notify(); // 通知所有订阅者
- }
- });
- }
- function Dep() {
- this.subs = [];
- }
- Dep.prototype = {
- addSub: function(sub) {
- this.subs.push(sub);
- },
- notify: function() {
- this.subs.forEach(function(sub) {
- sub.update();
- });
- }
- };
那么问题来了, 谁是订阅者? 怎么往订阅器添加订阅者?
没错, 上面的思路整理中我们已经明确订阅者应该是 Watcher, 而且 var dep = new Dep(); 是在 defineReactive 方法内部定义的, 所以想通过 dep 添加订阅者, 就必须要在闭包内操作, 所以我们可以在 getter 里面动手脚:
- // Observer.js
- // ... 省略
- Object.defineProperty(data, key, {
- get: function() {
- // 由于需要在闭包内添加 watcher, 所以通过 Dep 定义一个全局 target 属性, 暂存 watcher, 添加完移除
- Dep.target && dep.addDep(Dep.target);
- return val;
- }
- // ... 省略
- });
- // Watcher.js
- Watcher.prototype = {
- get: function(key) {
- Dep.target = this;
- this.value = data[key]; // 这里会触发属性的 getter, 从而添加订阅者
- Dep.target = null;
- }
- }
这里已经实现了一个 Observer 了, 已经具备了监听数据和数据变化通知订阅者的功能, 完整代码. 那么接下来就是实现 Compile 了
2, 实现 Compile
compile 主要做的事情是解析模板指令, 将模板中的变量替换成数据, 然后初始化渲染页面视图, 并将每个指令对应的节点绑定更新函数, 添加监听数据的订阅者, 一旦数据有变动, 收到通知, 更新视图, 如图所示:
图片描述
因为遍历解析的过程有多次操作 dom 节点, 为提高性能和效率, 会先将跟节点 el 转换成文档碎片 fragment 进行解析编译操作, 解析完成, 再将 fragment 添加回原来的真实 dom 节点中
- function Compile(el) {
- this.$el = this.isElementNode(el) ? el : document.querySelector(el);
- if (this.$el) {
- this.$fragment = this.node2Fragment(this.$el);
- this.init();
- this.$el.appendChild(this.$fragment);
- }
- }
- Compile.prototype = {
- init: function() { this.compileElement(this.$fragment); },
- node2Fragment: function(el) {
- var fragment = document.createDocumentFragment(), child;
- // 将原生节点拷贝到 fragment
- while (child = el.firstChild) {
- fragment.appendChild(child);
- }
- return fragment;
- }
- };
compileElement 方法将遍历所有节点及其子节点, 进行扫描解析编译, 调用对应的指令渲染函数进行数据渲染, 并调用对应的指令更新函数进行绑定, 详看代码及注释说明:
- Compile.prototype = {
- // ... 省略
- compileElement: function(el) {
- var childNodes = el.childNodes, me = this;
- [].slice.call(childNodes).forEach(function(node) {
- var text = node.textContent;
- var reg = /\{\{(.*)\}\}/; // 表达式文本
- // 按元素节点方式编译
- if (me.isElementNode(node)) {
- me.compile(node);
- } else if (me.isTextNode(node) && reg.test(text)) {
- me.compileText(node, RegExp.$1);
- }
- // 遍历编译子节点
- if (node.childNodes && node.childNodes.length) {
- me.compileElement(node);
- }
- });
- },
- compile: function(node) {
- var nodeAttrs = node.attributes, me = this;
- [].slice.call(nodeAttrs).forEach(function(attr) {
- // 规定: 指令以 v-xxx 命名
- // 如 <span v-text="content"></span> 中指令为 v-text
- var attrName = attr.name; // v-text
- if (me.isDirective(attrName)) {
- var exp = attr.value; // content
- var dir = attrName.substring(2); // text
- if (me.isEventDirective(dir)) {
- // 事件指令, 如 v-on:click
- compileUtil.eventHandler(node, me.$vm, exp, dir);
- } else {
- // 普通指令
- compileUtil[dir] && compileUtil[dir](node, me.$vm, exp);
- }
- }
- });
- }
- };
- // 指令处理集合
- var compileUtil = {
- text: function(node, vm, exp) {
- this.bind(node, vm, exp, 'text');
- },
- // ... 省略
- bind: function(node, vm, exp, dir) {
- var updaterFn = updater[dir + 'Updater'];
- // 第一次初始化视图
- updaterFn && updaterFn(node, vm[exp]);
- // 实例化订阅者, 此操作会在对应的属性消息订阅器中添加了该订阅者 watcher
- new Watcher(vm, exp, function(value, oldValue) {
- // 一旦属性值有变化, 会收到通知执行此更新函数, 更新视图
- updaterFn && updaterFn(node, value, oldValue);
- });
- }
- };
- // 更新函数
- var updater = {
- textUpdater: function(node, value) {
- node.textContent = typeof value == 'undefined' ? '' : value;
- }
- // ... 省略
- };
这里通过递归遍历保证了每个节点及子节点都会解析编译到, 包括了 {{}} 表达式声明的文本节点. 指令的声明规定是通过特定前缀的节点属性来标记, 如 < span v-text="content" other-attr 中 v-text 便是指令, 而 other-attr 不是指令, 只是普通的属性.
监听数据, 绑定更新函数的处理是在 compileUtil.bind()这个方法中, 通过 new Watcher()添加回调来接收数据变化的通知
至此, 一个简单的 Compile 就完成了, 完整代码. 接下来要看看 Watcher 这个订阅者的具体实现了
3, 实现 Watcher
Watcher 订阅者作为 Observer 和 Compile 之间通信的桥梁, 主要做的事情是:
1, 在自身实例化时往属性订阅器 (dep) 里面添加自己
2, 自身必须有一个 update()方法
3, 待属性变动 dep.notice()通知时, 能调用自身的 update()方法, 并触发 Compile 中绑定的回调, 则功成身退.
如果有点乱, 可以回顾下前面的思路整理
- function Watcher(vm, exp, cb) {
- this.cb = cb;
- this.vm = vm;
- this.exp = exp;
- // 此处为了触发属性的 getter, 从而在 dep 添加自己, 结合 Observer 更易理解
- this.value = this.get();
- }
- Watcher.prototype = {
- update: function() {
- this.run(); // 属性值变化收到通知
- },
- run: function() {
- var value = this.get(); // 取到最新值
- var oldVal = this.value;
- if (value !== oldVal) {
- this.value = value;
- this.cb.call(this.vm, value, oldVal); // 执行 Compile 中绑定的回调, 更新视图
- }
- },
- get: function() {
- Dep.target = this; // 将当前订阅者指向自己
- var value = this.vm[exp]; // 触发 getter, 添加自己到属性订阅器中
- Dep.target = null; // 添加完毕, 重置
- return value;
- }
- };
- // 这里再次列出 Observer 和 Dep, 方便理解
- Object.defineProperty(data, key, {
- get: function() {
- // 由于需要在闭包内添加 watcher, 所以可以在 Dep 定义一个全局 target 属性, 暂存 watcher, 添加完移除
- Dep.target && dep.addDep(Dep.target);
- return val;
- }
- // ... 省略
- });
- Dep.prototype = {
- notify: function() {
- this.subs.forEach(function(sub) {
- sub.update(); // 调用订阅者的 update 方法, 通知变化
- });
- }
- };
实例化 Watcher 的时候, 调用 get()方法, 通过 Dep.target = watcherInstance 标记订阅者是当前 watcher 实例, 强行触发属性定义的 getter 方法, getter 方法执行的时候, 就会在属性的订阅器 dep 添加当前 watcher 实例, 从而在属性值有变化的时候, watcherInstance 就能收到更新通知.
四, 简单实现方法
- <body>
- <div id="app">
- <input type="text" id="txt">
- <p id="show-txt"></p>
- </div>
- <script>
- var obj = {}
- Object.defineProperty(obj, 'txt', {
- get: function () {
- return obj
- },
- set: function (newValue) {
- document.getElementById('txt').value = newValue
- document.getElementById('show-txt').innerhtml = newValue
- }
- })
- document.addEventListener('keyup', function (e) {
- obj.txt = e.target.value
- })
- </script>
- </body>
注, 可能有很多不正确的地方, 请多多指教. along
来源: https://www.cnblogs.com/alongup/p/9022180.html