vue的nextTick解析

用过 vue 的同学应该都知道,双向绑定中,更改了数据会去更新dom,但不是马上更新的。
直接跟在修改数据后,拿到的 dom 还是旧的。
vue有个this.$nextTick用法,用这个的回调可以保证拿到更新后的dom。

直接看看 next-tick 源码

import { noop } from 'shared/util'
import { handleError } from './error'
import { isIE, isIOS, isNative } from './env'

export let isUsingMicroTask = false
const callbacks = []
let pending = false

// 清异步队列,全部执行
function flushCallbacks () {
  pending = false
  const copies = callbacks.slice(0)
  callbacks.length = 0
  for (let i = 0; i < copies.length; i++) {
    copies[i]()
  }
}
let timerFunc
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  // 判断有没有 promise 且有没有被重写过,
  // promise完好无损就使用 promise 做异步队列的触发,设置使用微任务标志为true
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  // PhantomJS and iOS 7.x
  // 特殊场景用MutationObserver
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  // Use MutationObserver where native Promise is not available,
  // e.g. PhantomJS, iOS7, Android 4.4
  // (#6466 MutationObserver is unreliable in IE11)
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  // Fallback to setImmediate.
  // Techinically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // 再不济看看有没有 setImmediate,虽然也是宏任务,但总比 setTimeout 强
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  // 实在没办法了,setTimeout 兜底
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// nextTick 具体操作所在,其他队列方法也是用这个,比如 data 改动后通知 dom 修改,也是用这个
// 即 vue 里面的响应式,不是实时更新的,而是存在一个队列,在下一回合进行任务更新。不然太耗损性能
// 也就是解释了,在修改了 data 之后,dom 是没有马上更新,而在 $nextTick 的回调后,则可以看到 dom 更新
// 原因就在于同样的异步任务,$nextTick 的回调,是在 dom 修改的操作后面,所以 $nextTick 能看到 dom 更新
// 以下以 this.$nextTick(()=>{}) 为例子,cb 为回调,ctx 为 vue 实例
export function nextTick (cb?: Function, ctx?: Object) {
  let _resolve
  // 把任务塞进异步队列
  callbacks.push(() => {
    // 有回调就触发回调
    if (cb) {
      try {
        cb.call(ctx)
      } catch (e) {
        handleError(e, ctx, 'nextTick')
      } 
      // 这里注意,结合下面来看
      //用 promise 的 resolve 触发,把 vue 实例当成参数传进去
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 在清任务 flushCallbacks 里赋值 pending 为 false
  // 目的为了 timerFunc 在周期内只触发一次
  // 执行 timerFunc,让其下一周期执行清异步队列任务
  if (!pending) {
    pending = true
    timerFunc()
  }
  // 当没有回调且 Promise 正常
  // $nextTick 返回的是一个 promise,触发时机与上面有回调一样
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

除了常规的回调用法之外,还可以这样用

this.$nextTick().then( vue实例 => { console.log(vue实例 === this) }) // => true

如果不传入回调,则返回promise,因为是 promise,也可以直接就 await 等待。参考官网

接着来个简陋版简单模拟一下这个 nextTick 的实现。这其中还涉及到事件循环的知识。

// 模拟vue nextTick
// 存储所有$nextTick的回调,变量的set修改后通知dom更新的,与$nextTick的回调进入同个微队列
const callbacks = []
// 微队列的触发载体,这里是Promise.resolve()
// vue是先找Promise.resolve,没有就找setTimeout
let run = null

function next() {
  callbacks.forEach((cb) => {
    cb()
  })
}
// 这里的change可以看成修改了变量,里面的回调就是与nextTick的回调如出一辙
function change() {
  nextTick(() => {
    // 这里面是同步
    console.log('update dom -- 1')
    const child = document.createElement('div')
    child.id = 'new'
    document.getElementById('box').appendChild(child)
    console.log('update dom')
    console.log('update dom -- 2')
  })
}

function nextTick(cb) {
  if(!run) {
    // 初始化下个队列
    run = Promise.resolve()
    // 把next方法放在下个队列运行
    // 看上面的next函数,是把callbacks的函数遍历执行
    run.then(next)
  }
  // nextTick的回调加入数组等着,遍历触发里面的每个函数
  // 由于是同步,所以其实只是一个微任务,
  callbacks.push(cb)
}

// 调用了第一次nextTick
nextTick(() => {
  // 表示该次微队列任务开始
  console.log('nextTick -- 1')
})
Promise.resolve().then(() => {
  console.log('then -- ')
})
// 修改变量,更改了dom
change()
// 同步任务,最早打印,但此时没有new元素
console.log('script -- ', document.getElementById('new'))
// 调用了第二次nextTick
nextTick(() => {
  // 在修改变量之后调nextTick,已有new元素
  console.log('nextTick -- 2', document.getElementById('new'))
})
// nextTick -- 2 之所以在 then -- 前打印,是因为回调都放在callbacks里,同步触发了


垂直居中的几个方法

四种方法,先在外面放四个容器, 赋予 “.container” 类。让其子元素居中,先赋予 “.box” 类,水平居中。

<!DOCTYPE>
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>baidu</title>
</head>
<style type="text/css">
.container {
  width: 22%;
  height: 300px;
  float: left;
  background-color: #abcdef;
  margin: 0 10px;
  position: relative;
}
.box {
  width: 100px;
  height: 100px;
  background-color: red;
  text-align: center;
}
<!-- 绝对定位大法,设置为绝对定位,四个方向位移为0,外边距设为auto -->
.middle1 {
  position: absolute;
  left: 0;
  right: 0;
  top: 0;
  bottom: 0;
  margin: auto;
}
<!-- flex弹性布局,特别容易完成这个事 -->
.middle2 {
  display: flex;
  justify-content: center;
  align-items: center;
}
<!-- 利用表格居中,注意的是还要多一层table-cell,真正居中是在table-cell -->
.middle3 {
  display: table-cell;
  vertical-align: middle;
  background-color: initial;
}
.block {
  display: inline-block;
}
<!-- 绝对定位加translate大法,水平垂直位移50%,translate里面的50%是相对于自己的,所以不知宽高也可以使用 -->
.middle4 {
  position: absolute;
  left: 50%;
  top: 50%;
  transform: translateX(-50%) translateY(-50%);
}
</style>
<body>
<div class="container"><div class="box middle1"></div></div>
<div class="container middle2"><div class="box"></div></div>
<div class="container" style="display: table;">
  <div class="box middle3">
    <p class="box block"></p>
  </div>
</div>
<div class="container"><div class="box middle4"></div></div>
</body>
</html>

总结:个人觉得,首先 flex 布局最方便也最帅气。然后绝对定位加 translate 也好理解。至于第一种绝对定位加四个方向位移为0,其实不太理解其原理~ = =!

vue生命周期

对于 vue 的几个生命周期老是记不住,应该还是理解不够透彻,这里记录一下。

代码与注释参考了 segmentfaultimooc 这两篇文章,感谢。

<!DOCTYPE>
<html>
<head>
  <title></title>
  <script type="text/javascript" src="https://cdn.jsdelivr.net/vue/2.1.3/vue.js"></script>
</head>
<body>
<div id="app">
  <p>{{ message }}</p>
  <a @click="change">change</a>
  <a @click="destroy">destroy</a>
  <span v-if="false"></span>
</div>
<script type="text/javascript">
var i = 0
var app = new Vue({
  // render > template > el,但 el 不能缺少
  el: '#app',
  // template: '<div id="app">{{message1}}</div>',
  // render: function(createElement) {
  //   const menu_items = ["首页","搜索","分类","系统"]
  //   return createElement('ul', 
  //     menu_items.map(item => {
  //       return createElement('li', {
  //         class: {
  //           'uk-nav': true
  //         },
  //         domProps: {
  //           innerHTML: item
  //         }
  //       })
  //     })
  //   )
  // },
  data: {
    message : 'init',
    message1 : 'ok'  
  },
  methods: {
    change () {
      this.message = 'ok' + i++
    },
    destroy () {
      this.$destroy()
    }
  },
  beforeCreate: function () {
    // 创建实例前,此时虚拟dom和属性全都拿不到
    // 主要就是给vm对象添加了 $parent、$root、$children 属性,以及一些其它的生命周期相关的标识
    // 初始化事件相关的属性
    // vm 添加了一些虚拟 dom、slot 等相关的属性和方法
    console.info('beforeCreate 创建前状态===============》')
    console.log('%c%s', 'color:red' , 'el     : ' + this.$el) //undefined
    console.log('%c%s', 'color:red','data   : ' + this.$data) //undefined 
    console.log('%c%s', 'color:red','message: ' + this.message) //undefined 
  },
  created: function () {
    // 初始化,可以拿到属性,应该是完成了数据劫持,但dom依旧拿不到
    // props、methods、data、watch、computed等数据初始化
    console.info('created 创建完毕状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el) //undefined
    console.log('%c%s', 'color:red','data   : ' + JSON.stringify(this.$data)) //已被初始化 
    console.log('%c%s', 'color:red','message: ' + this.message) //已被初始化
  },
  beforeMount: function () {
    // 根据el和template属性来初始化dom
    console.info('beforeMount 挂载前状态===============》')
    console.log('%c%s', 'color:red','el     : ' + (this.$el)) //已被初始化
    // 这里的dom还没进行模板替换,也就是还显示着 {{message}} 这种占位符,span 元素也还在
    // 这里就算有 template render,也是出现 outer html
    console.log(this.$el)
    console.log('%c%s', 'color:red','data   : ' + this.$data) //已被初始化  
    console.log('%c%s', 'color:red','message: ' + this.message) //已被初始化 
  },
  mounted: function () {
    // 完成挂载,{{message}} 被数据替换,span 元素也被移除
    console.info('mounted 挂载结束状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el) //已被初始化
    console.log(this.$el)    
    console.log('%c%s', 'color:red','data   : ' + this.$data) //已被初始化
    console.log('%c%s', 'color:red','message: ' + this.message) //已被初始化 
  },
  beforeUpdate: function () {
    console.info('beforeUpdate 更新前状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el)
    // 这里的dom还是旧的
    console.log(this.$el)   
    console.log('%c%s', 'color:red','data   : ' + JSON.stringify(this.$data))
    // 数据已更新,,这里修改数据会引起死循环
    console.log('%c%s', 'color:red','message: ' + this.message)
  },
  updated: function () {
    console.info('updated 更新完成状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el)
    // dom已更新,这里修改数据会引起死循环
    console.log(this.$el) 
    console.log('%c%s', 'color:red','data   : ' + this.$data) 
    console.log('%c%s', 'color:red','message: ' + this.message)
  },
  beforeDestroy: function () {
    // 所有东西还在
    console.info('beforeDestroy 销毁前状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el)
    console.log(this.$el)    
    console.log('%c%s', 'color:red','data   : ' + this.$data) 
    console.log('%c%s', 'color:red','message: ' + this.message)
    this.message = 'destroy'
  },
  destroyed: function () {
    // 数据绑定被卸除,监听被移出,子实例也统统被销毁,dom保留着
    console.info('destroyed 销毁完成状态===============》')
    console.log('%c%s', 'color:red','el     : ' + this.$el)
    console.log(this.$el)  
    console.log('%c%s', 'color:red','data   : ' + this.$data) 
    console.log('%c%s', 'color:red','message: ' + this.message)
    setTimeout(() => {
      // 实例还在
      console.log('vue 实例===============》', app)
    }, 1000)
  }
})
</script>
</body>
</html>

判断变量类型的几个方法

前置

function Person() {}
const p = new Person()

一、 typeof

console.log(typeof []) // object
console.log(typeof {}) // object
console.log(typeof function(){}) // function
console.log(typeof '') // string
console.log(typeof 1) // number
console.log(typeof true) // boolean
console.log(typeof undefined) // undefined
console.log(typeof null) // object
console.log(typeof NaN) // number
console.log(typeof /abc/) // object
console.log(typeof Symbol()) // symbol
console.log(typeof new Date()) // object
console.log(typeof new Map()) // object
console.log(typeof new Set()) // object
console.log(typeof Person) // function
console.log(typeof p) // object

区分不了更精细的对象,比如数组和后面三个也被判断为对象。虽说数组和函数也是对象的一种。但函数都有自己的姓名。
参考了 w3help 上的资料来看,标准如此:

最新的 标准 图是这样子:

PS:
__native object:__原生对象,ECMAScript 实现中,并非由宿主环境,而是完全由本规范定义其语义的对象。这些引用类型在运行过程中需要通过new来创建所需的实例对象。如:Object、Array、Date、RegExp、Function、Boolean、Number、String。(听起来就是语言标准自带的)
__built-in object:__内置对象,由 ECMAScript 实现提供,独立于宿主环境的对象,ECMAScript 程序开始执行时就存在。内置对象是本地对象的子集。包含:Global和Math、JSON。
__host object:__宿主对象,由宿主环境提供的对象,用于完善 ECMAScript 执行环境,如 Window 和 Document。

看起来数组属于 native object,同时没有 implement [[Call]] (没有这个内置属性?),所以返回会是个对象。

不过如果不需要判断得那么精细,那也够用了。而且这个可以直接 typeof 一个没有定义过的变量,会返回undefined。或许有些地方也可以利用起来。
另,可以看下 segmentfault 大佬对于 typeof(null) === 'object' 的回答。

二、 constructor

console.log([].constructor.name) // Array
console.log({}.constructor.name) // Object
console.log(function(){}.constructor.name) // Function
console.log(''.constructor.name) // String
const number = 1
console.log(number.constructor.name) // Number
console.log(true.constructor.name) // Boolean
// console.log(undefined.constructor.name) // 报错
// console.log(null.constructor.name) // 报错
console.log(NaN.constructor.name) // Number
console.log(/abc/.constructor.name) // RegExp
console.log(Symbol().constructor.name) // Symbol
console.log(new Date().constructor.name) // Date
console.log(new Map().constructor.name) // Map
console.log(new Set().constructor.name) // Set
console.log(Person.constructor.name) // Function
console.log(p.constructor.name) // Person

只有 constructor 也行,返回就是这个这个变量的原型。

需要注意的是实例 p 返回的是 Person 这个原型。

三、 instanceof

先看 定义。从名字上看,该变量是否为某原型的实例。

console.log([] instanceof Array) // true
console.log([] instanceof Object) // true
console.log({} instanceof Object) // true
console.log(function(){} instanceof Function) // true
console.log('' instanceof String) // false
console.log(1 instanceof Number) // false
console.log(true instanceof Boolean) // false
// console.log(undefined instanceof undefined)
// 报错:Right-hand side of 'instanceof' is not an object
// console.log(null instanceof null)
// 报错:Right-hand side of 'instanceof' is not an object
console.log(NaN instanceof Number) // false
console.log(/abc/ instanceof RegExp) // true
console.log(Symbol() instanceof Symbol) // false
console.log(new Date() instanceof Date) // true
console.log(new Map() instanceof Map) // true
console.log(new Set() instanceof Set) // true
console.log(Person instanceof Function) // true
console.log(p instanceof Person) // true
console.log(p instanceof Object) // true

[]既是数组的实例,又是对象的实例。

可参阅此文章介绍,这里直接摘取下里面的翻译与代码

function instance_of(L, R) {//L 表示左表达式,R 表示右表达式
  var O = R.prototype;// 取 R 的显示原型
  L = L.__proto__;// 取 L 的隐式原型
  while (true) {
    if (L === null)
      return false;
    if (O === L)// 这里重点:当 O 严格等于 L 时,返回 true
      return true;
    L = L.__proto__;
  }
}
// 上面的代码就很好地解释了,根据原型链的知识,我们知道以下两条等式成立,所以 [] 也是 Object 的实例
[].__proto__ === Array.prototype
Array.prototype.__proto__ === Object.prototype

这时候又出现了个问题,NaN instanceof Number === false…真是没完没了。按照上面的方法来应该是true的。
其实这个可以跟上面的 '' instanceof String === false 1 instanceof Number === false true instanceof Boolean === false 一起看。
NaN也是一个数字值,跟1、2、3这种一样。上面那样写就是返回 false,写出 new 出来的实例就为 true。比如:

const num = new Number(1)
console.log(num instanceof Number) // true

个人理解:这里的 num 是 Number 的一个实例,但其实它是对象。实例首先肯定是个对象。比如 1 ‘’ true,都是基础类型。上面的方法是站在真正的实例对象来判断,是一个简单化步骤的方法。
但标准里面说的,还有调用内部方法与内部属性,当判断到不是对象,其实就返回 false 了。这里与 typeof 联系起来,typeof 不是 object 的,用 instanceof 都是 false 了

四、 Object.prototype.toString.call

console.log(Object.prototype.toString.call([])) // [object Array]
console.log(Object.prototype.toString.call({})) // [object Object]
console.log(Object.prototype.toString.call(function(){})) // [object Function]
console.log(Object.prototype.toString.call('')) // [object String]
console.log(Object.prototype.toString.call(1)) // [object Number]
console.log(Object.prototype.toString.call(true)) // [object Boolean]
console.log(Object.prototype.toString.call(undefined)) // [object Undefined]
console.log(Object.prototype.toString.call(null)) // [object Null]
console.log(Object.prototype.toString.call(NaN)) // [object Number]
console.log(Object.prototype.toString.call(/abc/)) // [object RegExp]
console.log(Object.prototype.toString.call(Symbol())) // [object Symbol]
console.log(Object.prototype.toString.call(new Date())) // [object Date]
console.log(Object.prototype.toString.call(new Map())) // [object Map]
console.log(Object.prototype.toString.call(new Set())) // [object Set]
console.log(Object.prototype.toString.call(Person)) // [object Function]
console.log(Object.prototype.toString.call(p)) // [object Object]
console.log([1,2,3,4].toString()) // 1,2,3,4

这个就厉害了,居然精确到每一个都不一样。细看跟 typeof 还有点像,但是精细很多。所以一定也是有个什么内置属性在记录着这个东西。
要写成 Object.prototype.toString.call 这个样子,是因为别的原型重写了该方法,比如最后一个数组的 toString 方法返回就不一样了。
在控制台打印一个数组看看,就能清晰看到 Array 本身也有一个 toString 方法。

简而言之,分两种,一种是对象内置了 Symbol.toStringTag 这个属性,来返回 [object ${tag}] tag 部分的值。
另外一种是没有这个内置属性,但是 语言标准tc39 为其指定了返回 tag,比如:Array String 等这几个老面孔。
这也就意味着可以自定义自己的tag。


console.log(Promise.prototype[Symbol.toStringTag]) // Promise
console.log(Array.prototype[Symbol.toStringTag]) // undefined

function Tag() {}
Tag.prototype[Symbol.toStringTag] = 'newTag'
const t = new Tag()
console.log(Object.prototype.toString.call(t)) // [object newTag]

function Tag1() {}
Tag1.prototype[Symbol.toStringTag] = {}
const t1 = new Tag1()
console.log(Object.prototype.toString.call(t1)) // [object Object]
// 从上面 tc39 的介绍14,15,16来看,当 [Symbol.toStringTag] 不为 string,则将内置 tag 设置为 Object

里面还涉及其他知识,可以参阅 知乎MDN-Symbol.toStringTagMDN-toString

码后感:几个判断类型方法,居然有这么多细节点在里面。平时可能用了就用了,都不知道其中原理。真是路漫漫啊。。。

遍历属性的几个方法

这里列出五个

  • for in
  • Object.keys
  • Object.getOwnPropertyNames
  • Object.getOwnPropertySymbols
  • Reflect.ownKeys
Object.prototype.allattr = true
Object.prototype.allFn = function(){}

// 普通对象
const obj1 = {
  a: 1,
  b: 2,
  [Symbol('c')]: 3
}
Object.defineProperty(obj1, 'd', {
  enumerable: false,
  value: 4
})

// 原型对象
function Obj2 () {
  this.a = 1
  this[Symbol('b')] = 1
}
Obj2.prototype.c = 2
Obj2.prototype.d = function() {}
Obj2.e = 1

// 对象实例
const obj3 = new Obj2()

function show(obj) {
  let arr = []
  // 会打出原型的属性,可以用 hasOwnProperty 去除
  for (const attr in obj) {
    arr.push(attr)
  }
  console.log('--- for in ---', arr)

  arr = []
  // 不会打出原型的属性,相当于 for in 用 hasOwnProperty 去除
  Object.keys(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.keys ---', arr)

  arr = []
  // 上面两个只打出可枚举的,这个不可枚举也可以打出来,不会打出原型的属性
  Object.getOwnPropertyNames(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.getOwnPropertyNames ---', arr)

  arr = []
  // 打出 Symbols 属性
  Object.getOwnPropertySymbols(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Object.getOwnPropertySymbols ---', arr)

  arr = []
  // Reflect.ownKeys = Object.getOwnPropertyNames + Object.getOwnPropertySymbols
  Reflect.ownKeys(obj).forEach((attr) => {
    arr.push(attr)
  })
  console.log('--- Reflect.ownKeys ---', arr)
}

show(obj1)
console.log('%c --------------- 分割线 ---------------', 'background:#aaa;color:red')
show(Obj2)
console.log('%c --------------- 分割线 ---------------', 'background:#aaa;color:red')
show(obj3)