likes
comments
collection
share

我的源码学习之路(五)---vue-2.6.14

作者站长头像
站长
· 阅读数 43

前言

今天又是来记录一下前言心情:最近很是烦躁,上一篇文章写了两个问题,却花了3天时间,确实有部分原因是自己的浮躁,学习不进去。因为现在上班的状态我真的感觉快废了。每天焦虑每天不努力。写完上一篇文章的时候,是发了工资的一会感觉不能废了(12月21号),但是原本15号就应该的事情,而且还有一个月拖欠还没发,根本不给任何解释,也不发公告。发了一个月的之后,我就静下心来了一点。终于把上一篇文章写完了。
还是得静心!!!!

写这篇的过程中羊了。巨难受,前三天连续发烧39以上,退烧之后各种症状随之而来。第五天症状减轻一点点,公司要求轻症的去上班,我就来了。浑身乏力,眼睛睁不开,头晕的不行。感觉随时要倒下了似的。走路走两步感觉累得不行。真的需要恢复。需要长时间恢复。跟坐月子似的。


回顾

涉及的前端小知识【遇到就记录一下】

拓展:ES6中Set和Map的数据结构

我的源码学习之路(五)---vue-2.6.14

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].可以用于去重

我的源码学习之路(五)---vue-2.6.14

我的源码学习之路(五)---vue-2.6.14

结论:
    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'

我的源码学习之路(五)---vue-2.6.14

转换:

  • 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
        }
    }
})

我的源码学习之路(五)---vue-2.6.14 而对于这一块官网上也有解释关于数组响应的问题 结论

  • 不能监听超过数组本身长度的数字赋值:this.arr.length = 100-->X
  • 不能通过索引来修改数组的某一项
    • 借鉴网上的一个结论:原文

我的源码学习之路(五)---vue-2.6.14

那我们为什么数组(对象)又可以监听到呢?
  • 当我们给响应式的对象新增属性时,新增的属性并不会渲染到页面中
  • 对于响应式的数组,增加元素、修改数组长度时,数组的这些变化也不会反映到页面中

是因为vue内部针对数组的一套解决方案,通过数组上的push等方法触发视图的更新,重写了几个方法挂载到Array.prototype上

我的源码学习之路(五)---vue-2.6.14

如何监听改变呢?

就是利用vue提供的API:$set

我的源码学习之路(五)---vue-2.6.14

我的源码学习之路(五)---vue-2.6.14

使用$set

我的源码学习之路(五)---vue-2.6.14

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,就会造成一个变了全都会变的结果。(造成数据污染)
  • 根实例可以是一个对象,只因为只有一个跟实例

从源码来看

我的源码学习之路(五)---vue-2.6.14

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-2.6.14

vue中prop如何指定其类型要求的,集中写法

initProps:[src/core/instance/state.js] 在我们正常开发中,props是组件化的一个重要组成部分。我们需要这个属性传递值供我们使用,它的书写方式也有多种。

我的源码学习之路(五)---vue-2.6.14

这一段是源码中处理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
}

数组

我的源码学习之路(五)---vue-2.6.14

对象方法1

我的源码学习之路(五)---vue-2.6.14

对象方法2

我的源码学习之路(五)---vue-2.6.14

对象方法3

我的源码学习之路(五)---vue-2.6.14

vue中$nextTick()原理

一般回答:(大白话) 在下次dom更新循环结束之后执行延迟回调。修改数据后需要立即使用修改后的数据dom的需要使用nextTick才能获取到。因为Vue在更新Dom时是异步执行的。

实际上利用的就是事件循环的机制。vue2异步更新中有详细讲解

我的源码学习之路(五)---vue-2.6.14

我的源码学习之路(五)---vue-2.6.14

源码看[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
  }
})

我的源码学习之路(五)---vue-2.6.14

AST:抽象语法树

我的源码学习之路(五)---vue-2.6.14

AST编译过程

我的源码学习之路(五)---vue-2.6.14

后记

本文仅作为自己一个阅读记录,具体还是要看大佬们的文章

下一篇:我的源码学习之路(六)---vue-2.6.14