最近遇到的面试题
时间:2023-04-19 03:07:00
目录
1.promise的打印
2.setTimeout设置为0 为什么不能马上执行?
3.git 只merge一个commit的方法
4.for in 和 for of 、foreach 的区别
5.Promise原型方法then、catch、finally
1 Promise原型方法
2 Promise.prototype.then()
3 Promise.prototype.catch()
4 Promise.prototype.finally()
6.浏览器垃圾回收机制
7.vue3 watch和watchEffect使用和区别是什么?
1.watch侦听器
2.watchEffect
8.react中useEffect和useLayoutEffect的区别
9.Vue中模渲染原理
1.promise的打印
console.log(1) new Promise(resolve => { console.log(2) resolve(3) }).then(console.log).then(() => { console.log(4) }) Promise.resolve('5').then(res => { console.log(res) })
正确答案是什么? 1 2 3 5 4
看下面的打印
console.log(1); function a() { console.log('bbb'); return function () { console.log('cccccc') } } new Promise(resolve => { console.log(2) resolve(3) }).then(console.log).then(() => { console.log(4) }) setTimeout(() => { console.log(6) Promise.resolve(7).then(res => { console.log(res) }) }); setTimeout(a(),0) Promise.resolve('5').then(res => { console.log(res) })
答案是 1 2 bbb 3 5 4 6 7 cccccc
在这里,顺便说一下面试中遇到的另一个问题,如何让settimeout立即执行函数,我的答案是写a函数;
2.setTimeout设置为0 为什么不能马上执行?
setTimeout(function(){}, timer) 指延迟执行。第一个参数是回调函数,第二个参数是回调函数延迟多久。
setTimeout(function(){console.log(1);}, 0); console.log(2); //输入是 2 ,1
setTimeout(fn, 0)的意思是指定一个任务在主线程的最早空闲时间执行,也就是说,在当前代码执行(执行栈清空)后,尽可能早地执行。它在任务队列的末尾添加了一个事件,所以它将在同步任务和任务队列现有事件完成之前执行。
HTML5标准规定了setTimeout()第二个参数的最小值不得小于4毫秒,如果低于4毫秒。在此之前。旧版本的浏览器将最短时间设置为10毫秒。另外,对于那些DOM通常间隔16页面重新渲染的部分)通常间隔16毫秒执行。这时使用requestAnimationFrame()效果好于setTimeout();
注意:setTimeout()只将事件插入任务队列,必须等待当前代码(执行栈)执行,主线程才能执行其指定的回调函数。如果当前代码消耗时间长,可能要等很久,所以不能保证回调函数会在setTimeout()执行指定时间。所以,setTimeout()第二个参数表示最少时间,而不是确切时间。
3.git 只merge一个commit的方法
eg:说实话,这个问题真的让我的大脑停止了思考,真的是无卷啊
gil log 来查看commit的记录
git cherry-pick e43a6
4.for in 和 for of 、foreach 的区别
- for...in 循环:只能得到对象的键名,不能得到键值,for...of 循环:允许通历获得键值
- 对于普通对象,没有本地部署 iterator 直接使用接口for...of 会报错
- for...in 循环不仅是遍历数字键名,还有手动添加的其他键,甚至是原型链上的键。for...of 则不会这样
-
forEach 循环无法中途跳出,break 命令或 return 命令无效
-
for...of 循环可以与break、continue 和 return 配合使用,跳出循环,for...in可以使用breack和continue不能使用return,否则会报错
-
无论是 for...in 还是 for...of 不能遍历出 Symbol 类型值,遍历 Symbol 需要使用类型值 Object.getOwnPropertySymbols() 方法
总之,for...in 循环主要是为遍历对象而生,不适用于遍历数组
for...of 循环可用于数组数组、类数组对象、字符串、Set、Map 以及 Generator 对象
5.Promise原型方法then、catch、finally
原文链接: https://www.jianshu.com/p/79327bd1511f
1 Promise原型方法
Promise
构造函数的原型上实现了then
, catch
, finally
方法,也就意味着每个 Promise
实例都拥有这样 3 个函数。
另外提一下 Promise
构造函数上实现了all
, race
, reject
, resolve
, allSettled
, any
方法,==这些方法只属于 Promise 构造函数自己,是不能在实例中被调用的==(也就是只会以这种形式出现:如 Promise.all()
)
2 Promise.prototype.then()
then()
方法可以处理 Promise
被接受或拒绝的情况。
参数
then()
最多可以接受两个参数 (回调函数):
① onFulfilled
当 Promise
变成接受状态 (fulfilled
) 时调用的函数。该函数有一个参数,即接受的最终结果 (the fulfillment value)。
// then()方法的第一个参数,用来处理接受状态的Promise
Promise.resolve(1)
.then((res) => {
console.log(res) //1
})
如果该参数 (指的是onFulfilled
) 不是函数,则会在内部被替换为 (x) => x
,即原样返回 promise 最终结果 (后面会讨论 then
方法的返回值)。
let param;
Promise.resolve(1).then(param)
//与下面的代码是等价的:
Promise.resolve(1).then((x) => x)
注意:这里说的参数不是函数也 ==包括没有参数== 的情况!下图说明了这 3 钟情况:
001.jpg
② onRejected
1)当 Promise 变成拒绝状态 (rejected
) 时调用的函数。该函数有一个参数,即拒绝的原因 (rejection reason)。
// then()方法的第二个参数,用来处理拒绝状态的Promise
Promise.reject(2)
.then(()=>{},
(reason)=>{
console.log(reason) //2
})
2)如果该参数 (指的是 onRejected
) 不是函数,则会在内部被替换为一个 "Thrower" 函数 (it throws an error it received as argument),也就是会报错。
let param
Promise.reject(2)
.then(()=>{},param)
注意:同理,这里说的参数不是函数也 包括没有参数
的情况:
002.jpg
返回值
当 Promise
完成 (fulfilled) 或者失败 (rejected) 时,会异步调用返回函数,==返回一个 Promise
对象==,不过返回值与 then()
方法中的回调函数是有关系的,如果then() 的回调函数:
① 没有返回任何值。那么 then
返回一个接受状态的 Promise
,并且该接受状态回调函数的参数值为 undefined
。
也就是说在处理then()
返回的 Promise
的回调函数中参数是 undefined
,不理解就看下面的例子:
Promise.resolve(1)
.then(res=>{})
// 这里 then() 中的回调函数没有返回任何值,看看结果吧:
003.jpg
下面的例子可能对你理解第二句话有帮助 (后面的几种情况都与此类似,都举例说明了):
Promise.resolve(1)
.then(res=>{})
.then(param=>{
console.log('参数',param)
})
//这里试着打印下参数,看下结果吧:
004.jpg
② 返回一个值。那么 then
返回一个接受状态的 Promise
,并且将返回的值作为接受状态的回调函数的参数值。
与 ① 情况类似,只是返回的 undefined
变成值
Promise.resolve(1)
.then(res=>{return 6})
Promise.resolve(1)
.then(res=>{return 6})
.then(param=>{
console.log('参数',param)
})
005.jpg
③ 抛出一个错误。那么 then
返回一个拒绝状态的 Promise
,并且将抛出的错误作为拒绝状态的回调函数的参数值
Promise.resolve(1).then(()=>{
throw new Error('Oops,Error')
})
//因为是拒绝,这次要用then()的第二个参数捕捉原因
Promise.resolve(1).then(()=>{
throw new Error('Oops,Error')
}).then(null,reason=>{
console.log(reason)
})
06.jpg
后面三种是分别返回不同状态的 Promise
④ 返回一个接受状态的 Promise
。那么 then 也会返回一个接受状态的 Promise
,并且将那个 Promise
的接受状态的回调函数的参数值作为该被返回的 Promise
的接受状态回调函数的参数值
Promise.resolve(1).then(()=>{
return Promise.resolve(3)
})
Promise.resolve(1).then(()=>{
return Promise.resolve(3)
}).then(param=>{
console.log('参数',param)
})
007.jpg
⑤ 返回一个拒绝状态的 Promise
。那么 then 也会返回一个拒绝状态的 Promise
,并且将那个 Promise 的拒绝状态的回调函数的参数值作为该被返回的Promise的拒绝状态回调函数的参数值
Promise.resolve(1).then(()=>{
return Promise.reject(5)
})
//因为是拒绝状态,要用then()的第二个参数捕捉原因
Promise.resolve(1).then(()=>{
return Promise.reject(5)
}).then(null,param=>{
console.log('参数',param)
})
008.jpg
⑥ 返回一个未定状态 (pending
) 的 Promise
。那么 then 返回 Promise 的状态也是未定的,并且它的终态与那个 Promise 的终态相同;同时,它变为终态时调用的回调函数参数与那个 Promise 变为终态时的回调函数的参数是相同的
function func(){
return new Promise((resolve,reject)=>{
setTimeout(()=>{
resolve('过了两秒')
},2000)
})
}
func()
示例中延时返回以得到 pending
状态的 Promise
,立即查看就得到还未有返回值的 pending
状态,2秒钟之后就能看到有返回值的终态
009.png
也可以试着用 then()
方法捕捉一下参数:
func().
then(param=>{
console.log('参数',param)
})
10.png
3 Promise.prototype.catch()
catch()
方法用来处理 Promise
被拒绝的情况,相当于 then(undefined, onRejected)
参数
catch()
接受一个参数 (回调函数):
① onRejected
1)当 Promise
被 rejected
时调用的函数。 该函数拥有一个参数 reason
(rejection
的原因)
2)如果这个参数 (指的是 onRejected
) 不是函数时,也是会报错的。这与 then()
方法中第二个参数不是函数的情况吻合。
返回值
如果 catch()
中的回调函数抛出一个错误
或返回一个本身失败的 Promise
, 则返回一个 rejected
状态的 Promise
;其他情况返回 resolved
状态的 Promise
rejected的两种状态
catch()链式调用参考
4 Promise.prototype.finally()
要点理解
-
finally
方法用来指定在promise
结束时,无论结果是fulfilled
或者是rejected
,都会执行的回调函数。这样可以避免同样的语句需要在then()
和catch()
中都要写一次的情况 -
finally
方法的回调函数不接受任何参数,这意味着没有办法知道前面的Promise
状态到底是fulfilled
还是rejected
。这表明,finally
仅用于无论最终结果如何都要执行的情况,而不能依赖Promise
执行结果 -
finally
方法本身无异常抛出的情况下,总是会返回原来的Promise
对象值;若抛出异常,则返回异常的Promise
对象。
另一篇类似文章:图解 Promise 实现原理(三)—— Promise 原型方法实现
6.浏览器垃圾回收机制
原文链接:https://blog.csdn.net/a8725585/article/details/106836648
栈
定义
栈用来在函数执行时存储保存执行上下文环境,我们一般也称调用栈,如基本类型的变量,引用类型的引用地址等都保存在栈中。执行到当前函数时进行入栈,执行完毕进行出栈。
回收方式
有一个记录当前执行状态的指针(称为 ESP)指向活动栈,函数执行完毕,esp下移到后一节点,销毁当前函数执行上下文。新的函数执行上下文入栈直接覆盖掉销毁的空间即可 .
function test() {
const a = { name: 'a' };
function showName() {
const b = { name: 'b' };
}
showName();
}
test();
堆
定义
对象的值等大数据保存在堆中。
回收方式
使用 JavaScript 中的垃圾回收器进行回收,通过标记的方式进行回收。
采用 可访问性(reachablility)算法来判断堆中的对象是否为活动对象。这个算法其实就将一些GC Root 作为初始存活对象的集合,从 GC Root 对象触发,遍历 GC Root中的所有对象
能够通过 GC Root(全局 wimdow 、dom树、栈上的变量、回收非活动对象占据的内存)遍历到的对象会被认为是可访问的,我们将其标记为活动对象,必须保留
如果一个对象无法通过 GC Root 遍历到,那么就认为这个对象是不可访问的,可能需要被回收,并标记为非活动对象。
堆的垃圾回收
在垃圾回收领域有一个代际假说的概念,大致意思就是数据分为生命周期短的和长久的。
根据代际假说,v8对堆的数据保存分为新生代区域,老生代区域两大块。这两区域的垃圾回收是不同的,分别为副垃圾回收器和主垃圾回收器。
新生代区域
存储空间小,不能存放大对象
老生代区域
空间大,存活时间长。新生代经过2次GC仍存活的的变量
副垃圾回收器
通过将新生代区域平分为2块区域:对象区、空闲区。当活动区中内存满时启动垃圾回收,遍历对象进行垃圾标记,将活动的对象复制到空闲区,移动到空闲区的对象是按顺序来的,因此不用去整理。处理完后,将对象区与空闲区翻转,完成垃圾回收。复制操作需要时间成本,因此为了效率,空间一般设置的很小。空间小,容易填满,因此又采用了对象晋升策略,经过两次垃圾回收依然存活的对象就会被移动到老生区
主垃圾回收器
采用标记-清除(Mark-Sweep)的算法进行垃圾回收。解决标记-清除空间不连续引入标记-整理(Mask-Compact),标记完后,先将所有存活的对象向一端移动,然后直接清理掉这一端以外的内存。
优化效率
并发标记: 在主程序运行阶段,并发的对变量进行标记处理,防止中断js主线程。笔辩主线程、辅助线程同时修改同一对象,需实现读写锁等功能。
并行整理:进行整理时占用主线程,在此基础上,启动多个辅助线程一起处理,加快速度。
增量整理:由于在整理时是占用的主线程,会阻塞js运行,因此通过增量的方式,将一次处理分成多个小任务处理,减小对用户的影响。js运行与标记相互交叉,防止变量引用在a,b之间变换,造成标记失败,使用黑白灰三色标记法进行处理。
黑白灰三色标记
黑:节点以及子节点已处理完
灰:正在处理的节点。黑节点的子节点不能为白节点,直接置灰(写屏障)
白: 非活动对象,等待回收
类似文章:垃圾回收机制
7.vue3 watch和watchEffect的使用以及有哪些区别
1.watch侦听器
引入watch
1 |
|
对基本数据类型进行监听----- watch特性:
1.具有一定的惰性lazy 第一次页面展示的时候不会执行,只有数据变化的时候才会执行
2.参数可以拿到当前值和原始值
3.可以侦听多个数据的变化,用一个侦听起承载
1 2 3 4 5 6 7 |
|
对引用类型进行监听-----
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
2.watchEffect
没有过多的参数 只有一个回调函数
1.立即执行,没有惰性,页面的首次加载就会执行。
2.自动检测内部代码,代码中有依赖 便会执行
3.不需要传递要侦听的内容 会自动感知代码依赖,不需要传递很多参数,只要传递一个回调函数
4.不能获取之前数据的值 只能获取当前值
5.一些=异步的操作放在这里会更加合适
1 2 3 |
|
侦听器的取消 watch 取消侦听器用法相同
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
watch也可以变为非惰性的 立即执行的 添加第三个参数 immediate: true
1 2 3 4 5 6 7 8 |
|
8.react中useEffect和useLayoutEffect的区别
原文链接:https://blog.csdn.net/qq_38164763/article/details/113532855
使用方式
这两个函数的使用方式其实非常简单,他们都接受一个函数一个数组,只有在数组里面的值改变的情况下才会再次执行 effect。所以对于使用方式我就不过多介绍了,不清楚的可以先参考官网
。
差异
useEffect 是异步执行的,而useLayoutEffect是同步执行的。
useEffect 的执行时机是浏览器完成渲染之后,而 useLayoutEffect 的执行时机是浏览器把内容真正渲染到界面之前,和 componentDidMount 等价。
具体表现
我们用一个很简单的例子
import React, { useEffect, useLayoutEffect, useState } from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
const [state, setState] = useState("hello world")
useEffect(() => {
let i = 0;
while(i <= 100000000) {
i++;
};
setState("world hello");
}, []);
// useLayoutEffect(() => {
// let i = 0;
// while(i <= 100000000) {
// i++;
// };
// setState("world hello");
// }, []);
return (
<>
{state}
>
);
}
export default App;
这是它的效果
而换成 useLayoutEffect 之后闪烁现象就消失了
看到这里我相信你应该能理解他们的区别了,因为 useEffect 是渲染完之后异步执行的,所以会导致 hello world 先被渲染到了屏幕上,再变成 world hello,就会出现闪烁现象。而 useLayoutEffect 是渲染之前同步执行的,所以会等它执行完再渲染上去,就避免了闪烁现象。也就是说我们最好把操作 dom 的相关操作放到 useLayouteEffect 中去,避免导致闪烁。
ssr
也正是因为 useLayoutEffect 可能会导致渲染结果不一样的关系,如果你在 ssr 的时候使用这个函数会有一个 warning。
Warning: useLayoutEffect does nothing on the server, because its effect cannot be encoded into the server renderer's output format. This will lead to a mismatch between the initial, non-hydrated UI and the intended UI. To avoid this, useLayoutEffect should only be used in components that render exclusively on the client. See https://fb.me/react-uselayouteffect-ssr for common fixes.
1
这是因为 useLayoutEffect 是不会在服务端执行的,所以就有可能导致 ssr 渲染出来的内容和实际的首屏内容并不一致。而解决这个问题也很简单:
放弃使用 useLayoutEffect,使用 useEffect 代替
如果你明确知道 useLayouteffect 对于首屏渲染并没有影响,但是后续会需要,你可以这样写
import { useEffect, useLayoutEffect } from 'react';
export const useCustomLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect;
当你使用 useLayoutEffect 的时候就用 useCustomLayoutEffect 代替。这样在服务端就会用 useEffect ,这样就不会报 warning 了。
源码剖析
那么 useEffect 和 useLayoutEffect 到底是在什么时候被调用的呢?我们去源码中一探究竟。
useEffect
首先找到 useEffect 调用的入口
function updateEffect(create, deps) {
{
// $FlowExpectedError - jest isn't a global, and isn't recognized outside of tests
if ('undefined' !== typeof jest) {
warnIfNotCurrentlyActingEffectsInDEV(currentlyRenderingFiber$1);
}
}
return updateEffectImpl(Update | Passive, Passive$1, create, deps);
}
调用 updateEffectImpl 时传入的 hookEffectTag 为 Passive$1 , 所以我们找一下:Passive$1。
然后我们找到是在这里传入了 Passive$1 类型来调用 useEffect 。
function commitPassiveHookEffects(finishedWork) {
if ((finishedWork.effectTag & Passive) !== NoEffect) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
// TODO (#17945) We should call all passive destroy functions (for all fibers)
// before calling any create functions. The current approach only serializes
// these for a single fiber.
commitHookEffectListUnmount(Passive$1 | HasEffect, finishedWork);
commitHookEffectListMount(Passive$1 | HasEffect, finishedWork);
break;
}
}
}
}
那我们继续顺藤摸瓜找 commitPassiveHookEffects
function flushPassiveEffectsImpl() {
...省略
while (_effect2 !== null) {
{
setCurrentFiber(_effect2);
invokeGuardedCallback(null, commitPassiveHookEffects, null, _effect2);
}
}
...省略
}
老样子,找flushPassiveEffectsImpl
function flushPassiveEffects() {
if (pendingPassiveEffectsRenderPriority !== NoPriority) {
var priorityLevel = pendingPassiveEffectsRenderPriority > NormalPriority ? NormalPriority : pendingPassiveEffectsRenderPriority;
pendingPassiveEffectsRenderPriority = NoPriority;
return runWithPriority$1(priorityLevel, flushPassiveEffectsImpl);
}
}
再往上一层是commitBeforeMutationEffects,这里面调用flushPassiveEffects的方法是scheduleCallback,这是一个调度操作,是异步执行的。
function commitBeforeMutationEffects{
...省略
if ((effectTag & Passive) !== NoEffect) {
// If there are passive effects, schedule a callback to flush at
// the earliest opportunity.
if (!rootDoesHavePassiveEffects) {
rootDoesHavePassiveEffects = true;
scheduleCallback(NormalPriority, function () {
flushPassiveEffects();
return null;
});
}
}
...省略
}
继续顺着 commitBeforeMutationEffects方法往上找的话,我们可以找到最终调用 useEffect 的地方是 commitRootImpl ,这是我们 commit 阶段会调用的一个函数,所以就是在这里面对 useEffect 进行了调度,在完成渲染工作以后去异步执行了 useEffect 。
useLayoutEffect
老样子,从入口找起
function updateLayoutEffect(create, deps) {
return updateEffectImpl(Update, Layout, create, deps);
}
这里传进去的 hookEffectTag 是Layout,那么我们找一下Layout。
function commitLifeCycles(finishedRoot, current, finishedWork, committedExpirationTime) {
switch (finishedWork.tag) {
case FunctionComponent:
case ForwardRef:
case SimpleMemoComponent:
case Block:
{
// At this point layout effects have already been destroyed (during mutation phase).
// This is done to prevent sibling component effects from interfering with each other,
// e.g. a destroy function in one component should never override a ref set
// by a create function in another component during the same commit.
commitHookEffectListMount(Layout | HasEffect, finishedWork);
return;
}
case ClassComponent:
{
var instance = finishedWork.stateNode;
if (finishedWork.effectTag & Update) {
if (current === null) {
startPhaseTimer(finishedWork, 'componentDidMount'); // We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
{
if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
if (instance.props !== finishedWork.memoizedProps) {
error('Expected %s props to match memoized props before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
}
if (instance.state !== finishedWork.memoizedState) {
error('Expected %s state to match memoized state before ' + 'componentDidMount. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
}
}
}
instance.componentDidMount();
stopPhaseTimer();
} else {
var prevProps = finishedWork.elementType === finishedWork.type ? current.memoizedProps : resolveDefaultProps(finishedWork.type, current.memoizedProps);
var prevState = current.memoizedState;
startPhaseTimer(finishedWork, 'componentDidUpdate'); // We could update instance props and state here,
// but instead we rely on them being set during last render.
// TODO: revisit this when we implement resuming.
{
if (finishedWork.type === finishedWork.elementType && !didWarnAboutReassigningProps) {
if (instance.props !== finishedWork.memoizedProps) {
error('Expected %s props to match memoized props before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
}
if (instance.state !== finishedWork.memoizedState) {
error('Expected %s state to match memoized state before ' + 'componentDidUpdate. ' + 'This might either be because of a bug in React, or because ' + 'a component reassigns its own `this.props`. ' + 'Please file an issue.', getComponentName(finishedWork.type) || 'instance');
}
}
}
instance.componentDidUpdate(prevProps, prevState, instance.__reactInternalSnapshotBeforeUpdate);
stopPhaseTimer();
}
}
...省略
}
而在这里我们可以看到,class 组件的 componentDidMount生命周期也是在这里被调用的,所以其实useLayoutEffect是和componentDidMount等价的。
而一直往上找最后还是会找到 commitRootImpl方法中去,同时在这个过程中并没有找到什么调度的方法,所以 useLayoutEffect会同步执行。
总结
优先使用 useEffect,因为它是异步执行的,不会阻塞渲染
会影响到渲染的操作尽量放到 useLayoutEffect中去,避免出现闪烁问题
useLayoutEffect和componentDidMount是等价的,会同步调用,阻塞渲染
useLayoutEffect在服务端渲染的时候使用会有一个 warning,因为它可能导致首屏实际内容和服务端渲染出来的内容不一致。
9.Vue中模板渲染原理
原文链接:https://blog.csdn.net/weixin_39503495/article/details/100827445
1、概念
模板:本质字符串。
与字符串的区别:
有逻辑(vue中有v-if、v-for)
嵌入js变量({
{变量}})
需要转化为html,页面才会识别并正确解析。
2、实现过程
过程:模板(字符串)解析成render函数---->render函数执行返回JS的vnode的结构,是如何实现嵌入变量和执行逻辑。转化为html则由updateComponent函数调用patch方法实现。
例子1(只有js变量、无涉及逻辑):
{
{price}}
经过vue模板生成的结果(实现了模板转化为JS的vnode并填充data中的变量):
// 此处vm为VUE的实例
//在控制台可以打印vm._c创建元素的,vm_v创建文本节点,vm_s是用来转化为字符串的
vm._c(
'div',
{
attrs: {'id': 'app'}
},
[
vm._c('p', [vm._v(vm._s(vm.price))])
]
)
用法备注:
vm._c是基于snabbdom中的h(‘节点标签’,{属性、特性、事件},[子节点]/文本)实现的,是用来创建vnode的。vue中的render函数,实现了传入Vue实例后返回vnode的功能。
vm._v是用来创建文本节点或实现换行的。
vm._s是用来转化字符串的。
render内部实际是使用with(vm),这样在内部就可以直接使用_c、_v、_s和data中的属性
例子2(以todolist为例子,涉及v-for逻辑):
-
{
{item}}
v-for逻辑实现是通过v._l()方法,生成对应的li结构后返回数组,将此数组作为ul节点的子节点。以下是返回的vnode:
with(vm) {
_c(
// 第一层(根元素)div
'div',
{
attrs:{"id":"app"}
},
// 包含所有子节点的数组
[
_c(
// 第二层第一个div(包含input、button以及其attrs、绑定事件还有值)
'div',
[
_c(
'input',
{
// v-model数据的双向绑定
directives:[
{
name:"model",
rawName:"v-model",
value:(inputValue),
expression:"inputValue"
}
],
attrs:{
"type":"text"
},
domProps:{
"value":(inputValue)
},
on:{
// 监听input内容,数据改变时,让Model层的数据更新
"input":function($event){
if($event.target.composing)return;
inputValue=$event.target.value
}
}
}
),
_v(" "),
_c(
'button',
{
// 渲染绑定事件
on:{
"click":handleClick
}
},
[_v("submit")]
)
]),
_v(" "),//创建空文本节点,用于换行
_c(
// 第二层第二个div(包含ul、li,其中li是通过for循环生成的)
'div',
[
_c(
// ul标签
'ul',
// _l返回的是包含子元素的数组
_l((list),function(item){
// v-for循环,实际是返回所有创建li标签的数组
return _c(
'li',
[
_v("\n "+_s(item)+"\n ")
]
)
}),0)//数字表示list有多少项,要返回多少个li
]
)
]
)}
}
3、总结:
个人理解(有误的话,欢迎指正):
1、 模板解析成render函数---->返回JS模拟的虚拟DOM结构:模板是一段字符串,模板解析生成render函数,执行render函数返回为vnode,vnode表明了各个节点的层级关系、特性、样式、绑定的事件。
2、 vnode---->html:通过 updateComponent函数调用vm._update()传入vnode,利用基于snabbdom的patch()方法改造的生成真实DOM节点并渲染页面。
注:vm.__patch__:初次渲染时,调用vm.__patch__(containe, vnode),生成真实的DOM结构渲染到容器中。re-render时,调用vm.__patch__(vnode, newVnode)利用diff算法对比新旧vnode之间的差异,生成需要更新的真实DOM,渲染到容器的对应位置。