Giter Club home page Giter Club logo

notebook's People

Contributors

theydy avatar

notebook's Issues

异步求和

const addRemote = async (a, b) => new Promise(resolve => {
  setTimeout(() => resolve(a + b), 1000)
})

async function add(...args) {
  if (args.length <= 1) return Promise.resolve(...args);

  if (args.length === 2) {
    return addRemote(args[0], args[1]);
  }

  const promiseList = [];

  for (let i = 0; i < args.length ; i+=2) {
    promiseList.push(addRemote(args[i], args[i + 1] || 0))
  }

  const res = await Promise.all(promiseList);
  return add(...res);
}

// test
add(1, 2).then(res => {
  console.log(res);
})

add(1, 2, 3, 4, 5, 6, 7).then(res => {
  console.log(res);
})

TCP 协议

TCP 协议的特点

  1. TCP 是面向连接的,开始传输数据前需要先建立连接
  2. TCP 连接只能是一对一
  3. TCP 提供可靠的交付服务,TCP 传输的数据可以保证:无差错,无重复,无丢失,并且按顺序到达
  4. TCP 协议是全双工的

TCP 如何保证可靠传输

停止等待协议

停止等待协议的原理是每发完一组数据就停止发送,等待对方确认,在收到确认后再发下一组数据。如果对方接收到重复的数据,会丢弃该分组,但还要发送确认。

在停止等待协议中,如果出现数据错误而没有收到确认,会执行超时重传。超时重传有两种,自动超时重传 ARQ 协议 连续 ARQ 协议

自动超时重传 ARQ 协议:每组数据都等待确认,如果没有收到确认就重传当前分组。

连续 ARQ 协议:发送方维持一个发送窗口,位于窗口内的分组可以连续发送,不需等待确认,接收方采用累计确认,对按需到达的最后一个分组发送确认。

滑动窗口

滑动窗口是一种流量控制技术,TCP 利用滑动窗口实现流量控制的机制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。接收方会把当前接收窗口的大小写入应答报文,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据,发送方让自己的发送窗口取为拥塞窗口和接收方的接收窗口中较小的一个

当出现 接收窗口剩余空间 0 的情况,发送端会停止发送数据,但会定时发送 1 字节的请求给接收方,让接收方告知窗口大小。

拥塞控制

拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。

  • 慢启动
    • 慢启动算法的思路是当主机开始发送数据时,如果立即把大量数据字节注入到网络,那么可能会引起网络阻塞,因为现在还不知道网络的符合情况,所以会由小到大逐渐增大发送窗口,拥塞窗口(cwnd)初始值为1,每经过一个传播轮次,cwnd 加倍,当拥塞窗口大于慢启动门限 (ssthresh) 的变量时,进入拥塞避免阶段。
  • 拥塞避免
    • 拥塞避免算法的思路是让拥塞窗口cwnd 缓慢增大,即每经过一个往返时间 RTT 就把 cwnd 加 1,当出现超时重传的情况时,TCP 会认为出现网络拥塞了,此时会把慢启动门限 (ssthresh) 降为拥塞窗口(cwnd)的一半,拥塞窗口(cwnd)重新置为 1,然后重新进入慢启动阶段。
  • 快速重传与快速恢复
    • 一旦接收端收到的报文出现失序的情况,接收端只会回复最后一个顺序正确的报文序号。如果发送端收到三个重复的 ACK,即连续三个数据包都不是正确的顺序,这说明正确顺序的数据包很可能丢失了。这时无需等待定时器超时而是直接启动快速重传,把慢启动门限 (ssthresh) 降为拥塞窗口(cwnd)的一半,拥塞窗口(cwnd)的值也置为原来的一半,然后重新进入拥塞避免阶段。快速恢复在于最后一步不是进入拥塞避免阶段,而是进入快速恢复,快速恢复会重新发送丢失的那个包,当正确收到 ACK 时,就退除快速恢复阶段,进入拥塞避免阶段。

拖动窗口指令 v-el-draggable-dialog

export const elDraggableDialog = {
  bind(el, _, vnode) {
    const dragDom = el.querySelector('.el-dialog');
    const dialogHeaderEl = el.querySelector('.el-dialog__header');
    dragDom.style.cssText += ';top:0px;';
    dialogHeaderEl.style.cssText += ';cursor:move;';

    dialogHeaderEl.onmousedown = (e) => {
      const disX = e.clientX - dialogHeaderEl.offsetLeft;
      const disY = e.clientY - dialogHeaderEl.offsetTop;

      const dragDomWidth = dragDom.offsetWidth;
      const dragDomHeight = dragDom.offsetHeight;

      const screenWidth = document.body.clientWidth;
      const screenHeight = document.body.clientHeight;

      const minDragDomLeft = dragDom.offsetLeft;
      const maxDragDomLeft = screenWidth - dragDom.offsetLeft - dragDomWidth;

      const minDragDomTop = dragDom.offsetTop;
      const maxDragDomTop = screenHeight - dragDom.offsetTop - dragDomHeight;

      const styleLeftStr = getComputedStyle(dragDom).left;
      const styleTopStr = getComputedStyle(dragDom).top;
      if (!styleLeftStr || !styleTopStr) return
      let styleLeft;
      let styleTop;

      // Format may be "##%" or "##px"
      if (styleLeftStr.includes('%')) {
        styleLeft = +document.body.clientWidth * (+styleLeftStr.replace(/%/g, '') / 100);
        styleTop = +document.body.clientHeight * (+styleTopStr.replace(/%/g, '') / 100);
      } else {
        styleLeft = +styleLeftStr.replace(/px/g, '');
        styleTop = +styleTopStr.replace(/px/g, '');
      }

      document.onmousemove = (e) => {
        let left = e.clientX - disX;
        let top = e.clientY - disY;

        // Handle edge cases
        if (-(left) > minDragDomLeft) {
          left = -minDragDomLeft;
        } else if (left > maxDragDomLeft) {
          left = maxDragDomLeft;
        }
        if (-(top) > minDragDomTop) {
          top = -minDragDomTop;
        } else if (top > maxDragDomTop) {
          top = maxDragDomTop;
        }

        // Move current element
        dragDom.style.cssText += `;left:${left + styleLeft}px;top:${top + styleTop}px;`;

        // Emit on-dialog-drag event
        // See https://stackoverflow.com/questions/49264426/vuejs-custom-directive-emit-event
        if (vnode.componentInstance) {
          vnode.componentInstance.$emit('on-dialog-drag');
        } else if (vnode.elm) {
          vnode.elm.dispatchEvent(new CustomEvent('on-dialog-drag'));
        }
      }

      document.onmouseup = () => {
        document.onmousemove = null;
        document.onmouseup = null;
      }
    }
  }
}

// Vue.directive('el-draggable-dialog', elDraggableDialog); // v-el-draggable-dialog

模块化相关

es6 使用 ESModule, import export default

nodejs 使用 CommonJS,require module.exports

参考

import、require、export、module.exports 混合使用详解

require('./expample.js).default详解

Webpack 模块打包原理 | springleo's blog

ESModule 和 CommonJS 的区别

  • CommonJS 是加载时运行,ESModule是编译时输出接口
  • CommonJS 是动态语法可以写在判断里,ESModule 静态语法只能写在顶层
  • CommonJS 输出的是值的浅拷贝,ESModule输出值的引用
  • CommonJS 是单个值导出,ESModule可以导出多个(实际上都是单个值导出,ESModule 有语法糖才让人感觉能导出多个?)

babel

js 文件经过 babel 转换,无论是 ESModule 还是 CommonJS 最终都会被编译成 CommonJS 规范。

export

// ESModule 编译前
export default 123;

export const a = 123;

const b = 3;
const c = 4;
export { b, c};

// 经过 babel 编译为 CommonJS 后
"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.c = exports.b = exports.a = exports.default = void 0;
var _default = 123;
exports.default = _default;
var a = 123;
exports.a = a;
var b = 3;
exports.b = b;
var c = 4;
exports.c = c;

可以发现原本在 ESModule 中代表默认输出的 default 转为 CommonJS 后并不代表默认输出 exports = x,只是作为一个普通 exports.default 字段的输出,并且拦截了 __esModule 为 true 标记为 ESModule。

这就是为什么有时候使用 require 语法引入 ESModule 默认输出时需要加 .default 的原因。

为什么 babel 不处理这种情况以方便用户使用呢?

其实在 babel 5 中如果 es6 模块只有一个 default 输出,babel 是会特殊处理不需加 .default 的,但这样反而不符合 es6 的定义,因为实际上 ESModule 中的 default 就是 as 的语法糖。

// a.js
const a = 123;
export default a;

// b.js
import a from './a.js';

// <=> 相当与

// a.js
const a = 123;
export {
  a as default
};

//b.js
import { default as a } from './a.js';

default 输出其实也是一个普通字段,只是作为语法糖省略了 {} 的写法,babel 将 ESModule 转为 CommonJS 时将 default 作为 exports.default 输出才符合 es6 语意,同时,即使 babel 会特殊处理,一旦碰到有 default 输出又有某个具名输出的 es6 模块也没办法。

import

  1. 默认引入 default
// ESModule 编译前
import a from './a.js'

a.test();

// 经过 babel 编译为 CommonJS 后
"use strict";

var _a = _interopRequireDefault(require("./a.js"));

function _interopRequireDefault(obj) { 
  return obj && obj.__esModule ? obj : { default: obj };
}

_a.default.test();
// CommonJs 编译前
const a = require('./a.js')

a.test()

// 经过 babel 编译为 CommonJS 后
"use strict";

var a = require('./a.js');

a.test();

webpack

webpack 内部维护了一套自己的模块实现 webpack_require,无论 CommonJS 或者 ESModule 最后都会被编译 webpack 自己的模块实现,所以在 webpack 中可以两者混合使用。但 webpack 不会做 es6 语法的转换。

output.libraryTarget 字段

经过 webpack 编译后的代码默认是不能在被其他模块引用的,这和 output.libraryTarget 字段有关。

output.libraryTarget 默认值为 var

此时,需要配合 output.library 使用,将编译后的模块暴露给全局 output.library 指定值的变量。

output.libraryTarget 值为 commonjs

此时,需要配合 output.library 使用,将编译后的模块暴露给 exports 下 output.library 指定值的变量。

output.libraryTarget 值为 commonjs2

此时,会忽略 output.library ,将编译后的模块作为 exports 输出。

tree-shaking

tree-shaking 是 webpack 的一项功能,tree-shaking 只能在 ESModule 中使用,如果先通过 babel 转为 CommonJS 就用不了。

通过静态分析 es6 的语法,可以删除没有被使用的模块。要使用这项技术,只能使用 webpack 的模块处理,加上 babel 的 es6 转换能力(需要关闭模块转换)。

HTTP 协议相关知识点整理

HTTP

HTTP 是一种超文本传输协议,构建于 TCP/IP 协议之上,是一个应用层协议

HTTP 协议的主要特点

  • 灵活可扩展:1. 语义上的自由,只规定了基本格式(如:空格分隔单词、换行分隔字段),其他部分没有严格的语法限制;2. 传输格式的多样性(文本、图片等任意格式)
  • 无连接:每完成一个请求就断开连接,HTTP 1.1 开始默认开启 keep-alive 长连接
  • 无状态:单从 HTTP 协议上没有办法识别两次连接者身份,所以后来加入 cookie
  • 可靠传输:HTTP 基于 TCP 协议,TCP 协议拥有可靠传输的特性
  • 明文传输:协议里的报文直接使用文本形式传输,HTTP 2 后改为二进制传输

HTTP 报文

请求报文

<method> <request-url> <version>
<headers>

<entity-body>

响应报文

<version> <state-code> <state-desc>
<headers>

<entity-body>

GET 与 POST 区别

  • 语义不同,GET 获取数据,POST 修改数据
  • GET 回退没有影响,POST 会再次发送数据
  • GET 会被浏览器缓存下来,留下历史记录,POST 不会
  • GET 是幂等的,POST 不是(幂等表示执行相同的操作,结果也是相同的)

data 初始化时 getData 中取值前为啥要执行 pushTarget() 置空 Dep.target ?

在初始化 data 时,当 data 写成函数的形式,会进入 getData 函数

function initData (vm: Component) {
  let data = vm.$options.data
  data = vm._data = typeof data === 'function'
    ? getData(data, vm)
    : 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()
  }
}

这里有个有意思的地方,就是在真正执行 data.call(vm, vm) 取值前有一个 pushTarget() 置空 Dep.target 的操作,取值后再恢复 popTarget()

Dep.target = null

const targetStack = []

export function pushTarget (_target: ?Watcher) {
  if (Dep.target) targetStack.push(Dep.target)
  Dep.target = _target
}

export function popTarget () {
  Dep.target = targetStack.pop()
}

其实这么做的原因已经在源码的注释中写明了,就是为了解决 #7573 这个 issues。

Pitfalls of Vue dependency detection may cause redundant dependencies · Issue #7573 · vuejs/vue

这个 bug 的现象是当子组件 data 写成函数形式并且函数中使用了父组件传给子组件的 props,当父组件中做为 props 传入子组件的那个响应式数据改变时,会触发两次父组件的更新。而且触发两次更新只在数据第一次改变时发生,后续就是正常的只触发一次更新。

之所以会这样,是因为执行 data.call(vm, vm) 获取子组件 data 值时,里面使用了 props,此时会触发 propsgetter,造成 props 收集依赖。由于数据初始化的时机是 beforeCreated -> created 之间,此时还没有进入子组件的渲染阶段(生成渲染 Watcher 是在 mountComponent 中),也就没有子组件的渲染 Watcher。所以这时候 Dep.target 指向的依然是父组件的渲染 Watcher。

最终表现就是父组件的字段更新时,正确触发了一次父组件的渲染 Watcher 的 update,更新子组件的 props 时,又触发了一次父组件的渲染 Watcher 的 update。

而第一次更新后,后续收集依赖时子组件的渲染 Watcher 已经存在,所以不会收集到父组件的渲染 Watcher。

其实不只是这里,子组件的 beforeCreatecreatedbeforeMount 这三个生命周期钩子函数如果用了 props 的话,也会出现同样的问题,所以在 callHook 函数中也做了同样 Dep.target 置空的操作。

其实不只是这里,子组件的 beforeCreatecreatedbeforeMount 这三个生命周期钩子函数如果用了 props 的话,也会出现同样的问题,所以在 callHook 函数中也做了同样 Dep.target 置空的操作。

export function callHook (vm: Component, hook: string) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget()
  const handlers = vm.$options[hook]
  if (handlers) {
    for (let i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm)
      } catch (e) {
        handleError(e, vm, `${hook} hook`)
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook)
  }
  popTarget()
}

BFC 知识整理

BFC 知识整理

BFC 概念

BFC (块级格式化上下文)

BFC 如何创建

  • float 的值不为 none
  • position 的值不为 static 或者 relative
  • overflow 的值不为 visible
  • display 的值为 table-cell, table-caption, inline-block, flex, inline-flex

BFC 的原理(渲染规则)

  • BFC 内部的Box会在垂直方向上一个接一个的放置
  • BFC 导致垂直方向发生边距重叠,同时BFC 也是解决边距重叠的方案
  • BFC 区域不会与浮动元素的box 重叠,所以可以使用BFC 来清除浮动
  • BFC 内部每个元素的左外边距与包含块的左边界相接触(从右到左的格式化时,则为右边框紧挨),即使浮动元素也是如此,除非这个盒子的内部创建了一个新的 BFC
  • BFC 在页面上是一个独立的容器,外面的元素不会影响BFC 内的元素,同时BFC 内部的元素也不会影响外面的元素
  • BFC 计算高度时,浮动元素也参与计算

BFC 的应用

防止文字环绕 (不和浮动元素重叠)

CodePan查看效果

如果一个浮动元素后面跟着一个非浮动的元素,那么就会产生一个覆盖的现象,这时候在非浮动元素上添加文字,文字会环绕浮动元素。防止文字环绕也就是利用BFC 使非浮动元素与浮动元素不重叠。

<div class="eg eg-1">
  <div class="aside"></div>
  <div class="main">
  Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse
  </div>
</div>
// stylus
.eg-1
  width 300px
  position relative
  .aside
    width 100px
    height 150px
    float left
    background #f66
  .main
    height 200px
    background #fcc
    overflow hidden // 生成 bfc

当触发main生成BFC后,这个新的BFC不会与浮动的aside重叠,文字环绕的效果也消失了。main会根据包含块的宽度,和aside的宽度,自动变窄。

清除元素内部浮动 (计算浮动子元素高度)

CodePan查看效果

<div class="eg eg-2">
  <div class="par">
    <div class="child"></div>
    <div class="child"></div>
  </div>
</div>
// stylus
.par
  border 5px solid #fcc
  width 300px
  overflow hidden // 生成BFC
  .child
    border 5px solid #f66
    width 100px
    height 100px
    float left

触发par生成BFC,那么par在计算高度时,par内部的浮动元素child也会参与计算。

防止垂直 margin 重叠

CodePan查看效果

<div class="eg eg-3">
  <p class="p">第一个P</p>
  <div class="bfc-wrap">
    <p class="p">第二个P</p>
  </div>
</div>
// stylus
.p
  color #f55
  background #fcc
  width 200px
  line-height 100px
  text-align center
  margin 20px
.bfc-wrap
  overflow hidden // 生成BFC

同属于一个BFC时,两个元素发生垂直margin重叠,包括相邻元素,嵌套元素,只要他们之间没有阻挡,因此使用一个wrap 包裹住第二个p 元素并生成BFC ,此时两个P 元素不属于同一个BFC ,消除margin 重叠。

vue-dev-inspector loader

vue-dev-inspector.js

const loaderUtils = require('loader-utils');
const formatSource = require('./formatSource.js');

module.exports = function (source) {
  try {
    const {
      // 模块所在的根目录
      rootContext: rootPath,
      // 资源文件路径
      resourcePath: filePath,
    } = this;

    /**
     * example:
     * rootPath: /home/xxx/project
     * filePath: /home/xxx/project/src/ooo/xxx.js
     * relativePath: src/ooo/xxx.js
     */
    const relativePath = filePath.slice(rootPath.length + 1);

    const options = loaderUtils.getOptions(this);

    if ((options.exclude || []).length > 0) {
      const isSkip = options.exclude.some((path) => filePath.includes(path));
      if (isSkip) {
        return source;
      }
    }

    const code = formatSource(source, relativePath);
    return code;
  } catch (error) {
    console.error('\nvue-dev-inspector compiler error', error);
    return source;
  }
};

formatSource.js

const compiler = require('@vue/compiler-dom');

function transform(
  ast,
  nodeList,
  options = {
    entry: (node, nodeList) => {},
    out: (node, nodeList) => {},
  }
) {
  const _traverse = (ast, options) => {
    options.entry && options.entry(ast, nodeList);

    ast.children &&
      ast.children.map((node) => {
        _traverse(node, options);
      });

    options.out && options.out(ast, nodeList);
  };
  _traverse(ast, options);
}

function generator(nodeList, source) {
  let code = '';
  // 如果 vue 文件中自定义块,必须写针对自定义块的 rule,否则 vue-loader 会报错,具体看
  // https://theydy.github.io/notebook/vue/other.html#vue-loader-%E8%87%AA%E5%AE%9A%E4%B9%89%E5%9D%97
  let start = 0;

  for (let i = 0; i < nodeList.length; i++) {
    const node = nodeList[i];
    code += source.slice(start, node.insertIndex);
    code += node.desc;

    if (i === nodeList.length - 1) {
      code += source.slice(node.insertIndex);
    } else {
      start = node.insertIndex;
    }
  }

  return code;
}

function vueDevInspectorLoader(source, relativePath) {
  const ast = compiler.parse(source);

  const template = ast.children.find((node) => node.tag === 'template');

  // 如果没有 template 直接返回
  if (!template) return source;

  const nodeList = [];
  transform(template, nodeList, {
    entry: (node, nodeList) => {
      if (!node.tag) return;

      const nodeStart = node.loc.start.offset;
      const hasProps = node.props && node.props.length;

      const insertIndex = !hasProps
        ? nodeStart + node.tag.length + 1
        : node.props[node.props.length - 1].loc.end.offset;

      const target = {
        insertIndex,
        desc: ` data-inspector-line="${node.loc.start.line}" data-inspector-column="${node.loc.start.column}" data-inspector-relative-path="${relativePath}"`,
      };

      nodeList.push(target);
    },
  });

  let code = generator(nodeList, source);
  return code;
}

module.exports = vueDevInspectorLoader;

响应式原理

数据的响应式起点在于 _init 函数中的 initState 中,在这个方法里按 props、methods、data、computed、watch 顺序初始化数据,并将他们转换为响应式对象。

  function initState (vm) {
    vm._watchers = [];
    var opts = vm.$options;
    if (opts.props) { initProps(vm, opts.props); }
    if (opts.methods) { initMethods(vm, opts.methods); }
    if (opts.data) {
      initData(vm);
    } else {
      observe(vm._data = {}, true /* asRootData */);
    }
    if (opts.computed) { initComputed(vm, opts.computed); }
    if (opts.watch && opts.watch !== nativeWatch) {
      initWatch(vm, opts.watch);
    }
  }

这里只看 props 和 data 的响应式过程,因为最终调用的响应式方法其实都一样只是步骤有所不同,所以先列出 props 和 data 初始化的步骤,最后分析具体的响应式方法实现。

initProps

initProps 主要逻辑如下,遍历组件的 props 配置,获取 prop 值,执行 defineReactive 转为响应式对象,再将 _props 代理到 vm 实例上。这里最主要的是 defineReactive

    var props = vm._props = {};
    var loop = function ( key ) {
      keys.push(key);
      var value = validateProp(key, propsOptions, propsData, vm);
   
      {
        
        defineReactive(props, key, value, function () {
          // ...
        });
      }

      if (!(key in vm)) {
        proxy(vm, "_props", key);
      }
    };

    for (var key in propsOptions) loop( key );

initData

initData 主要逻辑如下,先取到 data 的配置,因为一般 data 会写成一个函数,所以 getData 其实就是执行这个函数获取最终的返回值做为 data 值。

然后将 _data 代理到 vm 实例上,最后调用 observe 转为响应式对象。这里最主要的是 observe

    var data = vm.$options.data;
    data = vm._data = typeof data === 'function'
      ? getData(data, vm)
      : data || {};

    var keys = Object.keys(data);
    var i = keys.length;

    while (i--) {
      var key = keys[i];
      // ...
      proxy(vm, "_data", key);
    }
    // observe data
    observe(data, true /* asRootData */);

响应式关键方法

由上可知,initPropsinitData 中响应式的关键方法分别是 defineReactiveobserve

  • defineReactive(props, key, value, function () {});
  • observe(data, true /* asRootData */);

其实在 defineReactive 会引出 Dep 类,Dep 类又会引出 Watcher 类;observe 会引出了 Observer 类。

实现数据响应式的关键:

  • observedefineReactive 两个方法。
  • ObserverDepWatcher 三个类。

observe

  function observe (value, asRootData) {
    if (!isObject(value) || value instanceof VNode) {
      return
    }
    var ob;
    if (hasOwn(value, '__ob__') && value.__ob__ instanceof Observer) {
      ob = value.__ob__;
    } else if (
      shouldObserve &&
      !isServerRendering() &&
      (Array.isArray(value) || isPlainObject(value)) &&
      Object.isExtensible(value) &&
      !value._isVue
    ) {
      ob = new Observer(value);
    }
    if (asRootData && ob) {
      ob.vmCount++;
    }
    return ob
  }

从代码可以看出,observe 函数的主要作用是对于一个引用类型对象新建一个 Observerob = new Observer(value);,并且这个 ob 对象最后会保存在 value.__ob__ 下。

defineReactive

  function defineReactive (
    obj,
    key,
    val,
    customSetter,
    shallow
  ) {
    var dep = new Dep();

    var property = Object.getOwnPropertyDescriptor(obj, key);
    if (property && property.configurable === false) {
      return
    }

    // cater for pre-defined getter/setters
    var getter = property && property.get;
    var setter = property && property.set;
    if ((!getter || setter) && arguments.length === 2) {
      val = obj[key];
    }

    var childOb = !shallow && observe(val);
    Object.defineProperty(obj, key, {
      enumerable: true,
      configurable: true,
      get: function reactiveGetter () {
        var 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) {
        var 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 (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();
      }
    });
  }

defineReactive 函数里真正调用 Object.defineProperty 劫持了 getter / setter 。getter 中通过 dep.depend() 收集依赖,setter 中通过 dep.notify() 触发更新。

这个函数中需要额外注意的地方有三点:

  • defineReactive 中生成了一个 Dep 对象,在 getter / setter 通过闭包访问。

    其实单纯只看 defineReactive 函数中的这个 Dep 对象,倒是没有什么特别的,但是稍后会交代 Observer 类中实际上也会生成一个 Dep 对象,这两个 Dep 对象的作用是一样的,为什么需要两个 Dep 呢?接下来的第二点 / 第三点都于此有关。

  • getter 中收集依赖部分不止是单纯 dep.depend() 还有对于 childOb 的判断。

    if (Dep.target) {
      dep.depend();
      if (childOb) {
        childOb.dep.depend();
        if (Array.isArray(value)) {
          dependArray(value);
        }
      }
    }

    实际上 childOb 是这样获取的 childOb = observe(val),从之前的 observe 介绍可以知道,这里返回的就是 Observer 实例,并且只有在 val 是一个引用类型时才会有值。

    判断这个 childOb 的目的是为了拿到第一点中说的第二个 Dep 对象执行依赖收集的操作。所以回到第一点中提出的问题「为什么需要两个 Dep?」这是因为 Object.defineProperty 存在缺陷,无法劫持对象类型数据的属性增删和数组类型数据的子项增删,对于这种情况,Vue 提供了 Vue.$set 方法,在该方法中需要手动触发更新,但是又拿不到通过闭包访问的 Dep,所以才需要在 val.__ob__ 上保存一个相同的 Dep 对象,实际上如果通过其他方式让我们能够访问 defineReactive 中的 Dep 对象,那么这里是不需要两个的。

    至于 dependArray(value); 则是对于数组的特殊操作,数组的索引是非响应式的,数组中的每一项引用类型的属性都应该收集到当前的依赖,确保在使用 $setVue.set 时,数组中嵌套的对象能正常响应。

  • setter 中会对新的值重新执行一次 observe(newVal)

    因为新的值有可能是一个引用类型,所以需要该操作。

Observer

  var Observer = function Observer (value) {
    this.value = value;
    this.dep = new Dep();
    this.vmCount = 0;
    def(value, '__ob__', this);
    if (Array.isArray(value)) {
      if (hasProto) {
        protoAugment(value, arrayMethods);
      } else {
        copyAugment(value, arrayMethods, arrayKeys);
      }
      this.observeArray(value);
    } else {
      this.walk(value);
    }
  };

  Observer.prototype.walk = function walk (obj) {
    var keys = Object.keys(obj);
    for (var i = 0; i < keys.length; i++) {
      defineReactive(obj, keys[i]);
    }
  };

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

  function protoAugment (target, src) {
    target.__proto__ = src;
  }

  function copyAugment (target, src, keys) {
    for (var i = 0, l = keys.length; i < l; i++) {
      var key = keys[i];
      def(target, key, src[key]);
    }
  }

Observer 的实例会保存在 value.__ob__ 上,接着对于数组和对象类型会执行不同的操作

  • 对象:walk 函数,遍历对象的 keys,执行 defineReactive 转为响应式属性。
  • 数组:protoAugmentcopyAugment 劫持数组的变异方法,observeArray 函数,遍历属性,执行 observe 转为响应式属性。

Dep

  var Dep = function Dep () {
    this.id = uid++;
    this.subs = [];
  };

  Dep.target = null; // 这是一个 Watcher 对象

  Dep.prototype.addSub = function addSub (sub) {
    this.subs.push(sub);
  };

  Dep.prototype.removeSub = function removeSub (sub) {
    remove(this.subs, sub);
  };

  Dep.prototype.depend = function depend () {
    if (Dep.target) {
      Dep.target.addDep(this);
    }
  };

  Dep.prototype.notify = function notify () {
    var subs = this.subs.slice();
    if (!config.async) {
      subs.sort(function (a, b) { return a.id - b.id; });
    }
    for (var i = 0, l = subs.length; i < l; i++) {
      subs[i].update();
    }
  };

Dep 是收集依赖的框,Dep.target 指向当前的 watcherDep.subs 存放依赖的所有 watcher

Watcher

  var Watcher = function Watcher (
    vm,
    expOrFn,
    cb,
    options,
    isRenderWatcher
  ) {
    this.vm = vm;
    if (isRenderWatcher) {
      vm._watcher = this;
    }
    vm._watchers.push(this);
    // options
    if (options) {
      this.deep = !!options.deep;
      this.user = !!options.user;
      this.lazy = !!options.lazy;
      this.sync = !!options.sync;
      this.before = options.before;
    } else {
      this.deep = this.user = this.lazy = this.sync = false;
    }
    this.cb = cb;
    this.id = ++uid$2; // uid for batching
    this.active = true;
    this.dirty = this.lazy; // for lazy watchers
    this.deps = [];
    this.newDeps = [];
    this.depIds = new _Set();
    this.newDepIds = new _Set();
    this.expression = expOrFn.toString();
    // parse expression for getter
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    } else {
      this.getter = parsePath(expOrFn);
      if (!this.getter) {
        this.getter = noop;
        warn(
          "Failed watching path: \"" + expOrFn + "\" " +
          'Watcher only accepts simple dot-delimited paths. ' +
          'For full control, use a function instead.',
          vm
        );
      }
    }
    this.value = this.lazy
      ? undefined
      : this.get();
  };

  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      if (this.user) {
        handleError(e, vm, ("getter for watcher \"" + (this.expression) + "\""));
      } else {
        throw e
      }
    } finally {
      // "touch" every property so they are all tracked as
      // dependencies for deep watching
      if (this.deep) {
        traverse(value);
      }
      popTarget();
      this.cleanupDeps();
    }
    return value
  };

  Watcher.prototype.addDep = function addDep (dep) {
    var id = dep.id;
    if (!this.newDepIds.has(id)) {
      this.newDepIds.add(id);
      this.newDeps.push(dep);
      if (!this.depIds.has(id)) {
        dep.addSub(this);
      }
    }
  };

  Watcher.prototype.update = function update () {
    /* istanbul ignore else */
    if (this.lazy) {
      this.dirty = true;
    } else if (this.sync) {
      this.run();
    } else {
      queueWatcher(this);
    }
  };

  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      if (
        value !== this.value ||
        // Deep watchers and watchers on Object/Arrays should fire even
        // when the value is the same, because the value may
        // have mutated.
        isObject(value) ||
        this.deep
      ) {
        // set new value
        var oldValue = this.value;
        this.value = value;
        if (this.user) {
          try {
            this.cb.call(this.vm, value, oldValue);
          } catch (e) {
            handleError(e, this.vm, ("callback for watcher \"" + (this.expression) + "\""));
          }
        } else {
          this.cb.call(this.vm, value, oldValue);
        }
      }
    }
  };

  Watcher.prototype.depend = function depend () {
    var i = this.deps.length;
    while (i--) {
      this.deps[i].depend();
    }
  };

  // ...省略 evaluate、cleanupDeps 和 teardown 的代码

老实说 Watcher 其实在响应式的流程中不太明显,不认真看都看不出来有用到。

Watcher 根据传的参数不同可以分为 render watcher、computed watcher、watch watcher,这里暂时当成只有 render watcher 即可。

响应式的收集 / 更新过程如下:

收集:访问某个响应式数据 → 触发 getter 收集依赖

更新:修改某个响应式数据 → 触发 setter 更新依赖

这里的依赖其实就是指的 watcher ,而「访问响应式数据」这个操作最明显的显然是在 template 中了吧,template$mount 过程中会被编译为 render 函数,之后执行 render,这里就触发了收集依赖的过程了,不过实际上 render 函数不是直接被调用,而是做为 Watcher 中的 getter 函数被调用的。

    
    var updateComponent = function () {
      vm._update(vm._render(), hydrating);
    };

    new Watcher(vm, updateComponent, noop, {
      before: function before () {
        if (vm._isMounted && !vm._isDestroyed) {
          callHook(vm, 'beforeUpdate');
        }
      }
    }, true /* isRenderWatcher */);

这就是 render watcher 构建入口,结合 Watcher 的构造函数看一下,删掉无关的代码,其实 render watcher 我认为在新建实例的过程中最重要的就是把 updateComponent 赋值给 getter,接着执行 get 函数取了一次值,而 get 函数的作用主要就是 pushTarget(this) 把当前的 watcher 赋值给 Dep.target,然后执行 getter 这里也就是 updateComponent ,然后pushTarget()Dep.target 置为空。

这样一套下来,只要 render 函数中访问到的响应式数据显然收集到的就是这个 render watcher 了。

响应式数据更新时,实际上会遍历 Depsubs 执行 subs[i].update(); 也就是 watcherupdate 方法,为了防止同一个 watcher 重复更新,会先收集到一个队列中最后在 nextTick 中遍历执行 watcher.run ,对于 render watcher 可以认为在 run 方法中又执行了一次 updateComponent 也就完成了视图的更新。

  var Watcher = function Watcher (
    vm, // vm
    expOrFn, // updateComponent
    cb, // noop
    options, // { before }
    isRenderWatcher // true
  ) {
    this.vm = vm;
    // ... 
    if (typeof expOrFn === 'function') {
      this.getter = expOrFn;
    }
    // ...
    this.value = this.get();
  };

  Watcher.prototype.get = function get () {
    pushTarget(this);
    var value;
    var vm = this.vm;
    try {
      value = this.getter.call(vm, vm);
    } catch (e) {
      // ...
    } finally {
      // ...
      popTarget();
    }
    return value
  };

  Watcher.prototype.update = function update () {
    // ...
    queueWatcher(this); // 在 nextTick 中遍历需要更新的 watcher 执行 watcher.run()
  };

  Watcher.prototype.run = function run () {
    if (this.active) {
      var value = this.get();
      // ...
    }
  };

Promise 实现 + 注释

const Promise = (function () {

  function MyPromise(fn) {
    const self = this;
    self.state = 'pending';
    self.value = undefined;
    self.callbacks = [];

    function resolve(value) {
      setTimeout(() => {
        if (self.state === 'pending') {
          self.state = 'resolved';
          self.value = value;
          self.callbacks.map(cb => {
            cb.onResolved(value);
          });
        }
      }, 0);
    }

    function reject(value) {
      setTimeout(() => {
        if (self.state === 'pending') {
          self.state = 'rejected';
          self.value = value;
          self.callbacks.map(cb => {
            cb.onRejected(value);
          });
        }
      }, 0);
    }

    try {
      fn(resolve, reject);
    } catch (e) {
      reject(e);
    }
  }
  
  const resolvePromise = function (promise, x, resolve, reject) {
    let called = false;
    let then;

    /**
     * 检测循环引用
     */
    if (promise === x) {
      return reject(new TypeError('Error'));
    }

    if (x instanceof MyPromise) {
      return x.then(y => {
        resolvePromise(promise, y, resolve, reject);
      }, reject);
    }

    if (
      /**
       * x !== null 不能省略,因为 `typeof null` 输出的是 "object",所以要排除 null。
       */
      (x !== null) &&
      (
        (typeof x === 'object') ||
        (typeof x === 'function')
      )
    ) {
      try {
        then = x.then;
        if (typeof then === 'function') {
          then.call(x,
          y => {
            if (called) return;
            called = true;
            resolvePromise(promise, y, resolve, reject);
          },
          r => {
            if (called) return;
            called = true;
            reject(r);
          });
        } else {
          resolve(x);
        }
      } catch (e) {
        if (called) return
        called = true;
        reject(e);
      }
    } else {
      resolve(x);
    }
  }

  MyPromise.prototype.then = function (onResolved, onRejected) {
    const self = this;
    let promise2;

    /**
     * onResolved 和 onRejected 的默认值处理数据传递的问题
     */
    onResolved = typeof onResolved === 'function' ? onResolved : v => v;
    onRejected = typeof onRejected === 'function' ? onRejected : r => { throw r };

    if (self.state === 'pending') {
      return (promise2 = new MyPromise((resolve, reject) => {
        self.callbacks.push({
          onResolved: () => {
            try {
              const x = onResolved(self.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          },
          onRejected: () => {
            try {
              const x = onRejected(self.value);
              resolvePromise(promise2, x, resolve, reject);
            } catch (e) {
              reject(e);
            }
          }
        });
      }))
    }

    if (self.state === 'resolved') {
      /**
       * 为了能够链式调用promise,所以then 方法最后要返回一个新promise。
       * new promise 时传的函数立即执行。然后调用resolve 或 reject 改变状态成 resolved 或 rejected。
       * 所以要注意的是在这个promise 的同步代码中根据情况调用 resolve 或 reject,
       * 正确的改变这个promise 的状态。
       */
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            const x = onResolved(self.value);
            resolvePromise(promise2, x, resolve, reject);
            /**
             * 如上面所说,新promise 中的同步代码的作用就是执行 上一个promise 的onResolved 或onRejected ,
             * 并根据返回的结果,正确的改变新promise 的状态。
             * 所以resolvePromise 这个函数就是先判断了一大堆情况,最后执行了 resolve(value) 或 reject(value)。
             * 把这个新promise 的状态给改变了。
             */
          } catch (e) {
            reject(e);
          }
        }, 0);
      }));
    }

    if (self.state === 'rejected') {
      return (promise2 = new MyPromise((resolve, reject) => {
        setTimeout(() => {
          try {
            const x = onRejected(self.value);
            resolvePromise(promise2, x, resolve, reject);
          } catch (e) {
            reject(e);
          }
        }, 0);
      }));
    }
  }

  MyPromise.prototype.catch = function (onRejected) {
    return this.then(null, onRejected);
  }
  
  MyPromise.prototype.finally = function (fn) {
    return this.then(fn, fn);
  }
  
  MyPromise.all = function (promises) {
    return new MyPromise((resolve, reject) => {
      let count = 0;
      let nums = promises.length;
      let values = new Array(nums);
  
      for (let i = 0; i < nums; i++) {
        Promise.resolve(promises[i]).then(value => {
          count++;
          values[i] = value;
          if (count === nums) {
            return resolve(values);
          }
        }, r => {
          return reject(r);
        })
      }
    })
  }
  
  MyPromise.race = function (promises) {
    return new MyPromise((resolve, reject) => {
      let nums = promises.length;
      for (let i = 0; i < nums; i++) {
        MyPromise.resolve(promises[i]).then(value => {
          return resolve(value);
        }, r => {
          return reject(r);
        })
      }
    })
  }
  
  MyPromise.resolve = function (value) {
    let promise = new MyPromise((resolve, reject) => {
      resolvePromise(promise, value, resolve, reject)
    })
    return promise;
  }
  
  MyPromise.reject = function (value) {
    let promise = new MyPromise((resolve, reject) => {
      reject(value);
    })
    return promise;
  }  
  
  // 使用 promises-aplus-tests 测试
  MyPromise.deferred = MyPromise.defer = function () {
    var dfd = {};
    dfd.promise = new MyPromise(function (resolve, reject) {
      dfd.resolve = resolve;
      dfd.reject = reject;
    })
    return dfd;
  }

  try {
    module.exports = MyPromise;
  } catch (e) { }
  // 使用 promises-aplus-tests 测试 end

  return MyPromise;
})();

flex 与 grid 常用属性总结

flex 布局

flex (flexible box) 弹性布局,分为两部分,采用flex 布局的容器(flex container) 和容器下的子元素项目(flex item),采用flex 布局的容器拥有两条轴线,分别是主轴和交叉轴,flex item 沿着主轴顺序排列。
主轴默认是水平线,交叉轴默认为垂直线,但主轴与交叉轴并没有固定的方向,可通过在flex container 上设置flex-direction 属性决定主轴的方向是水平线或垂直线。

容器(flex container) 属性

flex-direction

设置主轴方向的属性,一共有4种值。

  • row (默认值):主轴方向水平从左到右。
  • row-reverse:主轴方向水平从右到左。
  • column:主轴方向垂直从上到下。
  • column-reverse:主轴方向垂直从下到上。
flex-warp

flex item 的总宽度超过flex container 的宽度是否换行。在默认值nowarp,不换行的情况下,即使设置了flex item 的宽度,如果总宽度超过容器的宽度,那么子元素的宽度不会是真实设置的宽度,而是平分容器的宽度。

flex-flow

flex-directionflex-wrap 的缩写。

justify-content

决定flex item 在主轴上的对齐方式。一种常用的居中方式。

.container {
  display: flex;
  justify-content: center;  // 水平居中
  align-items: center;  // 垂直居中
}
align-items

决定flex item 在交叉轴上的对齐方式。一种常用的居中方式。

.container {
  display: flex;
  justify-content: center;  // 水平居中
  align-items: center;  // 垂直居中
}
align-content

类似align-items,但只对多行起效,对于单行无效。一般不用,都用align-items,但是容易混淆。

项目(flex item) 属性

flex-grow

定义项目占据容器多余空间的放大比列,默认是0,这时候只占据这个项目的真实宽度,即使容器有多余宽度,也不放大占据。

flex-shrink

定义项目在容器宽度不够时的缩小比列,默认是1,此时如果总宽度超过容器的宽度,那么项目元素的宽度不会是真实设置的宽度,而是平分容器的宽度。flex-shrink: 0; 则不缩放。

flex-basis

定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间,默认为auto即项目本来的大小。

flex

flex-growflex-shrinkflex-basis 的缩写。

grid 布局

grid 网格布局,分为两部分,采用grid 布局的容器(grid container) 和容器下的子元素项目(grid item),可通过grid-template-columnsgrid-template-rows 设置行与列。

容器(grid container) 属性

grid-template-columns

用来声明列。可以使用fr 这种grid 提供的新单位,表明占据的份数。

.wrapper {
  display: grid;
  grid-template-columns: 100px 200px 300px;
  // 则.wrapper 水平被分为100px 200px 300px 的三列。
}
grid-template-rows

从来声明行。可以使用fr 这种grid 提供的新单位,表明占据的份数。

.wrapper {
  display: grid;
  width: 800px;
  grid-template-rows: 1fr 200px 300px;
  // 则.wrapper 垂直被分为300px 200px 300px 的三行。
}
grid-template-areas

区分网格区域,命名相同的区域会合并。

.wrapper {
  display: grid;
  width: 800px;
  grid-template-columns: 100px 200px 300px;
  grid-template-rows: 1fr 200px 300px;
  grid-template-areas: "top-left top-center top-right"
                       "middle middle middle"
                       "bottom-left bottom-right bottom-right";
}
grid-column-gapgrid-row-gap

指定栅格线的大小,可以理解为设置行/列间隙。

justify-content

决定grid item 在列轴上的对齐方式,一种常用的居中方式。

.wraper {
  display: grid;
  justify-content: center; // 水平居中
  align-content: center; // 垂直居中
}
align-content

决定grid item 在行轴上的对齐方式,一种常用的居中方式。

.wraper {
  display: grid;
  justify-content: center; // 水平居中
  align-content: center; // 垂直居中
}

项目(grid item) 属性

grid-area

指定项目属于哪个网格区域。

.item {
  grid-area: top-left;
}

组件更新过程

先看下 new render watcher 时的定义,在 mountComponent 中。

// src/core/instance/lifecycle.js

export function mountComponent(vm, el, hydrating){
  //...
  updateComponent = () => {
    vm._update(vm._render(), hydrating)
  }

  new Watcher(vm, updateComponent, noop, {
    before () {
      if (vm._isMounted) {
        callHook(vm, 'beforeUpdate')
      }
    }
  }, true /* isRenderWatcher */)

  //...
}

组件更新时,执行 render watcherupdate → get → getter 这一个过程。

render watcher 这个 getter 函数就是 updateComponent

updateComponent = () => {
  vm._update(vm._render(), hydrating)
}

updateComponent 的职责就是 _render() 生成 VNode → _update() patch 真实 DOM

// src/core/instance/lifecycle.js
Vue.prototype._update = function (vnode, hydrating) {
  //...
  if (!prevVnode) {
    // initial render
    vm.$el = vm.__patch__(vm.$el, vnode, hydrating, false /* removeOnly */)
  } else {
    // updates
    vm.$el = vm.__patch__(prevVnode, vnode)
  }
  //...
}

更新于初始化的区别在于 __patch__patch 的第一个参数是否是 VNnode,如果是真实 DOM 元素走 init,是 VNode 走 update。

patch 函数

function patch(oldVnode, vnode, hydrating, removeOnly) {
  //...
  if (isUndef(oldVnode)) {
    // empty mount (likely as component), create new root element
    isInitialPatch = true
    createElm(vnode, insertedVnodeQueue)
  } else {
    const isRealElement = isDef(oldVnode.nodeType)
    if (!isRealElement && sameVnode(oldVnode, vnode)) {
      // patch existing root node
      patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    } else {
      if (isRealElement) {
        // mounting to a real element
        // check if this is server-rendered content and if we can perform
        // a successful hydration.
        if (oldVnode.nodeType === 1 && oldVnode.hasAttribute(SSR_ATTR)) {
          oldVnode.removeAttribute(SSR_ATTR)
          hydrating = true
        }

        // either not server-rendered, or hydration failed.
        // create an empty node and replace it
        oldVnode = emptyNodeAt(oldVnode)
      }
      // replacing existing element
      const oldElm = oldVnode.elm
      const parentElm = nodeOps.parentNode(oldElm)

      // create new node
      createElm(
        vnode,
        insertedVnodeQueue,
        // extremely rare edge case: do not insert if old element is in a
        // leaving transition. Only happens when combining transition +
        // keep-alive + HOCs. (#4590)
        oldElm._leaveCb ? null : parentElm,
        nodeOps.nextSibling(oldElm)
      )

      // update parent placeholder node element, recursively
      if (isDef(vnode.parent)) {
        let ancestor = vnode.parent
        const patchable = isPatchable(vnode)
        while (ancestor) {
          for (let i = 0; i < cbs.destroy.length; ++i) {
            cbs.destroy[i](ancestor)
          }
          ancestor.elm = vnode.elm
          if (patchable) {
            for (let i = 0; i < cbs.create.length; ++i) {
              cbs.create[i](emptyNode, ancestor)
            }
            // #6513
            // invoke insert hooks that may have been merged by create hooks.
            // e.g. for directives that uses the "inserted" hook.
            const insert = ancestor.data.hook.insert
            if (insert.merged) {
              // start at index 1 to avoid re-invoking component mounted hook
              for (let i = 1; i < insert.fns.length; i++) {
                insert.fns[i]()
              }
            }
          } else {
            registerRef(ancestor)
          }
          ancestor = ancestor.parent
        }
      }

      // destroy old node
      if (isDef(parentElm)) {
        removeVnodes(parentElm, [oldVnode], 0, 0)
      } else if (isDef(oldVnode.tag)) {
        invokeDestroyHook(oldVnode)
      }
    }
  }
  //...
}

patch 主要的流程如下。

  1. isUndef(oldVnode):oldVnode 是 undefined,即初始化时 options 没有传 $el 的情况。这种情况下只要按照新 vnode,也就是 patch 的 vnode 参数创建 DOM 即可,这里不赘述。
  2. 存在 oldVnode 的情况。
    1. !isRealElement && sameVnode(oldVnode, vnode):是 VNode 并且是相同的 VNode,需要进一步比较(关于 sameVnode 的判断稍后叙述)。这是最麻烦的一种情况,需要对新旧 vnode 做进一步的 diff 对比不同。patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
    2. oldVnode 是真实 DOM 元素新旧 vnode 不相同:这两种情况大致相同,只是对于 oldVnode 是真实 DOM 元素的情况下会先将一个空 VNode 赋给 oldVnode,后面流程与新旧 vnode 不相同的情况相同,大致分三步:
      1. 根据 vnode 创建新的真实 DOM 节点:to be continue
      2. 更新父占位符 vnode 的节点信息:写在最后(to be continue)
      3. DOM tree 上删除 oldVnode 的 DOM 节点:写在最后(to be continue)

sameVnode 判断

function sameVnode (a, b) {
  return (
    a.key === b.key && (
      (
        a.tag === b.tag &&
        a.isComment === b.isComment &&
        isDef(a.data) === isDef(b.data) &&
        sameInputType(a, b)
      ) || (
        isTrue(a.isAsyncPlaceholder) &&
        a.asyncFactory === b.asyncFactory &&
        isUndef(b.asyncFactory.error)
      )
    )
  )
}

sameVnode 的判断:

  • 优先比较 key 是否相同。
  • 其次对于同步组件判断 tagisCommentdatainput 这些类型是否相同。
  • 对于异步组件判断 asyncFactory 是否相同。

diff 算法

关于diff 算法 的部分主要在 patchVnode 函数中

// src/core/vdom/patch.js
function patchVnode (oldVnode, vnode, insertedVnodeQueue, removeOnly) {
  if (oldVnode === vnode) {
    return
  }

  const elm = vnode.elm = oldVnode.elm

  if (isTrue(oldVnode.isAsyncPlaceholder)) {
    if (isDef(vnode.asyncFactory.resolved)) {
      hydrate(oldVnode.elm, vnode, insertedVnodeQueue)
    } else {
      vnode.isAsyncPlaceholder = true
    }
    return
  }

  // reuse element for static trees.
  // note we only do this if the vnode is cloned -
  // if the new node is not cloned it means the render functions have been
  // reset by the hot-reload-api and we need to do a proper re-render.
  if (isTrue(vnode.isStatic) &&
    isTrue(oldVnode.isStatic) &&
    vnode.key === oldVnode.key &&
    (isTrue(vnode.isCloned) || isTrue(vnode.isOnce))
  ) {
    vnode.componentInstance = oldVnode.componentInstance
    return
  }

  let i
  const data = vnode.data
  if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
    i(oldVnode, vnode)
  }

  const oldCh = oldVnode.children
  const ch = vnode.children

  if (isDef(data) && isPatchable(vnode)) {
    for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
    if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
  }
  if (isUndef(vnode.text)) {
    if (isDef(oldCh) && isDef(ch)) {
      if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
    } else if (isDef(ch)) {
      if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
      addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
    } else if (isDef(oldCh)) {
      removeVnodes(elm, oldCh, 0, oldCh.length - 1)
    } else if (isDef(oldVnode.text)) {
      nodeOps.setTextContent(elm, '')
    }
  } else if (oldVnode.text !== vnode.text) {
    nodeOps.setTextContent(elm, vnode.text)
  }
  if (isDef(data)) {
    if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
  }
}

核心逻辑有一下四步:

  1. 执行 prepatch 钩子函数,具体流程看 to be continue

    let i
    const data = vnode.data
    if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
      i(oldVnode, vnode)
    }
  2. 执行 update 钩子函数,具体流程看 to be continue

    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
  3. 完成 patch 过程

    const oldCh = oldVnode.children
    const ch = vnode.children
    if (isDef(data) && isPatchable(vnode)) {
      for (i = 0; i < cbs.update.length; ++i) cbs.update[i](oldVnode, vnode)
      if (isDef(i = data.hook) && isDef(i = i.update)) i(oldVnode, vnode)
    }
    if (isUndef(vnode.text)) {
      if (isDef(oldCh) && isDef(ch)) {
        if (oldCh !== ch) updateChildren(elm, oldCh, ch, insertedVnodeQueue, removeOnly)
      } else if (isDef(ch)) {
        if (isDef(oldVnode.text)) nodeOps.setTextContent(elm, '')
        addVnodes(elm, null, ch, 0, ch.length - 1, insertedVnodeQueue)
      } else if (isDef(oldCh)) {
        removeVnodes(elm, oldCh, 0, oldCh.length - 1)
      } else if (isDef(oldVnode.text)) {
        nodeOps.setTextContent(elm, '')
      }
    } else if (oldVnode.text !== vnode.text) {
      nodeOps.setTextContent(elm, vnode.text)
    }
  4. 执行 postpatch 钩子函数,具体流程看 to be continue

    if (isDef(data)) {
      if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
    }

diff 算法主要指的是第三步,可以分为如下情况:

  1. oldCh 与 ch 都存在且不相同时:使用 updateChildren 函数来更新子节点,最重要,写在后面
  2. 只有 ch 存在:表示旧节点不需要了,只需要把老节点可能存在的文本置空,然后通过 addVnodes 将 ch 批量插入到新节点 elm 下
  3. 只有 oldCh 存在:更新的是空节点,需要将旧的节点通过 removeVnodes 全部清除
  4. 只有旧节点,注意这里是 旧节点(oldVnode) 而不是 旧节点 children(oldCh) 且是文本节点:清除其节点文本内容

updateChildren

// src/core/vdom/patch.js
function updateChildren (parentElm, oldCh, newCh, insertedVnodeQueue, removeOnly) {
  let oldStartIdx = 0
  let newStartIdx = 0
  let oldEndIdx = oldCh.length - 1
  let oldStartVnode = oldCh[0]
  let oldEndVnode = oldCh[oldEndIdx]
  let newEndIdx = newCh.length - 1
  let newStartVnode = newCh[0]
  let newEndVnode = newCh[newEndIdx]
  let oldKeyToIdx, idxInOld, vnodeToMove, refElm

  // removeOnly is a special flag used only by <transition-group>
  // to ensure removed elements stay in correct relative positions
  // during leaving transitions
  const canMove = !removeOnly

  if (process.env.NODE_ENV !== 'production') {
    checkDuplicateKeys(newCh)
  }

  while (oldStartIdx <= oldEndIdx && newStartIdx <= newEndIdx) {
    if (isUndef(oldStartVnode)) {
      oldStartVnode = oldCh[++oldStartIdx] // Vnode has been moved left
    } else if (isUndef(oldEndVnode)) {
      oldEndVnode = oldCh[--oldEndIdx]
    } else if (sameVnode(oldStartVnode, newStartVnode)) {
      patchVnode(oldStartVnode, newStartVnode, insertedVnodeQueue)
      oldStartVnode = oldCh[++oldStartIdx]
      newStartVnode = newCh[++newStartIdx]
    } else if (sameVnode(oldEndVnode, newEndVnode)) {
      patchVnode(oldEndVnode, newEndVnode, insertedVnodeQueue)
      oldEndVnode = oldCh[--oldEndIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldStartVnode, newEndVnode)) { // Vnode moved right
      patchVnode(oldStartVnode, newEndVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldStartVnode.elm, nodeOps.nextSibling(oldEndVnode.elm))
      oldStartVnode = oldCh[++oldStartIdx]
      newEndVnode = newCh[--newEndIdx]
    } else if (sameVnode(oldEndVnode, newStartVnode)) { // Vnode moved left
      patchVnode(oldEndVnode, newStartVnode, insertedVnodeQueue)
      canMove && nodeOps.insertBefore(parentElm, oldEndVnode.elm, oldStartVnode.elm)
      oldEndVnode = oldCh[--oldEndIdx]
      newStartVnode = newCh[++newStartIdx]
    } else {
      if (isUndef(oldKeyToIdx)) oldKeyToIdx = createKeyToOldIdx(oldCh, oldStartIdx, oldEndIdx)
      idxInOld = isDef(newStartVnode.key)
        ? oldKeyToIdx[newStartVnode.key]
        : findIdxInOld(newStartVnode, oldCh, oldStartIdx, oldEndIdx)
      if (isUndef(idxInOld)) { // New element
        createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
      } else {
        vnodeToMove = oldCh[idxInOld]
        if (sameVnode(vnodeToMove, newStartVnode)) {
          patchVnode(vnodeToMove, newStartVnode, insertedVnodeQueue)
          oldCh[idxInOld] = undefined
          canMove && nodeOps.insertBefore(parentElm, vnodeToMove.elm, oldStartVnode.elm)
        } else {
          // same key but different element. treat as new element
          createElm(newStartVnode, insertedVnodeQueue, parentElm, oldStartVnode.elm, false, newCh, newStartIdx)
        }
      }
      newStartVnode = newCh[++newStartIdx]
    }
  }
  if (oldStartIdx > oldEndIdx) {
    refElm = isUndef(newCh[newEndIdx + 1]) ? null : newCh[newEndIdx + 1].elm
    addVnodes(parentElm, refElm, newCh, newStartIdx, newEndIdx, insertedVnodeQueue)
  } else if (newStartIdx > newEndIdx) {
    removeVnodes(parentElm, oldCh, oldStartIdx, oldEndIdx)
  }
}

这块是真的多,流程如下:

  1. 过滤 oldCh 两侧的空 VNode

  2. 然后进行这四部分的比较

    • oldStart - newStart
    • oldEnd - newEnd
    • oldStart - newEnd
    • oldEnd - newStart

    如果比较成功,递归 patchVnode 进一步比较,如果有 children 元素,又会进入 updateChildren 直到一方没有 children 元素,更新新节点。

  3. 上面都没命中,将剩余 oldStart - oldEnd 中间的元素生成一个 oldKeyToIdxmap[vnode.key] = vnode ,然后判断此时的 newCh[newStart] 是否有传 key,如果有则在 oldKeyToIdx 表中查找,这里注意一点,想要查找成功必须新旧 children 都设置唯一 key,否则就查找不到,如果查找不到就将这个 newCh[newStart] 视为一个新元素即可。如果通过 key 查找到也只能说明 key 相同,通过 sameVnode 进一步比较,比较成功类似 (2.) 进一步递归patchVnode ,否则视为一个新元素。这一步最后都会 ++newStart

  4. 到这一步说明 oldCh 或 newCh 一方的 start > end ,跳出 while 循环。此时剩下的如果是 newCh 部分,将 newStart - newEnd 的节点全部添加,如果剩下的是 oldCh 部分,将 oldStart - oldEnd 的节点全部删除。

case🌰

更新父占位符 vnode 的节点信息

to be continue

DOM tree 上删除 oldVnode 的 DOM 节点

to be continue

keep-alive 原理

keep-alive 本质上只是存缓存和拿缓存的过程。

首先看下 keep-alive 的 render 函数。

{
    render: function render () {
      var slot = this.$slots.default;
      /**
       * getFirstComponentChild 找到第一个组件节点
       */
      var vnode = getFirstComponentChild(slot);
      var componentOptions = vnode && vnode.componentOptions;
      if (componentOptions) {
        // check pattern
        var name = getComponentName(componentOptions);
        var ref = this;
        var include = ref.include;
        var exclude = ref.exclude;

        /**
         * 不匹配直接返回 vnode
         */
        if (
          // not included
          (include && (!name || !matches(include, name))) ||
          // excluded
          (exclude && name && matches(exclude, name))
        ) {
          return vnode
        }

        var ref$1 = this;
        var cache = ref$1.cache;
        var keys = ref$1.keys;

        /**
         * 优先使用 key 属性做为缓存的 keys,否则使用 cid + tag 拼接做为 keys
         */
        var key = vnode.key == null
          // same constructor may get registered as different local components
          // so cid alone is not enough (#3269)
          ? componentOptions.Ctor.cid + (componentOptions.tag ? ("::" + (componentOptions.tag)) : '')
          : vnode.key;

        
        if (cache[key]) {
          /**
           * 命中缓存,直接将缓存的 vnode 上的 instance 赋给新 vnode
           * 接着更新当前组件的调用顺序
           */
          vnode.componentInstance = cache[key].componentInstance;
          // make current key freshest
          remove(keys, key);
          keys.push(key);
        } else {
          /**
           * 没有命中缓存,缓存 vnode
           * LRU 最近最少使用 根据 max 更新缓存 map
           */
          cache[key] = vnode;
          keys.push(key);
          // prune oldest entry
          if (this.max && keys.length > parseInt(this.max)) {
            pruneCacheEntry(cache, keys[0], keys, this._vnode);
          }
        }

        /**
         * 打上 keepAlive 标记
         * 在 prepatch 钩子中用到
         */
        vnode.data.keepAlive = true;
      }

      /**
       * keep-alive 最后无论是否命中缓存都需要返回 vnode。
       * 区别在于 vnode 上的 componentInstance 和 keepAlive 属性。
       * 初次渲染只有 keepAlive 属性
       * 重新渲染则 componentInstance 和 keepAlive 都存在。
       */
      return vnode || (slot && slot[0])
    }
}

大致流程如下:

  • 首先是获取 keep-alive 下插槽的内容,也就是 keep-alive 需要渲染的子组件。

      function getFirstComponentChild (children) {
        if (Array.isArray(children)) {
          for (var i = 0; i < children.length; i++) {
            var c = children[i];
            // 组件实例存在,则返回,理论上返回第一个组件vnode
            if (isDef(c) && (isDef(c.componentOptions) || isAsyncPlaceholder(c))) {
              return c
            }
          }
        }
      }
  • 然后判断组件是否满足缓存的匹配条件,如果不满足直接返回 vnode,不做任何处理,此时组件会进入正常的挂载阶段。

  • 如果是重新渲染,则将缓存的实例赋给新的 vnode;如果是初次渲染,缓存 vnode

    // 重新渲染,赋值 componentInstance
    vnode.componentInstance = cache[key].componentInstance;
    // 初次渲染时,将vnode缓存
    cache[key] = vnode;
    keys.push(key);
  • 将已经缓存的 vnode 打上标记,并将 vnode 返回

    // 为缓存组件打上标志
    vnode.data.keepAlive = true;

有个地方需要注意的是,vnode 上是会保存 DOM 的。

// vnode 上会保存 DOM 节点
vnode.elm = vnode.componentInstance.$el

由此可知,为什么 keep-alive 需要一个 max 来限制缓存组件的数量,原因就是 keep-alive 缓存的组件数据除了包括 vnode 这一描述对象外,还保留着真实的 DOM 节点。

初次渲染

初次渲染和普通的流程一致,只是在 keep-alive 内部缓存了 vnode,vnode.data.keepAlive = true

唯一需要注意的就是在 patch 最后调用了 invokeInsertHook 函数,执行了 insert 钩子,在执行 callHook(componentInstance, 'mounted'); 后,keepAlive 的判断为 true,会额外进入 activateChildComponent 函数,调用 callHook(vm, 'activated');

初次渲染的 activated 钩子直接在这里调用,重新渲染的 activated 钩子是 queueActivatedComponent 放进数组中,最后在 flushSchedulerQueue 中调用。

{
    insert: function insert (vnode) {
      var context = vnode.context;
      var componentInstance = vnode.componentInstance;
      if (!componentInstance._isMounted) {
        componentInstance._isMounted = true;
        callHook(componentInstance, 'mounted');
      }
      if (vnode.data.keepAlive) {
        if (context._isMounted) {
          queueActivatedComponent(componentInstance);
        } else {
          activateChildComponent(componentInstance, true /* direct */);
        }
      }
    }
}
function flushSchedulerQueue () {
    currentFlushTimestamp = getNow();
    flushing = true;
    var watcher, id;

    queue.sort(function (a, b) { return a.id - b.id; });

    // ...

    // keep copies of post queues before resetting state
    var activatedQueue = activatedChildren.slice();
    var updatedQueue = queue.slice();

    resetSchedulerState();

    // call component updated and activated hooks
    callActivatedHooks(activatedQueue);
    callUpdatedHooks(updatedQueue);

    // devtool hook
    /* istanbul ignore if */
    if (devtools && config.devtools) {
      devtools.emit('flush');
    }
  }

重新渲染

重新渲染就有不同了,假设一个案例如下:

// app.vue

<template>
  <div id="app">
    <keep-alive>
      <component 
        :is="compName"
      />
    </keep-alive>
    <button @click="change">switch</button>
  </div>
</template>
<script>
import A from './components/A';
import B from './components/B';

export default {
  name: 'app',
  components: {
    A,
    B,
  },
  data () {
    return {
      compName: 'A'
    }
  },
  methods: {
    change() {
      this.compName = this.compName === 'A' ? 'B' : 'A';
    }
  }
}
</script>

首先渲染 A 组件,然后渲染 B 组件,这两次都是初次渲染,所以 keep-alive 中缓存了 A,B 组件的 vnode。

接着再次切换为 A 组件,首先触发的是 app.vue 的更新,也就是会执行 app.vuerender 函数,进入 patch 过程。

app.vue 新旧 vnode diff 时,会发现 keep-alive 节点没有变化,所以会进 patchVnode 函数中,这个函数里面最重要的是会执行 prepatch 钩子函数,这个钩子函数会更新组件的 props、listeners、children 等。

{
    prepatch: function prepatch (oldVnode, vnode) {
      var options = vnode.componentOptions;
      var child = vnode.componentInstance = oldVnode.componentInstance;
      updateChildComponent(
        child,
        options.propsData, // updated props
        options.listeners, // updated listeners
        vnode, // new parent vnode
        options.children // new children
      );
    },
}

updateChildComponent 流程大致如下:

  • 更新 vnode 的父子关系
  • 更新 $props
  • 更新 $listeners
  • 如果有 slot 或 children 强制调用 $forceUpdate

对于 keep-alive 来说,必有 children,所以通过 $forceUpdate 进入 keep-alive 的 render 函数中,此时命中缓存,返回的 A 组件 vnode 上带有缓存的 componentInstance 和 keepAlive 标记。

进入 keep-alive 的 patch 过程,新旧 vnode 一个是 A 组件,一个是 B 组件,所以是根据新 vnode 生成 DOM 再替换旧 vnode 的 DOM 这一个过程。

createElm → createComponent

    function createComponent (vnode, insertedVnodeQueue, parentElm, refElm) {
      var i = vnode.data;
      if (isDef(i)) {
        var isReactivated = isDef(vnode.componentInstance) && i.keepAlive;
        if (isDef(i = i.hook) && isDef(i = i.init)) {
          i(vnode, false /* hydrating */);
        }
        if (isDef(vnode.componentInstance)) {
          initComponent(vnode, insertedVnodeQueue);
          insert(parentElm, vnode.elm, refElm);
          if (isTrue(isReactivated)) {
            reactivateComponent(vnode, insertedVnodeQueue, parentElm, refElm);
          }
          return true
        }
      }
    }

此时,isReactivated 的值是 true ,进入 init 钩子。

{
     init: function init (vnode, hydrating) {
      if (
        vnode.componentInstance &&
        !vnode.componentInstance._isDestroyed &&
        vnode.data.keepAlive
      ) {
        // kept-alive components, treat as a patch
        var mountedNode = vnode; // work around flow
        componentVNodeHooks.prepatch(mountedNode, mountedNode);
      } else {
        var child = vnode.componentInstance = createComponentInstanceForVnode(
          vnode,
          activeInstance
        );
        child.$mount(hydrating ? vnode.elm : undefined, hydrating);
      }
    }
}

这时候会进第一个分支逻辑,执行 prepatch 去更新 vnode 上保存的组件实例,而不是走新建实例、挂载的流程。

接着回到 patch 函数中。

{  
      { 
         // destroy old node
          if (isDef(parentElm)) {
            removeVnodes([oldVnode], 0, 0);
          } else if (isDef(oldVnode.tag)) {
            invokeDestroyHook(oldVnode);
          }
      }
      invokeInsertHook(vnode, insertedVnodeQueue, isInitialPatch);
}

大概是这么个流程,先 invokeDestroyHook 调用旧 vnode 的 destroy 钩子,因为 keepAlive 的关系,不会真的销毁而是 callHook(vm, 'deactivated');

接着的 invokeInsertHook 调用新 vnode 的 insert 钩子,向 activatedChildren 队列中加入当前实例,最后在 flushSchedulerQueue 中遍历调用 callHook(vm, 'activated');

el-tree 支持区域外往树内拖动节点

问题

有需求需要支持如下场景:
页面分左右两部分,左侧使用 el-tree 展示一个文件夹树,右侧显示当前文件夹下的文件。
文件夹可以相互拖动排序和改变层级,支持把右侧文件拖到左侧文件夹上改变文件所在文件夹位置。

在线演试

https://codesandbox.io/s/el-tree-drag-onumm?file=/src/App.vue

原理

树外的节点在 dragstartdragend 时需要手动触发 tree 实例的事件

    dragstart(event, item) {
      // 手动设置 el-tree draggingNode 为当前节点
      this.$refs.treeRef.dragState.draggingNode = {
        node: {
          data: {
            ...item,
            nodeType: "custom",
          },
          // 以下都是为了兼容 el 中 node 数据结构
          contains() {
            return false;
          },
          remove() {},
          insertBefore() {},
          insertAfter() {},
          insertChild() {},
        },
      };
    },
    dragend(event, item) {
      // 手动触发 el-tree tree-node-drag-end 事件
      if (!this.$refs.treeRef.draggable) return;
      this.$refs.treeRef.$emit("tree-node-drag-end", event);
    },

CSS 水平垂直居中方式

CSS 水平垂直居中方式

CodePan查看效果

方式一:absolute + 负 margin (需要确定 .content 宽高)

.wrapper {
  position: relative;
}
.content {
  position: absolute;
  top: 50%;
  left: 50%;
  width: 100px;
  height: 100px;
  margin-top: -50px;
  margin-left: -50px;
}

方式二:absolute + margin: auto(需要确定 .content 宽高)

.wrapper {
  position: relative;
}
.content {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  width: 100px;
  height: 100px;
  margin: auto;
}

方式三:absolute + transform 未知宽高

.wrapper {
  position: relative;
}
.content {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
}

方式四:flex 未知宽高

.wrapper {
  display: flex;
  justify-content: center;
  align-items: center;
}
.content {
}

方式五:table-cell 未知宽高

.wrapper {
  display: table-cell;
  text-align: center;
  vertical-align: middle;
}
.content {
  display: inline-block;
}

方式六:grid 未知宽高

.wrapper {
  display: grid;
  justify-content: center;
  align-content: center;
}
.content {
}

常用跨域方法

什么是同源策略及限制

判断是否同源(协议,域名,端口三者都相同)

同源限制是浏览器的行为,浏览器为了客户端安全(防止XSS、CSRF等攻击)会拦截服务端返回的response,所以跨域并不是请求发不出去,只是服务端返回的结果被浏览器拦截了。

不是一个源,将会限制以下操作:

  • Cookie,LocalStorage,IndexDB 无法读取
  • DOM 无法获取
  • AJAX 请求不能发送

跨域通信的几种方式

CORS (Cross-Origin Resource Sharing 跨域资源共享)

主要由后端配置,后端在返回的响应头添加Access-Control-Allow-Origin 就可以启用CORS。值得注意的是开启了CORS后的请求分为简单请求复杂请求,对于复杂请求客户端会先向服务端发送一条预检请求(OPTIONS)。

满足以下两个条件的请求为简单请求。

  1. 请求方法为HEAD, GET, POST
  2. HTTP 的头信息不超过以下几种字段。
    • Accept
    • Accept-Language
    • Content-Language
    • Last-Event-ID
    • Content-Typeapplication/x-www-form-urlencoded, multipart/form-data, text/plain

JSONP

利用script 标签的src 属性不受浏览器的跨域限制。所以能够使用get 方法向服务端发送请求,但是也只支持GET 请求而不支持POST 等其它类型的HTTP 请求。

具体实现戳jsonp 的原理和实现

postMessage

MDN postMessage

适用于以下场景:

  • 页面和其打开的新窗口的数据传递(window.open 会返回打开窗口的对象引用)
  • 多窗口之间消息传递
  • 页面与嵌套的iframe消息传递
  • 上面三个场景的跨域数据传递
// a 页面

let ifr = document.querySelector('#iframe'); // 获得a 页面内嵌套的iframe 元素。

ifr.contentWindow.postMessage('hello world', 'http://b.com');
// otherWindow.postMessage(message, targetOrigin, [transfer]);


// b 页面

window.addEventListener('message', function (e) {
  console.log(e.source); // 发送消息的窗口对象的引用
  console.log(e.origin); // http://a.com
  console.log(e.data); // hello world
})

nginx 反向代理

搭建一个中转nginx服务器,用于转发请求。配置最简单,只需要修改nginx的配置即可,不需要修改任何代码。


// nginx.conf
server {
    ...
    // 设置代理
    location / {
        proxy_pass http://127.0.0.1:8080;
    }
    ...
}

ABCD 四个人说假话真话,有一个人是小偷

function foo(){
  const list = ['A', 'B', 'C', 'D'];
  for(let i = 0; i < list.length; i++) {
    const target = list[i];

    const A = target !== 'A';
    const B = target === 'C';
    const C = target === 'D';
    const D = !C;

    if (
      (!A && B && C && D) ||
      (A && !B && C && D) ||
      (A && B && !C && D) ||
      (A && B && C && !D)
    ) {
      return target;
    }
  }

  return '';
}

响应式数据中的两个 dep ?

一段时间不看又忘了 😢

  1. defineReactive 中通过闭包引用的 dep
  2. Observer 类中的 dep(childOb.dep 或 ob.dep)

如果属性值是对象或数组实际上相当于有两个 dep。

第一个 dep 是正常触发 setter 时派发更新的 dep。

第二个 dep 是在调用 $set、Vue.set 这些实际上修改了响应式数据但又无法正常触发 setter 的情况下用的,毕竟拿不到闭包中的 dep,实际上如果用其他方法保存闭包的 dep,让我们在 $set、Vue.set 时能够拿到那就不需要额外的 dep 了。

点击查看例子🌰

Promise 并发个数限制

const timeout = (delay = 1000) => new Promise(resolve => {
  setTimeout(resolve, delay);
})

const addTask = (time, order) => {
  return () => timeout(time).then(() => console.log(order))
}

const multiPromise = (promiseList, nums = 2) => {
  const result = [];
  const list = promiseList.slice();
  let doneNum = 0;

  return new Promise((resolve, reject) => {
      const run = (index) => {
        if (doneNum === promiseList.length) {
          resolve(result);
          return;
        }
        
        const promise = list.shift();
        promise && promise().then(res => {
          doneNum++;
          result[index] = res;
          const runIndex = promiseList.length - list.length;
          run(runIndex);
        }, err => {
          reject(err);
        })
      }

      for(let i = 0; i < nums; i++) {
        run(i);
      }
  })
}

const list = [
  addTask(1000,1),
  addTask(500,2),
  addTask(300,3),
  addTask(400,4),
];

multiPromise(list);

slot 原理

普通插槽

基础用法

var A = {
  template: `<div class="A"><slot /></div>`
}
var vm = new Vue({
  el: '#app',
  components: {
    A
  },
  template: `<div id="app"><A>普通插槽</A></div>`
})

// 最终渲染结果
<div class="A">普通插槽</div>

父组件处理

首先 vm new Vue 根实例后,进入 vm 的挂载阶段,首先需要执行 render 函数生成 vnode,然后 patch 生成真实 DOM,此时的 render 函数如下:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", { attrs: { id: "app" } }, [_c("A", [_vm._v("普通插槽")])], 1)
}

可以看到 vm 最后 _c 执行前需要先执行 childern 的 vnode 生成(其实应该是 A 组件的占位 vnode) _c("A", [_vm._v("普通插槽")])_c_h 函数都是对于 createElement 函数的封装,在 initRender 中都通过闭包保存了当前的 vm 实例做为上下文。

function initRender (vm) {
    // ...
    vm._c = function (a, b, c, d) { return createElement(vm, a, b, c, d, false); };
    // normalization is always applied for the public version, used in
    // user-written render functions.
    vm.$createElement = function (a, b, c, d) { return createElement(vm, a, b, c, d, true); };

    // ...
  }

接着进入 A 组件占位 vnode 生成流程,在 createElement 函数中,vnode = createComponent(Ctor, data, context, children, tag);

function createComponent (
    Ctor, // A options
    data, // undefined
    context, // vm 实例
    children,  // [{text: "普通插槽"}]
    tag // 'A'
  ) {
  // ...
  // 这里生成 A 组件的构造函数
  // ...
  
  var vnode = new VNode(
    ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
    data, undefined, undefined, undefined, context,
    { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
    asyncFactory
  );

  return vnode
}

这里暂时只需要知到传入的 Ctor、propsData 这些都放在 componentOptions 即可。

{
  // ...
  componentOptions: {
    Ctor,  // 构造器
    children,  // children
    listeners, // 监听器
    propsData, // props 数据
    tag, // 占位符
  },
  context, // 父组件实例
  // ...
}

到此为止,父组件的处理完成。

子组件处理

在父组件 patch 的过程中,碰到子组件占位符 vnode,createComponent → init 钩子

var child = vnode.componentInstance = createComponentInstanceForVnode(
  vnode,
  activeInstance
);
child.$mount(hydrating ? vnode.elm : undefined, hydrating);

进入子组件初始化、挂载阶段。

_init 函数中,因为是子组件,所以会执行 initInternalComponent 方法,拿到父组件拥有的相关配置信息,并赋值给子组件自身的配置选项。

    Vue.prototype._init = function (options) {
      var vm = this;

      // merge options
      if (options && options._isComponent) {
        initInternalComponent(vm, options);
      } 
      // ... 
      initLifecycle(vm);
      initEvents(vm);
      initRender(vm);
      callHook(vm, 'beforeCreate');
      initInjections(vm); // resolve injections before data/props
      initState(vm);
      initProvide(vm); // resolve provide after data/props
      callHook(vm, 'created');

      if (vm.$options.el) {
        vm.$mount(vm.$options.el);
      }
    };

  function initInternalComponent (vm, options) {
    var opts = vm.$options = Object.create(vm.constructor.options);
    // doing this because it's faster than dynamic enumeration.
    var parentVnode = options._parentVnode;
    opts.parent = options.parent;
    opts._parentVnode = parentVnode;

    var vnodeComponentOptions = parentVnode.componentOptions;
    opts.propsData = vnodeComponentOptions.propsData;
    opts._parentListeners = vnodeComponentOptions.listeners;
    opts._renderChildren = vnodeComponentOptions.children;
    opts._componentTag = vnodeComponentOptions.tag;

    if (options.render) {
      opts.render = options.render;
      opts.staticRenderFns = options.staticRenderFns;
    }
  }

可以看到,最终占位 vnode 上的 componentOptions 都被放进子组件实例的 $options 中了。

children 放在 opts._renderChildren 上。

继续 _init 函数的流程,进入 initRender 方法,在这个过程中会将配置的 _renderChildren 属性做规范化处理,并将它赋值给子组件实例上的 $slot 属性。

  function initRender (vm) {
    // ...
    var options = vm.$options;
    // ...
    // 这里的 renderContext 是父组件实例
    var renderContext = parentVnode && parentVnode.context;
    vm.$slots = resolveSlots(options._renderChildren, renderContext);
    // ...
  }

  function resolveSlots (
    children,
    context
  ) {
    if (!children || !children.length) {
      return {}
    }
    var slots = {};
    for (var i = 0, l = children.length; i < l; i++) {
      var child = children[i];
      var data = child.data;
      // remove slot attribute if the node is resolved as a Vue slot node
      if (data && data.attrs && data.attrs.slot) {
        delete data.attrs.slot;
      }
      // named slots should only be respected if the vnode was rendered in the
      // same context.
   
      if ((child.context === context || child.fnContext === context) &&
        data && data.slot != null
      ) {
        var name = data.slot;
        var slot = (slots[name] || (slots[name] = []));
        if (child.tag === 'template') {
          slot.push.apply(slot, child.children || []);
        } else {
          slot.push(child);
        }
      } else {
        /**
         * 默认直接放进 default 中,$slot.default
         */
        (slots.default || (slots.default = [])).push(child);
      }
    }
    // ignore slots that contains only whitespace
    for (var name$1 in slots) {
      if (slots[name$1].every(isWhitespace)) {
        delete slots[name$1];
      }
    }
    return slots
  }

随后继续子组件的挂载流程,先生成 render 函数,这里会对 slot 标签做处理,使用 _t 函数包裹。

// slot render 部分处理函数
  function genSlot (el, state) {
    var slotName = el.slotName || '"default"';
    var children = genChildren(el, state);
    var res = "_t(" + slotName + (children ? ("," + children) : '');
    var attrs = el.attrs || el.dynamicAttrs
      ? genProps((el.attrs || []).concat(el.dynamicAttrs || []).map(function (attr) { return ({
          // slot props are camelized
          name: camelize(attr.name),
          value: attr.value,
          dynamic: attr.dynamic
        }); }))
      : null;
    var bind$$1 = el.attrsMap['v-bind'];
    if ((attrs || bind$$1) && !children) {
      res += ",null";
    }
    if (attrs) {
      res += "," + attrs;
    }
    if (bind$$1) {
      res += (attrs ? '' : ',null') + "," + bind$$1;
    }
    return res + ')'
  }

// 最终 render 函数。
var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c("div", { staticClass: "A" }, [_vm._t("default")], 2)
}

_t 其实是 renderSlot 函数的缩写,对于普通插槽,renderSlot 其实就是通过 default 取到 vnode 返回即可。

  function renderSlot (
    name,
    fallback, // 插槽默认内容
    props,
    bindObject
  ) {
      // ...
      let nodes = this.$slots[name] || fallback;
      return nodes
    }
  }

然后就是正常的子组件 patch 过程。

普通插槽使用默认内容

var A = {
  template: `<div class="A"><slot>普通插槽默认内容</slot></div>`
}
var vm = new Vue({
  el: '#app',
  components: {
    A
  },
  template: `<div id="app"><A /></div>`
})

// 最终渲染结果
<div class="A">普通插槽默认内容</div>

区别在与子组件的 render 函数现在变成这样

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { staticClass: "A" },
    [_vm._t("default", [_vm._v("普通插槽默认内容")])],
    2
  )
}

多了 [_vm._v("普通插槽默认内容")] 这段。对应在 renderSlot 的第二个参数就是 fallback

如果插槽中使用了父组件的响应式属性

var A = {
  template: `<div class="A"><slot /></div>`
}
var vm = new Vue({
  el: '#app',
  components: {
    A
  },
  data () {
    return { message: 'hello world' }
  } 
  template: `<div id="app"><A>{{ message }}</A></div>`
})

// 最终渲染结果
<div class="A">hello world</div>

此时 vm 的构造函数是这样:

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [_c("A", [_vm._v(_vm._s(_vm.message))])],
    1
  )
}

可以看到 [_vm._v(_vm._s(_vm.message))] 这里通过 _vm.message 取到了父组件的属性值。

父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。

父组件模板的内容在父组件编译阶段就确定了,并且保存在 componentOptions 属性中,而子组件有自身初始化 init 的过程,这个过程同样会进行子作用域的模板编译,因此两部分内容是相对独立的。

抽取 vue-cli 中 devServer.proxy 支持配置代理服务器字符串的逻辑

问题

在给一个 react 项目配置本地代理服务的时候,发现 webpack 本身的 devServer.proxy 不像 vue-cli 的 devServer.proxy 那样支持配置一个指向开发环境 API 服务器的字符串。当配置代理服务器字符串的时候,根据 vue-cli 文档的说法:这会告诉开发服务器将任何未知请求 (没有匹配到静态文件的请求) 代理到开发环境 API 服务器

不幸的是,我现在就是要用这个,所以看了下 vue-cli 的实现,把这段代码抄过来了。当然由于 webpack 代理服务器配置实际上用的是 http-proxy-middleware,而 vue-cli 只是在 webpack 上又包了一层,所以直接按照 http-proxy-middleware 文档肯定也能写,但是怎么想还是抄 vue-cli 的代码更靠谱。

最后实现

// prepareProxy.js
const fs = require('fs');
const url = require('url');
const path = require('path');
const address = require('address');

const defaultConfig = {
  logLevel: 'silent',
  secure: false,
  changeOrigin: true,
  ws: true,
  xfwd: true,
};

/**
 * @param proxy
 * @param appPublicFolder
 */
function prepareProxy(proxy, appPublicFolder) {
  if (!proxy) {
    return undefined;
  }

  if (typeof proxy !== 'string') {
    console.log('proxy must be a string');
    process.exit(1);
  }

  /**
   * @param pathname
   */
  function mayProxy(pathname) {
    const maybePublicPath = path.resolve(appPublicFolder, pathname.slice(1));
    const isPublicFileRequest = fs.existsSync(maybePublicPath) && fs.statSync(maybePublicPath).isFile();
    const isWdsEndpointRequest = pathname.startsWith('/sockjs-node'); // used by webpackHotDevClient
    return !(isPublicFileRequest || isWdsEndpointRequest);
  }

  /**
   * @param target
   * @param usersOnProxyReq
   * @param context
   */
  function createProxyEntry(target, usersOnProxyReq, context) {
    // #2478
    // There're a little-known use case that the `target` field is an object rather than a string
    // https://github.com/chimurai/http-proxy-middleware/blob/master/recipes/https.md
    if (typeof target === 'string' && process.platform === 'win32') {
      target = resolveLoopback(target);
    }
    return {
      target,
      context(pathname, req) {
        // is a static asset
        if (!mayProxy(pathname)) {
          return false;
        }
        if (context) {
          // Explicit context, e.g. /api
          return pathname.match(context);
        } else {
          // not a static request
          if (req.method !== 'GET') {
              return true;
          }
          // Heuristics: if request `accept`s text/html, we pick /index.html.
          // Modern browsers include text/html into `accept` header when navigating.
          // However API calls like `fetch()` won’t generally accept text/html.
          // If this heuristic doesn’t work well for you, use a custom `proxy` object.
          return req.headers.accept && req.headers.accept.indexOf('text/html') === -1;
        }
      },
      onProxyReq(proxyReq, req, res) {
        if (usersOnProxyReq) {
          usersOnProxyReq(proxyReq, req, res);
        }
        // Browsers may send Origin headers even with same-origin
        // requests. To prevent CORS issues, we have to change
        // the Origin to match the target URL.
        if (!proxyReq.agent && proxyReq.getHeader('origin')) {
          proxyReq.setHeader('origin', target);
        }
      },
      onError: onProxyError(target),
    };
  }

  if (!/^http(s)?:\/\//.test(proxy)) {
    console.log('When "proxy" is specified in package.json it must start with either http:// or https://');
    process.exit(1);
  }

  return [{ ...defaultConfig, ...createProxyEntry(proxy) }];
}

/**
 * @param proxy
 */
function resolveLoopback(proxy) {
  const o = new url.URL(proxy);
  o.host = undefined;
  if (o.hostname !== 'localhost') {
    return proxy;
  }
  // Unfortunately, many languages (unlike node) do not yet support IPv6.
  // This means even though localhost resolves to ::1, the application
  // must fall back to IPv4 (on 127.0.0.1).
  // We can re-enable this in a few years.
  /* try {
    o.hostname = address.ipv6() ? '::1' : '127.0.0.1';
  } catch (_ignored) {
    o.hostname = '127.0.0.1';
  } */

  try {
    // Check if we're on a network; if we are, chances are we can resolve
    // localhost. Otherwise, we can just be safe and assume localhost is
    // IPv4 for maximum compatibility.
    if (!address.ip()) {
      o.hostname = '127.0.0.1';
    }
  } catch (_ignored) {
    o.hostname = '127.0.0.1';
  }
  return url.format(o);
}

// We need to provide a custom onError function for httpProxyMiddleware.
// It allows us to log custom error messages on the console.
/**
 * @param proxy
 */
function onProxyError(proxy) {
  return (err, req, res) => {
    const host = req.headers && req.headers.host;
    console.log('Proxy error:' + ' Could not proxy request ' + req.url + ' from ' + host + ' to ' + proxy + '.');
    console.log(
      'See https://nodejs.org/api/errors.html#errors_common_system_errors for more information (' +
        err.code +
        ').'
    );
    console.log();

    // And immediately send the proper error response to the client.
    // Otherwise, the request will eventually timeout with ERR_EMPTY_RESPONSE on the client side.
    if (res.writeHead && !res.headersSent) {
      res.writeHead(500);
    }
    res.end(
      'Proxy error: Could not proxy request ' +
        req.url +
        ' from ' +
        host +
        ' to ' +
        proxy +
        ' (' +
        err.code +
        ').'
    );
  };
}

module.exports.prepareProxy = prepareProxy;
// webpack.config.js

var { prepareProxy } = require('./prepareProxy');

var proxy = prepareProxy(
  'http://xxx.test.com',
  path.resolve(__dirname, 'public')
);

module.exports = {
  devServer: {
    proxy,
  }
}

typescript 类型

在线测试环境

https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88#code/Q

内置类型

Partial : 将 T 所有字段变为可选

type Partial<T> = { [P in keyof T]?: T[P] | undefined; }

Required : 将 T 所有字段变为必选

type Required<T> = { [P in keyof T]-?: T[P]; }

Readonly : 将 T 所有字段变为 readonly, 不可修改

type Readonly<T> = { readonly [P in keyof T]: T[P]; }

Pick<T, K> : 从 T 中过滤出属性 K

type Pick<T, K extends keyof T> = { [P in K]: T[P]; }

Omit<T, K> : 从 T 中移除属性 K

type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }

Record<K, T> : 标记对象 key [K] value [T] 类型

type Record<K extends string | number | symbol, T> = { [P in K]: T; }

Exclude<T, U> : 取 T, U 两者的不相交的属性

type Exclude<T, U> = T extends U ? never : T

// 'b' | 'c'

type A = Exclude<'a' | 'b' | 'c' | 'd' , 'b' | 'c' | 'e' >

Extract<T, U> : 取 T, U 两者的交集属性

type Extract<T, U> = T extends U ? T : never

NonNullable : 排除 T 的 null | undefined 属性

type NonNullable<T> = T extends null | undefined ? never : T

Parameters : 获取 T 函数的所有参数类型

type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never

ConstructorParameters : 获取 T 类的构造函数的所有参数类型

type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never

ReturnType : 获取 T 函数返回值的类型

type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any

InstanceType : 获取 T 类的实例类型

type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any

19.4.28 面试

电话面试

  1. CSS 中盒模型?
分为标准盒模型和怪异盒模型两种
标准盒模型宽高只指内容宽高,不包括 border, padding
怪异盒模型宽高包括 context + padding + border
  1. 适应性布局?
rem 根据 html 标签设置的 font-size 字体大小变化
em 指的一个字体大小,如果当前元素的字体是 16px,那么 1em === 16px
flex
  1. flex 如何设置垂直居中?
看是否设置主轴还是交叉轴为垂直,
主轴居中 justify-content
交叉轴居中 align-items
  1. 说说 js 中的原型原型链?
js 中所有的对象都有一个 __proto__ 属性,这个属性指向它的原型对象。
对于函数来说,如果这个函数能够作为构造函数,则这个函数也有一个 prototype 属性指向原型对象。
因为对于一个构造函数来说,通过它生成的对象实例指向的原型对象都是同一个,所以可以在这个原型对象上定义一些通用的属性和方法。
多个不同类型的对象通过 __proto__ 属性连接起来,就形成了原型链。
  1. js 如何实现继承?
1. 构造函数实现继承
在子类的构造函数中通过 call, apply 调用父类的构造函数
2. 原型链实现继承
修改子类的 prototype 指向一个父类的对象实例
3. 组合继承
4. 寄生组合继承
5. class extend 实现继承
  1. call, apply 作用是? call, apply 区别?
call, apply 作用在与改变函数内部的 this 指向
call, apply 在传递参数有所不同,call 只能一个一个传,apply 可以传一个数组对象
  1. 说说 Promise ?
Promise 代表一个暂未完成但是在未来某个时刻会完成的异步任务,
Promise 有三个状态,pending, resolved, rejected。一旦状态改变,
就不能再次改变。而且 Promise 的状态不受外部影响。
Promise 主要解决回调地狱的问题,因为 Promise.then 最后会返回一个新 Promise ,
可以链式调用,所以通过 then 方法注册回调函数可以避免多层嵌套的问题。
Promise 的缺陷是,一旦开始无法取消,处于 pending
状态时不知道具体执行到哪里,只能通过回调函数捕获错误。
  1. vue 中父子组件通信方法?
1. props 传参, $emit 触发事件
2. provide/inject
3. $parent, $children 获得 vm 实例进行通信
4. $refs 进行通信
5. event bus
6. vuex
  1. 什么情况下会使用 mixin,mixin 混入的钩子函数和构造器中写的钩子函数执行顺序?
碰到通用逻辑的时候,会把通用逻辑提取出来写成一个 mixin。
mixin 混入的钩子函数会比构造器写入的钩子函数早执行。
  1. mutation 和 action 区别?
mutation 处理同步操作
action 处理异步操作或者多个 mutation 的操作
  1. vuex 如何在 B 模块中调用 A 模块的 mutation?
// https://stackoverflow.com/questions/42606091/change-another-module-state-from-one-module-in-vuex
// 感觉万能的 stackoverflow
commit('a_mutation', null, { root: true })
commit('A/a_mutation', null, { root: true })
第三个参数传 { root: true }

  1. webpack 如何设置打包出多个 chunk 文件?
// https://zhuanlan.zhihu.com/p/26710831
这个问题有点类似 webpack 中如何处理 code splitting
code splitting 有俩点
1. 分离业务代码和第三方库( vendor )
2. 按需加载(利用 import() 语法)

那么针对第 1 点来说,有以下方法
1. 在 entry 中增加一个入口 
{ entry: { app: './src/main.js', vendor: ['vue', 'axios'] }}
这样就会把 vue, axios 的第三方库独立打包出一个 vendor
但是这样实际上 entry chunk 中还是有 vue, 和 axios,所以还要配合
CommonsChunkPlugin 插件使用,
这个插件会把依赖 2 次及以上的模块移到 vendor 这个 chunk 里面。

一面

  1. vue 多环境如何配置?
webpack 配置项中有一个 mode 选项,可配置的有 production development 两个选项
然后通过 process.env.NODE_ENV 判断引入哪一块的配置文件
一般会配合 webpack.DefinePlugin 使用
  1. 数据绑定原理?
吧啦吧啦,Object.defineProperty 和 重写数组变异方法
  1. vue 数据绑定设计模式?
发布-订阅模式

常用 git 操作

  • 常用 git 操作

    cherry-pick : 将某个分支上的 commit 摘取出来提交到当前分支, -x -s 可选参数,带上 hash 和签名信息

    revert : 通过再次 commit 相反的内容抵消一次 commit

    reflog : 查看 git 操作记录,然后可以通过 reset —hard HEAD@{n} 来返回到第 n 步是的状态

    rebase : 当前 rebase master,master 分支 merge

    reset : --hard --soft

    fetch : 拉取远程的分支 fetch origin test:test

    // 没有在git 版本控制中的文件,是不能被git stash 存起来的, 需要先 git add . 添加到 git 版本控制中

    git stash save ‘save message’

    git stash clear : 清空你所有的 stash 内容

    git stash drop stash@{0} : 这是删除第一个队列

    修改某一个 commit 的提交信息,但是如果是修改的远程上的commit 最后需要强制提交才行。

    git 全局设置

    git config --global merge.conflictstyle diff3 // 开启三路合并

    git config --global merge.ff false // merge 不允许使用 fast-forward

    git config --global pull.ff true // pull 可以使用 fast-forward

    git rebase -i HEAD~n // n 要大于等于要修改的commit 距离 HEAD 的步数

    git rebase -i xxx 合并 xxx 之后(不包含xxx)到 head 的 commit

    git merge xxx —no-ff 强行关闭fast-forward方式

    git merge xxx -Xignore-all-space 合并时忽略空白符,对于格式化文件之后的合并十分有效

    git log --grep={query} 按照{query} 的内容查找commit 提交

    stash 部分文件

    git stash -p

    a - stage this file

    d - do not stage this file

    y - stage this hunk

    n - do not stage this hunk

    q - quit

  • git commit 信息

    feat:: 类型为 feat 的提交表示在代码库中新增了一个功能。

    fix::类型为 fix 的 提交表示在代码库中修复了一个 bug 。

    docs:: 只是更改文档。

    style:: 不影响代码含义的变化(空白、格式化、缺少分号等)。

    refactor:: 代码重构,既不修复错误也不添加功能。

    perf:: 改进性能的代码更改。

    test:: 添加确实测试或更正现有的测试。

    build:: 影响构建系统或外部依赖关系的更改(示例范围:gulp、broccoli、NPM)。

    ci:: 更改持续集成文件和脚本(示例范围:Travis、Circle、BrowserStack、SauceLabs)。

    chore:: 其他,不修改srctest文件。

    revert:: commit 回退。

19.5.6 面试

电面

  1. vue 响应式原理
  2. es6 数组去重方法
  3. Set WeekSet 区别
  4. for-in for-of 区别
  5. for-of 能遍历对象吗
  6. 用 for-in 遍历对象要做什么判断
  7. 箭头函数的特点
  8. 构造函数关键字是什么
  9. yield 作用
  10. promise 哪三个状态

笔试

  1. 实现一个函数可以判断 String 类型
  2. js 中的数据类型
  3. alert 中如何换行
  4. 实现一个函数可以把 url 中的 query 参数转为一个对象
  5. ajax 含义,什么是异步,跨域方法
  6. 异步加载 js 的方式
  7. 继承与原型
  8. 深拷贝
  9. h5 css5 用过或了解的标签和属性
  10. 谈谈对任意一个 js 框架的理解

一面

  1. slice splice 区别
  2. slice splice 是变异方法吗
  3. 二叉树遍历方法
  4. js 实现继承的方法
  5. vue 响应式原理
  6. vuex 原理
  7. vue 组件间的通信方法
  8. 数据双向绑定原理
  9. promise 优缺点
  10. 实现一个函数可以从对象中取出指定 key 的数据组成一个新对象返回
  11. 实现一个通用的判断类型的函数
  12. webpack 优化方法
  13. 如何实现私有变量(就知道约定和闭包)
  14. 跨域方法
  15. 301, 302 代表的意思,有什么不同(只知道意思,具体不同说的不好)
  16. mutation 和 action 区别
  17. 点击一个 tab 页,切换组件有哪些实现
  18. nextTick 原理
  19. 项目中的问题
  20. 面向对象三要素
  21. 常见的 HTTP 请求类型,get post 区别
  22. cors 简单请求,复杂请求区别 (答的不好,简单请求的特征记不清)
  23. 用过 JSX 或者 $compile 吗?(没=-=)
  24. Symbol 的用法
  25. call, apply 区别

二面

  1. 看过哪些前端方面的书
  2. 有什么优缺点
  3. 巴拉巴拉...

三面 HR 面

fabric.js part 1

Why fabric?

时至今日,我们已经能够使用canvas 在web 上创建出一些非常精美的图形了。不过令人失望的是,原生canvas 提供的API 都太难用了,除非你只是想用canvas 画出一些简单的图形。否则一旦碰上需要实现各种交互效果,在图片上的任何一点做改动又或者是画一些更复杂的多边形的情况,使用原生API 就变成了一件非常痛苦的事情。

Fabric 就是为了解决这些问题而生的。

原生canvas 方法只允许我们使用一些简单的图形命令来盲目的修改整个canvas 画布。比如,想要画一个矩形?可以用 fillRect(left, top, width, height) 。想要画一条线?你需要 moveTo(left, top)lineTo(x, y) 的组合。这就像是我们用一只笔在画布上画画,除了最原始最简单的一笔一笔往上画之外别无它法。

和原生canvas 提供的这些低级API 不同的是,Fabric 在基于原生的方法上为我们提供了同样简单但是却更强大的对象模型,它不仅会帮我们负责画布的状态和渲染,还允许我们使用对象指令操作canvas。

现在让我们通过一个简单的例子来对比其中的差异吧,比如说我们想要在画布上画一个红色的矩形,使用原生的API 我们可以这么做。

// reference canvas element (with id="c")
var canvasEl = document.getElementById('c');

// get 2d context to draw on (the "bitmap" mentioned earlier)
var ctx = canvasEl.getContext('2d');

// set fill color of context
ctx.fillStyle = 'red';

// create rectangle at a 100,100 point, with 20x20 dimensions
ctx.fillRect(100, 100, 20, 20);

现在,让我们看看Fabric 是怎么做的。

// create a wrapper around native canvas element (with id="c")
var canvas = new fabric.Canvas('c');

// create a rectangle object
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20
});

// "add" rectangle onto canvas
canvas.add(rect);

image

目前为止,两者的差别可能并不大——这两个例子非常相似。然而,你已经可以看到两者对于canvas 的操作有所不同了。在使用原生的例子中,我们操作context——一个表示画布的canvas 对象。在使用Fabric 的例子中,我们操作一个Fabric 实例对象,改变它的属性,最后add 进canvas 中。你可以发现在fabric 中这些对象是一等公民。

只是画一个红色矩形未免太无聊了,也许我们还能做一些更有趣的事情,比如旋转?

让我们来试试旋转45度吧,首先,是使用原生canvas。

var canvasEl = document.getElementById('c');
var ctx = canvasEl.getContext('2d');
ctx.fillStyle = 'red';

ctx.translate(100, 100);
ctx.rotate(Math.PI / 180 * 45);
ctx.fillRect(-10, -10, 20, 20);

然后是使用Fabric。

var canvas = new fabric.Canvas('c');

// create a rectangle with angle=45
var rect = new fabric.Rect({
  left: 100,
  top: 100,
  fill: 'red',
  width: 20,
  height: 20,
  angle: 45
});

canvas.add(rect);

image

这里都发生了些什么?

我们在Fabric 中唯一做的事就是指定了object 的angle 属性值为 45。然而在使用原生方法的例子中,事情好像变得“有趣”了,记住在原生方法中,我们不能直接操作object,取而代之的是,我们调整了整个画布的位置和角度 (ctx.translate, ctx.rotate) 来适应我们的需求,然后再画一个矩形,不要忘了计算矩形坐标时需要考虑到画布坐标的偏移(-10,-10),以便它还是从100,100 这个点开始渲染。在旋转画布时我们还需要额外做一件事,就是把旋转的角度转换为弧度。

我相信你已经发现为什么Fabric 会有存在的必要了,并且发现Fabric 为我们隐藏了大量底层的细节。

让我们再看一个例子——追踪canvas 的状态。

假设现在有这么个需求,我们想要把原来的红色矩形移动到一个不同的位置?在不操作object 的情况向我们能怎么做?难道只是再调用一次 fillRect

不完全是这样,调用 fillRect 确实能够满足在画布任何位置画一个矩形的需求。还记得我之前说的用画笔在画布上画画的比喻吗?为了实现移动这个动作,我们需要先把画布上原来画的东西清空,然后再在新的位置画一个矩形。

var canvasEl = document.getElementById('c');

...
ctx.strokRect(100, 100, 20, 20);
...

// erase entire canvas area
ctx.clearRect(0, 0, canvasEl.width, canvasEl.height);
ctx.fillRect(20, 50, 20, 20);

在Fabric 中要如何实现相同的事?

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);
...

rect.set({ left: 20, top: 50 });
canvas.renderAll();

image

请注意这里有一个非常重要的不同,在Fabric 中,我们不再需要在修改绘画内容前先把画布清空了,我们只需要对object 做一些工作,简单的改变他的属性值,然后重新渲染画布获得新的图片。

Objects

我们已经知道如何通过实例化fabric.Rect 来操作一个矩形了,理所当然的Fabric 也支持其他的所有基本图形——圆,三角,椭圆等等,所有的这些基本图形都暴露在 fabirc 这个命名空间下,fabric.Circlefabric.Trianglefabric.Ellipse 等等。

Fabric 提供了7种内置基本图形。

  • fabric.Circle
  • fabric.Ellipse
  • fabric.Line
  • fabric.Polygon
  • fabric.Polyline
  • fabric.Rect
  • fabric.Triangle

想过画个圆?只要新建一个circle 的对象,然后添加进画布即可,你可以用其他基本图形做同样的事。

var circle = new fabric.Circle({
  radius: 20, fill: 'green', left: 100, top: 100
});
var triangle = new fabric.Triangle({
  width: 20, height: 30, fill: 'blue', left: 50, top: 50
});

canvas.add(circle, triangle);

image

通过上面的例子,我们就能够得到一个位于100,100 的绿色圆形,和一个位于50,50 的蓝色三角形。

Manipulating objects

创建一个图形对象——矩形,圆或者是其他——这只是开始的第一步,在某些情况下,我们可能想要对对象做些改动,可能某些改动需要触发状态的变化,或者是播放某种动画,又或者是在某个鼠标交互下改变对象的某个属性(颜色,透明度,大小,位置) 。

Fabric 会帮我们管理canvas 的渲染和转态,使得我们只需要关心改变object 本身。

早先的例子展示了 set 方法以及通过调用 set({ left: 20, top: 50 }) 是如何使一个对象从原有的位置上移开的,类似的,我们也能够改变对象的其他属性,那么都有哪些属性呢?

与位置有关的属性——left,top;尺寸——width,height;渲染——fill,opacity,stroke,strokeWidth;缩放和旋转——scaleX,scaleY,angle;翻转——flipX,flipY;倾斜——skewX,skewY。

是的,想要创建一个翻转的对象在Fabric 中非常简单,只要设置flip* 属性为true。

你能通过 get 方法获得任何一个属性值,或用 set 方法设置属性值,让我们来改变一些前面矩形的属性看看。

var canvas = new fabric.Canvas('c');
...
canvas.add(rect);

rect.set('fill', 'red');
rect.set({ strokeWidth: 5, stroke: 'rgba(100,200,200,0.5)' });
rect.set('angle', 15).set('flipY', true);

image

首先,我们设置fill 的值为red,实质上是指定对象为红色,接着声明了strokeWidth 和stroke 的值,给矩形设置5px 的淡绿色边框,最后,我们改变了angle 和flipY 属性,注意这三句声明代码在语法上都略有不同。

可以发现 set 是一个使用率很高的方法,你以后可能会经常使用它,这也意味着它应该竟可能易于使用。

我们说完setters,应该说getters 了?很明显的,不仅有通用的 get 方法,而且还有特殊的 get* 方法,想要获得"width"的值,你可以使用 get('width')getWidth()。读取"scaleX"的值——get('scaleX')getScaleX() 等等。每个属性都有类似 getWidth()getScaleX() 这样的方法("stroke", "strokeWidth", "angle" 等等)

通过前面的例子你也许会注意到,无论是我们在创建object 时传递options 设置属性值还是之后我们调用 set 方法设置属性值,最后结果是一样的。因为这两者确实有着相同的效果。你可以选择在创建时传递参数设置对象属性值,或者在之后使用 set 设置属性值。

var rect = new fabric.Rect({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

// or functionally identical

var rect = new fabric.Rect();
rect.set({ width: 10, height: 20, fill: '#f55', opacity: 0.7 });

Default options

看到这里,你可能会问——如果我们在创建对象时不传配置项,又会发生什么呢? 最后返回的对象实例有没有这些属性呢?

当然有,Fabric 中对象的属性都有一个默认值。在创建时省略配置项的话,就会使用这个默认值。

var rect = new fabric.Rect(); // notice no options passed in

rect.get('width'); // 0
rect.get('height'); // 0

rect.get('left'); // 0
rect.get('top'); // 0

rect.get('fill'); // rgb(0,0,0)
rect.get('stroke'); // null

rect.get('opacity'); // 1

使用默认值的对象,是一个位于0,0 坐标,黑色完全不透明并且无边框无大小(高度宽度都为0)的矩形。因为没有大小,所以我们不能在画布中看到它,但只要给它一个宽高,就能在左上角看到一个黑色矩形。

image

Hierarchy and Inheritance

Fabric 对象相互之间并不是完全独立的,而是有着非常精准的层次结构的。

大部分的对象都继承至 fabric.Objectfabric.Object 代表一个位于二维平面的二维图形,它也有left/top 和width/height 属性,以及一系列图形特征,我们前面接触的那些属性——fill,stroke,angle,opacity,flip* 等等——这些所有fabric 对象共有的属性都继承至 fabric.Object

由于有这一层继承的存在,就允许我们通过在 fabric.Object 上定义方法的方式来分享给所有的子类对象,比如,如果你想要给所有的对象上定义一个 getAngleInRadians 方法,你可以简单的在 fabric.Object.prototype 上定义它。

fabric.Object.prototype.getAngleInRadians = function() {
  return this.get('angle') / 180 * Math.PI;
};

var rect = new fabric.Rect({ angle: 45 });
rect.getAngleInRadians(); // 0.785...

var circle = new fabric.Circle({ angle: 30, radius: 10 });
circle.getAngleInRadians(); // 0.523...

circle instanceof fabric.Circle; // true
circle instanceof fabric.Object; // true

你会发现,这个方法在所有的实例对象上都能调用到。

当一个子类继承了 fabric.Object,它们常常还会定义一些自己特有的方法和属性。比如,fabric.Circle 需要有"radius"属性,fabric.Image——我们等会将要介绍的——需要有 getElement/setElement 方法,这两个方法在我们操作HTML 中 <img> 元素时会被使用到。

对于高级的项目来说,使用原型来获得自定义的渲染或行为是非常普遍的。

Canvas

我们已经详细的说完了fabric 对象方法,现在让我们回到canvas 看看。

你能看到在所有的Fabric 例子中,第一件事永远是先创建一个canvas 对象——new fabric.Canvas('...')。fabric.Canvas 充当一个<canvas> 元素的包裹器,并负责管理这块画布上的所有fabric 对象。它在创建时需要传递一个元素id,返回一个 fabric.Canvas 实例。

我们可以把fabric 对象 add 进fabric.Canvas 实例中,引用它们或删除它们。

var canvas = new fabric.Canvas('c');
var rect = new fabric.Rect();

canvas.add(rect); // add object

canvas.item(0); // reference fabric.Rect added earlier (first object)
canvas.getObjects(); // get all objects on canvas (rect will be first and only)

canvas.remove(rect); // remove previously-added fabric.Rect

fabric.Canvas 管理对象的同时,它还是一个主要配置对象,想要对画布设置背景颜色或图片?裁剪全部内容或部分内容?是否特殊处理一个交互,以上所有的这些选项都在 fabric.Canvas 中配置,相似的,可以选则在创建时配置或是对实例对象配置。

var canvas = new fabric.Canvas('c', {
  backgroundColor: 'rgb(100,100,200)',
  selectionColor: 'blue',
  selectionLineWidth: 2
  // ...
});

// or

var canvas = new fabric.Canvas('c');
canvas.setBackgroundImage('http://...');
canvas.onFpsUpdate = function(){ /* ... */ };
// ...

Interactivity

to be continue...

19.5.7 面试

一面

  1. 闭包
  2. this 指向
  3. 垂直水平居中
  4. 盒子模型
  5. Vue 组件通信
  6. css3 常用属性
  7. call, apply 区别
  8. 箭头函数使用 bind 后,this 指向?
  9. 闭包的优缺点
  10. 不记得了...

二面

  1. 实现一个函数能够把 "1 2 3 33 23" 这种字符串中的数字过滤成一个数字数组返回
  2. 常见排序算法和复杂度
  3. 解释下归并排序
  4. 解释下快速排序
  5. 解释下二分查找
  6. 不用第三方库实现一个深拷贝函数,要求能够检测出循环引用(母鸡)
  7. 常用的数组方法
  8. 5 层网络模型
  9. http 是哪一层,tcp 是哪一层,ip 是哪一层(ip 网络层说成传输层了)
  10. get post 区别
  11. 三次握手,为什么要三次握手
  12. git rebase 命令除了 --hard 还用过哪些?(只用过 hard =-=)
  13. git flow 用过吗?(没)

JS 类型和类型转换

JS 类型

基础类型

  • boolean
  • null
  • undefined
  • number
  • string
  • symbol

首先基础类型存储的都是值,然后需要注意的是 JS 中的 number 类型实际存储的都是 64 位浮点型,所以类似 0.1 + 0.2 !== 0.3 // true 也会因为精度的问题出现 bug,除此之外 JS 中能够进行安全运算的整数也有范围,超出这个范围的整数运算也会出现错误。这个范围是:

Math.pow(2, 53) - 1 // 9007199254740991

// 可以通过以下方式获得
console.log(Number.MAX_SAFE_INTEGER); // 9007199254740991
console.log(Number.MIN_SAFE_INTEGER); // -9007199254740991

而对于 null 使用 typeof 结果输出 object,这是 JS 中的一个 bug,实际上 null 并不是 object 类型。

引用类型

  • object

引用类型只有一种 object。引用类型和基础类型的区别是,引用类型存储的是一个地址而不是值,这个地址指向内存中的某一块。

类型转换

JS 中类型转换只有三种情况:

  • 转为 boolean
  • 转为 number
  • 转为 string

针对这三种情况,JS 中分别有 Boolean()Number()String() 三个方法,三个方法的转化规则如下:

Boolean()

除了 undefined, null, false, NaN, , 0, -0 会转为 false,其他都转为 true

Number()

  • boolean -> true1false0
  • undefined -> NaN
  • null -> 0
  • string -> "123" 这类合法的字符串直接转为对应的数字,非法的字符串则转为 NaN,比较特殊的是 ""这种空字符串可以转为 0,还有一点字符串两边添加任意空格不影响最后的转换结果
  • symobl -> 抛错
  • object -> 执行 valueOf() 方法,如果是基础类型再执行类型转换,否则执行 toString() 方法,如果是基础类型再执行类型转换。

对于 object 转为 number 再做个说明,如果没有手动重写 valueOf() 方法的话,那么都是返回 object 本身的,所以大部分情况下都是走的 toString() 再转为 number,object 转为 string,这种情况下只有空数组( [] -> "") 和只有一个可以合法转为数字元素的数组(['1'] -> "1") 可以正常转为数字类型,其他的都是 NaN

String()

  • boolean -> true"true"false"false"
  • undefined -> "undefined"
  • null -> "null"
  • number -> 合法的数字字符串
  • symobl -> 抛错
  • object -> 执行 toString() 方法,如果是基础类型再执行类型转换,否则执行 valueOf() 方法,如果是基础类型再执行类型转换。

类型转换的优先级

Boolean()Number()String() 如果直接写出来或者单独存在时是不难判断转换后的结果的,但是当这些类型转换处于一个复杂的表达式中时,就必须按照正确的先后顺序执行类型转化,否则就可能得到完全相反的结果,JS 在以下三种不同的表达式中有着不同的优先级。

  • + - * / 四则运算
  • > < >= <= 比较运算
  • == 双等号运算

+ - * / 四则运算

+ 加法运算比较特殊单独说:

  1. object 会转为基础类型( valueOf(), toString()),再进行运算,除非重写 valueOf() 方法,不然都是返回字符串。
  2. 运算中如果有一方为字符串,另一方也会转为字符串
  3. 如果没有字符串,则转为数字进行运算。
  4. 特殊用法: + '1' 会被转为 NaN,eg: 'a' + + 'b' === 'aNaN'

- * / 运算:

都转为数字类型再进行运算,非法就是 NaN 的结果。

> < >= <= 比较运算

  1. boolean -> number
  2. 一方为数字,则都转为数字进行计算,这种情况下如果另一方是 object,大概率会返回 false,因为 NaN 和任何数字比较都返回 false
  3. 否则转为字符串,按照 unicode 字符索引比较
  4. 两边都是对象,当然是转为字符串比较了。

== 双等号运算

  1. 如果存在 NaN,一律返回 false
  2. nullundefined 不会进行转换,但它们两相等
  3. boolean -> number
  4. 一方为数字类型,另一方也转为数字类型进行比较
  5. 一方为字符串类型,另一方也转为字符串类型进行比较
  6. [] == [], [] == {} 这种情况比较的是引用的地址值,一般是 false

有趣的类型转换案例

[] > [] // false
[] < [] // false
[] >= [] // true
[] <= [] // true
[] == [] // false
[] == ![] // true

Watcher 更新队列的实现?

Watcher 更新队列的实现主要是两个函数,queueWatcherflushSchedulerQueue

// src/core/observer/scheduler.js

const queue: Array<Watcher> = []
let has: { [key: number]: ?true } = {}
let waiting = false
let flushing = false
let index = 0

function flushSchedulerQueue () {
  flushing = true
  let watcher, id

  // Sort queue before flush.
  // This ensures that:
  // 1. Components are updated from parent to child. (because parent is always
  //    created before the child)
  // 2. A component's user watchers are run before its render watcher (because
  //    user watchers are created before the render watcher)
  // 3. If a component is destroyed during a parent component's watcher run,
  //    its watchers can be skipped.
  queue.sort((a, b) => a.id - b.id)

  // do not cache length because more watchers might be pushed
  // as we run existing watchers
  for (index = 0; index < queue.length; index++) {
    watcher = queue[index]
    if (watcher.before) {
      watcher.before()
    }
    id = watcher.id
    has[id] = null
    watcher.run()
    // in dev build, check and stop circular updates.
    if (process.env.NODE_ENV !== 'production' && has[id] != null) {
      circular[id] = (circular[id] || 0) + 1
      if (circular[id] > MAX_UPDATE_COUNT) {
        warn(
          'You may have an infinite update loop ' + (
            watcher.user
              ? `in watcher with expression "${watcher.expression}"`
              : `in a component render function.`
          ),
          watcher.vm
        )
        break
      }
    }
  }

  // keep copies of post queues before resetting state
  const activatedQueue = activatedChildren.slice()
  const updatedQueue = queue.slice()

  resetSchedulerState()

  // call component updated and activated hooks
  callActivatedHooks(activatedQueue)
  callUpdatedHooks(updatedQueue)

  // devtool hook
  /* istanbul ignore if */
  if (devtools && config.devtools) {
    devtools.emit('flush')
  }
}

export function queueWatcher (watcher: Watcher) {
  const id = watcher.id
  if (has[id] == null) {
    has[id] = true
    if (!flushing) {
      queue.push(watcher)
    } else {
      // if already flushing, splice the watcher based on its id
      // if already past its id, it will be run next immediately.
      let i = queue.length - 1
      while (i > index && queue[i].id > watcher.id) {
        i--
      }
      queue.splice(i + 1, 0, watcher)
    }
    // queue the flush
    if (!waiting) {
      waiting = true
      nextTick(flushSchedulerQueue)
    }
  }
}

queueWatcher

先来看 queueWatcher,这是主要暴露出来的调度函数。

首先会对传进来的 watcher 使用 has 数组保存 watcher.id 的方法做一个重复过滤,保证在进入 flushSchedulerQueue 之前 queue 中的 watcher 是不重复的。

接着判断如果不是 flushing 则加入队列中,否则会执行这一段代码。

// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
  i--
}
queue.splice(i + 1, 0, watcher)

能够进入这段代码说明当前的这个 watcher 是在 flushing 期间加入的,也就是在 flushSchedulerQueue 函数执行期间加入,在 flushSchedulerQueue 中,会把 queue 根据 watcher.id 从小到大排序,接着从头到尾遍历执行 watcher.run 方法,执行 get 获取新值,执行回调。这个 watcher.run 的过程中就存在又触发了其他响应式数据 setter → dep.notify → watcher.update → queueWatcher(watcher) 的这个过程,举个例子,比如死循环更新。

<template>
  <p>{{msg}}</p>
  <button @click="change">Add</button>
</template>
<script>
export default {
  data () {
    return {
      msg: 'msg'
    }
  },
  methods: {
    change () {
      this.msg = Math.random()
    }
  },
  watch: {
    msg () {
      this.msg = Math.random()
    }
  }
}
</script>

而且注意一点,flushSchedulerQueue 中遍历 queue 执行 watcher.run 方法之前,会把当前的 has[id] = null 置回 null,这也是死循环会出现的原因之一,就是因为这里会置回 null,所以在 flushing 期间加入的 watcher 都可以通过queueWatcher 中的 has[id] 的重复检查,即使是相同的 watcher。

回到这段代码

// if already flushing, splice the watcher based on its id
// if already past its id, it will be run next immediately.
let i = queue.length - 1
while (i > index && queue[i].id > watcher.id) {
  i--
}
queue.splice(i + 1, 0, watcher)

可以很容易理解,它是一个从后往前比较的插入排序操作,将当前 watcher 根据 id 插入已经排好序的 queue 中。

queueWatcher 的最后判断是否是 waiting 状态,在一个 event loop 中第一次走进 queueWatcher 时,会走这块逻辑执行 nextTick(flushSchedulerQueue) ,这里可以简单的理解为在微任务中调用 flushSchedulerQueue

flushSchedulerQueue

flushSchedulerQueue 函数主要做了这五件事

  1. 设置 flushing 状态
  2. queue 排序
  3. 遍历 queue 执行 watcher.run()
  4. resetSchedulerState() 还原状态变量
  5. 触发组件钩子函数

第一点 flushing = true 很好理解,整个遍历 queue 期间都是 flushing 状态。

第二点 queue 根据 watcher 的 id 从小到大排列,之所以从小到大有以下三点:

  • 组件的更新由父到子,父组件先于子组件创建,所以父组件的 watcher id 也比子组件的 watcher id 小
  • 用户的自定义 watcher 要优先于 render watcher 执行,用户自定义 watcher 是先于 render watcher 之前创建的
  • 如果一个组件在父组件的 watcher 执行期间被销毁,那么这个组件对应的 watcher 执行都可以被跳过。

第三点遍历 queue ,如果是组件并且 _isMounted,则调用 watcher.before() 的结果是执行 beforeUpdate 钩子函数,接下来的步骤在 queueWatcher 中其实说过了has[id] = nullwatcher.run() ,加上死循环的判断,死循环的判断就是在一个 flushing 期间同一个 watcher 执行的次数超过 MAX_UPDATE_COUNT 就断定为死循环。

第四点重置 indexhascircularwaitingflushing 这些状态变量。

第五点调用 callActivatedHookscallUpdatedHooks 执行钩子函数

jsonp 的原理和实现

工作原理

script 标签的src 属性不受浏览器的跨域限制。所以能够使用get 方法向服务端发送请求,接着服务端返回一段调用函数的js 代码,将json 格式的数据以参数的形式传递,如:

callback({"a":"a", "b":"b"});

这段服务端返回的js 代码在客户端环境下会直接执行。所以我们就可以事先在全局作用域下(window) 挂载一个名为callback 的函数,在这个函数中就可以拿到数据并执行具体的操作了。这里存在一个可以优化的点,回调函数执行完就delete 掉。

具体实现

const concatUrl = function (data) {
  // 拼接url
  let url = '';
  for (let k in data) {
    let value = data[k] !== undefined ? data[k] : '';
    url += `&${k}=${encodeURIComponent(value)}`;
  }
  return url ? url.substring(1) : '';
}

const josnp = function (url, callback, cbName, data) {
  let script = document.createElement('script');
  
  // 设置回调函数的函数名
  data['callback'] = cbName;
  
  url += (url.indexOf('?') < 0 ? '?' : '&') + concatUrl(data);
  
  script.src = url;
  
  window[cbName] = function (data) {
    callback(data);
    window[cbName] && (delete window[cbName]);
  };
  
  document.querySelector('head').appendChild(script);
}

提取 .vue 文件中非 scoped 的 less 块

var compiler = require('@vue/compiler-dom');
var path = require('path');
var fs = require('fs');

// 读取目标文件夹下的所有文件地址
var getAllFiles = function (targetList, regExp) {
  var result = [];

  while(targetList.length) {
    var target =  targetList.shift();

    // 读取目录的内容
    var files = fs.readdirSync(target);

    files.map((file) => {
      var fullpath = path.resolve(target, file);

      // 获取文件状态信息?
      var stat = fs.statSync(fullpath);

      if (stat.isDirectory()) {
        targetList.push(fullpath);
      } else if (!regExp || (regExp && regExp.test(fullpath))) {
        result.push(fullpath);
      }
    })
  }

  return result;
}

// 获取 vue 文件中的指定块
var getTagContent = function (file, fn) {
  var result = [];
  var content = getFileContent(file);
  // 获取 ast
  var ast;

  try {
    ast = compiler.parse(content);
  } catch (err) {
    console.log(file, err);
    return;
  }

  ast && ast.children && ast.children.map((node) => {
    if (fn(node, ast, file)) {
      result.push(node.loc.source);
    }
  })

  return result;
}

// 读取文件内容
var getFileContent = function (file) {
  // 读取文件 buffer 转为字符串
  return fs.readFileSync(file).toString();
}

// 输出内容
var emitAssets = function (output, content, flag = 'a+') {
  // 写入文件。
  fs.writeFile(output, content, { flag }, err => {
    if (err) {
      console.error(err)
      return
    }
  });
}

// 删除指定文件
var deleteFile = function(file) {
  if (fs.existsSync(file)) {
    fs.unlinkSync(file);
  }
}



function run() {
  var targetFileRex = /.*\.vue$/g;
  var output = path.resolve(__dirname, 'common-style.less');

  var list = getAllFiles([
    path.resolve(__dirname, 'src'),
  ], targetFileRex);

  var scopedList = [];

  list.map((file) => {
    var result = getTagContent(file, (node) => {
      return node.tag === 'style' && node.props.every(prop => prop.name !== 'scoped');
    });

    result.length && scopedList.push({
      file,
      data: result,
    });

    // 删除原本文件中的 less 块。
    var content = getFileContent(file);

    result.map(scope => {
      content = content.replace(scope, '');
    });

    emitAssets(file, content, 'w');
  });

  deleteFile(output);

  // 输出
  scopedList.map(({file, data}) => {

    var content = '';

    content += `// ${file}\n\n`;
    data.map(scope => {
      content += `${scope}\n\n`;
    })

    emitAssets(output, content);
  })

}

// test
run();

重写 el-dropdown 的 initEvent 函数,支持 el-dropdown 嵌套 hover 二级下拉框时一级下拉框不消失

问题

之前写需求碰到需要在 el-dropdown 里嵌套写 el-dropdown 的情况,表现形式类似 el-submenu 嵌套 el-submenu
在下拉选项里有一项 hover 会在旁边展开二级下拉选项。

然后写了个例子发现,现成的 el-dropdown 虽然可以嵌套,但是鼠标 hover 到二级下拉选项时,一级下拉选项就收起来了,
于是有了下面的修改尝试。

主要原理是让嵌套的 drowdown 元素在触发 mouseentermouseleave 事件时也触发父 drowdown 元素的 showhide 方法。

在线演试

https://codesandbox.io/s/el-dropdown-nested-1hs28?file=/src/main.js

main.js


(function () {
    // 为了 el-dropdown 组件 hover 时支持嵌套,重写 el-dropdown 的 initEvent 函数
    // [email protected]

    const components = Vue.options.components;
    const methods = components.ElDropdown.options.methods;
    const initEvent = methods.initEvent;
    methods.initEvent = function () {
        initEvent.call(this);
        // 对于嵌套的 el-dropdown 添加 mouseenter,mouseleave 回调
        try {
            let { trigger, show, hide } = this;
            if (trigger === 'hover') {
                const secondaryDropDown = [];
                let children = [...this.$children];
                while (children.length) {
                    const deepChildren = [];
                    children.map(child => {
                        child.$options.componentName === 'ElDropdown' && secondaryDropDown.push(child);
                        if (child.$children.length) {
                            deepChildren.push(...child.$children);
                        }
                    })
                
                    children = deepChildren.length ? deepChildren : [];
                }
                secondaryDropDown.map(secondary => {
                    secondary.dropdownElm.addEventListener('mouseenter', show);
                    secondary.dropdownElm.addEventListener('mouseleave', hide);
                })
            }
        } catch (error) {
            console.log('重写 el-dropdown initEvent 错误', error);
        }
    }

})();

.vue 文件

    <el-dropdown
        class="dropdown"
        placement="bottom-end"
        trigger="hover"
    >
        <span class="el-dropdown-link">更多</span>
        <el-dropdown-menu slot="dropdown">
            <el-dropdown
                class="dropdown"
                style="width:100%"
                placement="left"
                trigger="hover"
            >
                <li class="el-dropdown-menu__item">1</li>
                <el-dropdown-menu slot="dropdown">
                    <el-dropdown-item >1-1</el-dropdown-item>
                    <el-dropdown-item >1-2</el-dropdown-item>
                </el-dropdown-menu>
            </el-dropdown>
            <el-dropdown-item>2</el-dropdown-item>
            <el-dropdown-item>3</el-dropdown-item>
        </el-dropdown-menu>
    </el-dropdown>

eslint 配置记录

项目中初始化

# 项目中初始化 eslint
node_modules/.bin/eslint --init

# 修复全部 .ts 文件
node_modules/.bin/eslint --fix --ext .ts .

魔法注释

/* eslint-disable */
alert('该注释放在文件顶部,整个文件都不会出现 lint 警告')

/* eslint-enable */
alert('重新启用 lint 告警')

/* eslint-disable xxx */
alert('只禁止某一个或多个规则')

/* eslint-disable-next-line */
alert('当前行禁止 lint 警告')

alert('当前行禁止 lint 警告') // eslint-disable-line

eslint 配置文件

eslint 的配置文件可以有多种,比如初始化过程中提供的三个选项:

  • JavaScript (eslintrc.js)
  • YAML (eslintrc.yaml)
  • JSON (eslintrc.json)

又或者可以直接在 package.json 中添加 eslintConfig 配置字段

eslint 配置文件的优先级如下:

const configFilenames = [
  ".eslintrc.js",
  ".eslintrc.yaml",
  ".eslintrc.yml",
  ".eslintrc.json",
  ".eslintrc",
  "package.json"
];

基本配置模板

{
    "extends": "eslint:recommended", // 继承
    "rules": {
        "semi": ["error", "always"], // 必须行尾加分号
        "quotes": ["error", "single"], // 强制使用单引号
        "indent": ["error", 2, {
          "VariableDeclarator": "first", // 多变量声明换行时,所有声明符与第一个声明符对其
        }], // 强制缩进空格个数
        "jsx-quotes": ["error", "prefer-double"], // jsx 中强制使用双引号
        "arrow-parens": ["error", "always"], // 箭头函数无论参数个数必须使用括号
        "comma-dangle": ["error", { // 使用拖尾逗号
            "arrays": "always-multiline", // 单行不加,多行必须加拖尾逗号
            "objects": "always-multiline",
            "imports": "always-multiline",
            "exports": "always-multiline",
            "functions": "only-multiline" // 单行不加,多行可加可不加拖尾逗号
        }], // 多行时必须使用拖尾逗号
        "no-multi-spaces": ["error", { "ignoreEOLComments": false }], // 除了注释,否则不允许使用连续多个空格
        "object-property-newline": ["error", { "allowAllPropertiesOnSameLine": true }], // 对象属性必须放在不同行上,除非所有属性都在同一行上
        "space-before-function-paren": ["error", { // 定义声明函数参数()左边必须有空格
          "anonymous": "always",
          "named": "always",
          "asyncArrow": "always"
        }],
        "space-infix-ops": ["error", {"int32Hint": false}], // 中缀运算符周围必须有空格
        "space-unary-ops": [ // 一元操作符周围是否要空格,一元操作符不同与运算符
          "error", {
            "words": true, // new、delete、typeof、void、yield 周围必须要空格
            "nonwords": false, // -、+、--、++、!、!! 周围必须不要空格
        }],
        "arrow-spacing": ["error", { "before": true, "after": true }], // 箭头函数箭头前后必须有空格
        "no-var": ["error"], // 禁止使用 var
        "block-spacing": ["error", "always"], // 强制在代码块中开括号前和闭括号后有空格
        "space-before-blocks": ["error", "always"], // 块语句前必须有空格
        "keyword-spacing": ["error", { "before": true, "after": true }], // 强制关键字周围空格的一致性
        "array-callback-return": ["error", { "allowImplicit": true }], // 强制数组方法的回调函数中有 return
        "func-call-spacing": ["error", "never"], // 禁止在函数标识符和其调用之间有空格
        // "off" or 0 - 关闭规则
        // "warn" or 1 - 将规则视为一个警告(不会影响退出码)
        // "error" or 2 - 将规则视为一个错误 (退出码为1)
    }
}

computed watch 实现上的区别?

在说明不同之前首先需要区分一下 computed 的两个 getter 函数。第一个 getter 就是通过 Object.defineProperty 设置的 getter 函数,我们暂且称呼为 自身 getter 函数吧,这是 computed watcher 独有的 getter 函数。第二个是 Watcher 类中保存的 getter 函数我们称只为 watcher getter 函数 ,这个函数就是在 option 中定义 computed 时写的那个函数,或者对于 watch 来说只是一个访问了一下监听属性的函数。

现在我们现在有如下两个概念:

  • 自身 getter 函数:computed 独有
  • watcher getter 函数:computed watch 都有

所以 computed watch 的不同有如下三点:

  1. computedwatch 第一个不同是 computed 本身会经过 Object.defineProperty 设置 get / set 函数(在 defineComputed 函数中),这个自身 getter 函数做两件事
    • watcher.depend() :computed watcher 收集依赖
    • watcher.evaluate():返回 computed 的值
  2. 第二个不同就是 computed watcher 有自身的 dep,而 user watcher 没有。这个很好理解,computed 经过了 Object.defineProperty 设置 get ,当然应该有 dep 收集依赖了。
  3. 第三个不同是 computed watch 默认是 lazy 模式,new Watcher 时不会立即取值,而是在自身 getter 函数触发是才取,后续只有在 computed watcher 的依赖变化时,才会重新取值。而 user watcher 在初始化时就会执行一次 watcher getter 函数 ,访问监听的属性,触发监听属性的收集依赖过程。
  • 简化的 computed 主要代码实现

    const options = {
      data() {
        return {
          a: 1,
          b: 2,
        };
      },
      computed: {
        foo() {
          return this.a + this.b;
        }
      }
    }
    
    function initComputed(vm) {
      const computed = vm.computed;
      const keys = Object.keys(computed);
      const computedWatcherOpts = { computed: true };
      options._computedWatchers = [];
    
      for(let key in keys) {
        vm._computedWatchers[key] = new Watcher(vm, computed[key], computedWatcherOpts)
        defineComputed(vm, key);
      }
    }
    
    function defineComputed(vm, key) {
      Object.defineProperty(vm, key, {
        get: () => {
          const watcher = vm._computedWatchers[key];
          if (watcher) {
            watcher.dep.depend();
            return watcher.evaluate();
          }
        },
        set(){},
      })
    }
    
    class Watcher{
      constructor(vm, get, options) {
        this.getter = get;
        this.computed = this.dirty = !!options.computed;
        if (this.computed) {
          this.value = undefined;
          this.dep = new Deps();
        }
      }
    
      get() {
        pushTarget(this);
        try{
          this.value = this.getter();
        } catch (e) {
          console.log(e);
        } finally {
          popTarget();
        }
        return this.value;
      }
    
      update() {
        const value = this.get();
        const oldValue = this.value;
        if (value !== oldValue) {
          this.value = value;
          this.dep.notify();
        }
      }
    
      evaluate() {
         if (this.dirty) {
           this.value = this.get();
           this.dirty = false;
         }
         return this.value;
      }
    }
    
    initComputed(options);
  • 简化的 watch 主要代码实现

    const options = {
      data() {
        return {
          a: 1,
          b: 2,
        };
      },
      watch: {
        a(val, oldVal) {
          console.log('a has change', val, oldVal);
        }
      }
    }
    
    function initWatch(vm) {
      const watch = vm.watch;
      const keys = Object.keys(watch);
    
      for(let key in keys){
        createdWatch(vm, watch, key);
      }
    }
    
    function createWatcher(vm, watch, key) {
      const userWatcherOpts = { user: true, cb: watch[key] };
      const watcher = new Watcher(vm, key, userWatcherOpts);
    }
    
    function parsePath(path, vm) {
      const segments = path.split('.');
      const result = segments.reduce((acc, val) => {
        try {
          acc[val] && (acc = acc[val]);
        } finally {
          return acc;
        }
      }, vm);
      return result;
    }
    
    class Watcher{
      constructor(vm, get, options) {
        if (typeof get === 'Function') {
          this.getter = get;
        } else {
          this.getter = parsePath(get, vm);
        }
        this.user = !!options.user;
        this.cb = options.cb;
        this.computed = this.dirty = !!options.computed;
        if (this.computed) {
          this.value = undefined;
          this.dep = new Deps();
        } else {
          this.value = this.get();
        }
      }
    
      get() {
        pushTarget(this);
        try{
          this.value = this.getter();
        } catch (e) {
          console.log(e);
        } finally {
          popTarget();
        }
        return this.value;
      }
    
      update() {
        const value = this.get();
        const oldValue = this.value;
        if (value !== oldValue) {
          this.value = value;
          if (this.user) {
            this.cb(value, oldValue);
          } else if (this.compued) {
            this.dep.notify();
          }
    
        }
      }
    
      evaluate() {
         if (this.dirty) {
           this.value = this.get();
           this.dirty = false;
         }
         return this.value;
      }
    }
    
    initWatch(options)

props 原理

例子如下:

var A = {
  template: `
  <div>
    <p>{{ name }}</p>
    <p>info:</p>
    <ul>
      <li>{{ info.aa }}</li>
      <li>{{ info.bb }}</li>
    </ul>
  </div>`,
  props: {
    name: String,
    info: Object,
  },
}
var vm = new Vue({
  el: '#app',
  components: {
    A
  },
  template: `<div id="app"><A :name="name" :info="info" /></div>`,
  data () {
    return {
      name: 'test',
      info: {
        aa: 11,
        bb: 22,
      }
    }
  },
})

Props 的规范化过程

这里的规范化指的是对于子组件,规范 props 的定义。

mergeOptions → normalizeProps

  function normalizeProps (options, vm) {
    debugger
    var props = options.props;
    if (!props) { return }
    var res = {};
    var 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 {
          warn('props must be strings when using array syntax.');
        }
      }
    } else if (isPlainObject(props)) {
      for (var key in props) {
        val = props[key];
        name = camelize(key);
        res[name] = isPlainObject(val)
          ? val
          : { type: val };
      }
    } else {
      warn(
        "Invalid value for option \"props\": expected an Array or an Object, " +
        "but got " + (toRawType(props)) + ".",
        vm
      );
    }
    options.props = res;
  }

规范化后的格式,props 属性驼峰化,并且至少会有一个 type。

options.props = {
  xxYyy: {
    type,
  }
}

注意一点,因为这是对于子组件 props 定义的规范化,所以 type 才是必须的,而不一定有值,如果 props 属性写法如下:

{
  props: {
    test: {
      type: Number,
      default: 5,
    }
  }
}

对于这种写法 res[name] = isPlainObject(val) ? val : { type: val }; 最后会直接赋给 res['test']

Props 的初始化过程

这里要分两步,一步是父组件如何把绑定的值做为 props 传给子组件;一步是子组件如何获取父组件传过来的值做初始化。

父组件传值

首先,vm 的 render 函数如下,attrs 上保存着传下来的 props 值。

var render = function() {
  var _vm = this
  var _h = _vm.$createElement
  var _c = _vm._self._c || _h
  return _c(
    "div",
    { attrs: { id: "app" } },
    [
      _c("A", { attrs: { name: _vm.name, info: _vm.info } }),
    ],
    1
  )
}

接着执行 A 组件 vnode 的生成。

createElement → createComponent ,和 props 相关的关键逻辑大致如下:

  function createComponent (
    Ctor,
    data,
    context,
    children,
    tag
  ) {
    // ...
    var baseCtor = context.$options._base;
    // ...
    // 生成子组件构造函数,会走 props 的规范化、props 代理。
    Ctor = baseCtor.extend(Ctor);
    // 这里拿到父组件传下来的 props 值,{ attr: { name, info } }
    data = data || {};

    // ...
    // 最终保存在 vnode 上的 props 信息,{ name, info }
    var propsData = extractPropsFromVNodeData(data, Ctor, tag);

    // ...
    // return a placeholder vnode
    var name = Ctor.options.name || tag;
    var vnode = new VNode(
      ("vue-component-" + (Ctor.cid) + (name ? ("-" + name) : '')),
      data, undefined, undefined, undefined, context,
      { Ctor: Ctor, propsData: propsData, listeners: listeners, tag: tag, children: children },
      asyncFactory
    );
    // vnode.componentOptions.propsData 保存着父组件传下来的 props 值
    return vnode
  }

createComponent 中先生成 A 组件的构造函数,此时会执行 props 的规范化过程;然后执行 extractPropsFromVNodeData 得到 propsData,这是最后保存在 vnode 上关于 props 的信息;最后返回 A 组件的 vnode,vnode.componentOptions.propsData 保存着父组件传下来的 props 值。

extractPropsFromVNodeData 函数如下,大致流程:

  • 先拿到 A 组件 props 的定义:var propOptions = Ctor.options.props;
  • 拿到父组件传下来的 attrsprops
  • 接着遍历 A 组件中的 props,往 res 里塞对应的 key-value ,最后返回。这里的 hyphenatecheckProp 是为了兼容驼峰和全小写横线连接两种写法,在父组件中往子组件传值可以用 :userName="userName" 也可以用 :user-name="userName" 最后经过处理都会转成 { userName } 保存在 res 中返回。
  function extractPropsFromVNodeData (
    data,
    Ctor,
    tag
  ) {
    
    var propOptions = Ctor.options.props;
    if (isUndef(propOptions)) {
      return
    }
    var res = {};
    var attrs = data.attrs;
    var props = data.props;
    if (isDef(attrs) || isDef(props)) {
      for (var key in propOptions) {
        var altKey = hyphenate(key);
        {
          var keyInLowerCase = key.toLowerCase();
          // ...
        }
        checkProp(res, props, key, altKey, true) ||
        checkProp(res, attrs, key, altKey, false);
      }
    }
    return res
  }

子组件获取值初始化

现在我们知道子组件 vnode 上 componentOptions.propsData 是可以拿到父组件传下来的值的,而子组件 props 的初始化主要在 initProps 函数中。()

initState → initProps

主要流程如下:

  • 获取 propsData
  • 遍历 propsOptions 执行 validateProp 函数获得 props 属性最后的值,通过 defineReactive 转为响应式对象,将 props 属性都保存在 vm._props 上,最后将 vm._props 代理到实例 vm 上。
  function initProps (vm, propsOptions) {
    var propsData = vm.$options.propsData || {};
    var props = vm._props = {};
    // ...
    var isRoot = !vm.$parent;
    // root instance props should be converted
    if (!isRoot) {
      toggleObserving(false);
    }
    var loop = function ( key ) {
      keys.push(key);
      var value = validateProp(key, propsOptions, propsData, vm);

      {
        // ...
        defineReactive$$1(props, key, value, function () {
          // ...
        });
      }
      // ...
      if (!(key in vm)) {
        proxy(vm, "_props", key);
      }
    };

    for (var key in propsOptions) loop( key );
    toggleObserving(true);
  }

这一步最重要的就是 validatePropdefineReactive 这两步,响应式暂时不看,只看 validateProp

  • 获取 prop 的 type 和父组件传下来的 value
  • 然后是一段 boolean 类型兼容判断,这里跳过
  • 如果父组件没有传值,则取 default 值,这里需要额外走一次 observe 将 default 值转为响应式对象。
  • 如果父组件传值,assertProp 主要做的和看传的类型是否合法,这里有个需要注意的地方,之所以不需要对父组件传的值做响应式,是因为父子组件中对于引用类型的 prop,最后指向的是相同的引用对象。如果是普通类型,在 initPropsdefineReactive 会执行响应式转换。
  • 返回最后的 value。
  function validateProp (
    key,
    propOptions,
    propsData,
    vm
  ) {
    var prop = propOptions[key];
    var absent = !hasOwn(propsData, key);
    var value = propsData[key];
    // boolean ...
    // check default value
    if (value === undefined) {
      value = getPropDefaultValue(vm, prop, key);
      // since the default value is a fresh copy,
      // make sure to observe it.
      var prevShouldObserve = shouldObserve;
      toggleObserving(true);
      observe(value);
      toggleObserving(prevShouldObserve);
    }
    {
      assertProp(prop, key, value, vm, absent);
    }
    return value
  }

props 的初始化结束。

Props 的更新过程

子组件 props 更新。props 数据的值在父组件中发生变化,触发父组件的 render

patch 过程中执行 patchVnode 函数。

vnode hook prepatch 函数 → updateChildComponent 函数。

  function updateChildComponent (
    vm,
    propsData, // 父组件更新后的 props 数据
    listeners,
    parentVnode,
    renderChildren
  ) {
    // ...
    // update props
    if (propsData && vm.$options.props) {
      toggleObserving(false);
      var props = vm._props;
      var propKeys = vm.$options._propKeys || [];
      for (var i = 0; i < propKeys.length; i++) {
        var key = propKeys[i];
        var propOptions = vm.$options.props; // wtf flow?
        props[key] = validateProp(key, propOptions, propsData, vm);
      }
      toggleObserving(true);
      // keep a copy of raw propsData
      vm.$options.propsData = propsData;
    }
    // ...
  }

可以看到只是重新执行 validateProp 赋值即可,这里又有两种情况,一种是父组件传的值是普通类型,更新后 validateProp 返回赋值,会触发 prop 的 setter 触发子组件渲染 watcher 更新;

一种是父组件传的值是引用类型,此时可能只是对象中的某一项被修改,validateProp 返回的还是同一个引用,不会触发 setter,但是子组件中访问过这个 prop,子组件渲染 watcher 会被收集到这个 prop 的依赖中,父组件中修改 props 时触发 setter,也就会通知子组件渲染 watcher 更新。

实现 mergePromise

// 实现 mergePromise
// 最后输出: 1, 2, 3 'done' [1, 2, 3]
const timeout = (ms) =>
  new Promise((resolve, reject) => {
    setTimeout(() => resolve(), ms);
  });

const ajax1 = () =>
  timeout(2000).then(() => {
    console.log('1');
    return 1;
  });

const ajax2 = () =>
  timeout(1000).then(() => {
    console.log('2');
    return 2;
  });

const ajax3 = () =>
  timeout(2000).then(() => {
    console.log('3');
    return 3;
  });

const mergePromise = (ajaxArray) => {
  const result = [];
  const list = ajaxArray.slice();
  return new Promise(resolve => {
      const helper = () => {
          if (!list.length) {
            return resolve(result)
          }
          const ajax = list.shift();
          ajax().then(res => {
            result.push(res);
            helper();
          })
      }
      
      helper();
  })
};

mergePromise([ajax1, ajax2, ajax3]).then((data) => {
  console.log('done');
  console.log(data);
});

JavaScript 面向对象

JavaScript 面向对象

JavaScript 中类的实现

JS 中类的实现有两种方式。

  1. 构造函数模拟类
function A () {
  this.name = 'name';
}
  1. ES6 Class 关键字
class A () {
  constructor () {
    this.name = 'name';
  }
}

创建对象的3种方法

  1. 字面量创建
var o1 = {name: '01'};
  1. new 调用构造函数创建
var o2 = new A();
  1. Object.create 方法创建
var o3 = Object.create(A);

实现new 运算符

var thisNew = function (func) {
  var o = Object.create(func.prototype);
  // 创建一个继承至func.prototype 的新对象 o
  var k = func.call(o);
  // 执行构造函数,把执行时的this 指向新对象 o。
  if (typeof k === 'object') {
    o = k;
  }
  // 如果构造函数返回的是一个对象,这个对象会取代新对象 o。
  return o;
}

实现继承的 5 种方式

  • 构造函数实现继承
  • 原型链实现继承
  • 组合继承
  • 寄生组合继承
  • ES6 class extends 关键字继承
构造函数实现继承
function A () {}

function B () {
  A.call(this);
}

优点:可以实现多继承

缺点:只继承了父类在构造函数中的属性,无法继承父类原型上的方法

原型链实现继承
function A () {}

function B () {}

B.prototype = new A();
B.prototype.constructor = B;

优点:可以继承父类的属性和方法

缺点:无法实现多继承;如果父类的属性是引用类型的,一个子类改变这个引用类型,所有的子类都会改变

组合继承
function A () {}

function B () {
  A.call(this);
}

B.prototype = new A();
B.prototype.constructor = B;

组合继承由构造函数继承和原型链继承组合实现

  • 优点:可以继承父类的属性和方法,并且子类不会共用引用类型的属性

  • 缺点:执行了两次父类的构造函数,子类原型对象上会有多余的父类属性

寄生组合继承
function A () {}

function B () {
  A.call(this)
}

B.prototype = Object.create(A.prototype)
B.prototype.constructor = B

寄生组合继承其实就是组合继承的优化

ES6 class extends 关键字继承
class A {
  constructor () {}
}

class B extends A {
  constructor () {
    super(this)
  }
}

ES5/ES6 的继承除了写法以外还有什么区别

  1. class 声明内部会启用严格模式
  2. class 的所有方法都不可枚举
  3. class 的所有方法都没有原型对象 prototype
  4. class 必须使用 new 调用

前端性能优化总结

加快下载速度

  1. 静态文件使用 CDN 加快资源下载
  2. 减小资源文件大小,比如 Gzip 压缩

HTTP 相关优化

  1. 可以的话使用 HTTP2,所有的请求使用同一个 TCP 连接
  2. 资源尽量分成多个不同的域名,因为 Chrome 对同一域名下的 TCP 连接数量有限制,最多 6 个。
  3. 另一方面,资源和主站使用不同的域名,还可以避免下载资源时携带了多余的 Cookies (2, 3 两点主要使用 CDN 来完成)
  4. 合理使用缓存策略(强缓存,协商缓存)
  5. 减少不必要的 HTTP 请求数量

HTML 相关优化

  1. 避免多余的嵌套,无意义的标签
  2. 加少文件大小

CSS 相关优化

  1. 避免使用通配符,只对需要用到的元素进行格式化
  2. 少用标签选择器,用类选择器代替
  3. 不加多余的选择器,比如类选择器和标签选择器一起用
  4. 减少嵌套,层级尽量扁平
  5. 写在开头,浏览器需要 CSS 规则树才能生成渲染树开始渲染界面,所以要尽早下载资源

JS 相关优化

  1. 写在 body 标签末尾
  2. 使用 async 模式加载,异步加载,加载完毕后立即执行
  3. 使用 defer 模式加载,异步加载,等 DOMContentLoaded 事件完成后按顺序执行
  4. 注意节流防抖
  5. 避免回流重绘

图片优化

  1. 合理的使用图片格式
  2. 小图片可以使用雪碧图或 base64,减少 HTTP 请求
  3. 图片懒加载

WebPack 相关优化

  1. 使用最新版本的 node, npm, webpack ,因为新版本肯定对于打包有优化
  2. 优化 loader 使用范围
  3. loader 开启缓存
  4. DLLPlugin 缓存第三方库的打包文件,防止重复打包未做改动的第三方库
  5. Happypack,将 loader 由单进程转为多进程处理
  6. 开启代码压缩
  7. 按需加载
  8. 代码分割
  9. Tree shaking

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.