导航


HTML

CSS

JavaScript

浏览器 & 网络

版本管理

框架

构建工具

TypeScript

性能优化

软实力

算法

UI、组件库

Node

业务技能

针对性攻坚

AI


watch 与 computed 区别

案例:考试

后端 server

安装 express 和 nodemon

npm init -y

npm i express -S

npm i nodemon -g

更改 package.json

"dev": "nodemon ./src/index.js"

创建 data

src/data/question.json

[
  {
    "id": 1,
    "question": "1 + 1 = ?",
    "items": ["2", "5", "4", "3"],
    "answer": 0
  },
  {
    "id": 2,
    "question": "1 + 2 = ?",
    "items": ["5", "2", "4", "3"],
    "answer": 3
  },
  {
    "id": 3,
    "question": "1 + 3 = ?",
    "items": ["2", "4", "5", "3"],
    "answer": 2
  },
  {
    "id": 4,
    "question": "1 + 4 = ?",
    "items": ["3", "2", "5", "4"],
    "answer": 2
  }
]

编写 app

const express = require('express');
const bodyParser = require('body-parser');
const { readFileSync } = require('fs');
const { resolve } = require('path');
const app = express();

let myResult = [];

// 具备POST请求能力
app.use(bodyParser.urlencoded({ extended: false }));
// extended:false 方法内部使用querystring模块处理请求参数的格式
// extended:true 方法内部使用第三方模块qs处理请求参数的格式
app.use(bodyParser.json());

app.all('*', function(req, res, next) {
  res.header('Access-Control-Allow-Origin', '*');
  res.header('Access-Control-Allow-Methods', 'GET,POST');
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
});

app.post('/getQuestion', function(req, res) {
  const order = req.body.order;
  const questionData = JSON.parse(readFileSync(resolve(__dirname, 'data/question.json'), 'utf8'));
  const questionResult = questionData[order];
  if (questionResult) {
    const { id, question, items } = questionResult;
    res.send({
      errorCode: 0,
      msg: 'OK',
      data: {
        id,
        question,
        items
      }
    });
  } else {
    res.send({
      errorCode: 1,
      msg: 'NO_DATA',
      data: myResult
    });

    myResult = [];
  }
});

app.post('/uploadAnswer', function(req, res) {
  const { order, myAnswer } = req.body;
  const questionData = JSON.parse(readFileSync(resolve(__dirname, 'data/question.json'), 'utf-8'));
  const { id, question, items, answer } = questionData[order];
  myResult.push({
    qid: id,
    question,
    myAnswer: items[myAnswer], // myAnswer 是下标
    rightAnswer: items[answer],
    isRight: myAnswer == answer,
  });

  res.send({
    errorCode: 0,
    msg: 'OK'
  });
  // res.send(myResult);
});

app.listen(8888, function() {
  console.log('Welcome to use Express on 8888');
});

运行

npm run dev

前端

main.js

/**
 * [
  * {
  *   id: 1,
  *     question: 'xxxx',
  *     items: [2,3,4,5],
  *     answer: 2
  * }
 * ]
 * 
 * data: {
 *  order: 0,
 *  questionData: 试题数据,
 *  myAnswer: items index,
 *  myResults: [
 *    {
 *      qid,
 *      question,
 *      myAnswer: items[index],
 *      rightAnswer: items[index],
 *      isRight: myAnswer == answer
 *     }
 *  ]
 * }
 * 
 * 视图:什么时候显示试题(切换)?什么时候显示答案集
 * myResults.length
 * 
 * 试题
 * 
 * 编号
 * 题目
 * 4个选项(点击)-> selectAnswer -> 取到index -> 赋值给order
 * 切换order -> 上传该题的order myAnswer -> 获取新order对应的题
 * 如果切换到最后一道 -> 返回myResults
 */

// import qs from 'qs';

const App = {
  data() {
    return {
      order: 0,
      questionData: {},
      myAnswer: -1,
      myResults: []
    }
  },
  template: `
    <div>
      <div v-if="myResults.length > 0">
        <h1>考试结果</h1>
        <ul>
          <li v-for="(item, index) of myResults"
            :key="item.qid">
            <h2>编号:{{ item.qid }}</h2>
            <p>题目:{{ item.question }}</p>
            <p>你的答案:{{ item.myAnswer }}</p>
            <p>正确答案:{{ item.rightAnswer }}</p>
            <p>正确:{{ isRightText(item.isRight) }}</p>
          </li>
        </ul>
      </div>
      <div v-else>
        <h1>编号{{ questionData.id }}</h1>
        <p>{{ questionData.question }}</p>
        <div>
          <button
            v-for="(item, index) of questionData.items"
            :key="item"
            @click="selectAnswer(index)"
          >{{ item }}</button>
        </div>
      </div>
    </div>
  `,
  mounted() {
    this.getQuestion(this.order);
  },
  computed: {
    isRightText() {
      return function(isRight) {
        return isRight ? '是' : '否';
      }
    }
  },
  watch: {
    order(newOrder, oldOrder) {
      this.uploadAnswer(oldOrder, this.myAnswer);
      this.getQuestion(newOrder);
    }
  },
  methods: {
    getQuestion(order) {
      axios.post(
        '<http://localhost:8888/getQuestion>',
        { order },
      ).then(res => {
        if (res.errorCode) {
          this.myResults = res.data;
          console.log(this.myResults);
          return;
        }

        this.questionData = res.data;
      });
    },
    uploadAnswer(order, myAnswer) {
      axios.post('<http://localhost:8888/uploadAnswer>', {
        order,
        myAnswer
      }).then(res => {
        console.log(res);
      });
    },
    selectAnswer(index) {
      this.myAnswer = index;
      this.order += 1;
    },
  }
};

const vm = Vue.createApp(App).mount('#app');

watch 实现

实现思路

实现代码

实现数据响应式

module
└── Vue
    ├── Computed.js
    ├── Watcher.js
    ├── index.js
    └── reactive.js
src
└── main.js

src/main.js

import Vue from '../module/Vue';

const vm = new Vue({
  el: '#app',
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    total() {
      console.log('computed total');
      return this.a + this.b;
    }
  },
  watch: {
    total(newVal, oldVal) {
      console.log('watch total', newVal, oldVal);
    },
    a(newVal, oldVal) {
      console.log('watch a', newVal, oldVal);
    },
    b(newVal, oldVal) {
      console.log('watch b', newVal, oldVal);
    }
  }
});

console.log(vm);
console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.a = 100;

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.b = 200;

console.log(vm.total);

module/Vue/index.js

import { reactive } from './reactive';
class Vue {
  constructor(options) {
    const { data, computed, watch } = options;
    this.$data = data();
    this.init(this, computed, watch);
  }

  init(vm, computed, watch) {
    this.initData(vm);
    const computedIns = this.initComputed(vm, computed);
    const watcherIns = this.initWatcher(vm, watch);
  }
  
  initData(vm) {
    // 数据响应式
    reactive(vm, (key, value) => {
      console.log(key, value);
    }, (key, newValue, oldValue) => {
      console.log(key, newValue, oldValue);
    });
  }

  // computed没暴露在实例上,需要通过传参
  initComputed(vm, computed) {
    // 枚举 computed ,存到 computedData 中去
    // 返回computed实例 -> 实例里有update -> 更新 computedData 的 value
  }

  initWatcher(vm, watch) {
    // 枚举 watcher -> 增加侦听器
    // 返回watcher实例 -> 实例里有调用 watch 的方法 -> 执行侦听器
  }
}

export default Vue;

/**
 * Vue
 * 
 * data -> 是个函数得执行 -> data() ->
 * 得挂载到 vm.$data 上 -> 得实现响应式 reactive -> 
 * 利用 Object.defineProperty -> 挂载到vm上:vm.xxx
 *    get vm[key] -> 获取的是 vm.$data[key]
 *    set vm[key] -> 设置的是 vm.$data[key] = newVal
 *        触发后 -> updateComputedProp -> value更新
 *        触发后 -> updateWatchProp -> watch函数callback执行
 * 
 * 
 * computed -> computedData = {
 *    value -> 通过 get 计算而来
 *    get   -> method
 *    deps  -> [a, b] method中当前computed属性的依赖
 *            根据 setter 触发后的 key ,
 *            对比下依赖里边有没有,有就重新执行get
 * }
 * 
 * watch -> watchPool -> 存的fn ->
 *          $data下的setter触发 -> 执行 callback
 */

module/Vue/reactive.js

export function reactive(vm, __get__, __set__) {
  // 因为在 getter、setter 中要做很多事情,例如update
  // 所以声明两个回调, __get__、__set__
  const _data = vm.$data;

  for (const key in _data) {
    Object.defineProperty(vm, key, {
      get() {
        __get__(key, _data[key]);
        return _data[key];
      },
      set(newValue) {
        // 由于 watch 函数有 newVal 和 oldVal ,
        // 所以得保存一份修改之前的数据
        const oldValue = _data[key];
        _data[key] = newValue;
        __set__(key, newValue, oldValue);
      }
    });
  }
}

实现计算属性特性

src/main.js

import Vue from '../module/Vue';

const vm = new Vue({
  el: '#app',
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    // descriptor.value
    total() {
      console.log('computed total');
      return this.a + this.b;
    },
    // descriptor.value.get
    total2: {
      get() {
        console.log('computed total2');
        return this.a + this.b;
      }
    }
  },
  watch: {
    total(newVal, oldVal) {
      console.log('watch total', newVal, oldVal);
    },
    a(newVal, oldVal) {
      console.log('watch a', newVal, oldVal);
    },
    b(newVal, oldVal) {
      console.log('watch b', newVal, oldVal);
    }
  }
});

console.log(vm);
console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.a = 100;

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.b = 200;

console.log(vm.total);

module/Vue/Computed.js

class Computed {
  constructor() {
    /**
     * // descriptor.value
     * total() {
     *   console.log('computed total');
     *   return this.a + this.b;
     * },
     * // descriptor.value.get
     * total2: {
     *   get() {
     *     console.log('computed total2');
     *     return this.a + this.b;
     *   }
     * }
     * 
     * {
     *  key: total,
     *  value: 3
     *  get: total fn
     *  dep: [a, b]
     * }
     */
    this.computedData = [];
  }

  addComputed(vm, computed, key) {
    const descriptor = Object.getOwnPropertyDescriptor(computed, key),
          descriptorFn = descriptor.value.get
                          ? descriptor.value.get
                          : descriptor.value,
          value = descriptorFn.call(vm),
          get = descriptorFn.bind(vm), // 保证正确的 this 指向
          dep = this._collectDep(descriptorFn);
    this._addComputedProp({
      key,
      value,
      get,
      dep
    });
    console.log(this.computedData);

    const computedItem = this.computedData.find(item => item.key === key);
    Object.defineProperty(vm, key, {
      get() {
        return computedItem.value;
      },
      // this.total = 500 不生效,还是自身get计算过来的值
      // 保证计算属性是正确的
      set(newValue) {
        computedItem.value = computedItem.get()
      }
    });
  }

  update(key, cb) {
    // 遍历 computed 池,
    // 找有没有哪个 computed 的 dep 依赖中有此 key
    // 如果依赖(key)值变更了,
    // 就重新执行相应 computed 的 value(利用get())
    this.computedData.map(computed => {
      const dep = computed.dep;
      const find = dep.find(v => v === key);
      if (find) {
        computed.value = computed.get();
        cb && cb(computed.key, computed.value);
        // update值之后,可能还有事儿要干,所以
        // 留一个cb,使其更新后调用
      }
    });
  }

  _addComputedProp(computedProp) {
    this.computedData.push(computedProp);
  }

  _collectDep(fn) {
    const matched = fn.toString().match(/this\\.(.+?)/g);
    // console.log(matched); 
    // matched : ['this.a', 'this.b']
    return matched.map(item => item.split('.')[1]);
  }
}

export default Computed;

module/Vue/index.js

import { reactive } from './reactive';
import Computed from './Computed';
class Vue {
  constructor(options) {
    const { data, computed, watch } = options;
    this.$data = data();
    this.init(this, computed, watch);
  }

  init(vm, computed, watch) {
    this.initData(vm);
    const computedIns = this.initComputed(vm, computed);
    const watcherIns = this.initWatcher(vm, watch);

    this.$computed = computedIns.update.bind(computedIns); // 保证 update 的 this 指向 computedIns 实例
  }
  
  initData(vm) {
    // 数据响应式
    reactive(vm, (key, value) => {
      // console.log(key, value);
    }, (key, newValue, oldValue) => {
      // console.log(key, newValue, oldValue);

      // 更新依赖key的computed
      this.$computed(key);
    });
  }

  // computed没暴露在实例上,需要通过传参
  initComputed(vm, computed) {
    // 枚举 computed ,存到 computedData 中去
    // 返回实例 -> 实例里有update -> 更新 computedData 的 value
    const computedIns = new Computed();

    for (const key in computed) {
      computedIns.addComputed(vm, computed, key);
    }

    return computedIns;
  }

  initWatcher(vm, watch) {
    // 枚举 watcher -> 增加侦听器
    // 返回实例 -> 实例里有调用 watch 的方法 -> 执行侦听器
  }
}

export default Vue;

/**
 * Vue
 * 
 * data -> 是个函数得执行 -> data() ->
 * 得挂载到 vm.$data 上 -> 得实现响应式 reactive -> 
 * 利用 Object.defineProperty -> 挂载到vm上:vm.xxx
 *    get vm[key] -> 获取的是 vm.$data[key]
 *    set vm[key] -> 设置的是 vm.$data[key] = newVal
 *        触发后 -> updateComputedProp -> value更新
 *        触发后 -> updateWatchProp -> watch函数callback执行
 * 
 * 
 * computed -> computedData = {
 *    value -> 通过 get 计算而来
 *    get   -> method
 *    deps  -> [a, b] method中当前computed属性的依赖
 *            根据 setter 触发后的 key ,
 *            对比下依赖里边有没有,有就重新执行get
 * }
 * 
 * watch -> watchPool -> 存的fn ->
 *          $data下的setter触发 -> 执行 callback
 */

实现侦听器特性

src/main.js

import Vue from '../module/Vue';

const vm = new Vue({
  el: '#app',
  data() {
    return {
      a: 1,
      b: 2
    }
  },
  computed: {
    // descriptor.value
    total() {
      console.log('computed total');
      return this.a + this.b;
    },
    // descriptor.value.get
    total2: {
      get() {
        console.log('computed total2');
        return this.a + this.b;
      }
    }
  },
  watch: {
    total(newVal, oldVal) {
      console.log('watch total', newVal, oldVal);
    },
    a(newVal, oldVal) {
      console.log('watch a', newVal, oldVal);
    },
    b(newVal, oldVal) {
      console.log('watch b', newVal, oldVal);
    }
  }
});

console.log(vm);
console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.a = 100;

console.log(vm.total);
console.log(vm.total);
console.log(vm.total);

vm.b = 200;

console.log(vm.total);

module/Vue/Watcher.js

class Watcher {
  /**
   * addWatcher(vm, watcher, key)
   * 
   * this.watchers -> watch
   * 
   * 
   * total(newVal, oldVal) {
   *   console.log('watch total', newVal, oldVal);
   * }
   * 
   * 
   * {
   *  key,
   *  fn
   * }
   */
  constructor() {
    this.watchers = [];
  }

  addWatcher(vm, watcher, key) {
    this._addWatchProp({
      key,
      fn: watcher[key].bind(vm)
    });
    // console.log(this.watchers);
  }

  // 调用 watcher
  invoke(key, newValue, oldValue) {
    this.watchers.map(item => {
      if (item.key === key) {
        item.fn(newValue, oldValue);
      }
    });
  }

  _addWatchProp(watchProp) {
    this.watchers.push(watchProp);
  }
}

export default Watcher;