Skip to content

一、nextTick

1、nextTick的作用

nextTick接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新之后才执行;

因为vue采用异步更新策略,当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更;

这种做法的优点是可以将多次数据更新合并成一次,减少操作DOM的次数。

2、nextTick的实现原理

将传入的回调函数包装成异步任务,异步任务又分为微任务和宏任务,为了尽快执行所以优先选择微任务;

nextTick提供了四种异步方法:Promise.thenMutilationObserversetImmediatesetTimeout(fn, 0)

3、源码解读

js
/* globals MutationObserver */

import { noop } from 'shared/util' // 没有操作的空函数
import { handleError } from './error' // 错误处理函数
import { isIE, isIOS, isNative } from './env' // 判断是否为IE、IOS环境,是否为原生函数

export let isUsingMicroTask = false // 标记nextTick是否以微任务执行

const callbacks: Array<Function> = [] // 存放待调用的传入到nextTick中的回调函数
let pending = false // 标记是否已经向任务队列中添加了一个任务,如果已经添加了就不能再添加了
// 当向任务队列中添加了任务时,将 pending 置为 true,当任务被执行时将 pending 置为 false

/**
 * 通过 flushCallbacks 函数遍历 callbacks 数组的拷贝并执行其中的回调
 */
function flushCallbacks() {
  pending = false
  const copies = callbacks.slice(0) // 拷贝一份callbacks,避免nextTick中又有nextTick
  callbacks.length = 0 // 清空callbacks
  for (let i = 0; i < copies.length; i++) { // 遍历执行传入的回调
    copies[i]()
  }
}

/**
 * 判断当前环境支持的异步方法,优先选择微任务
 * 优先级:Promise --》 MutilationObserver --》 setImmediate --》 setTimeout
 * setImmediate 在 IE10 和 node 中支持
 *
 * 如果多次调用nextTick,会依次执行下面的方法,将nextTick的回调放在callbacks数组中
 * 当在同一轮事件循环中多次调用nextTick时,timerFunc只会执行一次
 */
let timerFunc

// 判断当前环境是否支持原生的promise,在IOS >= 9.3.3 的版本中,MutationObserver会有重大问题,优先使用promise
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    // 用 promise.then 把 flushCallbacks 函数包裹成一个异步微任务
    p.then(flushCallbacks)
    // 这里的 setTimeout 是用来强制刷新微任务队列的
    // 因为在 ios 下 promise.then 后面没有宏任务的话,微任务队列不会刷新
    if (isIOS) setTimeout(noop)
  }
  // 标记当前 nextTick 使用的微任
  isUsingMicroTask = true
} else if (
  // 如果不支持 promise,就判断是否支持 MutationObserver
  // 不是IE环境,并且原生支持 MutationObserver,那也是一个微任务
  !isIE &&
  typeof MutationObserver !== 'undefined' &&
  (isNative(MutationObserver) ||
    // PhantomJS and iOS 7.x
    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
  // new 一个 MutationObserver 类
  const observer = new MutationObserver(flushCallbacks)
  // 创建一个文本节点
  const textNode = document.createTextNode(String(counter))
  // 监听这个文本节点,当数据发生变化就执行flushCallbacks
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    // 变更创建的文本节点,更新DOM,执行flushCallbacks
    textNode.data = String(counter)
  }
  // 标记当前 nextTick 使用的微任
  isUsingMicroTask = true
} else if (
  // 判断当前环境是否原生支持 setImmediate
  typeof setImmediate !== 'undefined' && isNative(setImmediate)
) {
  // Fallback to setImmediate.
  // Technically it leverages the (macro) task queue,
  // but it is still a better choice than setTimeout.
  // setImmediate 会在主线程执行完后立刻执行,setTimeout可能会有一定的延迟
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

export function nextTick(): Promise<void>
export function nextTick<T>(this: T, cb: (this: T, ...args: any[]) => any): void
export function nextTick<T>(cb: (this: T, ...args: any[]) => any, ctx: T): void
/**
 * 声明 nextTick 函数,接收一个回调函数和一个执行上下文作为参数
 * 回调的 this 自动绑定到调用它的实例上
 */
export function nextTick(cb?: (...args: any[]) => any, ctx?: object) {
  let _resolve
  // 将传入的回调函数存放到数组中,后面会遍历执行其中的回调
  callbacks.push(() => {
    if (cb) { // 对传入的回调进行 try catch 错误捕获
      try {
        cb.call(ctx)
      } catch (e: any) { // 进行统一的错误处理
        handleError(e, ctx, 'nextTick')
      }
    } else if (_resolve) {
      _resolve(ctx)
    }
  })
  // 如果当前没有在 pending 的回调, 就执行 timeFunc 函数选择当前环境优先支持的异步方法
  if (!pending) {
    pending = true
    timerFunc()
  }

  // 如果没有传入回调,并且当前环境支持 promise,就返回一个 promise
  // 在返回的这个 promise.then 中 DOM 已经更新好了
  if (!cb && typeof Promise !== 'undefined') {
    return new Promise(resolve => {
      _resolve = resolve
    })
  }
}

二、vue响应式依赖收集

vue2.x:使用Object.defineProperty()来实现对对象的监听。

vue3.x:使用proxy实现。

1、Object.defineProperty(obj, prop, descriptor)

Object.defineProperty(obj, prop, descriptor)中

obj:要定义属性的对象

prop:要定义或修改的属性名称

descriptor:要定义或修改的属性描述符(configurable:可改变;writable:可写的:enumerable:可枚举的;get\set:设置或者获取对象的某个属性的值)

js
const data = {}
const name = 'zhangsan'
Object.defineProperty(data, 'name', {
    writable: true,
    configurable: true,
    get: function () {
        console.log('get')
        return name
    },
    set: function (newVal) {
        console.log('set')
        name = newVal
    }
})

当把一个普通的JavaScript对象传入Vue实例作为data选项,Vue将遍历此对象所有的property,并使用Object.defineProperty把这些property全部转换为getter/setter。

2、响应式原理Observer

Observer类是将每个目标对象,即data的键值转换为getter/setter形式,用于进行依赖收集以及调度更新。

  • observe实例绑定在data的属性上,防止重复绑定;
  • 若data为数组,先实现对应的变异方法,Vue重写了数组的7中原生方法(push,pop、shift、unshift、split、sort、reverse),再将数组的每个成员进行observe,使之称为响应式数据
  • 否则执行walk()方法,遍历data所有的数据,进行getter/setter绑定
js
class Observer{
  constructor(public value: any, public shallow = false, public mock = false) {
    if (isArray(value)) { // 如果是数组对象,按照重写的原生方法对数组中的成员进行响应式处理
      if (!mock) {}
      if (!shallow) {
        this.observeArray(value)
      }
    } else {
      const keys = Object.keys(value)
      for (let i = 0; i < keys.length; i++) {
        const key = keys[i]
        // 否则就按照对象的形式做响应式处理
        defineReactive(value, key, NO_INITIAL_VALUE, undefined, shallow, mock)
      }
    }
  }
    
  observeArray(value: any[]) {
    for (let i = 0, l = value.length; i < l; i++) {
      observe(value[i], false, this.mock)
    }
  }
}
js
export function defineReactive(
  obj: object,
  key: string,
  val?: any,
  customSetter?: Function | null,
  shallow?: boolean,
  mock?: boolean,
  observeEvenIfShallow = false
) {
  const dep = new Dep()

  Object.defineProperty(obj, key, {
    enumerable: true,
    configurable: true,
    get: function reactiveGetter() {
      // 向dep中添加对象
      dep.depend({
        target: obj,
        type: TrackOpTypes.GET,
        key
      })
      return isRef(value) && !shallow ? value.value : value
    },
    set: function reactiveSetter(newVal) {
      childOb = shallow ? newVal && newVal.__ob__ : observe(newVal, false, mock)
      // 触发对象的响应式
      dep.notify({
        type: TriggerOpTypes.SET,
        target: obj,
        key,
        newValue: newVal,
        oldValue: value
      })
    }
  })

  return dep
}
js
export function observe(
  value: any,
  shallow?: boolean,
  ssrMockReactivity?: boolean
): Observer | void {
    return new Observer(value, shallow, ssrMockReactivity)
}

缺点:

  • 对于复杂的对象需要深度监听,递归到底,一次性计算量大
  • 无法监听新增属性和删除属性,vue增加Vue.set和Vue.delete
  • 无法监听数组,需要特殊处理

3、依赖收集Watcher、Dep

依赖收集:收集只在实际页面中用到的data数据

被Oberver的data在触发getter的时候,Dep就会收集依赖,然后打上标记:Dep.target

Watcher是一个观察者对象,依赖收集以后的watcher对象保存在Dep的subs中,数据变动的时候Dep会通知watcher实例,然后由watcher实例回调cb进行视图更新

js
// 订阅者Dep,存放观察者对象
export default class Dep {
  static target?: DepTarget | null
  id: number
  subs: Array<DepTarget | null>
  // pending subs cleanup
  _pending = false

  constructor() {
    this.id = uid++
    this.subs = []
  }

  // 添加观察者对象
  addSub(sub: DepTarget) {
    this.subs.push(sub)
  }

  // 依赖收集,当存在Dep.target的时候添加观察者对象
  depend(info?: DebuggerEventExtraInfo) {
    if (Dep.target) {
      Dep.target.addDep(this)
    }
  }

  // 通知所有watcher对象更新视图
  notify(info?: DebuggerEventExtraInfo) {
    // stabilize the subscriber list first
    const subs = this.subs.filter(s => s) as DepTarget[]
    for (let i = 0, l = subs.length; i < l; i++) {
      const sub = subs[i]
      if (__DEV__ && info) {
        sub.onTrigger &&
          sub.onTrigger({
            effect: subs[i],
            ...info
          })
      }
      // 更新视图
      sub.update()
    }
  }
}
js
// 观察者的构造函数,接收一个表达式和回调函数
class Watcher {
  constructor(vm, expOrFn, cb) {
    this.vm = vm
    this.getter = parsePath(expOrFn)
    this.cb = cb
    this.value = this.get()
  }

  // watcher 实例触发值读取时,将依赖收集的目标对象设置成自身,
  // 通过 call 绑定当前 Vue 实例进行一次函数执行,在运行过程中收集函数中用到的数据
  // 此时会在所有用到数据的 dep 依赖管理中插入该观察者实例
  get() {
    Dep.target = this
    const value = this.getter.call(this.vm, this.vm)
    // 函数执行完毕后将依赖收集目标清空,避免重复收集
    Dep.target = null
    return value
  }

  // dep 依赖更新时会调用,执行回调函数
  update() {
    const oldValue = this.value
    this.value = this.get()
    this.cb.call(this.vm, this.value, oldValue)
  }
}

三、js 模块化 commonjs 和 esmodel

Commonjs 和 Es Module

四、js instanceof原理

instanceof 实现

五、洋葱圈模型

js
class _Koa {
    constructor() {
        this._middlewareList = []
        this._isRunning = false
        this._currentIndex = 0
        //调用下一个中间件
        this._next = async () => {
            this._currentIndex++
            await this._runMiddleware()
        }
    }

    // 添加中间件
    use(middleware) {
        this._middlewareList.push(middleware)
    }
    run() {
        if (this._isRunning || !this._middlewareList.length) {
            return
        }
        this._isRunning = true
        //将任务取出来运行
        this._runMiddleware()
    }
    async _runMiddleware() {
        if (this._currentIndex >= this._middlewareList.length) {
            //任务完成
            this._reset()
            return
        }
        const middleware = this._middlewareList[this._currentIndex]
        let i = this._currentIndex
        await middleware(this._next)
        let j = this._currentIndex
        //如果没有调用next,就任务完成后自行调用
        if (i == j) {
            this._next()
        }
    }
    _reset() {
        this._currentIndex = 0
        this._isRunning = false
        this._middlewareList = []
    }
}

module.exports = _Koa
js
//调用
const _Koa = require('./_Koa')

const t = new _Koa()

t.use(async (next) => {
    console.log('1 start')
    await next()
    console.log('1 end')
})

t.use(async (next) => {
    await next()
    console.log('2 end')
})

t.use(() => {
    console.log('3')
})
t.run()
js
PS D:\Code\Source\common-question\my-koa> node ./index.js
1 start
3
2 end
1 end
PS D:\Code\Source\common-question\my-koa>

六、flex

七、CSS响应式

八、算法

js
/**
 *
 * 题目:两数之和
 *
 * 题目描述: 给定一个整数数组 nums 和一个目标值 target,请你在该数组中找出和为目标值的那两个整数,并返回它们的数组下标。
 *
 * 你可以假设每种输入只会对应一个答案。但是,数组中同一个元素不能使用两遍。
 */

function sumIndex(nums, target) {
    for (let i = 0; i < nums.length; i++) {
        const curNum = nums[i];
        const resultNum = target - curNum;
        const index = nums.indexOf(resultNum);
        if (index > -1 && i !== index) {
            return [i, index];
        }
    }
    return [-1, -1];
}


function sumIndexMap(nums, target) {
    const m = new Map();
    for (let i = 0; i < nums.length; i++) {
        if (m.has(nums[i])) {
            return [m.get(nums[i]), i]
        }
        m.set(target - nums[i], i)
    }
    return [-1, -1];
}
js
// [].filter(() => {})

function myFilter(fn) {
    const newArr = [];
    const curArr=  this;
    for (let i = 0; i < curArr.length - 1; i++) {
        if (fn(curArr[i], i, curArr)) {
            newArr.push(curArr[i]);
        }
    }
    return newArr;
}

Array.prototype.myFilter = myFilter