写 React / Vue
项目时为什么要在列表组件中写 key
,其作用是什么?
v-for一般用在列表的渲染,渲染的时候会默认遵守就地复用策略。
就地复用策略:
当 Vue 正在更新使用 v-for 渲染的元素列表时,它默认使用“就地更新”的策略。如果数据项的顺序被改变,Vue 将不会移动 DOM 元素来匹配数据项的顺序,而是就地更新每个元素,并且确保它们在每个索引位置正确渲染。
这种方式只适用于列表渲染不依赖子组件状态,或临时 DOM 状态变化。
vue
和 react
都是采用 diff
算法来对比新旧虚拟节点,从而更新节点。
在 vue
的 diff
函数交叉对比中,当新节点跟旧节点头尾交叉对比
没有结果时,会根据新节点的 key
去对比旧节点数组中的 key
,从而找到相应旧节点(这里对应的是一个 key => index
的 map
映射)。
如果没有找到就认为是一个新增节点。
而如果没有 key
,那么就会采用遍历查找的方式去找到对应的旧节点。
种一个 map
映射,另一种是遍历查找。
相比而言,map
映射的速度更快。
为什么 Vuex
的 mutation
和 Redux
的 reducer
中不能做异步操作?
Mutation
必须是同步函数
一条重要的原则就是要记住 mutation
必须是同步函数。为什么?请参考下面的例子
mutations: {
someMutation (state) {
api.callAsyncMethod(() => {
state.count++
})
}
}
观察 devtool
中的 mutation
日志。每一条 mutation
被记录,devtools
都需要捕捉到前一状态和后一状态的快照。
然而,在上面的例子中 mutation
中的异步函数中的回调让这不可能完成:因为当 mutation
触发的时候,回调函数还没有被调用,devtools
不知道什么时候回调函数实际上被调用——实质上任何在回调函数中进行的状态的改变都是不可追踪的。
区分 actions
和 mutations
并不是为了解决竞态问题,而是为了能用 devtools
追踪状态变化。
- 事实上在
vuex
里面actions
只是一个架构性的概念,并不是必须的,说到底只是一个函数,你在里面想干嘛都可以,只要最后触发mutation
就行。 - 异步竞态怎么处理那是用户自己的事情。
vuex
真正限制你的只有mutation
必须是同步的这一点(在redux
里面就好像reducer
必须同步返回下一个状态一样)。 - 同步的意义在于这样每一个
mutation
执行完成后都可以对应到一个新的状态(和reducer
一样),这样devtools
就可以打个snapshot
存下来,然后就可以随便time-travel
了。 - 如果你开着
devtool
调用一个异步的action
,你可以清楚地看到它所调用的mutation
是何时被记录下来的,并且可以立刻查看它们对应的状态。
在此前提下,开发者们总结出:
vuex
的处理方式是同步在mutation
里面,异步在actions
里面。
在 Vue
中,子组件为何不可以修改父组件传递的 Prop
,如果修改了,Vue
是如何监控到属性的修改并给出警告的。
子组件为何不可以修改父组件传递的 Prop
?
- 一个父组件不只有你一个子组件。同样,使用这份
prop
数据的也不只有你一个组件。如果每个子组件都能修改prop
的话,将会导致修改数据的源头不止一处。 - 单向数据流,易于监测数据的流动。出现了错误可以更加迅速的定位到错误的位置。
如果修改了,Vue
是如何监控到属性的修改并给出警告的
// 在initProps的时候,在defineReactive时通过判断是否在开发环境
// 如果是开发环境,会在触发set的时候判断是否此key是否处于updatingChildren中被修改
// 如果不是,说明此修改来自子组件,触发warning提示
if (process.env.NODE_ENV !== "production") {
var hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
'"' +
hyphenatedKey +
'" is a reserved attribute and cannot be used as component prop.',
vm
);
}
defineReactive$$1(props, key, value, function () {
if (!isRoot && !isUpdatingChildComponent) {
warn(
"Avoid mutating a prop directly since the value will be " +
"overwritten whenever the parent component re-renders. " +
"Instead, use a data or computed property based on the prop's " +
'value. Prop being mutated: "' +
key +
'"',
vm
);
}
});
}
需要特别注意的是,当你从子组件修改的
prop
属于基础类型时会触发提示。这种情况下,你是无法修改父组件的数据源的,因为基础类型赋值时是值拷贝。
你直接将另一个非基础类型(
Object, array
)赋值到此key
时也会触发提示(但实际上不会影响父组件的数据源),当你修改object
的属性时不会出发提示,并且会修改父组件数据源的数据
所有的
prop
都使得其父子prop
之间形成了一个单向下行绑定:父级prop
的更新会向下流动到子组件中,但是反过来不行。这样会防止从子组件意外改变父级组件的状态,从而导致你的应用的数据流向难以理解。
同时,不仅仅是判断是否在updatingChildren
中修改,当其内部还会传入第四个参数,如果不是root
根组件,并且不是更新子组件,那么说明更新的是prop
,所以会警告。
vue
相关源码
// src/core/instance/state.js 源码路径
function initProps(vm: Component, propsOptions: Object) {
const propsData = vm.$options.propsData || {};
const props = (vm._props = {});
// cache prop keys so that future props updates can iterate using Array
// instead of dynamic object key enumeration.
const keys = (vm.$options._propKeys = []);
const isRoot = !vm.$parent;
// root instance props should be converted
if (!isRoot) {
toggleObserving(false);
}
for (const key in propsOptions) {
keys.push(key);
const value = validateProp(key, propsOptions, propsData, vm);
/* istanbul ignore else */
if (process.env.NODE_ENV !== "production") {
const hyphenatedKey = hyphenate(key);
if (
isReservedAttribute(hyphenatedKey) ||
config.isReservedAttr(hyphenatedKey)
) {
warn(
`"${hyphenatedKey}" is a reserved attribute and cannot be used as component prop.`,
vm
);
}
defineReactive(props, key, value, () => {
if (!isRoot && !isUpdatingChildComponent) {
warn(
`Avoid mutating a prop directly since the value will be ` +
`overwritten whenever the parent component re-renders. ` +
`Instead, use a data or computed property based on the prop's ` +
`value. Prop being mutated: "${key}"`,
vm
);
}
});
} else {
defineReactive(props, key, value);
}
// static props are already proxied on the component's prototype
// during Vue.extend(). We only need to proxy props defined at
// instantiation here.
if (!(key in vm)) {
proxy(vm, `_props`, key);
}
}
toggleObserving(true);
}
// src/core/observer/index.js
/**
* Define a reactive property on an Object.
*/
export function defineReactive(
obj: Object,
key: string,
val: any,
customSetter?: ?Function,
shallow?: boolean
) {
const dep = new Dep();
const property = Object.getOwnPropertyDescriptor(obj, key);
if (property && property.configurable === false) {
return;
}
// cater for pre-defined getter/setters
const getter = property && property.get;
const setter = property && property.set;
if ((!getter || setter) && arguments.length === 2) {
val = obj[key];
}
let childOb = !shallow && observe(val);
Object.defineProperty(obj, key, {
enumerable: true,
configurable: true,
get: function reactiveGetter() {
const value = getter ? getter.call(obj) : val;
if (Dep.target) {
dep.depend();
if (childOb) {
childOb.dep.depend();
if (Array.isArray(value)) {
dependArray(value);
}
}
}
return value;
},
set: function reactiveSetter(newVal) {
const value = getter ? getter.call(obj) : val;
/* eslint-disable no-self-compare */
if (newVal === value || (newVal !== newVal && value !== value)) {
return;
}
/* eslint-enable no-self-compare */
if (process.env.NODE_ENV !== "production" && customSetter) {
customSetter();
}
// #7981: for accessor properties without setter
if (getter && !setter) return;
if (setter) {
setter.call(obj, newVal);
} else {
val = newVal;
}
childOb = !shallow && observe(newVal);
dep.notify();
},
});
}
双向绑定和 vuex
是否冲突
会发生冲突;
无需使用v-model
<input v-model="obj.message">
<!-- 错误使用方法 -->
假设这里的 obj
是在计算属性中返回的一个属于 Vuex store 的对象,在用户输入时,v-model
会试图直接修改 obj.message
。在严格模式中,由于这个修改不是在 mutation 函数中执行的, 这里会抛出一个错误。
用“Vuex 的思维”去解决这个问题的方法是:给 <input>
中绑定 value,然后侦听 input
或者 change
事件,在事件回调中调用一个方法:
<input :value="message" @input="updateMessage">
// ...
computed: {
...mapState({
message: state => state.obj.message
})
},
methods: {
updateMessage (e) {
this.$store.commit('updateMessage', e.target.value)
}
}
下面是 mutation
函数:
// ...
mutations: {
updateMessage (state, message) {
state.obj.message = message
}
}
双向绑定的计算属性
必须承认,上面的做法比简单地使用“v-model
+ 局部状态”要啰嗦得多,并且也损失了一些 v-model
中很有用的特性。
另一个方法是使用带有 setter
的双向绑定计算属性:
<input v-model="message">
// ...
computed: {
message: {
get () {
return this.$store.state.obj.message
},
set (value) {
this.$store.commit('updateMessage', value)
}
}
}
vue
在 v-for
时给每项元素绑定事件需要用事件代理吗?
- 数据少时可以不用,数据多时,一定要用事件代理(事件委托)。
Vue
框架没有为该指令做事件代理,如果需要,得我们自己做(vue
本身不做事件代理)。- 普通
html
元素和在组件上挂了.native
修饰符的事件。最终EventTarget.addEventListener()
挂载事件。 - 组件上的,
vue
组件实例上的自定义事件(不包括.native
)会调用原型上的$on,$emit
(包括一些其他api
$off,$once
等等)。
react
代理到了document
。涉及到安卓机小程序,建议直接使用代理
<body>
<div id="app">
<my-component></my-component>
</div>
<script src="./vue.js"></script>
<script>
let component = {
template: `
<ul @click="handleClick">
<li v-for="(item, index) in data" :data-index="index">
{{ item.text }}
</li>
</ul>
`,
data() {
return {
data: [
{
id: 0,
text: '0',
},
{
id: 1,
text: '1',
},
{
id: 2,
text: '2',
}
]
}
},
// 通过在li元素中额外加一个data-index就可以实现委托了
methods: {
handleClick(e) {
// 需要过滤掉ul,不然会出问题
if (e.target.nodeName.toLowerCase() === 'li') {
const index = parseInt(e.target.dataset.index)
// 获得引索后,只需要修改data数据就能改变UI了
this.doSomething(index)
}
},
doSomething(index) {
// do what you want
alert(index)
}
}
}
new Vue({
el: '#app',
components: {
'my-component': component
}
})
</script>
</body>
Q.E.D.