一、nextTick
1、nextTick的作用
nextTick
接收一个回调函数作为参数,并将这个回调函数延迟到DOM更新之后才执行;
因为vue采用异步更新策略,当监听到数据发生变化的时候不会立即去更新DOM,而是开启一个任务队列,并缓存在同一事件循环中发生的所有数据变更;
这种做法的优点是可以将多次数据更新合并成一次,减少操作DOM的次数。
2、nextTick的实现原理
将传入的回调函数包装成异步任务,异步任务又分为微任务和宏任务,为了尽快执行所以优先选择微任务;
nextTick提供了四种异步方法:Promise.then
、MutilationObserver
、setImmediate
、setTimeout(fn, 0)
3、源码解读
/* 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:设置或者获取对象的某个属性的值)
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绑定
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)
}
}
}
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
}
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进行视图更新
// 订阅者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()
}
}
}
// 观察者的构造函数,接收一个表达式和回调函数
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
四、js instanceof原理
五、洋葱圈模型
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
//调用
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()
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响应式
八、算法
/**
*
* 题目:两数之和
*
* 题目描述: 给定一个整数数组 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];
}
// [].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