可鱼不是鱼

可鱼不是鱼 查看完整档案

成都编辑  |  填写毕业院校聚美优品  |  前端开发 编辑 blog.flqin.com 编辑
编辑

keep fight

个人动态

可鱼不是鱼 发布了文章 · 2019-09-10

使用proxy实现一个简单完整的MVVM库

前言

本文首发在 前端开发博客

MVVM 是当前时代前端日常业务开发中的必备模式(相关框架如reactvueangular 等), 使用 MVVM 可以将开发者的精力更专注于业务上的逻辑,而不需要关心如何操作 dom。虽然现在都 9012 年了,mvvm 相关原理的介绍已经烂大街了,但出于学习基础知识的目的(使用 proxy 实现的 vue3.0 还在开发中), 在参考了之前 vue.js 的整体思路之后,自己动手实现了一个简易的通过 proxy 实现的 mvvm

本项目代码已经开源在github,项目正在持续完善中,欢迎交流学习,喜欢请点个 star 吧!

最终效果

<html>
  <body>
    <div id="app">
      <div>{{title}}</div>
    </div>
  </body>
</html>
import MVVM from '@fe_korey/mvvm';
new MVVM({
  view: document.getElementById('app'),
  model: {
    title: 'hello mvvm!'
  },
  mounted() {
    console.log('主程编译完成,欢迎使用MVVM!');
  }
});

结构概览

  • Complier 模块实现解析、收集指令,并初始化视图
  • Observer 模块实现了数据的监听,包括添加订阅者和通知订阅者
  • Parser 模块实现解析指令,提供该指令的更新视图的更新方法
  • Watcher 模块实现建立指令与数据的关联
  • Dep 模块实现一个订阅中心,负责收集,触发数据模型各值的订阅列表

流程为:Complier收集编译好指令后,根据指令不同选择不同的Parser,根据ParserWatcher中订阅数据的变化并更新初始视图。Observer监听数据变化然后通知给 WatcherWatcher 再将变化结果通知给对应Parser里的 update 刷新函数进行视图的刷新。

mvvm.js整体流程图

模块详解

Complier

  • 将整个数据模型 data 传入Observer模块进行数据监听

    this.$data = new Observer(option.model).getData();
  • 循环遍历整个 dom,对每个 dom 元素的所有指令进行扫描提取

    function collectDir(element) {
      const children = element.childNodes;
      const childrenLen = children.length;
    
      for (let i = 0; i < childrenLen; i++) {
        const node = children[i];
        const nodeType = node.nodeType;
    
        if (nodeType !== 1 && nodeType !== 3) {
          continue;
        }
        if (hasDirective(node)) {
          this.$queue.push(node);
        }
        if (node.hasChildNodes() && !hasLateCompileChilds(node)) {
          collectDir(element);
        }
      }
    }
  • 对每个指令进行编译,选择对应的解析器Parser

    const parser = this.selectParsers({ node, dirName, dirValue, cs: this });
  • 将得到的解析器Parser传入Watcher,并初始化该 dom 节点的视图

    const watcher = new Watcher(parser);
    parser.update({ newVal: watcher.value });
  • 所有指令解析完毕后,触发 MVVM 编译完成回调$mounted()

    this.$mounted();
  • 使用文档碎片document.createDocumentFragment()来代替真实 dom 节点片段,待所有指令编译完成后,再将文档碎片追加回真实 dom 节点

    let child;
    const fragment = document.createDocumentFragment();
    while ((child = this.$element.firstChild)) {
      fragment.appendChild(child);
    }
    //解析完后
    this.$element.appendChild(fragment);
    delete $fragment;

Parser

  • Complier模块编译后的指令,选择不同听解析器解析,目前包括ClassParser,DisplayParser,ForParser,IfParser,StyleParser,TextParser,ModelParser,OnParser,OtherParser等解析模块。

    switch (name) {
      case 'text':
        parser = new TextParser({ node, dirValue, cs });
        break;
      case 'style':
        parser = new StyleParser({ node, dirValue, cs });
        break;
      case 'class':
        parser = new ClassParser({ node, dirValue, cs });
        break;
      case 'for':
        parser = new ForParser({ node, dirValue, cs });
        break;
      case 'on':
        parser = new OnParser({ node, dirName, dirValue, cs });
        break;
      case 'display':
        parser = new DisplayParser({ node, dirName, dirValue, cs });
        break;
      case 'if':
        parser = new IfParser({ node, dirValue, cs });
        break;
      case 'model':
        parser = new ModelParser({ node, dirValue, cs });
        break;
      default:
        parser = new OtherParser({ node, dirName, dirValue, cs });
    }
  • 不同的解析器提供不同的视图刷新函数update(),通过update更新dom视图

    //text.js
    function update(newVal) {
      this.el.textContent = _toString(newVal);
    }
  • OnParser 解析事件绑定,与数据模型中的 methods字段对应

    //详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/on.ts
    el.addEventListener(handlerType, e => {
      handlerFn(scope, e);
    });
  • ForParser 解析数组

    //详见 https://github.com/zhaoky/mvvm/blob/master/src/core/parser/for.ts
  • ModelParser 解析双向绑定,目前支持input[text/password] & textarea,input[radio],input[checkbox],select四种情况的双向绑定,双绑原理:

    • 数据变化更新表单:跟其他指令更新视图一样,通过update方法触发更新表单的value

      function update({ newVal }) {
        this.model.el.value = _toString(newVal);
      }
    • 表单变化更新数据:监听表单变化事件如input,change,在回调里set数据模型

      this.model.el.addEventListener('input', e => {
        model.watcher.set(e.target.value);
      });

Observer

  • MVVM 模型中的核心,一般通过 Object.definePropertygetset 方法进行数据的监听,在 get 里添加订阅者,set 里通知订阅者更新视图。在本项目采用 Proxy 来实现数据监听,好处有三:

    • Proxy 可以直接监听对象而非属性
    • Proxy 可以直接监听数组的变化
    • Proxy 有多达 13 种拦截方法,查阅
      而劣势是兼容性问题,且无法通过 polyfill 磨平。查阅兼容性
  • 注意 Proxy 只会监听自身的每一个属性,如果属性是对象,则该对象不会被监听,所以需要递归监听
  • 设置监听后,返回一个 Proxy 替代原数据对象
var proxy = new Proxy(data, {
  get: function(target, key, receiver) {
    //如果满足条件则添加订阅者
    dep.addDep(curWatcher);
    return Reflect.get(target, key, receiver);
  },
  set: function(target, key, value, receiver) {
    //如果满足条件则通知订阅者
    dep.notfiy();
    return Reflect.set(target, key, value, receiver);
  }
});

Watcher

  • Complier 模块里对每一个解析后的 Parser 进行指令与数据模型直接的绑定,并触发 Observerget 监听,添加订阅者(Watcher

    this._getter(this.parser.dirValue)(this.scope || this.parser.cs.$data);
  • 当数据模型变化时,就会触发 -> Observerset 监听 -> Depnotfiy 方法(通知订阅者的所有订阅列表) -> 执行订阅列表所有 Watcherupdate 方法 -> 执行对应 Parserupdate -> 完成更新视图
  • Watcher 里的 set 方法用于设置双向绑定值,注意访问层级

Dep

  • MVVM 的订阅中心,在这里收集数据模型的每个属性的订阅列表
  • 包含添加订阅者,通知订阅者等方法
  • 本质是一种发布/订阅模式
class Dep {
  constructor() {
    this.dependList = [];
  }
  addDep() {
    this.dependList.push(dep);
  }
  notfiy() {
    this.dependList.forEach(item => {
      item.update();
    });
  }
}

后记

目前该 mvvm 项目只实现了数据绑定视图更新的功能,通过这个简易轮子的实现,对 dom 操作,proxy发布订阅模式等若干基础知识都进行了再次理解,查漏补缺。同时欢迎大家一起探讨交流,后面会继续完善!

查看原文

赞 0 收藏 0 评论 0

可鱼不是鱼 赞了文章 · 2018-05-07

理解数据驱动视图原理

源代码1

// 响应式原理 defineProperty
 
//数据
const data = {
  obj: {
    a: 4,
    b: 6
  },
  arr: [1, 5, 9]
}
 
// 观察数据
function observe(data) {
  Object.keys(data).forEach(function(key) {
    let value = data[key]
    if (value && typeof value === 'object') observe(value) // 递归
    Object.defineProperty(data, key, {
      get() {
        console.log(`get ${key}`)
        return value
      },
      set(newVal) {
        console.log(`set ${key} = ${newVal}`)
        if (newVal && typeof newVal === 'object') observe(newVal)
        value = newVal
      }
    })
  })
 
}
 
observe(data)
 
let obj = data.obj
// get obj
let arr = data.arr
// get arr
 
 
obj.a = 8
// set a = 8
obj.a
// get a
delete obj.b
// 无反应
obj.c = 9
// 无反应
obj.c
// 无反应
 
data.obj = {...obj, c: 7}
// set obj = [object Object]
obj = data.obj
// get obj
 
obj.c = 9
// set c = 9
obj.c
// get c
 
arr.push(9) // 包括pop,shift,unshift,splice,sort,reverse
// 无反应
data.arr = [...arr,9]
// set arr = 1,5,9,9,9

讲解

  1. vue只所以能实现双向绑定,是利用es5里面的Object.defineProperty(这就是为什么vue只支持es9及以上)
  2. 从以上代码可以看出,对象属性的删除(delete obj.b)和添加新属性(obj.c = 9),不会触发对应的set方法(vue对于初始化没有定义的属性,设置值不能触发视图层渲染)
  3. 在项目开发中肯定会遇到有些属性,初始化时没有定义,通过改变其父元素的值去实现(data.obj = {...obj, c: 7}),父元素的值改变后,会触发其set方法,会对其子元素重新进行双向绑定
  4. 对于数组的处理,push,pop,shift,unshift,splice,sort,reverse都不会触发set,但我们看到的vue,这些方法是会触发视图层的变化,这是因为vue针对这些方法做了特殊的处理,原理如
const arr = [5, 9, 8]
const push = Array.prototype.push
Array.prototype.push = function () {
  const result = push.apply(this, arguments)
  console.log('做自己喜欢的事情')
  return result
}
arr.push(7)
console.log(arr)

vue代码片段

/*
 * not type checking this file because flow doesn't play well with
 * dynamically accessing methods on Array prototype
 */
 
var arrayProto = Array.prototype;
var arrayMethods = Object.create(arrayProto);[
  'push',
  'pop',
  'shift',
  'unshift',
  'splice',
  'sort',
  'reverse'
]
.forEach(function (method) {
  // cache original method
  var original = arrayProto[method];
  def(arrayMethods, method, function mutator () {
    var args = [], len = arguments.length;
    while ( len-- ) args[ len ] = arguments[ len ];
 
    var result = original.apply(this, args);
    var ob = this.__ob__;
    var inserted;
    switch (method) {
      case 'push':
      case 'unshift':
        inserted = args;
        break
      case 'splice':
        inserted = args.slice(2);
        break
    }
    if (inserted) { ob.observeArray(inserted); }
    // notify change
    ob.dep.notify();
    return result
  });
});
 
/*  */

源代码2

// 响应式原理 defineProperty
 
//数据
const data = {
  obj: {
    a: 4,
    b: 6
  },
  arr: [1, 5, 9]
}
function Dep() {}
Dep.target = null // 当前函数
function watcher(fn) { // 函数
  Dep.target = fn
  fn()
}
 
// 初始化
function init() {
  const a = () => {
    console.log(data.obj.a)
  }
  const mix = () => {
    const c = data.obj.a + data.obj.b
    console.log(c)
  }
  watcher(a)
  watcher(mix)
}
 
// 观察数据
function observe(data) {
  Object.keys(data).forEach(function(key) {
    let value = data[key]
    const dep = [] // 存放函数的容器
    if (value && typeof value === 'object') observe(value) // 递归
    Object.defineProperty(data, key, {
      get() {
        dep.push(Dep.target)
        return value
      },
      set(newVal) {
        if (newVal && typeof newVal === 'object') observe(newVal)
        value = newVal
        dep.forEach(fn => fn())
      }
    })
  })
 
}
 
observe(data)
init()
 
setTimeout(() => {
  data.obj.a = 10
}, 2000)

讲解

以上代码可以看出,当obj.a值变化的时候,会触发a函数和mix函数,具体执行步骤如下

  1. 先将属性a绑定了对应的set和get方法
  2. 初始化init()时,会调用watcher(a),此时 Dep.target等于a函数
  3. 执行a()函数时,会执行里面的console.log(data.obj.a)
  4. data.obj.a会调用a的get方法,此时dep.push(Dep.target)就相当于dep.push(a函数),mix函数同理
  5. 当属性a的值改变时(data.obj.a = 10),会触发其set方法,dep.forEach(fn => fn())将a函数和mix函数循环执行一遍,从而实现了数据驱动视图

注意点

  1. 在驱动视图之前都要先赋值,比如源代码2的(value = newVal)和(dep.forEach(fn => fn()))不能互换位置
  2. 以上代码不是vue的源代码,但原理是一致的,帮助开发者更好的理解自己写的代码
查看原文

赞 4 收藏 2 评论 0

可鱼不是鱼 赞了文章 · 2018-05-07

页面回退历史记录

应用场景

a页面跳到b页面,再由b页面回到a页面
期望:a页面通过一些筛选条件,得到列表,点击列表跳转到b页面,b页面返回到a页面后,希望恢复到离开a页面时的状态

方法

  1. 首次进入a页面是什么状态,再次进入a页面还是什么状态,不做任何处理
    优点:简单,开发快
    缺点:不能得到预期效果
  2. 将a页面的列表数据直接保存在内存中,直接渲染
    优点:简单,开发快
    缺点:

       只适合单页面开发
       浏览器刷新后,数据不存在
       a页面的数据还是之前的状态,不能及时更新
  3. 将a页面的搜索条件存储出来,进入页面后,重新搜索
    a.将搜索条件存在url里面(最稳定)

       优点:可以获取到最新的数据,刷新浏览器数据还存在
       缺点:
           url长度限制(一般不会超过)
    
           游览器    最大长度(字符数)
           Internet Explorer    2083
           Firefox    65,536
           chrome    8182
           Safari    80,000
    
           开发难度增加,每一步都要去操作url,有洁癖的人看着不爽
    

    b.将搜索条件存在内存中

       优点:开发比上者快,可以获取到最新的数据
       缺点:
           只适合单页面开发
           浏览器刷新后,数据不存在
    

疑问解答

  1. 问:为什么上面的保存都是保存在内存中,而不是保存在本地,保存在本地就可以解决刷新浏览器数据不在的问题
    答:localStorage永久保存是优势也是劣势,不容易更新到最新的数据,不知道什么时候去删除和刷新数据,容易错乱

注意点

  1. 选择上面的哪一个方式,根据实际需求为准
  2. 目前我们的开发都是采用的单页面,浏览器刷新后,数据不存在,也可以接受,个人建议当前项目可以采用3b
查看原文

赞 6 收藏 5 评论 0

可鱼不是鱼 赞了文章 · 2018-05-07

虚拟dom比对原理

dom对比步骤

1.用js对象来表达dom结构

tagName 标签名
props 元素属性
key 唯一标识
children 子元素,格式和父元素一样
count 有几个子元素,用于计算当前元素的索引,处于整个dom中的第几个,方便dom操作

原js对象

{
    "tagName": "div",
    "props": {
        "id": "container"
    },
    "children": [
        {
            "tagName": "h1",
            "props": {
                "style": "color:red"
            },
            "children": [
                "simple virtual dom"
            ],
            "count": 1
        },
        {
            "tagName": "p",
            "props": {},
            "children": [
                "hello world"
            ],
            "count": 1
        },
        {
            "tagName": "ul",
            "props": {},
            "children": [
                {
                    "tagName": "li",
                    "props": {},
                    "children": [
                        "item #1"
                    ],
                    "count": 1
                },
                {
                    "tagName": "li",
                    "props": {},
                    "children": [
                        "item #2"
                    ],
                    "count": 1
                }
            ],
            "count": 4
        }
    ],
    "count": 9
}

2.原js对象渲染成dom结构

<div id="container">
    <h1 style="color: red;">simple virtual dom</h1>
    <p>hello world</p>
    <ul>
        <li>item #1</li>
        <li>item #2</li>
    </ul>
</div>

3.修改原js对象

{
    "tagName": "div",
    "props": {
        "id": "container2"
    },
    "children": [
        {
            "tagName": "h5",
            "props": {
                "style": "color:red"
            },
            "children": [
                "simple virtual dom"
            ],
            "count": 1
        },
        {
            "tagName": "p",
            "props": {},
            "children": [
                "hello world2"
            ],
            "count": 1
        },
        {
            "tagName": "ul",
            "props": {},
            "children": [
                {
                    "tagName": "li",
                    "props": {},
                    "children": [
                        "item #1"
                    ],
                    "count": 1
                },
                {
                    "tagName": "li",
                    "props": {},
                    "children": [
                        "item #2"
                    ],
                    "count": 1
                },
                {
                    "tagName": "li",
                    "props": {},
                    "children": [
                        "item #3"
                    ],
                    "count": 1
                }
            ],
            "count": 6
        }
    ],
    "count": 11
}

4.对比哪些节点被修改
type 类型,0为标签名改变,1为子元素改变(删除或添加),2为属性改变,3为内容改变
key 对象第一层中key值表示索引,原dom中第几个元素发生变化

{
    "0": [
        {
            "type": 2,
            "props": {
                "id": "container2"
            }
        }
    ],
    "1": [
        {
            "type": 0,
            "node": {
                "tagName": "h5",
                "props": {
                    "style": "color:red"
                },
                "children": [
                    "simple virtual dom"
                ],
                "count": 1
            }
        }
    ],
    "4": [
        {
            "type": 3,
            "content": "hello world2"
        }
    ],
    "5": [
        {
            "type": 1,
            "moves": [
                {
                    "index": 2,
                    "item": {
                        "tagName": "li",
                        "props": {},
                        "children": [
                            "item #3"
                        ],
                        "count": 1
                    },
                    "type": 1
                }
            ]
        }
    ]
}

5.渲染修改后的js对象

a.标签名改变,直接重新渲染整个元素,包括元素下的子元素
b.子元素改变,该删除的删除,该添加的添加(针对列表框架有一套自己的计算方法,可以自行百度去研究)
c.属性改变,操作对应元素的属性
d.内容改变,操作对应元素的内容

<div id="container2">
    <h5 style="color: red;">simple virtual dom</h5>
    <p>hello world2</p>
    <ul>
        <li>item #1</li>
        <li>item #2</li>
        <li>item #3</li>
    </ul>
</div>

虚拟dom比对原理图

查看原文

赞 5 收藏 5 评论 0

可鱼不是鱼 赞了文章 · 2018-05-07

axios请求缓存+防止重复提交

源代码

import axios from 'axios'

// 数据存储
export const cache = {
  data: {},
  set (key, data) {
    this.data[key] = data
  },
  get (key) {
    return this.data[key]
  },
  clear (key) {
    delete this.data[key]
  }
}

// 建立唯一的key值
export const buildUniqueUrl = (url, method, params = {}, data = {}) => {
  const paramStr = (obj) => {
    if (toString.call(obj) === '[object Object]') {
      return JSON.stringify(Object.keys(obj).sort().reduce((result, key) => {
        result[key] = obj[key]
        return result
      }, {}))
    } else {
      return JSON.stringify(obj)
    }
  }
  url += `?${paramStr(params)}&${paramStr(data)}&${method}`
  return url
}

// 防止重复请求
export default (options = {}) => async config => {
  const defaultOptions = {
    time: 0, // 设置为0,不清除缓存
    ...options
  }
  const index = buildUniqueUrl(config.url,config.method,config.params,config.data)
  let responsePromise = cache.get(index)
  if (!responsePromise) {
    responsePromise = (async () => {
      try {
        const response = await axios.defaults.adapter(config)
        return Promise.resolve(response)
      } catch (reason) {
        cache.clear(index)
        return Promise.reject(reason)
      }
    })()
    cache.set(index, responsePromise)
    if (defaultOptions.time !== 0) {
      setTimeout(() => {
        cache.clear(index)
      }, defaultOptions.time)
    }
  }
  return responsePromise.then(data => JSON.parse(JSON.stringify(data))) // 为防止数据源污染
}

例子

例如

import axios from 'axios'
import cache from './cache'
 
// get请求
export async function getData (payload) {
  return axios.get('/path', {
    params: payload,
    adapter: cache({
      time: 0
    })
  })
}

// post请求
export async function postData (payload) {
  return axios.post('/path', payload, {
    adapter: cache({
      time: 1000
    })
  })
}

API

time 表示可缓存的时间,默认为0,在没有清除内存之前永久缓存(浏览器窗口标签关闭,应用程序关闭等会清除内存)

防止重复提交

time 设置一个极短的时间,比如1000,就表示在1000毫秒之内不会重复向服务器发出请求,当然你也可以用new CancelToken,但会导致本次请求报错,需要做异常处理

查看原文

赞 17 收藏 8 评论 5

可鱼不是鱼 赞了文章 · 2018-05-07

js轮询及踩过的坑

背景

下午四点,天气晴朗,阳光明媚,等着下班
产品:我希望页面上的这个数据实时变化
开发:···,可以,用那个叫着WebSocket的东西,再找一个封装好框架,如:mqtt(感觉自己好机智)
产品:要开发好久
开发:嗯,三天,五天,还是···
产品:我希望今天上线
开发:···,···,···(不能描述的语言,话说segmentfault为什么不支持表情)
开发:果断选择轮询

开发中

<!DOCTYPE HTML>
<html>
<head>
  <title>轮询的坑</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
</body>
<script type="text/javascript">
  function getData() {
      return new Promise((resolve,reject) => {
          setTimeout(() => {
              resolve({data:666})
          },500)
      })
  }
  // 轮询
  async function start () {
    const { data } = await getData() // 模拟请求
    console.log(data)
    timerId = setTimeout(start, 1000)
  }
  start ()
</script>
</html>

开发:今晚的月亮真圆啊,下班了···

第二天

产品:我希望这个实时加载,能随心所欲,我喊它加载就加载,喊它停就停
研发:(石化中···)

继续开发中

<!DOCTYPE HTML>
<html>
<head>
  <title>轮询的坑</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
    <button id="button">暂停</button>
</body>
<script type="text/javascript">
  let timerId = null
  function getData() {
      return new Promise((resolve,reject) => {
          setTimeout(() => {
              resolve({data:666})
          },500)
      })
  }
  // 轮询
  async function start () {
    const { data } = await getData() // 模拟请求
    console.log(data)
    timerId = setTimeout(start, 1000)
  }
  // 暂停
  function stop () {
    clearTimeout(timerId)
  }

  start ()

  const botton = document.querySelector("#button")
  let isPlay = true
  botton.addEventListener("click", function(){
    isPlay = !isPlay
    botton.innerHTML = isPlay ? '暂停' : '播放'
    isPlay ? start() : stop()
  }, false)
</script>
</html>

开发:(这么难得需求我都实现了,我是不是已经是专家了,我是不是应该升职加薪,接着赢娶白富美,走向人生巅峰,哈哈哈)
正沉醉于自己的成果中
产品:你的有bug
开发:(绝对不信中,肯定是你握鼠标的姿势不对,手感不好),怎么可能有bug,你是不是环境有问题,还在用ie6,多刷新几次
产品:···,你按钮多点几次,点快点,试试,数据会多次请求
开发:半信半疑的去尝试,还真是(好奇怪,检查了一圈没有发现任何问题)

分析过程

  1. 一进去页面执行start(),start是一个async函数,使得里面的异步也会像同步一样执行,函数的末尾timerId = setTimeout(start, 1000),1000毫秒后再次执行start(),形成了一个轮询(这里的每一个请求之间的间隔肯定是大于1000+500的,至于为什么,可以去了解一下浏览器异步执行原理)
  2. 将setTimeout的id赋值给timerId,点击按钮后,清除当前定时器

看似没有任何问题,找不到问题的时候就只有一点点试错,最终发现去掉const { data } = await getData()之后,问题消失,请求的时间越长,出现的概率越高
画个图分析一下
图片描述
先看一下js执行过程,按钮的click事件也相当于异步,然后我们再来文字分析一下,问题出现的原因

bug出现原因

  1. 假如没有const { data } = await getData()这步,点击的时候,click的回调函数能够执行,说明当前js肯定处于空闲状态(永远记住,js的单线程的),这时的setTimeout(start, 1000)一定处于异步状态(js一次只有执行一个任务),
  2. clearTimeout(timerId)可以很轻松的清除这次任务,不会让它进入js执行线程中执行
  3. 加上const { data } = await getData()之后,如果js现在处于setTimeout的回调函数已经执行并且等待await getData()中,js是空闲的,click可以执行,click清除了setTimeout的回调函数的执行(回调函数已经执行了),没有清除await getData()回调函数的执行,代码会继续执行console.log(data);timerId = setTimeout(start, 1000),从而不能停止循环,这就是bug产生的原因

bug产生的时机
图片描述

这就是为什么,请求的时间越长,出现的概率越高

解决方案

<!DOCTYPE HTML>
<html>
<head>
  <title>轮询的坑</title>
  <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
</head>
<body>
    <button id="button">暂停</button>
</body>
<script type="text/javascript">
  let timerId = 1 // 模拟计时器id,唯一性
  let timerObj = {} // 计时器存储器
  function getData() {
      return new Promise((resolve,reject) => {
          setTimeout(() => {
              resolve({data:666})
          },500)
      })
  }
  // 轮询
  function start () {
    const id = timerId++
    timerObj[id] = true
    async function timerFn () {
      if (!timerObj[id]) return
      const { data } = await getData() // 模拟请求
      console.log(data)
      setTimeout(timerFn, 1000)
    }
    timerFn()
  }
  // 暂停
  function stop () {
    timerObj = {}
  }

  start ()

  const botton = document.querySelector("#button")
  let isPlay = true
  botton.addEventListener("click", function(){
    isPlay = !isPlay
    botton.innerHTML = isPlay ? '暂停' : '播放'
    isPlay ? start() : stop()
  }, false)
</script>
</html>
查看原文

赞 41 收藏 34 评论 9

可鱼不是鱼 赞了文章 · 2018-05-07

深入理解js对象的引用

JavaScript 有七种内置类型,其中:

基本类型

• 空值(null)
• 未定义(undefined)
• 布尔值( boolean)
• 数字(number)
• 字符串(string)
• 符号(symbol,ES6 中新增)

引用类型

• 对象(object)

对于基本类型,赋值(=)是值的拷贝,比较(===)的是实际的值,而对于引用类型(Array也是一种Object),赋值(=)是引用地址的拷贝,比较(===)的是引用地址:

注:下面图例中用类似于X000001,X000002表示引用地址,只为了更好的举例说明,并不是真实的值

案例一

const a = '哈哈'
const b = '哈哈'
console.log(a === b) // true

const c = {}
const d = {}
console.log(c === d) // false

注解:

1.a和b是字符串,比较的是值,完全相等
2.c和d是对象,比较的是引用地址,c和d都是一个新对象,方别指向不同的地址,所以不相等

图片描述

案例二

let a = { z: 5, y: 9 }
let b = a
b.z = 6
delete b.y
b.x = 8 
console.log(a) // {z: 6, x: 8}
console.log(a === b) // true

注解:

1.a是对象,b=a是将a的引用地址赋值给b
2.a和b都指向与同一个对象,修改这个对象,a和b都会变化

图片描述

案例三

let a = { z: 5 }
let b = a
b = {z: 6}
console.log(a.z) // 5
console.log(a === b) // false

注解:

1.a是对象,b=a是将a的引用地址赋值给b
2.b = {z: 6}新对象赋值给b,切断了a和b的联系,分别指向于不同的对象

图片描述

总结:(精髓所在)

  1. 只操作(修改,删除,添加)对象的属性,不会与之前对象断开连接(案例二)
  2. 直接操作对象本身,也就是最外层,会和之前的对象断开连接(案例三)
  3. 数组也是对象

案例四

let a = { z: 5, y: {x: 8}, w: {r: 10} }
let b = {...a}
b.z = 6
b.y.x = 9
b.w = {r: 11}
console.log(a) // { z: 5, y: {x: 9}, w: {r: 10}}
console.log(a.y === b.y) // true
console.log(a.w === b.w) // false
console.log(a === b) // false

注解:

1.b = {...a}中,z是基本类型直接拷贝值,y和w是对象,是引用地址的拷贝
2.y是只操作属性,连接不会断开,w操作了本身,生产了一个新对象,连接断开(参考上面的总结)

图片描述
案例四理解之后应该就知道为什么js对象有浅拷贝和深拷贝的区分了

应用

场景:目前有多个用户,每个用户有自己的属性,展示并且可以修改
程序实现(例如vue)

  1. 首先我们将每一个用户都封装成一个单独的模块(.vue),用户初始数据存放在model里面(vuex)
  2. 一般来说修改model里面数据,都需要用它的mutations和actions里面的方式,如果用户属性比较多,每一项都需要去定义一个mutations或actions的话,那开发量是相当的大
  3. 利用对象的引用关系,传过来的数据不和源对象,切断关系,是直接可以操作源对象,组件与组件之间的通信也可以这个实现
  4. 有利也有弊,这种操作起来很简单,但一旦切断他们的联系之后,不好维护,用这种方式需要对对象引用有深入的理解,知道什么时候会断开联系
  5. 个人建议只在这种多个相同组件中使用。

图片描述

附加福利

判断两个对象值是否相等(只是纯值,不管引用地址)
https://segmentfault.com/a/11...

查看原文

赞 20 收藏 14 评论 9

可鱼不是鱼 关注了用户 · 2017-04-26

逍遥乡 @hiory

全栈工程师,目前主要使用java和js语言,负责公司新业务的开发工作

关注 9

可鱼不是鱼 关注了问题 · 2016-12-13

解决javascript 连等赋值问题

javascriptvar a = {n:1};  
var b = a; // 持有a,以回查  
a.x = a = {n:2};  
alert(a.x);// --> undefined  
alert(b.x);// --> {n:2}

请问结果为何是这样?

我的理解是连等赋值从右向左运算的,当a被复制为{n:2}之后,

为什么a.x中的a仍然指向{n:1}?

关注 76 回答 17

可鱼不是鱼 关注了问题 · 2016-07-06

解决javascript中JSON是干嘛的?谁能形象通俗的说一下,本人小白 O.O!

只知道它是一段原生JS代码,具体能干嘛,怎么实现的不知道。

关注 10 回答 8

认证与成就

  • 获得 0 次点赞
  • 获得 0 枚徽章 获得 0 枚金徽章, 获得 0 枚银徽章, 获得 0 枚铜徽章

擅长技能
编辑

(゚∀゚ )
暂时没有

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2015-11-15
个人主页被 5k 人浏览