影绰

影绰 查看完整档案

上海编辑  |  填写毕业院校  |  填写所在公司/组织 lingmissing.github.io/ 编辑
编辑
_ | |__ _ _ __ _ | '_ \| | | |/ _` | | |_) | |_| | (_| | |_.__/ \__,_|\__, | |___/ 该用户太懒什么也没留下

个人动态

影绰 发布了文章 · 2019-01-08

MutationObserver 监听 DOM 树变化

MutationObserver 是用于代替 MutationEvents 作为观察 DOM 树结构发生变化时,做出相应处理的 API 。为什么要使用 MutationObserver 去代替 MutationEvents 呢,我们先了解一下 MutationEvents

MutationEvents

它简单的用法如下:

document.getElementById('list').addEventListener(
  'DOMSubtreeModified',
  () => {
    console.log('列表中子元素被修改')
  },
  false
)
// Mutation 事件列表
DOMAttrModified // 监听元素的修改
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted // 监听新增
DOMNodeRemoved // 监听删除
DOMNodeInsertedIntoDocument
DOMSubtreeModified // 监听子元素的修改

其中 DOMNodeRemovedDOMNodeInsertedDOMSubtreeModified分别用于监听元素子项的删除,新增,修改(包括删除和新增),DOMAttrModified 是监听元素属性的修改,并且能够提供具体的修改动作。

Mutation Events 遇到的问题

  • IE9 不支持 MutationEvents。Webkit 内核不支持 DOMAttrModified 特性,DOMElementNameChangedDOMAttributeNameChanged 在 Firefox 上不被支持。
  • 性能问题 1. MutationEvents 是同步执行的,它的每次调用,都需要从事件队列中取出事件,执行,然后事件队列中移除,期间需要移动队列元素。如果事件触发的较为频繁的话,每一次都需要执行上面的这些步骤,那么浏览器会被拖慢。 2. MutationEvents 本身是事件,所以捕获是采用的是事件冒泡的形式,如果冒泡捕获期间又触发了其他的 MutationEvents 的话,很有可能就会导致阻塞 Javascript 线程,甚至导致浏览器崩溃。

Mutation Observer

MutationObserver 是在 DOM4 中定义的,用于替代 MutationEvents 的新 API,它的不同于 events 的是,所有监听操作以及相应处理都是在其他脚本执行完成之后异步执行的,并且是所以变动触发之后,将变得记录在数组中,统一进行回调的,也就是说,当你使用 observer 监听多个 DOM 变化时,并且这若干个 DOM 发生了变化,那么 observer 会将变化记录到变化数组中,等待一起都结束了,然后一次性的从变化数组中执行其对应的回调函数。

特点

  • 所有脚本任务完成后,才会运行,即采用异步方式
  • DOM 变动记录封装成一个数组进行处理,而不是一条条地个别处理 DOM 变动。
  • 可以观察发生在 DOM 节点的所有变动,也可以观察某一类变动

目前,Firefox(14+)、Chrome(26+)、Opera(15+)、IE(11+) 和 Safari(6.1+) 支持这个 API。 Safari 6.0 和 Chrome 18-25 使用这个 API 的时候,需要加上 WebKit 前缀(WebKitMutationObserver)。可以使用下面的表达式检查浏览器是否支持这个 API。

const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 监测浏览器是否支持
const observeMutationSupport = !!MutationObserver

如何使用 MutationObserver

在应用中集成 MutationObserver 是相当简单的。通过往构造函数 MutationObserver 中传入一个函数作为参数来初始化一个 MutationObserver 实例,该函数会在每次发生 DOM 发生变化的时候调用。MutationObserver 的函数的第一个参数即为单个批处理中的 DOM 变化集。每个变化包含了变化的类型和所发生的更改。

const mutationObserver = new MutationObserver(mutations => {
  mutations.forEach(mutation => {
    console.log(mutation)
  })
})

创建的实例对象拥有三个方法:

  • observe -开始进行监听。接收两个参数-要观察的 DOM 节点以及一个配置对象。
  • disconnect -停止监听变化。
  • takeRecords -触发回调前返回最新的批量 DOM 变化。

observer 方法

observer 方法指定所要观察的 DOM 元素,以及要观察的特定变动。

const article = document.querySelector('article')
observer.observer(article, {
  childList: true,
  arrtibutes: true
})

上面代码分析:

  1. 指定所要观察的 DOM 元素 article
  2. 指定所要观察的变动是子元素的变动和属性变动。
  3. 将这两个限定条件作为参数,传入observer 对象 observer方法。

disconnect 方法

  • disconnect 方法用来停止观察。发生相应变动时,不再调用回调函数。
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 选择目标节点
const target = document.querySelector('#some-id')
// 创建观察者对象
const observer = new MutationObserver(mutation =>  {
  mutations.forEach(function(mutation) {
    console.log(mutation.type)
  })
})
// 配置观察选项:
const config = { attributes: true, childList: true, characterData: true }
// 传入目标节点和观察选项
observer.observe(target, config)
// 随后,你还可以停止观察
observer.disconnect()

takeRecord 方法

takeRecord 方法用来清除变动记录,即不再处理未处理的变动。

在观察者对象上调用 takeRecords 会返回 其观察节点上的变化记录(MutationRecord)数组。其中 MutationRecord 数组也会作为,观察者初始化时的回调函数的第一个参数。

其包含的属性如下:

  • type 如果是属性发生变化,则返回 attributes.如果是一个CharacterData 节点发生变化,则返回 characterData ,如果是目标节点的某个子节点发生了变化,则返回 childList .
  • target 返回此次变化影响到的节点,具体返回那种节点类型是根据 type 值的不同而不同的,如果 type 为 attributes ,则返回发生变化的属性节点所在的元素节点,如果 type 值为 characterData ,则返回发生变化的这个 characterData 节点.如果 type 为 childList ,则返回发生变化的子节点的父节点.
  • addedNodes 返回被添加的节点
  • removedNodes 返回被删除的节点
  • previousSibling 返回被添加或被删除的节点的前一个兄弟节点
  • nextSibling 返回被添加或被删除的节点的后一个兄弟节点
  • attributeName 返回变更属性的本地名称
  • oldValue 根据 type 值的不同,返回的值也会不同.如果 type 为 attributes,则返回该属性变化之前的属性值.如果 type 为 characterData,则返回该节点变化之前的文本数据.如果 type 为 childList,则返回 null
observer.takeRecord()

MutationObserver 类型

MutationObserver 所观察的 DOM 变动(即上面代码的 option 对象),包含以下类型:

  • childList:子元素的变动
  • attributes:属性的变动
  • characterData:节点内容或节点文本的变动
  • subtree:所有下属节点(包括子节点和子节点的子节点)的变动
想要观察哪一种变动类型,就在 option 对象中指定它的值为 true。
需要注意的是,不能单独观察 subtree 变动,必须同时指定 childList、attributes 和 characterData 中的一种或多种。

除了变动类型,option 对象还可以设定以下属性:

  • attributeOldValue:值为 true 或者为 false。如果为 true,则表示需要记录变动前的属性值。
  • characterDataOldValue:值为 true 或者为 false。如果为 true,则表示需要记录变动前的数据值。
  • attributesFilter:值为一个数组,表示需要观察的特定属性(比如['class', 'str'])。

创建 MutationObserver 并 获取 dom 元素,定义回调数据。

// 获取MutationObserver,兼容低版本的浏览器
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
// 获取dom元素
const list = document.querySelector('ol')
// 创建Observer
const Observer = new MutationObserver((mutations, instance) => {
  console.log(mutations)
  console.log(instance)
  mutations.forEach(mutation => {
    console.log(mutation)
  })
})
  • 子元素的变动
Observer.observe(list, {
  childList: true,
  subtree: true
})
// 追加div标签
list.appendChild(document.createElement('div'))
// 追加文本
list.appendChild(document.createTextNode('foo'))
// 移除第一个节点
list.removeChild(list.childNodes[0])
// 子节点移除创建的div
list.childNodes[0].appendChild(document.createElement('div'))
  • 监测 characterData 的变动
Observer.observe(list, {
  childList: true,
  characterData: true,
  subtree: true
})
// 将第一个子节点的数据改为cha
list.childNodes[0].data = 'cha'
  • 监测属性的变动
Observer.observe(list, {
  attributes: true
})
// 设置节点的属性  会触发回调函数
list.setAttribute('data-value', '111')
// 重新设置属性 会触发回调
list.setAttribute('data-value', '2222')
// 删除属性 也会触发回调
list.removeAttribute('data-value')
  • 属性变动前,记录变动之前的值
Observer.observe(list, {
  attributes: true,
  attributeOldValue: true
})
// 设置节点的属性  会触发回调函数
list.setAttribute('data-value', '111')
// 删除属性
list.setAttribute('data-value', '2222')
  • characterData 变动时,记录变动前的值。
Observer.observe(list, {
  childList: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
// 设置数据 触发回调
list.childNodes[0].data = 'aaa'
// 重新设置数据 重新触发回调
list.childNodes[0].data = 'bbbb'
  • attributeFilter {Array} 表示需要观察的特定属性 比如 ['class', 'src'];
Observer.observe(list, {
  attributes: true,
  attributeFilter: ['data-value']
})
// 第一次设置属性 data-key 不会触发的,因为data-value 不存在
list.setAttribute('data-key', 1)
// 第二次会触发
list.setAttribute('data-value', 1)

案例分析—demo 编辑器

下面我们做一个简单的 demo 编辑器:

  1. 首先给父级元素 ol 设置 contenteditable 让容器可编辑;
  2. 然后构造一个 observer 监听子元素的变化;
  3. 每次回车的时候,控制台输出它的内容;
<div id="demo">
  <ol contenteditable style="border: 1px solid red">
    <li>111111</li>
  </ol>
</div>
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const list = document.querySelector('ol')
const Observer = new MutationObserver((mutations, instance) => {
  mutations.forEach(mutation => {
    if (mutation.type === 'childList') {
      const list_values = [].slice
        .call(list.children)
        .map(node => node.innerHTML)
        .filter(s => s !== '<br>')
      console.log(list_values)
    }
  })
})
Observer.observe(list, {
  childList: true
})

现在我们继续可以做一个类似于 input 和 textarea 中的 valueChange 的事件一样的,监听值变化,之前的值和之后的值,如下代码:

const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const list = document.querySelector('ol')
const Observer = new MutationObserver((mutations, instance) => {
  mutations.forEach(mutation => {
    const enter = {
      mutation: mutation,
      el: mutation.target,
      newValue: mutation.target.textContent,
      oldValue: mutation.oldValue
    }
    console.log(enter)
  })
})

Observer.observe(list, {
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
注意: 对 input 和 textarea 不起作用的。

案例分析—编辑器统计字数

<div
  id="editor"
  contenteditable
  style="width: 240px; height: 80px; border: 1px solid red;"
></div>
<p id="textInput">还可以输入100字</p>
const MutationObserver =
  window.MutationObserver ||
  window.WebKitMutationObserver ||
  window.MozMutationObserver
const editor = document.querySelector('#editor')
const textInput = document.querySelector('#textInput')
const observer = new MutationObserver(mutations => {
  mutations.forEach(function(mutation) {
    if (mutation.type === 'characterData') {
      const newValue = mutation.target.textContent
      textInput.innerHTML = `还可以输入${1000 - newValue.length}字`
    }
  })
})
observer.observe(editor, {
  childList: true,
  attributes: true,
  characterData: true,
  subtree: true,
  characterDataOldValue: true
})
查看原文

赞 3 收藏 2 评论 0

影绰 提出了问题 · 2018-09-03

小程序事件绑定失效

https://img.mukewang.com/5b70...
clipboard.png

使用mpvue框架,如图所示,点击事件绑定失效,请问是什么导致的?

关注 1 回答 0

影绰 提出了问题 · 2018-09-03

小程序事件绑定失效

https://img.mukewang.com/5b70...
clipboard.png

使用mpvue框架,如图所示,点击事件绑定失效,请问是什么导致的?

关注 1 回答 0

影绰 提出了问题 · 2018-07-24

用contenteditable实现编辑器

用contenteditable实现编辑器中,在输入的过程中需要将特定单词高亮
我的操作是获取原来的html,再用正则匹配后直接替换html,但是这样的问题是文本失去了焦点而不是在原来输入的位置
如何处理

关注 1 回答 0

影绰 发布了文章 · 2018-06-22

二叉树的实现

概念

二叉树(Binary Tree)是另一种树型结构,它的特点是每个结点至多只有两棵子树(即二叉树中不存在度大于 2 的结点),并且,二叉树的子树有左右之分(其次序不能任意颠倒。)

性质

  • 二叉树的第 i 层上最多有 2 的(i-1)方个节点。(i>=1)
  • 深度为 k 的树最多有 2 的 k 次方-1 个节点。(k>=1)
  • 对任何一棵二叉树 T,如果其终端结点数为 n0,度为 2 的结点数为 n2,则 n0 = n2 + 1;
    _ 一棵深度为 k 且有 2 的 k 次方-1 个结点的二叉树称为满二叉树
    _ 深度为 k 的,有 n 个结点的二叉树,当且仅当其每一个结点都与深度为 k 的满二叉树中编号从 1 至 n 的结点一一对应时,称之为完全二叉树
    <!-- more -->

初始化二叉树结构

// 创建节点
class Node {
  constructor(key) {
    this.key = key
    this.left = null
    this.right = null
  }
}
// 创建二叉树格式
class BinaryTree {
  constructor(...args) {
    this.root = null
    // 初始化依次插入数据
    args.forEach(key => {
      this.insert(key)
    })
  }
  // 实例化
  static create(args) {
    return new BinaryTree(...args)
  }
}

新增方法

思路:判断插入的节点和当前节点,若大于当前节点,去右子树插入,否则左子树插入。

insert(key) {
  const newNode = new Node(key);
  const insertNode = function(node, newNode) {
    if (newNode.key < node.key) {
      // 如果新节点的值小于老节点的值
      if (node.left === null) {
        // 如果老节点没有左孩子
        node.left = newNode;
      } else {
        // 如果老节点有左孩子,那么讲数据插入到老节点的左孩子
        insertNode(node.left, newNode);
      }
    } else {
      // 如果新节点的值大于老节点
      if (node.right === null) {
        node.right = newNode;
      } else {
        insertNode(node.right, newNode);
      }
    }
  };
  if (this.root === null) {
    // 如果root不存在,将newNode设为根节点
    this.root = newNode
  } else {
    insertNode(this.root, newNode)
  }
}

调用形式

const nodes = [8, 3, 10, 1, 6, 14, 4, 7, 13]
const binaryTree = BinaryTree.create(nodes)
binaryTree.insert(55)
console.log(binaryTree.root)

遍历方法

二叉树的遍历(traversing binary tree)是指从根结点出发,按照某种次序依次访问二叉树中所有结点,使得每个结点被访问一次且仅被访问一次。

前序遍历(DLR)

首先访问根结点,然后遍历左子树,最后遍历右子树。简记根-左-右。
  • 用途:用于复制一颗二叉树
  • 算法思路
    若二叉树为空,则遍历结束;否则 1. 访问根结点 2. 先序遍历左子树(递归调用本算法) 3. 先序遍历右子树(递归调用本算法)
// 前序遍历
preOrderTraverse () {
    const preOrderTraverse = node => {
      if (node !== null) {
        console.log(node.key)
        preOrderTraverse(node.left)
        preOrderTraverse(node.right)
      }
    }
    preOrderTraverse(this.root)
}

中序遍历(LDR)

首先遍历左子树,然后访问根结点,最后遍历右子树。简记左-根-右。
  • 用途:用于从小到大排序二叉树
  • 算法思路
    若二叉树为空,则遍历结束;否则 1. 中序遍历左子树(递归调用本算法) 2. 访问根结点 3. 中序遍历右子树(递归调用本算法)
//使用递归方式实现中序遍历
inOrderTraverse () {
    const inOrderTraverseNode = node => {
      if (node !== null) {
        // 如果当前节点非空,则访问左子树
        inOrderTraverseNode(node.left)
        // 直到访问到最底部的左子树才进入callback,每个节点都会有callback
        console.log(node.key)
        // 此时已经是最底部的左子树
        inOrderTraverseNode(node.right)
      }
      // 解释:首先进入8,发现左边有3,执行左边遍历,遍历完后执行3的回调,在执行3的右边的回调,
    }
    inOrderTraverseNode(this.root)
}

后序遍历(LRD)

首先遍历左子树,然后遍历右子树,最后访问根结点。简记左-右-根。
  • 算法思路
    若二叉树为空,则遍历结束;否则 1. 后序遍历左子树(递归调用本算法); 2. 后序遍历右子树(递归调用本算法) ; 3. 访问根结点 。
postOrderTraverse () {
    const postOrderTraverse = node => {
      if (node !== null) {
        postOrderTraverse(node.left)
        postOrderTraverse(node.right)
        console.log(node.key)
      }
    }
    postOrderTraverse(this.root)
  }

查找算法

查找最大值

思路:传入二叉树,寻找右子树,直到找到不存在右子树的节点。

// 查找最大值
max () {
    const maxNode = node => {
      if (node !== null) {
        if (node.right) {
          return maxNode(node.right)
        } else {
          return node.key
        }
      }
    }
    return maxNode(this.root)
}

查找最小值

思路:传入二叉树,寻找左子树,直到找到不存在左子树的节点。

// 查找最小值
min () {
    const minNode = node => {
      if (node !== null) {
        if (node.left) {
          return minNode(node.left)
        } else {
          return node.key
        }
      }
    }
    return minNode(this.root)
}

查找任意值

思路:根据传入的 key 与当前节点比较,如果大于当前 key 则去右子树查找,否则去左子树查找。

// 查找指定值
search (key) {
  const searchNode = function(node, key) {
    if (node === null) {
      return false
    }
    if (key < node.key) {
      return searchNode(node.left, key)
    } else if (key > node.key) {
      return searchNode(node.right, key)
    } else {
      return true
    }
  }
  return searchNode(this.root, key)
}

删除算法

思路:如果删除的 key 小于当前节点,去左子树种查找,否则去右子树查找。找到节点后,判断是否存在子节点,若不存在,直接删除,若只存在左节点或者右节点,将当前节点替换为它的子节点。若左右节点都存在,将右节点中的最小值(左子树)移除并替换为删除的节点位置(为了满足二叉树的左子树小于右子树)

remove (key) {
  // 用于查找最小节点
  const findMinNode = node => {
    if (node) {
      // 如果node的左孩子存在
      while (node && node.left !== null) {
        // 将node设为node的左孩子再次进入循环
        node = node.left
      }
      // 直到返回没有左孩子的node
      return node
    }
  }
  const removeNode = (node, key) => {
    if (node === null) {
      return false
    }
    if (key < node.key) {
      // 当前node大于删除key,去左孩子中查找
      node.left = removeNode(node.left, key)
      return node
    } else if (key > node.key) {
      // 当前node小于删除key,去右孩子中查找
      node.right = removeNode(node.right, key)
    } else {
      // key和当前node相等
      if (node.left === null && node.right === null) {
        node = null
        return node
      }
      // 任意一边没有值,取另一边
      if (node.left === null) {
        node = node.right
        return node
      } else if (node.right === null) {
        node = node.left
        return node
      }
      // 同时存在左孩子和右孩子
      // 找出右边的最小值
      const aux = findMinNode(node.right)
      // 将最小值替换为删除的key
      node.key = aux.key
      // 在右孩子中删除最小值
      node.right = removeNode(node.right, aux.key)
      return node
    }
  }
  const result = removeNode(this.root, key)
  console.log(result)
}
查看原文

赞 1 收藏 1 评论 0

影绰 关注了标签 · 2018-04-20

关注 2

影绰 关注了标签 · 2018-04-20

promise

Promise 是异步编程的一种解决方案,比传统的解决方案——回调函数和事件——更合理和更强大。它由社区最早提出和实现,ES6 将其写进了语言标准,统一了用法,原生提供了 Promise 对象。

所谓 Promise,简单说就是一个容器,里面保存着某个未来才会结束的事件(通常是一个异步操作)的结果。从语法上说,Promise 是一个对象,从它可以获取异步操作的消息。Promise 提供统一的 API,各种异步操作都可以用同样的方法进行处理。

关注 173

影绰 关注了标签 · 2018-04-20

es6

ECMAScript 6.0(以下简称 ES6)是 JavaScript 语言的下一代标准,已经在 2015 年 6 月正式发布了。它的目标,是使得 JavaScript 语言可以用来编写复杂的大型应用程序,成为企业级开发语言。

标准的制定者有计划,以后每年发布一次标准,使用年份作为版本。因为 ES6 的第一个版本是在 2015 年发布的,所以又称ECMAScript 2015(简称 ES2015)。

2016 年 6 月,小幅修订的《ECMAScript 2016 标准》(简称 ES2016)如期发布。由于变动非常小(只新增了数组实例的includes方法和指数运算符),因此 ES2016 与 ES2015 基本上是同一个标准,都被看作是 ES6。根据计划,2017 年 6 月将发布 ES2017。

标准请参读 ECMAScript® 2015 Language Specification

关注 2564

影绰 关注了专栏 · 2018-04-02

Ethan Li 的技术专栏

全栈工程师的修炼旅途

关注 176

影绰 关注了收藏夹 · 2018-03-22

JavaScript 深入系列

重点讲解了如原型、作用域、执行上下文、变量对象、this、闭包、按值传递、call、apply、bind、new、继承等 JS 语言中的比较难懂的概念。 作者:冴羽

关注 621

认证与成就

  • 获得 162 次点赞
  • 获得 7 枚徽章 获得 0 枚金徽章, 获得 1 枚银徽章, 获得 6 枚铜徽章

擅长技能
编辑

开源项目 & 著作
编辑

(゚∀゚ )
暂时没有

注册于 2016-07-29
个人主页被 999 人浏览