我的源码学习之路(五)---vue-2.6.14
前言
今天又是来记录一下前言心情:最近很是烦躁,上一篇文章写了两个问题,却花了3天时间,确实有部分原因是自己的浮躁,学习不进去。因为现在上班的状态我真的感觉快废了。每天焦虑每天不努力。写完上一篇文章的时候,是发了工资的一会感觉不能废了(12月21号),但是原本15号就应该的事情,而且还有一个月拖欠还没发,根本不给任何解释,也不发公告。发了一个月的之后,我就静下心来了一点。终于把上一篇文章写完了。
还是得静心!!!!
写这篇的过程中羊了。巨难受,前三天连续发烧39以上,退烧之后各种症状随之而来。第五天症状减轻一点点,公司要求轻症的去上班,我就来了。浑身乏力,眼睛睁不开,头晕的不行。感觉随时要倒下了似的。走路走两步感觉累得不行。真的需要恢复。需要长时间恢复。跟坐月子似的。
回顾
涉及的前端小知识【遇到就记录一下】
拓展:ES6中Set和Map的数据结构
Set
类似于数组,但是成员的值都是唯一的,没有重复的值。Set
本身就是一个构造函数,用来生成Set数据结构
。
Set函数也可以接收一个数组(或者具有iterable接口的其他数据结构)作为参数,用来初始化.
向Set加入值的时候,不会发生类型转换。
const s = new Set()
[1,2,3,4,5,6,7,5,6,7,5,12,2,32,31,1,2,5,4].forEach(x => s.add(x))
for(let i of s){console.log(i)}
// 数组
const set = new Set([1, 2, 3, 4, 4]);
[...set] // [1,2,3,4].可以用于去重
结论:
Set可以用于数组去重操作:[...new Set(arr)],Array.from(new Set(arr))
Set可以去除字符串重复的:[...new.Set(str)].join('')
WeakSet
与Set类似,区别是:
- WeakSet成员只能是对象,不能是其他类型的值
- WeakSet中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用
- 无法遍历
Map
类似于对象,也是键值对的集合,但是‘键’范围不限制于字符串,各种类型的值(包括对象)都可以当做键,也就是说Map结构提供了‘值---值’的对应。更完善
任何具有 Iterator 接口、且每个成员都是一个双元素的数组的数据结构都可以当作Map
构造函数的参数
Map.prototype.keys()
:返回键名的遍历器。Map.prototype.values()
:返回键值的遍历器。Map.prototype.entries()
:返回所有成员的遍历器。Map.prototype.forEach()
:遍历 Map 的所有成员。
const m = new Map()
const o = {'p': 'q'}
m.set(o, 'consent')
m
.set(1, 'aaa')
.set(1, 'bbb'); // 会覆盖前一个m.get(1) = 'bbb'
转换:
- Map转换成数组:[...new Map()]
- 数组转换成Map:new Map([true, 7])
- 对象转换成Map: new Map(Object.entries(obj))
function objToStrMap(obj) {
let strMap = new Map();
for (let k of Object.keys(obj)) {
strMap.set(k, obj[k]);
}
return strMap;
}
objToStrMap({yes: true, no: false})
// Map {"yes" => true, "no" => false}
- Map转换成对象:
function strMapToObj(strMap) {
let obj = Object.create(null);
for (let [k,v] of strMap) {
obj[k] = v;
}
return obj;
}
const myMap = new Map()
.set('yes', true)
.set('no', false);
strMapToObj(myMap)
// { yes: true, no: false }
- Map转换成Json:
Map 的键名都是字符串
function strMapToJson(strMap) {
return JSON.stringify(strMapToObj(strMap));
}
let myMap = new Map().set('yes', true).set('no', false);
strMapToJson(myMap)
// '{"yes":true,"no":false}'
Map 的键名有非字符串
function mapToArrayJson(map) {
return JSON.stringify([...map]);
}
let myMap = new Map().set(true, 7).set({foo: 3}, ['abc']);
mapToArrayJson(myMap)
// '[[true,7],[{"foo":3},["abc"]]]'
- JSON转成Map:
function jsonToStrMap(jsonStr) {
return objToStrMap(JSON.parse(jsonStr));
}
jsonToStrMap('{"yes": true, "no": false}')
// Map {'yes' => true, 'no' => false}
WeakMap
与Map类似,区别是:
- WeakMap只能是对象作为键名(
null
除外) - WeakMap中的对象都是弱引用,即垃圾回收机制不考虑WeakSet对该对象的引用
- 无法遍历
- 无法清空
vue常见的一些面试问题
## vue如何监听数组的变化?set()怎么实现的
## vue中data为什么是一个函数
## vue中prop如何指定其类型要求的,集中写法
## vue中$nextTick()原理
## vue模板是如何编译的
vue如何监听数组的变化?set()怎么实现的
过往经常被问到为什么不能改变数组?
在vue里,我们是不能通过索引或者长度来改变数组的,比如在vue中使用:this.arr[0]='vue'
,这样是不能正确显示在视图上的
new Vue({
el: '#app',
data(){
return {
arr2:[
{id:1, title: 'one'},
{id:2, title: 'two'}
]
}
},
methods:{
click(){
this.arr2[0] = '666'
this.arr2.length = 158
}
}
})
而对于这一块官网上也有解释关于数组响应的问题
结论:
- 不能监听超过数组本身长度的数字赋值:this.arr.length = 100-->X
- 不能通过索引来修改数组的某一项
- 借鉴网上的一个结论:原文
那我们为什么数组(对象)又可以监听到呢?
- 当我们给响应式的对象新增属性时,新增的属性并不会渲染到页面中
- 对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中
是因为vue内部针对数组的一套解决方案,通过数组上的push等方法触发视图的更新,重写了几个方法挂载到Array.prototype上
如何监听改变呢?
就是利用vue提供的API:$set
使用$set
export function set (target: Array<any> | Object, key: any, val: any): any {
...
if (Array.isArray(target) && isValidArrayIndex(key)) { // 判断是不是数组
target.length = Math.max(target.length, key)
target.splice(key, 1, val)
return val
}
if (key in target && !(key in Object.prototype)) { // 属性已存在于对象中,直接更新返回
target[key] = val
return val
}
const ob = (target: any).__ob__ //响应式对象都存在__ob__
if (target._isVue || (ob && ob.vmCount)) {
process.env.NODE_ENV !== 'production' && warn(
'Avoid adding reactive properties to a Vue instance or its root $data ' +
'at runtime - declare it upfront in the data option.'
)
return val
}
if (!ob) { // 不是响应式地话,进行赋值
target[key] = val
return val
}
defineReactive(ob.value, key, val) //响应式核心代码。是响应式,进行处理
ob.dep.notify()// 进行更新视图
return val // 返回值
}
vue中data为什么是一个函数
一般回答:
- 如果data是一个函数的话,这样每复用一次组件,就会返回一份新的data
- Object是引用数据类型,里面保存的是内存地址,单纯的写成对象形式,就使得所有组件实例共用了一份data,就会造成一个变了全都会变的结果。(造成数据污染)
- 根实例可以是一个对象,只因为只有一个跟实例
从源码来看
function initData (vm: Component) {
let data = vm.$options.data
data = vm._data = typeof data === 'function'
? getData(data, vm)//如果是函数执行getData
: data || {}//直接赋值。这一点会对应上面回答的第二条,vue每次组件初始化data都是一个对象,就会放在同一个对象中,后来的组件的属性会覆盖之前的组件的属性。这样会造成之前的值的丢失,也就是数据污染。
...
}
export function getData (data: Function, vm: Component): any {
// #7573 disable dep collection when invoking data getters
pushTarget()
try {
return data.call(vm, vm)
} catch (e) {
handleError(e, vm, `data()`)
return {}
} finally {
popTarget()
}
}
根实例可以是对象
vue中prop如何指定其类型要求的,集中写法
initProps:[src/core/instance/state.js]
在我们正常开发中,props
是组件化的一个重要组成部分。我们需要这个属性传递值供我们使用,它的书写方式也有多种。
这一段是源码中处理props的格式的位置:src/core/util/options.js
function normalizeProps (options: Object, vm: ?Component) {
const props = options.props
if (!props) return
const res = {}
let i, val, name
if (Array.isArray(props)) {
i = props.length
while (i--) {
val = props[i]
if (typeof val === 'string') {
name = camelize(val)
res[name] = { type: null }
} else if (process.env.NODE_ENV !== 'production') {
warn('props must be strings when using array syntax.')
}
}
} else if (isPlainObject(props)) {
for (const key in props) {
val = props[key]
name = camelize(key)
res[name] = isPlainObject(val)
? val
: { type: val }
}
} else if (process.env.NODE_ENV !== 'production') {
warn(
`Invalid value for option "props": expected an Array or an Object, ` +
`but got ${toRawType(props)}.`,
vm
)
}
options.props = res
}
数组
对象方法1
对象方法2
对象方法3
vue中$nextTick()原理
一般回答:(大白话) 在下次dom更新循环结束之后执行延迟回调。修改数据后需要立即使用修改后的数据dom的需要使用nextTick才能获取到。因为Vue在更新Dom时是异步执行的。
实际上利用的就是事件循环的机制。vue2异步更新中有详细讲解
源码看[src/core/util/next-tick.js]
export let isUsingMicroTask = false
const callbacks = []
let pending = false
function flushCallbacks () {
pending = false
const copies = callbacks.slice(0)
callbacks.length = 0
for (let i = 0; i < copies.length; i++) {
copies[i]()
}
}
let timerFunc
...
// 这一部分代码主要依照浏览器支持情况,做的区别处理分别赋值
...
export function nextTick (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 => {
_resolve = resolve
})
}
}
可以在源码中看到还有一个位置也用到了nextTick()
: queueWatcher 这个方法主要也是数据变更后触发DOM更新操作,这样vue源码中更新DOM也是使用nextTick来实现异步执行的。源码暴露出来的nextTick是同一个。
vue模板是如何编译的
这一块是一个大的工程。详细看看。
前面讲解初始化过程中其实遇到了编译的方法,当时只是一笔带过。并没有着重来介绍。
在src/platforms/web/entry-runtime-with-compiler.js(65)
调用了一个compileToFunctions(template,{},this)
方法。接下来找到createCompiler(src/compiler/index.js)
export const createCompiler = createCompilerCreator(function baseCompile (
template: string,
options: CompilerOptions
): CompiledResult {
const ast = parse(template.trim(), options) // parse转化成AST抽象语法树[src/compiler/parser/index.js(79)]
console.log('ast',ast)
if (options.optimize !== false) {
optimize(ast, options) // 对AST的静态节点进行标记[src/compiler/optimizer.js(21)]
}
console.log('optimize之后', ast)
const code = generate(ast, options)// generate:根据AST抽象语法树拼接render函数[src/compiler/codegen/index.js(43)]
console.log('generate', code)
return {
ast,
render: code.render,
staticRenderFns: code.staticRenderFns
}
})
AST:抽象语法树
AST编译过程
后记
本文仅作为自己一个阅读记录,具体还是要看大佬们的文章
转载自:https://juejin.cn/post/7182515192226381879