React 的官方教程井字棋很好的引导初学者一步步走进 React 的世界,我想类似的教程对 Vue.js 的初学者应该也会有启发,于是使用 Vue.js 进行了改写

可以先查看最终的结果,尝试点击体验,我们将逐步地实现这个效果

初始状态代码

初始状态查看

打开初始状态直接编辑,或者将对应的文件复制下来放置在同一文件夹中

此时只是一个简单的井字棋格子,以及写死的下一个选手

初始代码分析

目前定义了三个组件,分别为 Square,Board 和 Game

Square 目前只是一个普通的按钮

  1. Vue.component('Square', {
  2. template: `
  3. <button class="square">
  4. {{ /* TODO */ }}
  5. </button>
  6. `
  7. })
  • 这样定义了组件后,别的组件就可以直接以 <Square /> 的方式引用该组件

Board 模版由当前状态和 9 个 Square 组成

  1. Vue.component('Board', {
  2. data() {
  3. return {
  4. status: `${nextLabel}X`,
  5. board: [
  6. [0, 1, 2],
  7. [3, 4, 5],
  8. [6, 7, 8]
  9. ]
  10. }
  11. },
  12. template: `
  13. <div>
  14. <div class="status">{{ status }}</div>
  15. <div class="board-row" v-for="(row, index) in board" :key="index">
  16. <Square v-for="square in row" :key="square" />
  17. </div>
  18. </div>
  19. `
  20. });
  • data 定义了当前状态 status,和 board 的值,这样在模版中就可以用 {{ status }} 的方式引用状态值,使用 v-for 将 board 二维数组里的值两次循环组装成井字格
  • 在组件中的 data 必须是返回对象的函数而非对象字面值
  • v-for 需要有 key 确保性能以及不报警告

Game 模版由 Board 与 后面需要增加的状态和历史组成

  1. Vue.component('Game', {
  2. template: `
  3. <div class="game">
  4. <div class="game-board">
  5. <Board />
  6. </div>
  7. <div class="game-info">
  8. <div>{{ /* status */ }}</div>
  9. <ol>{{ /* TODO */ }}</ol>
  10. </div>
  11. </div>
  12. `
  13. });

增加数据处理

增加 Props

在 Board 中传递一个名为 value 的 prop 到 Square

  1. <Square v-for="square in row" :key="square" :value="square" />
  • :value 是 v-bind:value 的缩写,表示其值是一个表达式

在 Square 的组件定义和模版中增加 value prop

  1. Vue.component('Square', {
  2. props: ['value'],
  3. template: `
  4. <button class="square">
  5. {{ value }}
  6. </button>
  7. `
  8. })
  • props 为父组件可传递给子组件的变量,在父组件调用子组件时在标签中设置对应属性,在子组件中使用方法与 data 一致

目前的代码和效果:0 - 8 的数字分别填充进井字棋格中

增加交互

增加点击事件至按钮元素以更新值

  1. Vue.component('Square', {
  2. //props: ['value'],
  3. data() {
  4. return {
  5. value: null
  6. }
  7. },
  8. methods: {
  9. setValue() {
  10. this.value = 'X';
  11. }
  12. },
  13. template: `
  14. <button class="square" @click="setValue">
  15. {{ value }}
  16. </button>
  17. `
  18. })
  • @click 为 v-on:click 的缩写,其值为点击需要运行的函数,这里为组件定义的方法 methods 中的 setValue
  • 子组件不能直接更新父组件的值,所以将 value 从 props 改为 data
  • data 的值更新,对应模版就会自动更新展示内容

目前的代码和效果:点击井字棋格,对应填充 X

完善游戏

数值提升

为交替落子和确认输赢,需要统一判断各格状态,所以将 value 提升至 Board

Board 增加数据 squares 和方法 handleClick

  1. Vue.component('Board', {
  2. data() {
  3. return {
  4. ...
  5. squares: Array(9).fill(null),
  6. }
  7. },
  8. methods: {
  9. handleClick(i) {
  10. const squares = this.squares.slice();
  11. if (squares[i]){
  12. alert('此位置已被占!');
  13. return
  14. }
  15. squares[i] = 'X';
  16. this.squares = squares;
  17. }
  18. },
  19. template: `
  20. ...
  21. <div class="board-row" v-for="(row, index) in board" :key="index">
  22. <Square v-for="square in row" :key="square" :value="squares[square]" @click="handleClick(square)" />
  • squares 初始为 9 个 null 组成的数组,井字棋盘为空的状态
  • handleClick 接收对应格子序号的参数,并更新对应的 square 元素
  • 事件处理器不是 handleClick(square) 的返回值,而是 handleClick,只是在触发时会带上参数值 square

在 Square 的点击事件处理器中触发 Board 的点击事件

  1. Vue.component('Square', {
  2. props: ['value'],
  3. methods: {
  4. setValue() {
  5. this.$emit('click');
  6. }
  7. },
  • value 要从 data 改回到 props
  • $emit 可以调用父组件传递的事件处理器
  • prop 里的值在父组件更新,子组件模版也会对应更新展示内容

目前的代码和效果:点击井字棋格,如果未被占,则填充 X

轮流落子

增加数据 xIsNext,并在点击时切换

  1. data() {
  2. return {
  3. ...
  4. xIsNext: true
  5. }
  6. },
  7. methods: {
  8. handleClick(i) {
  9. ...
  10. squares[i] = this.xIsNext ? 'X' : 'O';
  11. this.squares = squares;
  12. this.xIsNext = !this.xIsNext;
  13. this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
  • xIsNext 初始值为 true,即 X 先落子
  • 点击后,通过取反交替 xIsNext
  • 更新状态值 status 为下一个落子者

目前的代码和效果:点击井字棋格,X 和 O 交替落子

判断胜者

增加计算胜者的函数

  1. function calculateWinner(squares) {
  2. const lines = [
  3. [0, 1, 2],
  4. [3, 4, 5],
  5. [6, 7, 8],
  6. [0, 3, 6],
  7. [1, 4, 7],
  8. [2, 5, 8],
  9. [0, 4, 8],
  10. [2, 4, 6],
  11. ];
  12. for (let i = 0; i < lines.length; i++) {
  13. const [a, b, c] = lines[i];
  14. if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
  15. return squares[a];
  16. }
  17. }
  18. return null;
  19. }
  • 列举可能获胜的组合,与 squares 数组的值进行比对

增加点击处理函数的胜者逻辑

  1. if (calculateWinner(squares)) {
  2. alert('胜负已定!');
  3. return;
  4. }
  5. ...
  6. const winner = calculateWinner(squares);
  7. if (winner) {
  8. this.status = '获胜者: ' + winner;
  9. return;
  10. }
  • 点击后,如果之前已有取胜,则点击无效
  • 处理落子后,再次判断是否取胜,更新状态

目前的代码和效果:有一方获胜后, 状态和点击处理更新

增加时间旅行

保存历史记录

为实现“悔棋”功能,需要记录每一次落子的整体状态,相当于棋盘的快照,作为一个历史记录,提升至 Game 组件中

在 Game 增加数据 history,将 xIsNext,status 和 handleClick 方法 从 Board 中转移到 Game 中

  1. Vue.component('Game', {
  2. data() {
  3. return {
  4. history: [{
  5. squares: Array(9).fill(null),
  6. }],
  7. xIsNext: true,
  8. status: `${nextLabel}X`
  9. }
  10. },
  11. methods: {
  12. handleClick(i) {
  13. const history = this.history;
  14. const current = history[history.length - 1]
  15. const squares = current.squares.slice();
  16. ...
  17. squares[i] = this.xIsNext ? 'X' : 'O';
  18. history.push({
  19. squares: squares
  20. });
  21. ...
  22. }
  23. },
  24. template: `
  25. <div class="game">
  26. <div class="game-board">
  27. <Board :squares="history[history.length - 1].squares" @click="handleClick" />
  28. `
  29. })
  • squares 从 history 的最后一个记录取值(目前只有一个记录)
  • 落子后,squares 把落子记录进去后,history 再增加一个记录

Board 增加 prop squares,handleClick 更新为调用父组件的事件处理器

  1. Vue.component('Board', {
  2. props: ['squares'],
  3. methods: {
  4. handleClick(i) {
  5. this.$emit('click', i);
  6. }
  7. },

目前的代码和效果:状态位置更新,历史记录已存储

展示历史步骤记录

把历史记录循环展示出来,并绑定点击事件,通过 stepNumber 的更新显示对应步骤的记录

  1. Vue.component('Game', {
  2. data() {
  3. ...
  4. stepNumber: 0,
  5. ...
  6. }
  7. },
  8. methods: {
  9. handleClick(i) {
  10. const history = this.history.slice(0, this.stepNumber + 1);
  11. ...
  12. this.history = history.concat([{
  13. squares: squares
  14. }]);
  15. this.stepNumber = history.length;
  16. ...
  17. },
  18. jumpTo(step) {
  19. if(step === this.stepNumber){
  20. alert('已在' + (0 === step ? '最开始' : `步骤#${step}!`));
  21. return;
  22. }
  23. this.stepNumber = step;
  24. this.xIsNext = (step % 2) === 0;
  25. this.status = `${nextLabel}${this.xIsNext ? 'X' : 'O'}`;
  26. }
  27. },
  28. template: `
  29. <div class="game">
  30. <div class="game-board">
  31. <Board :squares="history[this.stepNumber].squares" @click="handleClick" />
  32. </div>
  33. <div class="game-info">
  34. <div>{{ status }}</div>
  35. <ol>
  36. <li v-for="(squares, index) in history" :key="index" :class="{'move-on': index === stepNumber}">
  37. <button @click="jumpTo(index)">{{ 0 === index ? '回到开始' : '回到步骤#' + index }}</button>
  38. ...
  39. `
  40. })
  • 在 Game 中增加 stepNumber,初始为 0,记录当前展示的步骤
  • 将 Board 的 prop squares 的取值更新为 this.stepNumber 对应的步骤
  • handleClick 中以已当前步骤为基础处理 history,并更新 stepNumber
  • 增加方法 jumpTo 处理回到历史的展示,更新 stepNumber,xIsNext 和 status

最终的代码和效果:每落一子,都会增加一个历史步骤,点击步骤可回到该步

总结

游戏实现内容

  • 交替落子
  • 判断输赢
  • 悔棋重来

展示技术内容

  • v-bind:在模版中进行数据绑定
  • v-for:在模版中进行数组循环
  • v-on, $emit:在组件间进行事件传递和触发
  • data:在组件的定义和模版自动更新
  • prop:在组件间的传递和模版自动更新

使用 Vue.js 改写 React 的官方教程井字棋的更多相关文章

  1. Vue.js与React的全面对比

    Vue与React的对比 Vue.js与React.js从某些反面来说很相似,通过两个框架的学习,有时候对一些用法会有一点思考,为加深学习的思索,特翻阅了两个文档,从以下各方面进行了对比,加深了对这两 ...

  2. Vue.js vs React vs Angular 深度对比[转]

    这个页面无疑是最难编写的,但我们认为它也是非常重要的.或许你曾遇到了一些问题并且已经用其他的框架解决了.你来这里的目的是看看 Vue 是否有更好的解决方案.这也是我们在此想要回答的. 客观来说,作为核 ...

  3. vue.js与react.js相比较的优势

    vue.js的简介 vue.js是一个javascript mvvm库,它是以数据驱动和组件化的思想构建的.我们平时多用js去操作dom,vue.js则是使用了数据绑定驱动来操作dom的,也就是说创建 ...

  4. 【软件编程】乐易贵宾VIP教程 - JS改写+网页操作系列教程

    JS改写系列教程: 1.MD5加密改写教程(爱拍网登录)2.解密如何快速找到真确的js加密算法3.多重MD5加密改写教程(5173登录)4.DZ论坛登录加密改写5.唯品会手机登录加密改写6.新浪微博密 ...

  5. Vue.js + Webpack + ECMAScript 6 入门教程

    Vue.js学习教程 1.Vue.js——60分钟快速入门 2.Vue.js——60分钟组件快速入门(上篇) 3.Vue.js——60分钟组件快速入门(下篇) 4.Vue.js——基于$.ajax实现 ...

  6. 你是否有一个梦想?用JavaScript[vue.js、react.js......]开发一款自定义配置视频播放器

    前言沉寂了一周了,打算把这几天的结果呈现给大家.这几天抽空就一直在搞一个自定义视频播放器,为什么会有如此想法?是因为之前看一些学习视频网站时,看到它们做的视频播放器非常Nice!于是,就打算抽空开发一 ...

  7. 我从Angular 2转向Vue.js, 也没有选择React

    译者按: 通过使用Angular的经历,作者已经完全转为Vue粉了!我们Fundebug目前还是用AngularJS 1,坦白说,学习曲线蛮陡的. 原文: Why we moved from Angu ...

  8. 为什么我们从Angular 2迁移到Vue.js(为什么我们没有选择React)

    在Rever(www.reverscore.com),我们刚刚使用Vue.js发布了我们的Web客户端的新版本.经过641次提交和16周的紧张开发,我们非常自豪之前做出的决定.8个月前,我们的前端在使 ...

  9. 公司内部技术分享之Vue.js和前端工程化

    今天主要的核心话题是Vue.js和前端工程化.我将结合我这两年多的工作学习经历来谈谈这个,主要侧重点是前端工程化,Vue.js侧重点相对前端工程化,比重不是特别大. Vue.js Vue.js和Rea ...

随机推荐

  1. Oracle介绍

    Published: 2016-11-08 22:15:00 In Data Mining. tags: SQL 版本与配置 企业版 标准版 个人版 事务性数据表 分析型数据表 PL/SQL 配置 控 ...

  2. sycPHPCMS v1.6 cookie sqlinjection

    ./user/index.php include "../include/conn.php"; include "../include/function.php" ...

  3. mysql 分表实现方法详解

    如果你需要进行mysql分表了我们就证明你数据库比较大了,就是把一张表分成N多个小表,分表后,单表的并发能力提高了,磁盘I/O性能也提高了.并发能力为什么提高了呢,因为查寻一次所花的时间变短了,如果出 ...

  4. 滑动表层div时 禁止底层滑动

    $(".container").bind("touchstart", function (events) { startX = events.originalE ...

  5. CSS——NO.4(继承、层叠、特殊性、重要性)

    */ * Copyright (c) 2016,烟台大学计算机与控制工程学院 * All rights reserved. * 文件名:text.cpp * 作者:常轩 * 微信公众号:Worldhe ...

  6. C++学习之旅

    到现在为止学习C++也已经有一个半月了.一个半个月里我怀着好奇与敬畏一步步的走来,一步步的走向C++的内心深处,也发现了C++"内心的复杂".虽有坎坷,但从未放弃. 我承认,我不是 ...

  7. android逆向---charles抓包

    手机与电脑处于同一网络环境,且正确设置代理后,charles显示CONNECT失败,提示信息SSL handshake with client failed: An unknown issue occ ...

  8. hadoop地址配置、内存配置、守护进程设置、环境设置

    1.1  hadoop配置 hadoop配置文件在安装包的etc/hadoop目录下,但是为了方便升级,配置不被覆盖一般放在其他地方,并用环境变量HADOOP_CONF_DIR指定目录. 1.1.1  ...

  9. dubbo与trivial超时机制的深入思考

    说在前面 trivial是根据之前设计的RPC框架而来的(还在增进当中),其中较为不同的一个点为,在客户端去掉了业务线程池,因为既然都要等待,不必要再加一层. 进入正题 有在网上看到这样的信息,“之前 ...

  10. Nuxt 项目性能优化调研

    性能优化,这是面试中经常会聊到的话题.我觉得性能优化应该因具体场景而异,因不同项目而异,不同的手段不同的方案并不一定适合所有项目,当然这其中不乏一些普适的方案,比如耳熟能详的文件压缩,文件缓存,CDN ...