我们都知道vue是数据驱动视图,而vue中视图更新是异步的。在业务开发中,有没有经历过当改变了数据,视图却没有按照我们的期望渲染?而需要将对应的操作放在nextTick中视图才能按照预期的渲染,有的时候nextTick也不能生效,而需要利用setTimeout来解决?
搞清楚这些问题,那么就需要搞明白以下几个问题:
1、vue中到底是如何来实现异步更新视图;
2、vue为什么要异步更新视图;
3、nextTick的原理;
4、nextTick如何来解决数据改变视图不更新的问题的;
5、nextTick的使用场景。
以下分享我的思考过程。
vue中每个组件实例都对应一个watcher实例,它会在组件渲染的过程中把“接触”过的数据属性记录为依赖。之后当依赖项的 setter 触发时,会通知 watcher,从而使它关联的组件重新渲染。
如果对vue视图渲染的思想还不是很清楚,可以参考这篇defineProperty实现视图渲染用defineProty模拟的Vue的渲染视图,来了解整个视图渲染的思想。
但是Vue的视图渲染是异步的,异步的过程是数据改变不会立即更新视图,当数据全部修改完,最后再统一进行视图渲染。
在渲染的过程中,中间有一个对虚拟dom进行差异化的计算过程(diff算法),大量的修改带来频繁的虚拟dom差异化计算,从而导致渲染性能降低,异步渲染正是对视图渲染性能的优化。
/**
* Subscriber interface.
* Will be called when a dependency changes.
*/
update () {
/* istanbul ignore else */
if (this.lazy) {
this.dirty = true
} else if (this.sync) {
this.run()
} else {
queueWatcher(this)
}
}
/**
* Push a watcher into the watcher queue.
* Jobs with duplicate IDs will be skipped unless it's
* pushed when the queue is being flushed.
*/
export function queueWatcher (watcher: Watcher) {
const id = watcher.id
if (has[id] == null) {
has[id] = true
if (!flushing) {
queue.push(watcher)
} else {
// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
i--
}
queue.splice(i + 1, 0, watcher)
}
// queue the flush
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
}
}
当第一次依赖有变化就会调用nextTick方法,将更新视图的回调设置成微任务或宏任务,然后后面依赖更新对应的watcher对象都只是被加入到队列中,只有当nextTick回调执行之后,才会遍历调用队列中的watcher对象中的更新方法更新视图。
这个nextTick和我们在业务中调用的this.$nextTick()是同一个函数。
if (!waiting) {
waiting = true
nextTick(flushSchedulerQueue)
}
flushSchedulerQueue刷新队列的函数,用于更新视图
function flushSchedulerQueue () {
flushing = true
let watcher, id
// Sort queue before flush.
// This ensures that:
// 1. Components are updated from parent to child. (because parent is always
// created before the child)
// 2. A component's user watchers are run before its render watcher (because
// user watchers are created before the render watcher)
// 3. If a component is destroyed during a parent component's watcher run,
// its watchers can be skipped.
queue.sort((a, b) => a.id - b.id)
// do not cache length because more watchers might be pushed
// as we run existing watchers
for (index = 0; index < queue.length; index++) {
watcher = queue[index]
id = watcher.id
has[id] = null
watcher.run()
// in dev build, check and stop circular updates.
if (process.env.NODE_ENV !== 'production' && has[id] != null) {
circular[id] = (circular[id] || 0) + 1
if (circular[id] > MAX_UPDATE_COUNT) {
warn(
'You may have an infinite update loop ' + (
watcher.user
? `in watcher with expression "${watcher.expression}"`
: `in a component render function.`
),
watcher.vm
)
break
}
}
}
那么nextTick到底是个什么东西呢?
vue 2.5中nextTick的源码如下(也可以跳过源码直接看后面的demo,来理解nextTick的用处):
/**
* Defer a task to execute it asynchronously.
*/
export const nextTick = (function () {
const callbacks = []
let pending = false
let timerFunc
function nextTickHandler () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
// An asynchronous deferring mechanism.
// In pre 2.4, we used to use microtasks (Promise/MutationObserver)
// but microtasks actually has too high a priority and fires in between
// supposedly sequential events (e.g. #4521, #6690) or even between
// bubbling of the same event (#6566). Technically setImmediate should be
// the ideal choice, but it's not available everywhere; and the only polyfill
// that consistently queues the callback after all DOM events triggered in the
// same loop is by using MessageChannel.
/* istanbul ignore if */
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
return function queueNextTick (cb?: Function, ctx?: Object) {
let _resolve
callbacks.push(() => {
if (cb) {
try {
cb.call(ctx)
} catch (e) {
handleError(e, ctx, 'nextTick')
}
} else if (_resolve) {
_resolve(ctx)
}
})
if (!pending) {
pending = true
timerFunc()
}
// $flow-disable-line
if (!cb && typeof Promise !== 'undefined') {
return new Promise((resolve, reject) => {
_resolve = resolve
})
}
}
})()
用下面这个demo来感受依赖更新时和nextTick的关系以及nextTick的用处:
function isNative(Ctor) {
return typeof Ctor === 'function' && /native code/.test(Ctor.toString())
}
const nextTick = (function () {
let pending = false;
let callbacks = []
let timerFunc
function nextTickHandler() {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
console.log('timerFunc:', timerFunc)
return function queueNextTick(cb, ctx) {
callbacks.push(() => {
if (cb) {
cb.call(ctx)
}
})
// console.log('callbacks:', callbacks)
if (!pending) {
pending = true
console.log('pending...', true)
timerFunc()
}
}
})()
// 模拟异步视图更新
// 第一次先将对应新值添加到一个数组中,然后调用一次nextTick,将读取数据的回调作为nextTick的参数
// 后面的新值直接添加到数组中
console.time()
let arr = []
arr.push(99999999)
nextTick(() => {
console.log('nextTick one:', arr, arr.length)
})
function add(len) {
for (let i = 0; i < len; i++) {
arr.push(i)
console.log('i:', i)
}
}
add(4)
// console.timeEnd()
// add()
// add()
nextTick(() => {
arr.push(888888)
console.log('nextTick two:', arr, arr.length)
})
add(8)的值之后
console.timeEnd()
在chrome运行结果如下:
可以看到第二个nextTick中push的值最后渲染在add(8)的值之后,这也就是nextTick的作用了,nextTick的作用就是用来处理需要在数据更新(在vue中手动调用nextTick时对应的是dom更新完成后)完才执行的操作。
nextTick的原理:
首先nextTick会将外部传进的函数回调存在内部数组中,nextTick内部有一个用来遍历这个内部数组的函数nextTickHandler,而这个函数的执行是异步的,什么时候执行取决于这个函数是属于什么类型的异步任务:微任务or宏任务。
主线程执行完,就会去任务队列中取任务到主线程中执行,任务队列中包含了微任务和宏任务,首先会取微任务,微任务执行完就会取宏任务执行,依此循环。nextTickHandler设置成微任务或宏任务就能保证其总是在数据修改完或者dom更新完然后再执行。(js执行机制可以看promise时序问题&js执行机制)
为什么vue中对设置函数nextTickHandler的异步任务类型会有如下几种判断?
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
timerFunc = () => {
setImmediate(nextTickHandler)
}
} else if (typeof MessageChannel !== 'undefined' && (
isNative(MessageChannel) ||
// PhantomJS
MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
const channel = new MessageChannel()
const port = channel.port2
channel.port1.onmessage = nextTickHandler
timerFunc = () => {
port.postMessage(1)
}
} else
/* istanbul ignore next */
if (typeof Promise !== 'undefined' && isNative(Promise)) {
// use microtask in non-DOM environments, e.g. Weex
const p = Promise.resolve()
timerFunc = () => {
p.then(nextTickHandler)
}
} else {
// fallback to setTimeout
timerFunc = () => {
setTimeout(nextTickHandler, 0)
}
}
浏览器环境中常见的异步任务种类,按照优先级:
而为什么最后才判断使用setTimeout?
vue中目的就是要尽可能的快地执行回调渲染视图,而setTimeout有最小延迟限制:如果嵌套深度超过5级,setTimeout(回调,0)就会有4ms的延迟。
所以首先选用执行更快的setImmediate,但是setImmediate有兼容性问题,目前只支持Edge、Ie浏览器:
可以用同样执行比setTimeout更快的宏任务MessageChannel来代替setImmediate。MessageChannel兼容性如下:
当以上都不支持的时候,就使用new Promise().then(),将回调设置成微任务,Promise不支持才使用setTimeout。
nextTick就是利用了js机制执行任务的规则,将nextTick的回调函数设置成宏任务或微任务来达到在主线程的操作执行完,再执行的目的。
在vue中主要提供对依赖Dom更新完成后再做操作的情况的支持
当改变数据,视图没有按预期渲染时;都应该考虑是否是因为本需要在dom执行完再执行,然而实际却在dom没有执行完就执行了代码,如果是就考虑使用将逻辑放到nextTick中,有的时候业务操作复杂,有些操作可能需要更晚一些执行,放在nextTick中仍然没有达到预期效果,这个时候可以考虑使用setTimeout,将逻辑放到宏任务中。
基于以上分析,可以列举几个nextTick常用到的使用场景:
// input 定位
scrollToInputBottom() {
this.$nextTick(() => {
this.$refs.accept_buddy_left.scrollTop =
this.$refs.accept_buddy_left.scrollTop + 135
this.$refs.accept_buddy_ipt[
this.$refs.accept_buddy_ipt.length - 1
].$refs.ipt.focus()
})
},
// 监听来自 url 的期数变化,跳到该期数
urlInfoTerm: {
immediate: true,
handler(val) {
if (val !== 0) {
this.$nextTick(function() {
// 计算期数所在位置的高度
this.setCellsHeight()
//设置滚动距离
this.spaceLenght = this.getColumnPositionIndex(
this.list,
)
setTimeout(() => {
this.setScrollPosition(val)
}, 800)
})
}
},
BFC已经是一个耳听熟闻的词语了,网上有许多关于 BFC 的文章,介绍了如何触发 BFC 以及 BFC 的一些用处(如清浮动,防止 margin 重叠等)。BFC直译为\"块级格式化上下文\"。它是一个独立的渲染区域,只有Block-level box参与
作为前端,我们每天都在与CSS打交道,那么CSS的原理是什么呢?开篇,我们还是不厌其烦的回顾一下浏览器的渲染过程,学会使用永远都是最基本的标准,但是懂得原理,你才能触类旁通,超越自我。
做了一些研究,我发现了函数式编程概念,如不变性和纯函数。 这些概念使你能够构建无副作用的功能,而函数式编程的一些优点,也使得系统变得更加容易维护。我将通过 JavaScript 中的大量代码示例向您详细介绍函数式编程和一些重要概念。
如果你阅读过关于Angular 2变化检测的资料,那么你很可能听说过zone。Zone是一个从Dart中引入的特性并被Angular 2内部用来判断是否应该触发变化检测
updateComponent在更新渲染组件时,会访问1或多个数据模版插值,当访问数据时,将通过getter拦截器把componentUpdateWatcher作为订阅者添加到多个依赖中,每当其中一个数据有更新,将执行setter函数
一个继承自 Foo.prototype 的新对象被创建;使用指定的参数调用构造函数 Foo,并将 this 绑定到新创建的对象。new Foo 等同于 new Foo(),也就是没有指定参数时,Foo 不带任何参数调用的情况
Http 缓存机制作为 web 性能优化的重要手段,对于从事 Web 开发的同学们来说,应该是知识体系库中的一个基础环节,同时对于有志成为前端架构师的同学来说是必备的知识技能。
HTTPS = HTTP + TLS/SSL,简单理解 HTTPS 其实就是在 HTTP 上面加多了一层安全层。HTTP 可以是 Http2.0 也可以是 Http1.1,不过现在 Http2.0 是强制要求使用 Https 的。使用非对称密钥(即公钥私钥))和对称密钥)(即共享密钥)相结合
HTTP是无状态协议。例:打开一个域名的首页,进而打开该域名的其他页面,服务器无法识别访问者。即同一浏览器访问同一网站,每次访问都没有任何关系。Cookie的原理是
Promise 必须为以下三种状态之一:等待态(Pending)、执行态(Fulfilled)和拒绝态(Rejected)。一旦Promise 被 resolve 或 reject,不能再迁移至其他任何状态(即状态 immutable)。
内容以共享、参考、研究为目的,不存在任何商业目的。其版权属原作者所有,如有侵权或违规,请与小编联系!情况属实本人将予以删除!