派大星

vuePress-theme-reco 派大星    2021 - 2022
派大星 派大星
主页
博客
  • 前端
  • JavaScript文章
  • 三级目录
  • 笔记
  • 学习笔记
  • 页面
  • CSS
  • HTML
  • 技术
  • 技术文档
  • GitHub技巧
  • 技术笔记
  • 实用工具
  • Nodejs
  • 博客搭建
  • 更多
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • JavaScript
  • 浏览器&网络
  • 前端框架
  • 资源工具
  • 持续集成
  • 小程序
  • 杂谈
  • React
  • Mysql
  • 面试题
  • uniapp
  • 《ES6 教程》笔记
  • 《Git》学习笔记
  • 《JavaScript教程》笔记
  • 《React》笔记
  • 核心概念
  • 高级指引
  • Hook
  • 案例演示
  • 《TypeScript 从零实现 axios》
  • 初识 TypeScript
  • TypeScript 常用语法
  • ts-axios 项目初始化
  • ts-axios 基础功能实现
  • ts-axios 异常情况处理
  • ts-axios 接口扩展
  • ts-axios 拦截器实现
  • ts-axios 配置化实现
  • ts-axios 取消功能实现
  • ts-axios 更多功能实现
  • ts-axios 单元测试
  • ts-axios 部署与发布
  • 《Vue》笔记
  • 基础
  • 组件
  • 过渡&动画
  • 工具
  • 规模化
  • 可复用性&组合
  • 其他
  • Vuex
标签
时间轴
关于
author-avatar

派大星

272

文章

69

标签

主页
博客
  • 前端
  • JavaScript文章
  • 三级目录
  • 笔记
  • 学习笔记
  • 页面
  • CSS
  • HTML
  • 技术
  • 技术文档
  • GitHub技巧
  • 技术笔记
  • 实用工具
  • Nodejs
  • 博客搭建
  • 更多
  • 学习
  • 面试
  • 心情杂货
  • 实用技巧
  • JavaScript
  • 浏览器&网络
  • 前端框架
  • 资源工具
  • 持续集成
  • 小程序
  • 杂谈
  • React
  • Mysql
  • 面试题
  • uniapp
  • 《ES6 教程》笔记
  • 《Git》学习笔记
  • 《JavaScript教程》笔记
  • 《React》笔记
  • 核心概念
  • 高级指引
  • Hook
  • 案例演示
  • 《TypeScript 从零实现 axios》
  • 初识 TypeScript
  • TypeScript 常用语法
  • ts-axios 项目初始化
  • ts-axios 基础功能实现
  • ts-axios 异常情况处理
  • ts-axios 接口扩展
  • ts-axios 拦截器实现
  • ts-axios 配置化实现
  • ts-axios 取消功能实现
  • ts-axios 更多功能实现
  • ts-axios 单元测试
  • ts-axios 部署与发布
  • 《Vue》笔记
  • 基础
  • 组件
  • 过渡&动画
  • 工具
  • 规模化
  • 可复用性&组合
  • 其他
  • Vuex
标签
时间轴
关于

【vue】🔥vue2.x底层的问题,你能答多少

vuePress-theme-reco 派大星    2021 - 2022

【vue】🔥vue2.x底层的问题,你能答多少

派大星 2021-01-20 掘金分享Vue

有句老话说,在父母那里我们永远是孩子,同样在各位大佬这里,我永远是菜鸡🐣🐣🐣。不管怎样,学习的激情永远不可磨灭。答案如有错误,感谢指教🌚

# 首发个人博客

种一棵树,最好的时机是十年前,其次是现在 (opens new window)

# vue源码中值得学习的点

  1. 科里化: 一个函数原本有多个参数, 只传入一个参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
  2. 偏函数: 一个函数原本有多个参数, 只传入一部分参数, 生成一个新函数, 由新函数接收剩下的参数来运行得到结构
  3. 高阶函数: 一个函数参数是一个函数, 该函数对参数这个函数进行加工, 得到一个函数, 这个加工用的函数就是高阶函数
  4. ...

# vue 响应式系统

简述: vue 初始化时会用Object.defineProperty()给data中每一个属性添加getter和setter,同时创建dep和watcher进行依赖收集与派发更新,最后通过diff算法对比新老vnode差异,通过patch即时更新DOM

# 简易图解:

# 详细版本

可以参考下图片引用地址: 图解 Vue 响应式原理 (opens new window)

# Vue的数据为什么频繁变化但只会更新一次

  1. 检测到数据变化
  2. 开启一个队列
  3. 在同一事件循环中缓冲所有数据改变
  4. 如果同一个 watcher (watcherId相同)被多次触发,只会被推入到队列中一次

不优化,每一个数据变化都会执行: setter->Dep->Watcher->update->run

优化后:执行顺序update -> queueWatcher -> 维护观察者队列(重复id的Watcher处理) -> waiting标志位处理 -> 处理$nextTick(在为微任务或者宏任务中异步更新DOM)

# vue使用Object.defineProperty() 的缺陷

数组的length属性被初始化configurable false,所以想要通过get/set方法来监听length属性是不可行的。

vue中通过重写了七个能改变原数组的方法来进行数据监听

对象还是使用Object.defineProperty()添加get和set来监听

参考

  • vue通过数组索引修改数据,视图会变化吗? (opens new window)

  • 为什么defineProperty不能检测到数组长度的变化 (opens new window)

# Vue.nextTick()原理

在下次DOM更新循环结束之后执行延迟回调。在修改数据之后立即使用这个方法,获取更新后的DOM。

源码实现:Promise > MutationObserver > setImmediate > setTimeout

参考文章:浅析Vue.nextTick()原理 (opens new window)

# computed 的实现原理

computed 本质是一个惰性求值的观察者computed watcher。其内部通过 this.dirty 属性标记计算属性是否需要重新求值。

  • 当 computed 的依赖状态发生改变时,就会通知这个惰性的 watcher,computed watcher 通过 this.dep.subs.length 判断有没有订阅者,
  • 有的话,会重新计算,然后对比新旧值,如果变化了,会重新渲染。 (Vue 想确保不仅仅是计算属性依赖的值发生变化,而是当计算属性最终计算的值发生变化时才会触发渲染 watcher 重新渲染,本质上是一种优化。)
  • 没有的话,仅仅把 this.dirty = true (当计算属性依赖于其他数据时,属性并不会立即重新计算,只有之后其他地方需要读取属性的时候,它才会真正计算,即具备 lazy(懒计算)特性。)

# watch 的理解

watch没有缓存性,更多的是观察的作用,可以监听某些数据执行回调。当我们需要深度监听对象中的属性时,可以打开deep:true选项,这样便会对对象中的每一项进行监听。这样会带来性能问题,优化的话可以使用字符串形式监听

注意:Watcher : 观察者对象 , 实例分为渲染 watcher (render watcher),计算属性 watcher (computed watcher),侦听器 watcher(user watcher)三种

# vue diff 算法

  • 只对比父节点相同的新旧子节点(比较的是Vnode),时间复杂度只有O(n)
  • 在 diff 比较的过程中,循环从两边向中间收拢

新旧节点对比过程

1、先找到 不需要移动的相同节点,借助key值找到可复用的节点是,消耗最小

2、再找相同但是需要移动的节点,消耗第二小

3、最后找不到,才会去新建删除节点,保底处理

注意:新旧节点对比过程,不会对这两棵Vnode树进行修改,而是以比较的结果直接对 真实DOM 进行修改

Vue的patch是即时的,并不是打包所有修改最后一起操作DOM(React则是将更新放入队列后集中处理)

参考文章:Vue 虚拟dom diff原理详解 (opens new window)

# vue 渲染过程

  1. 调用 compile 函数,生成 render 函数字符串 ,编译过程如下:
  • parse 使用大量的正则表达式对template字符串进行解析,将标签、指令、属性等转化为抽象语法树AST。模板 -> AST (最消耗性能)
  • optimize 遍历AST,找到其中的一些静态节点并进行标记,方便在页面重渲染的时候进行diff比较时,直接跳过这一些静态节点,优化runtime的性能
  • generate 将最终的AST转化为render函数字符串
  1. 调用 new Watcher 函数,监听数据的变化,当数据发生变化时,Render 函数执行生成 vnode 对象
  2. 调用 patch 方法,对比新旧 vnode 对象,通过 DOM diff 算法,添加、修改、删除真正的 DOM 元素

# 结合源码,谈一谈vue生命周期

vue 生命周期官方图解 (opens new window)

# Vue 中的 key 到底有什么用?

key 是给每一个 vnode 的唯一 id,依靠 key,我们的 diff 操作可以更准确、更快速

更准确 : 因为带 key 就不是就地复用了,在 sameNode 函数 a.key === b.key 对比中可以避免就地复用的情况。所以会更加准确,如果不加 key,会导致之前节点的状态被保留下来,会产生一系列的 bug。

更快速 : key 的唯一性可以被 Map 数据结构充分利用,相比于遍历查找的时间复杂度 O(n),Map 的时间复杂度仅仅为 O(1),源码如下:

function createKeyToOldIdx(children, beginIdx, endIdx) {
  let i, key;
  const map = {};
  for (i = beginIdx; i <= endIdx; ++i) {
    key = children[i].key;
    if (isDef(key)) map[key] = i;
  }
  return map;
}

# vue-router 路由模式有几种

默认值: "hash" (浏览器环境) | "abstract" (Node.js 环境)

可选值: "hash" | "history" | "abstract"

配置路由模式:

  • hash: 使用 URL hash 值来作路由。支持所有浏览器,包括不支持 HTML5 History Api 的浏览器。
  • history: 依赖 HTML5 History API 和服务器配置。
  • abstract: 支持所有 JavaScript 运行环境,如 Node.js 服务器端。如果发现没有浏览器的 API,路由会自动强制进入这个模式.

# 说一说keep-alive实现原理

# 定义

keep-alive组件接受三个属性参数:include、exclude、max

  • include 指定需要缓存的组件name集合,参数格式支持String, RegExp, Array。当为字符串的时候,多个组件名称以逗号隔开。
  • exclude 指定不需要缓存的组件name集合,参数格式和include一样。
  • max 指定最多可缓存组件的数量,超过数量删除第一个。参数格式支持String、Number。

# 原理

keep-alive实例会缓存对应组件的VNode,如果命中缓存,直接从缓存对象返回对应VNode

LRU(Least recently used)算法根据数据的历史访问记录来进行淘汰数据,其核心思想是“如果数据最近被访问过,那么将来被访问的几率也更高”。(墨菲定律:越担心的事情越会发生)

# 对对象属性访问的解析方法

eg:访问 a.b.c.d

函数柯里化 + 闭包 + 递归

    function createGetValueByPath( path ) {
      let paths = path.split( '.' ); // [ xxx, yyy, zzz ]
      
      return function getValueByPath( obj ) {
        let res = obj;
        let prop;
        while( prop = paths.shift() ) {
          res = res[ prop ];
        }
        return res;
      }
    }
    
    let getValueByPath = createGetValueByPath( 'a.b.c.d' );
    
    var o = {
      a: {
        b: {
          c: {
            d: {
              e: '正确了'
            }
          }
        }
      }
    };
    var res = getValueByPath( o );
    console.log( res );

# vue中针对7个数组方法的重写

Vue 通过原型拦截的方式重写了数组的 7 个方法,首先获取到这个数组的Observer。如果有新的值,就调用 observeArray 对新的值进行监听,然后调用 notify,通知 render watcher,执行 update

const arrayProto = Array.prototype;
export const arrayMethods = Object.create(arrayProto);
const methodsToPatch = [
  "push",
  "pop",
  "shift",
  "unshift",
  "splice",
  "sort",
  "reverse"
];

methodsToPatch.forEach(function(method) {
  // cache original method
  const original = arrayProto[method];
  def(arrayMethods, method, function mutator(...args) {
    const result = original.apply(this, args);
    const ob = this.__ob__;
    let inserted;
    switch (method) {
      case "push":
      case "unshift":
        inserted = args;
        break;
      case "splice":
        inserted = args.slice(2);
        break;
    }
    if (inserted) ob.observeArray(inserted);
    // notify change
    ob.dep.notify();
    return result;
  });
});

Observer.prototype.observeArray = function observeArray(items) {
  for (var i = 0, l = items.length; i < l; i++) {
    observe(items[i]);
  }
};

# vue处理响应式 defineReactive 实现

    // 简化后的版本 
    function defineReactive( target, key, value, enumerable ) {
      // 折中处理后, this 就是 Vue 实例
      let that = this;

      // 函数内部就是一个局部作用域, 这个 value 就只在函数内使用的变量 ( 闭包 )
      if ( typeof value === 'object' && value != null && !Array.isArray( value ) ) {
        // 是非数组的引用类型
        reactify( value ); // 递归
      }

      Object.defineProperty( target, key, {
        configurable: true,
        enumerable: !!enumerable,

        get () {
          console.log( `读取 ${key} 属性` ); // 额外
          return value;
        },
        set ( newVal ) {
          console.log( `设置 ${key} 属性为: ${newVal}` ); // 额外

          value = reactify( newVal );

        }
      } );
    }

# vue响应式 reactify 实现

// 将对象 o 响应式化
    function reactify( o, vm ) {
      let keys = Object.keys( o );

      for ( let i = 0; i < keys.length; i++ ) {
        let key = keys[ i ]; // 属性名
        let value = o[ key ];
        if ( Array.isArray( value ) ) {
          // 数组
          value.__proto__ = array_methods; // 数组就响应式了
          for ( let j = 0; j < value.length; j++ ) {
            reactify( value[ j ], vm ); // 递归
          }
        } else {
          // 对象或值类型
          defineReactive.call( vm, o, key, value, true );
        }
      }
    }

# 为什么访问data属性不需要带data

vue中访问属性代理this.data.xxx 转换 this.xxx的实现

    /** 将 某一个对象的属性 访问 映射到 对象的某一个属性成员上 */
    function proxy( target, prop, key ) {
      Object.defineProperty( target, key, {
        enumerable: true,
        configurable: true,
        get () {
          return target[ prop ][ key ];
        },
        set ( newVal ) {
          target[ prop ][ key ] = newVal;
        }
      } );
    }

# 参考文章(除文章已指出的)

  • Vue.js 技术揭秘 (opens new window)
  • 「面试题」20+Vue面试题整理 (opens new window)
  • 12道vue高频原理面试题,你能答出几道? (opens new window)
  • B站 vue源码分析(视频有缺失) (opens new window)
  • 你要找的 vue 源码 全宇宙的都在这 (opens new window)
  • Vue技术内幕 (opens new window)