本文将回答以下几个问题
1、什么是作用域?
2、为什么会有变量提升?
3、为什么会有闭包?
4、with的作用?下发代码执行结果是什么?
js
const foo = { name: 'foo' }
const bar = { name: 'bar' }
let sayName = function () {};
with(foo) {
sayName = function () { console.log(name); }
}
with(bar) {
sayName()
}
5、Vue作用域插槽为何需要子组件传值?能否直接访问子组件属性?
1、作用域
1.1、何为作用域
存储、查找变量的规则被称为作用域。
作用域共有两种主要的工作模型。
第一种是最为普遍的,被大多数编程语言所采用的词法作用域
另外一种叫作动态作用域
1.2、词法作用域
js采用的是静态的词法作用域,在编译时,会根据各变量的声明位置构建一个嵌套的词法作用域。
这句话是涵盖很多信息的,一些js的机制会因此变得清晰,比如说:
1、变量提升:
为什么会有变量提升?因为对变量声明的处理发生在编译时 ,构建词法作用域的阶段,而变量的使用是发生在运行时。
所以最后一行的变量声明的处理,也比第一行的变量使用的处理要早,自然而然就有变量提升了。
那么let、const呢?也有提升么?
js引擎在扫描代码发现变量声明时,要么将它们提升至作用域顶部(遇到 var 声明) ,要么将声明放TDZ(临时死区)中(遇到let、const 声明)。
访问TDZ 中的变量会触发运行时错误。只有执行过变量声明语句后,变量才会TDZ中移出,然后方可正常访问。
2、闭包:
既然编译阶段就会生成词法作用域,那么函数的作用域就只会取决与他声明的位置,而与他执行的位置无关。
不管函数的引用最终被传递到哪里,在什么时机执行,他都保有在其声明位置的作用域。
1.3、动态作用域
动态作用域并不关心函数和作用域是如何声明以及在何处声明的,只关心它们从何处调用。换句话说,作用域链是基于调用栈的,而不是代码中的作用域嵌套。
如果js使用的是动态作用域,那他的执行效果就会像下面这样:
js
function foo() {
console.log( a ); // 3(不是 2 !)
}
function bar() {
var a = 3;
foo();
}
var a = 2;
bar();
这个其实就有点类似于js中的this值的机制,其值取决去运行时的上下文。
1.4、块作用域
函数是常见的作用域块,此外with、try/catch等也会创建一个块作用域,我们重点关注下if与for。
我们来看一下一下代码会输出什么结果
js
if (1) {
var a = 123
}
console.log(a)
if (1) {
let a2 = 123
}
console.log(a2)
答案是
let、const关键字可以将变量绑定到所在的任意作用域中(通常是 { .. } 内部)。
1.5、改变作用域的方法,with的作用
'with'语句将某个对象添加到作用域链的顶部
用 with 从对象中创建出的作用域仅在 with 声明中而非外部作用域中有效。
2、Vue插槽
2.1、普通插槽
1、示例代码
js
<template>
<div class="test">
<SlotTest>
<div>
<p>i m slot</p>
</div>
</SlotTest>
<div>
<p>i m test</p>
</div>
</div>
</template>
<script>
const SlotTest = {
template: `
<div>
i m SlotText
<div>
<slot></slot>
</div>
</div>
`
}
export default {
name: 'test',
components: { SlotTest },
}
</script>
2、当一个vnode是组件的vnode时,其在模板中编写的子节点vnode并不像普通节点一样放在children属性中,而是存放在componentOptions对象的某个属性下
3、然后子组件中,可以通过标签,来指定父组件传入节点的渲染位置
js
const SlotTest = {
template: `
<div>
i m SlotText
<div>
<slot></slot>
</div>
</div>
`,
};
这个是如何实现的呢?
4、Vue不管是我们平常写的template标签、还是template字符串模板;vue都会转成render函数去执行,而对于 标签,vue则会将其转成._t函数。
(注意一下,这里生成的render函数的函数体是用with(this)包裹的,这是我们在写vue模板的时候可以直接访问vue实例的属性而无需this.的原因;但后面会看到另一个有意思的场景)
而这个函数本身,就会去取上面那个在父组件渲染过程中被放在子组件占位VNode的componentOptions.children 里的vnode返回。
所以,对于普通插槽而言,插槽中的节点的vnode是在父组件的render过程中被生成,在子组件render的过程被返回添加到子组件的vnode树中,并在子组件patch的过程被挂载的。
2.2、作用域插槽
1、示例代码
js
<template>
<div class="test">
<SlotTest v-slot:default="{ msg }">
<div>
<p>i m slot {{ msg }}</p>
</div>
</SlotTest>
<div>
<p>i m test</p>
</div>
</div>
</template>
<script>
const SlotTest = {
template: `
<div>
i m SlotText
<div>
<slot :msg="msg"></slot>
</div>
</div>
`,
data() {
return {
msg: 'haha'
}
}
};
export default {
name: 'TestComp',
components: { SlotTest },
};
</script>
2、对于作用域插槽,其vnode并不会在父组件的render中生成,而是变成一个函数(该函数会返回这些vnode),存放在子组件的占位vnode的属性中。
父组件生成的render函数
父组件执行完render后返回的vnode
可以看到原先普通插槽存放插槽vnode的componentOptions.children已经是一个undefined,
取而代之的是放在data.scopedSlots中的一个函数。
3、然后在子组件中,由标签转换来的_t方法就会去执行这个函数,并将标签的props属性作为参数值传入。
但这里有一个地方需要注意就是,vue组件的生成而来的render函数都会使用with(this)将其包裹。
那么对于作用域插槽的生成方法,就是在父组件实例的with中生成,在子组件实例的with中执行。
js
with($parentVm) {
scopedSlots.default = function () {...}
}
with($childVm) {
scopedSlots.default(params)
}
通过回顾前面的内容我们知道,函数的作用域是取决于其声明的位置,而不是其执行的位置,那么我们的问题4、5就有答案了