前面的啰嗦话, 写一点吧, 或许就有点用呢
使用过 vue 的小伙伴都会感觉, 哇, 这个框架对开发者这么友好, 简直都要笑出声了.
确实, 使用过 vue 的框架做开发的人都会感觉到, 以前写一大堆操作 dom,bom 的东西, 现在用不着了, 对开发者来说更容易去注重对操作逻辑的思考和实现, 省了不少事儿呢!!!
我是直接从原生 JS,jq 的开发用过度到使用 vue, 对这个框架也是喜爱有加, 闲来无事, 去看了看它的一些实现原理.
下面来介绍一下 vue 的一个非常 "牛逼" 的功能, 数据双向绑定, 也就是我们在项目里用到的 v-model 指令.
v-model 在 vue 官方文档上是介绍在 "表单输入绑定" 那一节.
对于表单, 大家肯定用得都已经超级熟练了, 对于 <input>,<textarea> 和 <select> 标签在项目里面使用都已经没话说了
官方提到的 v-model 是一个语法糖, 为什么这么说呢? 下面看个例子:
- <div id="test1">
- <input v-model="input">
- <span>input: {{ input }}</span>
- </div>
如上, 是一个简单的使用 v-model 的双向绑定, 我们在改变 input 这个变量的值, 即在输入框中去写内容的时候, 在 span 标签内的插值 (mustache) 会同步更新我们刚刚输入的值
其实上面的也可以这样写:
- <div id="test1">
- <input v-on:input="input = $event.target.value" v-bind:value='input'>
- <span>input: {{ input }}</span>
- </div>
好了, 前面啰里啰嗦半天, 现在进入正题
想对比 react 和 angular 的双向绑定实现, 我也不清楚, 哈哈哈, 直接说 vue 吧, 不扯了
Reactivity 响应式系统
拿尤雨溪大佬做 vue 测试的的那个例子来说吧 (购物车的例子)
- <div id='app'>
- <div>
- <span> 价格:</span>
- <input v-model.number="price">
- </div>
- <div>
- <span> 数量:</span>
- <input v-model.number="quantity">
- </div>
- <p> 价格:{{ price }}</p>
- <p> 数量:{{ quantity }}</p>
- <p> 总计:{{ total }}</p>
- </div>
的
- data() {
- return {
- price: 5,
- quantity: 3
- }
- },
- computed: {
- total() {
- return this.price * this.quantity;
- }
- }
当我们在使用输入框的值的时候, 下面的 total 会更新, 我们对应输入值的变量也会更新
哇, 好神奇, 为什么呢, 这不是 JavaScript 编程常规的工作方式!!!
因为我们用原生 JS 写的时候是这样的:
- let price = 5;
- let quantity = 3;
- let total = price * quantity; // 等于 15 吧
- price = 10; // 改变价格;
- console.log(total); // bingo, 打印的还是 15
我们需要在找一种办法, 把要运行计算的 total 放到别的时候去运行, 当我们的价格, 数量变化的时候执行
- let price = 5;
- let quantity = 3;
- let total = 0;
- let storage = []; // 储存将要计算的操作, 等到变量变化的时候去执行
- let target= () => { total = price * quantity;}
- function record () {
- storage.push(target);
- }
- function replay() {
- storage.forEach(run => run());
- }
- record();
- target();
- price = 10;
- console.log(total); // 依然是 15
- replay();
- console.log(total); // 执行结果是 30
目的达到, 但是这样肯定不是 vue 用来扩展使用的方式, 我们用 ES6 的 class 类来做一个可维护的扩展, 实现一个标准的观察者模式的依赖类
- class Depend {
- constructor () {
- this.subscribers = [];
- }
- depend() {
- if(target && this,this.subscribers.includes(target)) {
- this.subscribers.push(target);
- }
- }
- notify() {
- this.subscribers.forEach(sub => sub());
- }
- }
- // 来执行上面写的 class
- const dep = new Depend();
- let price = 5;
- let quantity = 3;
- let total = 0;
- let target = () => { total = price * quantity };
- dep.depend();
- target();
- console.log(total); // total 是 15
- price = 10;
- console.log(total); // 因为没有执行 target, 依旧是 15
- dep.notify();
- console.log(total); // 执行了存入的 target,total 为 30
为了给每一个变量都设置一个 Depend 类. 并且很好地控制监视更新的匿名函数的行为, 我们把上面的代码做一些调整:
- let target = () => {
- total = price * quantity
- };
- dep.depend();
- target();
修改为:
watcher(() => { total = price * quantity });
然后我们在 watcher 函数里面来做刚刚上面的 result 的设置和执行的功能
- function watcher(fn) {
- target = fn;
- dep.depend();
- target();
- target = null; // 重置一下, 等待储存和执行下一次
- }
这儿就是官方文档提到的订阅者模式: 在每次 watcher 函数执行的时候, 把参数 fn 设置成为我们全局目标属性, 调用 dep.depend() 将目标添加为订阅者, 调用然后重置
然后再继续
我们的目标是把每一个变量都设置一个 Depend 类, 但是这儿有个问题:
先存一下数据:
let data = { price: 5, quantity: 3}
假设我们每个属性都有自己的内部 Depend 类
当我们运行代码时:
watcher(() => { total = data.price * data.quantity})
由于访问了 data.price 值, 希望 price 属性的 Depend 类将我们的匿名函数 (存储在目标中) 推送到其订阅者数组 (通过调用 dep.depend()). 由于访问了 data.quantity, 还希望 quantity 属性 Depend 类将此匿名函数 (存储在目标中) 推送到其订阅者数组中.
如果有另一个匿名函数, 只访问 data.price, 希望只推送到价格属性 Depend 类.
什么时候想要在价格订阅者上调用 dep.notify()? 我希望在设定价格时调用它们. 为此, 我们需要一些方法来挂钩数据属性 (价格或数量), 所以当它被访问时我们可以将目标保存到我们的订阅者数组中, 当它被更改时, 运行存储在我们的订阅者数组中的函数. let's go
Object.defineProperty 来解决这个问题
Object.defineProperty 函数是简单的 ES5 JavaScript. 它允许我们为属性定义 getter 和 setter 函数. 继续啃
- let data = { price: 5, quantity: 3};
- let value = data.price
- Object.defineProperty(data, 'price', {
- getter() {
- console.log(` 获取 price 的值: ${value}`);
- return value;
- },
- setter(newValue) {
- console.log(` 更改 price 的值': ${newValue}`);
- value = newValue;
- }
- })
- total = data.price * data.quantity;
- data.price = 10; // 更改 price 的值
上面通过 defineProperty 方法给 price 设置了获取和修改值的操作
如何给 data 对象所有的都加上这个 defineProperty 方法去设置值
大家还记得 Object.keys 这个方法吗? 返回对象键的数组, 咱们把上面的代码改造一下
- let data = { price: 5, quantity: 3 };
- Object.keys(data).forEach(key => {
- let value = data[key];
- Object.defineProperty(data, key, {
- getter() {
- console.log(` 获取 ${key} 的值: ${value}`);
- return value;
- },
- setter(newValue) {
- console.log(` 更改 ${key} 值': ${newValue}`);
- value = newValue;
- }
- })
- })
- total = data.price * data.quantity;
- data.price = 10; // 更改 price 的值
接着上面的东西, 在每次运行完获取 key 的值, 我们希望 key 能记住这个匿名函数 (target), 这样有 key 的值变化的时候, 它将触发这个函数来重新计算, 大致思路是这样的:
getter 函数执行的时候, 记住这个匿名函数, 当值在发生变化的时候再次运行它
setter 函数执行的时候, 运行保存的匿名函数, 把当前的值存起来
用上面定义的 Depend 类来说就是:
getter 执行, 调用 dep.depend() 来保存当前的 target
setter 执行, 在价格上调用 dep.notify(), 重新运行所有的 target
来来来, 把上面的东西结合到一起来
- let data = { price: 5, quantity: 3 };
- let total = 0;
- let target = null;
- class Depend {
- constructor() {
- this.subscribers = [];
- }
- depend() {
- if (target && this, this.subscribers.includes(target)) {
- this.subscribers.push(target);
- }
- }
- notify() {
- this.subscribers.forEach(sub => sub());
- }
- }
- Object.keys(data).forEach(key => {
- let value = data[key];
- const dep = new Depend();
- Object.defineProperty(data, key, {
- getter() {
- dep.depend();
- return value;
- },
- setter(newValue) {
- value = newValue;
- dep.notify();
- }
- })
- });
- function watcher(fn) {
- target = fn;
- target();
- target = null;
- }
- watcher(() => {
- total = data.price * data.quantity;
- });
至此, vue 的数据双向绑定已经实现, 当我们去改变 price 和 quantity 的值, total 会实时更改
然后咱们来看看 vue 的文档里面提到的这个插图:
是不是感觉这个图很熟悉了? 对比咱们上面研究的流程, 这个图的 data 和 watcher 就很清晰了, 大致思路如此, 可能 vue 的内部实现和封装远比我这个研究流程内容大得多, 复杂得多, 不过有了这样的一个流程思路, 再去看 vue 双向绑定源码估计也能看懂个十之八九了.
听说 vue3.0 准备把这个数据劫持的操作用 ES6 提供的 proxy 来做, 效率更高, 期待!!!!
参考和学习原文 (可能需要翻墙, 毕竟是外站啊)
来源: https://segmentfault.com/a/1190000017107719