theydy / notebook Goto Github PK
View Code? Open in Web Editor NEW记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
记录读书笔记 + 知识整理,vuepress 迁移中 https://theydy.github.io/notebook/
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);
})
停止等待协议的原理是每发完一组数据就停止发送,等待对方确认,在收到确认后再发下一组数据。如果对方接收到重复的数据,会丢弃该分组,但还要发送确认。
在停止等待协议中,如果出现数据错误而没有收到确认,会执行超时重传。超时重传有两种,自动超时重传 ARQ 协议
连续 ARQ 协议
自动超时重传 ARQ 协议:每组数据都等待确认,如果没有收到确认就重传当前分组。
连续 ARQ 协议:发送方维持一个发送窗口,位于窗口内的分组可以连续发送,不需等待确认,接收方采用累计确认,对按需到达的最后一个分组发送确认。
滑动窗口是一种流量控制技术,TCP 利用滑动窗口实现流量控制的机制,滑动窗口的大小意味着接收方还有多大的缓冲区可以用于接收数据。接收方会把当前接收窗口的大小写入应答报文,发送方可以通过滑动窗口的大小来确定应该发送多少字节的数据,发送方让自己的发送窗口取为拥塞窗口和接收方的接收窗口中较小的一个
当出现 接收窗口剩余空间 0 的情况,发送端会停止发送数据,但会定时发送 1 字节的请求给接收方,让接收方告知窗口大小。
拥塞处理和流量控制不同,后者是作用于接收方,保证接收方来得及接受数据。而前者是作用于网络,防止过多的数据拥塞网络,避免出现网络负载过大的情况。
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
js 文件经过 babel 转换,无论是 ESModule 还是 CommonJS 最终都会被编译成 CommonJS 规范。
// 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 模块也没办法。
// 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_require,无论 CommonJS 或者 ESModule 最后都会被编译 webpack 自己的模块实现,所以在 webpack 中可以两者混合使用。但 webpack 不会做 es6 语法的转换。
经过 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 是 webpack 的一项功能,tree-shaking 只能在 ESModule 中使用,如果先通过 babel 转为 CommonJS 就用不了。
通过静态分析 es6 的语法,可以删除没有被使用的模块。要使用这项技术,只能使用 webpack 的模块处理,加上 babel 的 es6 转换能力(需要关闭模块转换)。
HTTP 是一种超文本传输协议,构建于 TCP/IP 协议之上,是一个应用层协议
<method> <request-url> <version>
<headers>
<entity-body>
<version> <state-code> <state-desc>
<headers>
<entity-body>
在初始化 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
,此时会触发 props
的 getter
,造成 props
收集依赖。由于数据初始化的时机是 beforeCreated -> created 之间,此时还没有进入子组件的渲染阶段(生成渲染 Watcher 是在 mountComponent
中),也就没有子组件的渲染 Watcher。所以这时候 Dep.target
指向的依然是父组件的渲染 Watcher。
最终表现就是父组件的字段更新时,正确触发了一次父组件的渲染 Watcher 的 update,更新子组件的 props 时,又触发了一次父组件的渲染 Watcher 的 update。
而第一次更新后,后续收集依赖时子组件的渲染 Watcher 已经存在,所以不会收集到父组件的渲染 Watcher。
其实不只是这里,子组件的 beforeCreate
、created
、beforeMount
这三个生命周期钩子函数如果用了 props 的话,也会出现同样的问题,所以在 callHook 函数中也做了同样 Dep.target
置空的操作。
其实不只是这里,子组件的 beforeCreate
、created
、beforeMount
这三个生命周期钩子函数如果用了 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 使非浮动元素与浮动元素不重叠。
<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的宽度,自动变窄。
清除元素内部浮动 (计算浮动子元素高度)
<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 重叠
<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 重叠。
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;
}
};
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
主要逻辑如下,遍历组件的 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
主要逻辑如下,先取到 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 */);
由上可知,initProps
和 initData
中响应式的关键方法分别是 defineReactive
和 observe
defineReactive(props, key, value, function () {});
observe(data, true /* asRootData */);
其实在 defineReactive
会引出 Dep
类,Dep
类又会引出 Watcher
类;observe
会引出了 Observer
类。
实现数据响应式的关键:
observe
、defineReactive
两个方法。Observer
、Dep
、Watcher
三个类。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
函数的主要作用是对于一个引用类型对象新建一个 Observer
类 ob = 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);
则是对于数组的特殊操作,数组的索引是非响应式的,数组中的每一项引用类型的属性都应该收集到当前的依赖,确保在使用 $set
或 Vue.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
转为响应式属性。protoAugment
或 copyAugment
劫持数组的变异方法,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
指向当前的 watcher
,Dep.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
了。
响应式数据更新时,实际上会遍历 Dep
的 subs
执行 subs[i].update();
也就是 watcher
的 update
方法,为了防止同一个 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();
// ...
}
};
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 (flexible box) 弹性布局,分为两部分,采用flex 布局的容器(flex container) 和容器下的子元素项目(flex item),采用flex 布局的容器拥有两条轴线,分别是主轴和交叉轴,flex item 沿着主轴顺序排列。
主轴默认是水平线,交叉轴默认为垂直线,但主轴与交叉轴并没有固定的方向,可通过在flex container 上设置flex-direction
属性决定主轴的方向是水平线或垂直线。
flex-direction
设置主轴方向的属性,一共有4
种值。
row
(默认值):主轴方向水平从左到右。row-reverse
:主轴方向水平从右到左。column
:主轴方向垂直从上到下。column-reverse
:主轴方向垂直从下到上。flex-warp
flex item 的总宽度超过flex container 的宽度是否换行。在默认值nowarp
,不换行的情况下,即使设置了flex item 的宽度,如果总宽度超过容器的宽度,那么子元素的宽度不会是真实设置的宽度,而是平分容器的宽度。
flex-flow
flex-direction
和 flex-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-grow
定义项目占据容器多余空间的放大比列,默认是0
,这时候只占据这个项目的真实宽度,即使容器有多余宽度,也不放大占据。
flex-shrink
定义项目在容器宽度不够时的缩小比列,默认是1
,此时如果总宽度超过容器的宽度,那么项目元素的宽度不会是真实设置的宽度,而是平分容器的宽度。flex-shrink: 0;
则不缩放。
flex-basis
定义了在分配多余空间之前,项目占据的主轴空间。浏览器根据这个属性,计算主轴是否有多余空间,默认为auto
即项目本来的大小。
flex
flex-grow
,flex-shrink
和 flex-basis
的缩写。
grid 网格布局,分为两部分,采用grid 布局的容器(grid container) 和容器下的子元素项目(grid item),可通过
grid-template-columns
与grid-template-rows
设置行与列。
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-gap
和 grid-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-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 watcher
的 update → 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。
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
主要的流程如下。
isUndef(oldVnode)
:oldVnode 是 undefined,即初始化时 options 没有传 $el 的情况。这种情况下只要按照新 vnode,也就是 patch 的 vnode 参数创建 DOM 即可,这里不赘述。!isRealElement && sameVnode(oldVnode, vnode)
:是 VNode 并且是相同的 VNode,需要进一步比较(关于 sameVnode 的判断稍后叙述)。这是最麻烦的一种情况,需要对新旧 vnode 做进一步的 diff
对比不同。patchVnode(oldVnode, vnode, insertedVnodeQueue, removeOnly)
。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
的判断:
tag
、isComment
、data
、input
这些类型是否相同。asyncFactory
是否相同。关于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)
}
}
核心逻辑有一下四步:
执行 prepatch 钩子函数,具体流程看 to be continue
let i
const data = vnode.data
if (isDef(data) && isDef(i = data.hook) && isDef(i = i.prepatch)) {
i(oldVnode, vnode)
}
执行 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)
}
完成 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)
}
执行 postpatch 钩子函数,具体流程看 to be continue
if (isDef(data)) {
if (isDef(i = data.hook) && isDef(i = i.postpatch)) i(oldVnode, vnode)
}
diff
算法主要指的是第三步,可以分为如下情况:
updateChildren
函数来更新子节点,最重要,写在后面addVnodes
将 ch 批量插入到新节点 elm 下removeVnodes
全部清除// 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)
}
}
这块是真的多,流程如下:
过滤 oldCh 两侧的空 VNode
然后进行这四部分的比较
如果比较成功,递归 patchVnode
进一步比较,如果有 children 元素,又会进入 updateChildren
直到一方没有 children 元素,更新新节点。
上面都没命中,将剩余 oldStart - oldEnd 中间的元素生成一个 oldKeyToIdx
表 map[vnode.key] = vnode
,然后判断此时的 newCh[newStart]
是否有传 key,如果有则在 oldKeyToIdx
表中查找,这里注意一点,想要查找成功必须新旧 children 都设置唯一 key,否则就查找不到,如果查找不到就将这个 newCh[newStart]
视为一个新元素即可。如果通过 key 查找到也只能说明 key 相同,通过 sameVnode
进一步比较,比较成功类似 (2.) 进一步递归patchVnode
,否则视为一个新元素。这一步最后都会 ++newStart
到这一步说明 oldCh 或 newCh 一方的 start > end ,跳出 while 循环。此时剩下的如果是 newCh 部分,将 newStart - newEnd 的节点全部添加,如果剩下的是 oldCh 部分,将 oldStart - oldEnd 的节点全部删除。
to be continue
to be continue
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.vue
的 render
函数,进入 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
流程大致如下:
对于 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 展示一个文件夹树,右侧显示当前文件夹下的文件。
文件夹可以相互拖动排序和改变层级,支持把右侧文件拖到左侧文件夹上改变文件所在文件夹位置。
https://codesandbox.io/s/el-tree-drag-onumm?file=/src/App.vue
树外的节点在 dragstart
、dragend
时需要手动触发 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);
},
.wrapper {
position: relative;
}
.content {
position: absolute;
top: 50%;
left: 50%;
width: 100px;
height: 100px;
margin-top: -50px;
margin-left: -50px;
}
.wrapper {
position: relative;
}
.content {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
width: 100px;
height: 100px;
margin: auto;
}
.wrapper {
position: relative;
}
.content {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
.wrapper {
display: flex;
justify-content: center;
align-items: center;
}
.content {
}
.wrapper {
display: table-cell;
text-align: center;
vertical-align: middle;
}
.content {
display: inline-block;
}
.wrapper {
display: grid;
justify-content: center;
align-content: center;
}
.content {
}
判断是否同源(协议,域名,端口三者都相同)
同源限制是浏览器的行为,浏览器为了客户端安全(防止XSS、CSRF等攻击)会拦截服务端返回的response
,所以跨域并不是请求发不出去,只是服务端返回的结果被浏览器拦截了。
不是一个源,将会限制以下操作:
主要由后端配置,后端在返回的响应头添加Access-Control-Allow-Origin
就可以启用CORS。值得注意的是开启了CORS后的请求分为简单请求
和复杂请求
,对于复杂请求客户端会先向服务端发送一条预检请求(OPTIONS)。
满足以下两个条件的请求为简单请求。
HEAD
, GET
, POST
。Accept
Accept-Language
Content-Language
Last-Event-ID
Content-Type
为 application/x-www-form-urlencoded
, multipart/form-data
, text/plain
。利用script
标签的src
属性不受浏览器的跨域限制。所以能够使用get
方法向服务端发送请求,但是也只支持GET 请求而不支持POST 等其它类型的HTTP 请求。
具体实现戳jsonp 的原理和实现。
适用于以下场景:
// 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.conf
server {
...
// 设置代理
location / {
proxy_pass http://127.0.0.1:8080;
}
...
}
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。
第一个 dep 是正常触发 setter 时派发更新的 dep。
第二个 dep 是在调用 $set、Vue.set 这些实际上修改了响应式数据但又无法正常触发 setter 的情况下用的,毕竟拿不到闭包中的 dep,实际上如果用其他方法保存闭包的 dep,让我们在 $set、Vue.set 时能够拿到那就不需要额外的 dep 了。
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);
基础用法
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
的过程,这个过程同样会进行子作用域的模板编译,因此两部分内容是相对独立的。
在给一个 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,
}
}
https://www.typescriptlang.org/play?ts=4.1.0-pr-40336-88#code/Q
type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
type Required<T> = { [P in keyof T]-?: T[P]; }
type Readonly<T> = { readonly [P in keyof T]: T[P]; }
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
type Omit<T, K extends string | number | symbol> = { [P in Exclude<keyof T, K>]: T[P]; }
type Record<K extends string | number | symbol, T> = { [P in K]: T; }
type Exclude<T, U> = T extends U ? never : T
// 'b' | 'c'
type A = Exclude<'a' | 'b' | 'c' | 'd' , 'b' | 'c' | 'e' >
type Extract<T, U> = T extends U ? T : never
type NonNullable<T> = T extends null | undefined ? never : T
type Parameters<T extends (...args: any) => any> = T extends (...args: infer P) => any ? P : never
type ConstructorParameters<T extends new (...args: any) => any> = T extends new (...args: infer P) => any ? P : never
type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any
type InstanceType<T extends new (...args: any) => any> = T extends new (...args: any) => infer R ? R : any
分为标准盒模型和怪异盒模型两种
标准盒模型宽高只指内容宽高,不包括 border, padding
怪异盒模型宽高包括 context + padding + border
rem 根据 html 标签设置的 font-size 字体大小变化
em 指的一个字体大小,如果当前元素的字体是 16px,那么 1em === 16px
flex
看是否设置主轴还是交叉轴为垂直,
主轴居中 justify-content
交叉轴居中 align-items
js 中所有的对象都有一个 __proto__ 属性,这个属性指向它的原型对象。
对于函数来说,如果这个函数能够作为构造函数,则这个函数也有一个 prototype 属性指向原型对象。
因为对于一个构造函数来说,通过它生成的对象实例指向的原型对象都是同一个,所以可以在这个原型对象上定义一些通用的属性和方法。
多个不同类型的对象通过 __proto__ 属性连接起来,就形成了原型链。
1. 构造函数实现继承
在子类的构造函数中通过 call, apply 调用父类的构造函数
2. 原型链实现继承
修改子类的 prototype 指向一个父类的对象实例
3. 组合继承
4. 寄生组合继承
5. class extend 实现继承
call, apply 作用在与改变函数内部的 this 指向
call, apply 在传递参数有所不同,call 只能一个一个传,apply 可以传一个数组对象
Promise 代表一个暂未完成但是在未来某个时刻会完成的异步任务,
Promise 有三个状态,pending, resolved, rejected。一旦状态改变,
就不能再次改变。而且 Promise 的状态不受外部影响。
Promise 主要解决回调地狱的问题,因为 Promise.then 最后会返回一个新 Promise ,
可以链式调用,所以通过 then 方法注册回调函数可以避免多层嵌套的问题。
Promise 的缺陷是,一旦开始无法取消,处于 pending
状态时不知道具体执行到哪里,只能通过回调函数捕获错误。
1. props 传参, $emit 触发事件
2. provide/inject
3. $parent, $children 获得 vm 实例进行通信
4. $refs 进行通信
5. event bus
6. vuex
碰到通用逻辑的时候,会把通用逻辑提取出来写成一个 mixin。
mixin 混入的钩子函数会比构造器写入的钩子函数早执行。
mutation 处理同步操作
action 处理异步操作或者多个 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 }
// 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 里面。
webpack 配置项中有一个 mode 选项,可配置的有 production development 两个选项
然后通过 process.env.NODE_ENV 判断引入哪一块的配置文件
一般会配合 webpack.DefinePlugin 使用
吧啦吧啦,Object.defineProperty 和 重写数组变异方法
发布-订阅模式
常用 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:
: 其他,不修改src
或test
文件。
revert:
: commit 回退。
时至今日,我们已经能够使用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);
目前为止,两者的差别可能并不大——这两个例子非常相似。然而,你已经可以看到两者对于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);
这里都发生了些什么?
我们在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();
请注意这里有一个非常重要的不同,在Fabric 中,我们不再需要在修改绘画内容前先把画布清空了,我们只需要对object 做一些工作,简单的改变他的属性值,然后重新渲染画布获得新的图片。
我们已经知道如何通过实例化fabric.Rect 来操作一个矩形了,理所当然的Fabric 也支持其他的所有基本图形——圆,三角,椭圆等等,所有的这些基本图形都暴露在 fabirc
这个命名空间下,fabric.Circle
,fabric.Triangle
,fabric.Ellipse
等等。
Fabric 提供了7种内置基本图形。
想过画个圆?只要新建一个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);
通过上面的例子,我们就能够得到一个位于100,100 的绿色圆形,和一个位于50,50 的蓝色三角形。
创建一个图形对象——矩形,圆或者是其他——这只是开始的第一步,在某些情况下,我们可能想要对对象做些改动,可能某些改动需要触发状态的变化,或者是播放某种动画,又或者是在某个鼠标交互下改变对象的某个属性(颜色,透明度,大小,位置) 。
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);
首先,我们设置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 });
看到这里,你可能会问——如果我们在创建对象时不传配置项,又会发生什么呢? 最后返回的对象实例有没有这些属性呢?
当然有,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)的矩形。因为没有大小,所以我们不能在画布中看到它,但只要给它一个宽高,就能在左上角看到一个黑色矩形。
Fabric 对象相互之间并不是完全独立的,而是有着非常精准的层次结构的。
大部分的对象都继承至 fabric.Object
。fabric.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> 元素时会被使用到。
对于高级的项目来说,使用原型来获得自定义的渲染或行为是非常普遍的。
我们已经详细的说完了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(){ /* ... */ };
// ...
to be continue...
首先基础类型存储的都是值,然后需要注意的是 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。引用类型和基础类型的区别是,引用类型存储的是一个地址而不是值,这个地址指向内存中的某一块。
JS 中类型转换只有三种情况:
针对这三种情况,JS 中分别有 Boolean()
,Number()
,String()
三个方法,三个方法的转化规则如下:
除了 undefined
, null
, false
, NaN
,
, 0
, -0
会转为 false
,其他都转为 true
。
boolean
-> true
为 1
,false
为 0
undefined
-> NaN
null
-> 0
string
-> "123"
这类合法的字符串直接转为对应的数字,非法的字符串则转为 NaN
,比较特殊的是 ""
这种空字符串可以转为 0
,还有一点字符串两边添加任意空格不影响最后的转换结果symobl
-> 抛错object
-> 执行 valueOf()
方法,如果是基础类型再执行类型转换,否则执行 toString()
方法,如果是基础类型再执行类型转换。对于 object 转为 number 再做个说明,如果没有手动重写 valueOf()
方法的话,那么都是返回 object 本身的,所以大部分情况下都是走的 toString()
再转为 number,object 转为 string,这种情况下只有空数组( []
-> ""
) 和只有一个可以合法转为数字元素的数组(['1']
-> "1"
) 可以正常转为数字类型,其他的都是 NaN
boolean
-> true
为 "true"
,false
为 "false"
undefined
-> "undefined"
null
-> "null"
number
-> 合法的数字字符串symobl
-> 抛错object
-> 执行 toString()
方法,如果是基础类型再执行类型转换,否则执行 valueOf()
方法,如果是基础类型再执行类型转换。Boolean()
,Number()
,String()
如果直接写出来或者单独存在时是不难判断转换后的结果的,但是当这些类型转换处于一个复杂的表达式中时,就必须按照正确的先后顺序执行类型转化,否则就可能得到完全相反的结果,JS 在以下三种不同的表达式中有着不同的优先级。
+ - * /
四则运算> < >= <=
比较运算==
双等号运算+ - * /
四则运算+
加法运算比较特殊单独说:
valueOf()
, toString()
),再进行运算,除非重写 valueOf()
方法,不然都是返回字符串。 + '1'
会被转为 NaN
,eg: 'a' + + 'b' === 'aNaN'
- * /
运算:
都转为数字类型再进行运算,非法就是 NaN
的结果。
> < >= <=
比较运算NaN
和任何数字比较都返回 false
==
双等号运算NaN
,一律返回 false
null
和 undefined
不会进行转换,但它们两相等false
[] > [] // false
[] < [] // false
[] >= [] // true
[] <= [] // true
[] == [] // false
[] == ![] // true
Watcher 更新队列的实现主要是两个函数,queueWatcher
和 flushSchedulerQueue
。
// 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
,这是主要暴露出来的调度函数。
首先会对传进来的 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 函数主要做了这五件事
第一点 flushing = true
很好理解,整个遍历 queue 期间都是 flushing
状态。
第二点 queue 根据 watcher 的 id 从小到大排列,之所以从小到大有以下三点:
第三点遍历 queue ,如果是组件并且 _isMounted
,则调用 watcher.before()
的结果是执行 beforeUpdate
钩子函数,接下来的步骤在 queueWatcher 中其实说过了has[id] = null
、watcher.run()
,加上死循环的判断,死循环的判断就是在一个 flushing
期间同一个 watcher 执行的次数超过 MAX_UPDATE_COUNT
就断定为死循环。
第四点重置 index
、has
、circular
、waiting
、flushing
这些状态变量。
第五点调用 callActivatedHooks
、callUpdatedHooks
执行钩子函数
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);
}
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 里嵌套写 el-dropdown 的情况,表现形式类似 el-submenu 嵌套 el-submenu
在下拉选项里有一项 hover 会在旁边展开二级下拉选项。
然后写了个例子发现,现成的 el-dropdown 虽然可以嵌套,但是鼠标 hover 到二级下拉选项时,一级下拉选项就收起来了,
于是有了下面的修改尝试。
主要原理是让嵌套的 drowdown 元素在触发 mouseenter
、mouseleave
事件时也触发父 drowdown 元素的 show
、hide
方法。
https://codesandbox.io/s/el-dropdown-nested-1hs28?file=/src/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);
}
}
})();
<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
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 的配置文件可以有多种,比如初始化过程中提供的三个选项:
又或者可以直接在 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
的两个 getter 函数。第一个 getter 就是通过 Object.defineProperty
设置的 getter 函数,我们暂且称呼为 自身 getter 函数吧,这是 computed watcher
独有的 getter 函数。第二个是 Watcher 类中保存的 getter 函数我们称只为 watcher getter 函数 ,这个函数就是在 option 中定义 computed 时写的那个函数,或者对于 watch 来说只是一个访问了一下监听属性的函数。
现在我们现在有如下两个概念:
所以 computed watch 的不同有如下三点:
computed
与 watch
第一个不同是 computed
本身会经过 Object.defineProperty
设置 get / set
函数(在 defineComputed
函数中),这个自身 getter 函数做两件事
computed watcher
有自身的 dep
,而 user watcher
没有。这个很好理解,computed
经过了 Object.defineProperty
设置 get
,当然应该有 dep
收集依赖了。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)
例子如下:
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 的定义。
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 传给子组件;一步是子组件如何获取父组件传过来的值做初始化。
父组件传值
首先,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
函数如下,大致流程:
var propOptions = Ctor.options.props;
attrs
和 props
。key-value
,最后返回。这里的 hyphenate
和 checkProp
是为了兼容驼峰和全小写横线连接两种写法,在父组件中往子组件传值可以用 :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
主要流程如下:
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);
}
这一步最重要的就是 validateProp
和 defineReactive
这两步,响应式暂时不看,只看 validateProp
observe
将 default 值转为响应式对象。assertProp
主要做的和看传的类型是否合法,这里有个需要注意的地方,之所以不需要对父组件传的值做响应式,是因为父子组件中对于引用类型的 prop,最后指向的是相同的引用对象。如果是普通类型,在 initProps
中 defineReactive
会执行响应式转换。 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
数据的值在父组件中发生变化,触发父组件的 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
// 最后输出: 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);
});
JS 中类的实现有两种方式。
function A () {
this.name = 'name';
}
class A () {
constructor () {
this.name = 'name';
}
}
var o1 = {name: '01'};
var o2 = new A();
var o3 = Object.create(A);
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;
}
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
寄生组合继承其实就是组合继承的优化
class A {
constructor () {}
}
class B extends A {
constructor () {
super(this)
}
}
class
声明内部会启用严格模式class
的所有方法都不可枚举class
的所有方法都没有原型对象 prototype
class
必须使用 new
调用A declarative, efficient, and flexible JavaScript library for building user interfaces.
🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.
TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
An Open Source Machine Learning Framework for Everyone
The Web framework for perfectionists with deadlines.
A PHP framework for web artisans
Bring data to life with SVG, Canvas and HTML. 📊📈🎉
JavaScript (JS) is a lightweight interpreted programming language with first-class functions.
Some thing interesting about web. New door for the world.
A server is a program made to process requests and deliver data to clients.
Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.
Some thing interesting about visualization, use data art
Some thing interesting about game, make everyone happy.
We are working to build community through open source technology. NB: members must have two-factor auth.
Open source projects and samples from Microsoft.
Google ❤️ Open Source for everyone.
Alibaba Open Source for everyone
Data-Driven Documents codes.
China tencent open source team.