ππ Blog


  • 首页

  • 归档

  • 标签

'Javascript知识点整理'

发表于 2019-01-15

常用算法

二分查找

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function binarySearch(target, arr) {
var start = 0;
var end = arr.length - 1;
while(end >= start) {
var mid = parseInt(start + (end - start) / 2);
if (target === arr[mid]) {
return mid;
} else if (target > arr[mid]) {
start = mid + 1;
} else {
end = mid -1;
}
}
return -1;
}

快速排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//快速排序
function quickSort(arr) {
if (arr.length <=1) {
return arr;
}
var pivotIndex = Math.floor(arr.length / 2);
var pivot = arr.splice(pivotIndex, 1)[0];
var left = [], right = [];
for(var i = 0, l = arr.length; i < l; i++) {
if (arr[i] <= pivot) {
left.push(arr[i]);
} else {
right.push(arr[i]);
}
}
return quickSort(left).concat(pivot, quickSort(right));
}

归并排序

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
//归并排序
function mergeSort(arr) {
if (arr.length <= 1) {
return arr;
}
var mid = Math.floor(arr.length / 2);
var left = arr.slice(0, mid);
var right = arr.slice(mid);
var merge = function(left, right){
var result = [];
while(left.length > 0 && right.length > 0) {
if(left[0] < right[0]) {
result.push(left.shift());
} else {
result.push(right.shift());
}
}
while(left.length > 0) {
result.push(left.shift());
}
while(right.length > 0) {
result.push(right.shift());
}
return result;
}
return merge(mergeSort(left), mergeSort(right));
}

数组扁平化

1
2
3
4
5
6
7
8
function arrFlatten(arr) {
return arr.reduce(function(accumulator, currenValue, currentIndex, arr){
if(Array.isArray(currenValue)) {
return accumulator.concat(arrFlatten(currenValue))
}
return accumulator.concat(currenValue);
}, [])
}

函数节流和防抖

防抖的原理就是:你尽管触发事件,但是我一定在事件触发 n 秒后才执行,如果你在一个事件触发的 n 秒内又触发了这个事件,那我就以新的事件的时间为准,n 秒后才执行,总之,就是要等你触发完事件 n 秒内不再触发事件,我才执行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
//函数防抖
function debounce(fn, delay, immediate) {
// immediate 参数判断是否是立刻执行
var timer = null, result;
var debounced = function() {
var context = this, //this指向dom元素
args = arguments; //传递event对象
if(timer)clearTimeout(timer);
if(immediate) {
// 如果已经执行过,不再执行
var callNow = !timer;
timer = setTimeout(function() {
timer = null;
}, delay)
if(callNow) result = fn.apply(context, args);
} else {
timer = setTimeout(function() {
fn.apply(context, args);
}, delay);
}
return result;
}
debounced.cancel = function() {
clearTimeout(timer);
timer = null;
}
return debounced;
}

节流的原理很简单:

如果你持续触发事件,每隔一段时间,只执行一次事件。

根据首次是否执行以及结束后是否执行,效果有所不同,实现的方式也有所不同。我们用leading代表首次是否执行,trailing代表结束后是否再执行一次。

关于节流的实现,有两种主流的实现方式,一种是使用时间戳,一种是设置定时器。
1.时间戳:马上执行,delay时间后不执行

1
2
3
4
5
6
7
8
9
10
11
12
13
function throttle(fn, delay) {
var context, args;
var last = 0;
return function() {
context = this;
args = arguments;
var now = +new Date();
if (now - last > delay) {
fn.apply(context, arguments);
last = now;
}
}
}

2.定时器: delay时间后执行

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function throttle(fn, delay) {
var context, args;
var timer = null;
return function() {
context = this;
args = arguments;
if (!timer) {
timer = setTimeout(function() {
timer = null;
fn.apply(context, args);
}, delay)
}
}
}

有头有尾

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function throttle(fn, delay) {
var context, args;
var timer = null;
var last = 0;
var later = function() {
last = +new Date();
timer = null;
fn.apply(context, args);
}
return function() {
context = this;
args = arguments;
var now = +new Date();
var remaining = delay - (now - last);
if (remaining <= 0 || remaining > delay) {
if(timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(context, args);
last = now;
} else if(!timer){
timer = setTimeout(later, remaining)
}
}
}

实现有头无尾或者无头有尾
leading:false 表示禁用第一次执行
trailing: false 表示禁用停止触发的回调

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
function throttle(fn, delay, options) {
var context, args, timer = null, last = 0;
if (!options) options = {};
var later = function() {
last = options.leading === false? 0 : new Date().getTime();
timer = null;
fn.apply(context, args);
}
var throttled = function() {
context = this;
args = arguments;
var now = +new Date();
if(!last && options.leading === false) last = now;
var remaining = delay - (now - last);
if (remaining <= 0 || remaining > delay) {
if (timer) {
clearTimeout(timer);
timer = null;
}
fn.apply(context, args);
last = now;
} else if(!timer && options.trailing !== false) {
timer = setTimeout(later, remaining);
}
}
throttled.cancel = function() {
clearTimeout(timeout);
previous = 0;
timeout = null;
}
return throttled;
}

判断是否为数字

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//判断是否是数字
function isNumber(num) {
var number = +num;
if ((number - number) !== 0) {
// Discard Infinity and NaN
return false;
}
if (number === num) {
return true;
}
if (typeof num === 'string') {
// String parsed, both a non-empty whitespace string and an empty string
// will have been coerced to 0. If 0 trim the string and see if its empty.
if (number === 0 && num.trim() === '') {
return false;
}
return true;
}
return false;
}

事件中心

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//Event
function Event() {
this._cache = {};
}
Event.prototype.on = function(type, callback) {
var fns = (this._cache[type] = this._cache[type] || []);
if (fns.indexOf(callback) === -1) {
fns.push(callback);
}
return this;
}
Event.prototype.trigger = function(type, data) {
var fns = this._cache[type];
if (Array.isArray(fns)) {
fns.forEach(fn => {
fn(data);
})
}
return this;
}
Event.prototype.off = function(type, callback) {
var fns = this._cache[type];
if (Array.isArray(fns)) {
if (Array.isArray(fns)) {
if(callback) {
var index = fns.indexOf(callback);
if (index !== -1) {
fns.splice(index, 1);
}
} else {
fns.length = 0;
}
}
}
return this;
}

Lazyman的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
//LazyMan
function _LazyMan(name) {
var self = this;
this.stack = [];
var fn = (function(n) {
var name = n;
return function() {
console.log('Hi, this is ' + name);
self.next();
}
})(name);
this.stack.push(fn);
setTimeout(function() {
self.next();
})
}
_LazyMan.prototype.next = function() {
var fn = this.stack.shift();
fn && fn();
}
_LazyMan.prototype.eat = function(name) {
var self = this;
var fn = (function(name) {
return function(){
console.log('eat ' + name);
self.next();
}
})(name)
this.stack.push(fn);
return this;
}
_LazyMan.prototype.sleep = function(time) {
var self = this;
var fn = (function(time) {
return function() {
setTimeout(function() {
console.log("Wake up after " + time + "s!");
self.next();
}, time * 1000)
}
})(time)
this.stack.push(fn);
return this;
}
_LazyMan.prototype.sleepFirst = function(time) {
var self = this;
var fn = (function(time) {
return function() {
setTimeout(function() {
console.log("Wake up after " + time + "s!");
self.next();
}, time * 1000)
}
})(time)
this.stack.unshift(fn);
return this;
}
function LazyMan(name) {
return new _LazyMan(name);
}

HTML5拖拽实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
//HTML5拖拽
var eleDrags = $('.draglist'),
lDrags = eleDrags.length,
eleDrag = null;
for (var i = 0; i < lDrags; i++) {
eleDrags[i].onselectstart = function() {
return false;
}
eleDrags[i].ondragstart = function(ev) {
ev.dataTransfer.effactAllow = 'move';
ev.dataTransfer.setData('text',ev.target.innerHTMl);
ev.dataTransfer.setDragImage(ev.target, 0, 0);
eleDrag = ev.target;
return true;
}
eleDrags[i].ondragend = function(ev) {
ev.dataTransfer.clearData('text');
eleDrag = null;
return false;
}
eleDrags[i].ondragover = function(ev) {
ev.preventDefault = true;
return false;
}
eleDrags[i].ondragenter = function(ev) {
this.style.color = "#fff";
return true;
}
eleDrags[i].ondragleave = function(ev) {
this.style.color = "#000";
return true;
}
eleDrags[i].ondrop = function(ev, divdom) {
var that = this;
if(eleDrag) {
var temp = eleDrag.innerHTML;
eleDrag.innerHTML = that.innerHTML;
that.innerHTML = temp;
}
return false;
}
}

原生ajax的实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
//原生ajax
var xhr = new XMLHttpRequest();
xhr.onreadystatechange = function() {
if (xhr.readystate === 4) {
if (chr.status === 200) {
console.log(xhr.responseTetx);
} else {
console.log("Error:" + chr.status);
}
}
}
xhr.open('GET', 'xxxx');
xhr.header('Content-Type','application/json');
xhr.send();

Promise模拟实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
//promise实现
function promise(fn) {
let self = this;
this.value = '';
this.error = '';
this.status = 'pending';
this.onResolveCallbacks = [];
this.onRejectCallbacks = [];
function resolve(value) {
if (slef.status === 'pending') {
self.status = 'resolved';
slef.value = value;
setTimeout(() =>{
self.onResolveCallbacks.forEach((callback) => {
callback.(slef.value);
})
}, 0)
}
}
function reject(error) {
if (slef.status === 'pending') {
self.status = 'rejected';
slef.error = error;
setTimeout(() =>{
self.onRejectCallbacks.forEach((callback) => {
callback.(slef.error);
})
}, 0)
}
}
fn(resolve, reject);
}
promise.prototype.then = function(onFulFilled, onRejected) {
if (this.status === 'pending'){
this.onResolveCallbacks.push(onFulFilled);
this.onRejectCallbacks.push(onRejected);
} else if(this.status === 'resolved') {
onFulFilled(this.value);
} else {
onRejected(this.error);
}
return this;
}

call和apply的模拟实现

call()方法在使用一个指定的this值和若干个指定的参数值的前提下调用某个函数或方法。

1
2
3
4
5
6
7
8
9
10
11
Function.prototype.call = Function.prototype.call || function(context) {
var context = context || window;
context.fn = this;
var arr = [];
for (var i = 1, l = arguments.length; i< l; i++) {
arr.push('arguments['+ i +']');
}
var result = eval('context.fn('+ arr +')');
delete context.fn;
return result;
}

apply功能与call一致,函数产地的参数是数组形式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.apply = Function.prototype.apply || function(context, arr) {
var context = context || window;
context.fn = this;
if (!arr) {
context.fn();
} else {
var args = [];
for(var i = 0, l = arr.length; i < l;i++) {
args.push('arr[+ i +]');
}
var result = eval('context.fn('+ args +')');
}
delete context.fn();
return result;
}

bind的模拟实现

bind() 方法会创建一个新函数。当这个新函数被调用时,bind() 的第一个参数将作为它运行时的 this,之后的一序列参数将会在传递的实参前传入作为它的参数。(来自于 MDN )

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.bind = Function.prototype.bind || function(context) {
if (typeof this !== "function") {
throw new Error("Function.prototype.bind - what is trying to be bound is not callable");
}
var self = this;
var args = Array.prototype.slice.call(arguments, 1);
var fNOP = function() {};
var fBound = function() {
// 当作为构造函数时,this 指向实例,此时结果为 true,将绑定函数的 this 指向该实例,可以让实例获得来自绑定函数的值
// 当作为普通函数时,this 指向 window,此时结果为 false,将绑定函数的 this 指向 context
self.apply(this instanceof fBound? this : context, args.concat(Array.prototype.slice.call(arguments)));
}
fNOP.prototype = this.prototype;
fBound.prototype = new fNOP();
return fBound;
}

new的模拟实现

new 运算符创建一个用户定义的对象类型的实例或具有构造函数的内置对象类型之一

1
2
3
4
5
6
7
function objectFactory() {
var obj = {};
var constructor = Array.prototype.shift.call(arguments);
obj.__proto__ = constructor.prototype;
var result = constructor.apply(obj, arguments);
return typeof result === 'object' : result : obj;
}

数组去重

双层循环

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function unique(array) {
var result = [];
for (var i = 0, arrLength = array.length; i < arrLength; i++) {
for(var j = 0, resLength = result.length; j < resLength; j++) {
if (array[i] === result[j]) {
break;
}
}
if (j === resLength) {
result.push(array[i])
}
}
return result;
}

indexOf

1
2
3
4
5
6
7
8
9
function unique(array) {
var result = [];
for (var i = 0, arrLength = array.length; i < arrLength; i++) {
if (result.indexOf(array[i]) === -1) {
result.push(array[i])
}
}
return result;
}

排序后去重

1
2
3
4
5
6
7
8
9
10
11
12
function unique(array) {
var result = [];
var sortedArray = array.concat().sort();
var seen;
for(var i = 0, len = sortedArray.length; i < len; i++) {
if (!i || seen !== sortedArray[i]) {
result.push(sortedArray[i]);
}
seen = sortedArray[i];
}
return result;
}

unique API

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// isSorted:是否排序 iteratee:迭代函数
function unique(array, isSorted, iteratee) {
var result = [];
var seen = [];
for(var i = 0, len = array.length; i < len; i++) {
var value = array[i];
var computed = iteratee? iteratee(value, index, array) : value;
if (isSorted) {
if (!i || seen !== computed) {
result.push(value);
}
seen = computed;
} else if(iteratee) {
if (seen.indexOf(computed) === -1) {
seen.push(computed);
result.push(value);
}
} else {
if(result.indexOf(value) === -1) {
result.push(value);
}
}
}
return result;
}

Array.filter

1
2
3
4
5
function unique(array) {
return array.filter((value, index) => {
return array.indexOf(value) === index;
})
}

Object键值对

1
2
3
4
5
6
function unique(array) {
var obj = {};
return array.filter((value, index) => {
return obj.hasOwnProperty(typeof value + JSON.stringify(value))? false : (obj[typeof value + JSON.stringify(value)] = true)
})
}

ES6

1
2
3
4
5
6
7
8
9
//Set
function unique(array) {
return Array.from(new Set(array));
}
//Map
function unique(array) {
var seen = new Map();
return array.filter((value) => !seen.has(value) && seen.set(value, 1));
}
1
var array = [1, 1, '1', '1', null, null, undefined, undefined, new String('1'), new String('1'), /a/, /a/, NaN, NaN];
方法 结果 说明
for循环 [1, “1”, null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
indexOf [1, “1”, null, undefined, String, String, /a/, /a/, NaN, NaN] 对象和 NaN 不去重
排序去重 [/a/, /a/, 1, “1”, String, String, NaN, NaN, null, undefined] 对象和 NaN 不去重
filter + indexOf [1, “1”, null, undefined, String, String, /a/, /a/] 对象不去重 NaN 会被忽略掉
优化后的键值对方法 [1, “1”, null, undefined, String, /a/, NaN] 全部去重
Set、Map [1, “1”, null, undefined, String, String, /a/, /a/, NaN] 对象不去重 NaN去重

类型判断

1
2
3
4
5
6
7
8
9
10
function type(obj) {
var class2type = {};
"Boolean Number String Function Array RegExp Object Error Date".split(' ').map((item) => {
return class2type['[object ' + item +']'] = item.toLowerCase();
})
if (obj == null) {
return obj + '';
}
return typeof obj === 'object' || typeof obj === 'function'? class2type[Object.prototype.toString.call(obj)] || 'object' : typeof obj;
}

EmptyObject

1
2
3
4
5
6
7
function isEmptyObject(obj) {
var name;
for (name in obj) {
return false;
}
return true;
}

Window对象

1
2
3
function isWindow(obj) {
return obj !== null && obj === obj.window;
}

isArrayLike

1
2
3
4
5
6
7
8
function isArrayLike(obj) {
var length = !!obj && "length" in obj && obj.length;
var resType = type(obj);
if (resType === 'function' || isWindow(obj)) {
return false;
}
return resType === 'array' || length === 0 || type(length) === 'number' && length > 0 && (length - 1) in obj;
}

isElement

1
2
3
function isElement(obj) {
return !!(obj && obj.nodeType === 1);
}

深浅拷贝

简单的数组深浅拷贝

1
concat(), slice(), Array.from(), [...spread], JSON.parse(JSON.stringify())

浅拷贝

1
2
3
4
5
6
7
8
9
10
function shallowCopy(obj) {
if(typeof obj !== 'object') return false;
var newObj = obj instanceof Array? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = obj[key];
}
}
return newObj;
}

深拷贝

1
2
3
4
5
6
7
8
9
10
function deepCopy(obj) {
if(typeof obj !== 'object') return false;
var newObj = obj instanceof Array? [] : {};
for (var key in obj) {
if (obj.hasOwnProperty(key)) {
newObj[key] = typeof obj[key] === 'object'? deepCopy(obj[key]) : obj[key];
}
}
return newObj;
}

Jquery的extend

定义:合并两个或者更多的对象的内容到第一个对象中

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
function extend() {
var deep = false;
var name, options, copy;
var length = arguments.length;
var target = arguments[0] || {};
var i = 1;
if (typeof target === 'boolean') {
deep = target;
target = arguments[i] || {};
i++
}
// 如果target不是对象,我们无法进行复制的所以设为{}
if (typeof target !== 'object') {
target = {}
}
for(; i < length; i++) {
options = arguments[i];
if (options !== null) {
for(name in options) {
copy = options[name];
if(deep && copy && typeof copy === 'object') {
target[name] = extend(true, target[name], options[name])
} else if(copy !== undefined){
target[name] = copy;
}
}
}
}
return target;
}

求数组的最大值和最小值

reduce

1
2
3
4
5
function max(arr) {
return arr.reduce((prev, current) => {
return Math.max(prev, current);
})
}

apply

1
2
3
function max(arr) {
return Math.max.apply(null, arr);
}

ES6

1
2
3
function max(arr) {
return Math.max(...arr);
}

Flatten

reduce

1
2
3
4
5
function flatten(arr) {
return arr.reduce((prev, next) => {
return prev.concat(Array.isArray(next)? flatten(next) : next);
},[])
}

ES6 flat的模拟

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* 数组扁平化
* @param {Array} input 要处理的数组
* @param {Number} depth 指定嵌套数组中的结构深度
*/
function flatten(input, depth) {
var output = [];
if (depth === 0) {
return output.concat(input);
}
var depth = depth || 1;
var j = 1;
for (var i = 0, len = input.length; i < len; i++) {
var value = input[i];
if (Array.isArray(value)) {
if (depth === Infinity) {
output = output.concat(flatten(value, depth - 1));
} else if (j <= depth) {
output = output.concat(flatten(value, depth - 1))
} else {
output.push(value);
}
}
else {
output.push(value);
}
}
return output;
}

数组中查找指定元素

findIndex

1
2
3
4
5
6
function findIndex(array, predicate, context) {
for (var i = 0, len = array.length; i < len; i++) {
if (predicate.call(context, array[i], i, array)) return i;
}
return -1;
}

findLastIndex

1
2
3
4
5
6
function findLastIndex(array, predicate, context) {
for (var i = array.length - 1; i >= 0; i--) {
if (predicate.call(context, array[i], i, array)) return i;
}
return -1;
}

createIndexFinder

1
2
3
4
5
6
7
8
9
10
function createIndexFinder(dir) {
return function(array, predicate, context) {
var length = array.length;
var i = dir > 0? 0 : length - 1;
for (; i >= 0 && i < length; i += dir) {
if (predicate.call(context, array[i], i, array)) return i;
}
return -1;
}
}

sortedIndex

在一个排好序的数组中找到value对应的位置,保证插入数组后,依然保持有序的状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
function sortedIndex(array, obj, iteratee, context) {
var cb = function(func, context) {
if (context === void 0) return func;
return function() {
return func.apply(context, arguments);
}
}
iteratee = cb(iteratee, context);
var low = 0, high = array.length;
while(high > low) {
var mid = Math.floor((low + high) / 2);
if (iteratee(array[mid]) < iteratee(obj)) {
low = mid + 1;
} else {
high = mid - 1;
}
}
return mid;
}

createIndexOfFinder

indexOf 方法也可以多传递一个参数 fromIndex,从 MDN 中看到 fromIndex 的讲究可有点多:

设定开始查找的位置。如果该索引值大于或等于数组长度,意味着不会在数组里查找,返回 -1。如果参数中提供的索引值是一个负值,则将其作为数组末尾的一个抵消,即 -1 表示从最后一个元素开始查找,-2 表示从倒数第二个元素开始查找 ,以此类推。 注意:如果参数中提供的索引值是一个负值,仍然从前向后查询数组。如果抵消后的索引值仍小于 0,则整个数组都将会被查询。其默认值为 0。

lastIndexOf 的 fromIndex:

从此位置开始逆向查找。默认为数组的长度减1,即整个数组都被查找。如果该值大于或等于数组的长度,则整个数组会被查找。如果为负值,将其视为从数组末尾向前的偏移。即使该值为负,数组仍然会被从后向前查找。如果该值为负时,其绝对值大于数组长度,则方法返回 -1,即数组不会被查找。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function createIndexOfFinder(dir) {
return function(array, item, idx) {
var i = dir > 0? 0 : array.length - 1;
length = array.length;
if (typeof idx === 'number') {
if (dir > 0) {
i = idx >=0 ? idx : Math.max(idx + length, 0)
} else {
i = idx >=0 ? Math.min(idx, length - 1) : idx + length;
}
}
for(; i >= 0 && i < length; i += dir) {
if (array[i] === item) return i;
}
return -1;
}
}

ES5实现ES6的class

es6的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class Parent {
constructor(name) {
this.name = name;
}
sayHello() {
return 'Hello, i am ' + this.name;
}
static onlySayHello() {
return 'Hello';
}
get name() {
return 'kevin';
}
set name(newName) {
console.log('my name is ' + newName);
}
}
class Child extends Parent {
constructor(name, age) {
super(name);
this.age = age;
}
}
var child1 = new Child('kevin', 18);

es5实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
'use strict'
//静态方法定义在实例对象上,方法定义在实例的原型对象上
var _createClass = function() {
function defineProperties(target, props) {
for(var i = 0, len = props.length; i < len; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ('value' in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function(Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
}
}()
function _possibleConstructorReturn(self, call) {
if (!self) {
throw new ReferenceError("this hasn't been initialised-super() hasn't been called")
}
return call && (typeof call === 'object' || typeof call === 'function')? call : self;
}
function _inherits(subClass, superClass) {
if (typeof superClass !== 'function' && superClass !== null) {
throw new ReferenceError("Super expression msut either be a function or null, not " + typeof superClass);
}
subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true }});
if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass;
}
//class只能通过new构造函数使用
function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError('Cannot call a class as a function')
}
}
var Parent = function() {
function Parent(name) {
_classCallCheck(this, Parent);
this.name = name;
}
_createClass(Parent, [{
key: 'sayHello',
value: function sayHello() {
return 'Hello, I am ' + this.name;
}
}, {
key: 'get',
value: function get() {
return 'kevin';
}
}, {
key: 'set',
value: function set(nemName) {
console.log('my name is ' + newName);
}
}], [{
key: 'onlySayHello',
value: function onlySayHello() {
return 'Hello';
}
}])
return Parent;
}();
var Child = function(_Parent) {
_inherits(Child, _Parent);
function Child(name, age) {
_classCallCheck(this, Child);
var _this = _possibleConstructorReturn(this, (Child.__proto__ || Object.getPrototypeOf(Child)).call(this, name))
_this.age = age;
return _this;
}
return Child;
}(Parent)
var child1 = new Child('kevin', 18);

CSS知识点整理

发表于 2019-01-03

css盒模型以及BFC

标准盒模型和IE盒子模型

1
2
3
4
5
6
7
8
9
10
11
12
13
css盒模型包括:content, padding, border, margin;
CSS盒模型和IE盒模型的区别:
在 标准盒子模型中,width 和 height 指的是内容区域的宽度和高度。增加内边距、边框和外边距不会影响内容区域的尺寸,但是会增加元素框的总尺寸。
IE盒子模型中,width 和 height 指的是内容区域+border+padding的宽度和高度。
CSS如何设置这两种模型
/* 设置当前盒子为 标准盒模型(默认) */
box-sizing: content-box;
/* 设置当前盒子为 IE盒模型 */
box-sizing: border-box;

JS如何设置、获取盒模型对应的宽和高

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
方式一:通过DOM节点的 style 样式获取
element.style.width/height
缺点:通过这种方式,只能获取行内样式,不能获取内嵌的样式和外链的样式。
方式二(通用型)
window.getComputedStyle(element).width/height
方式二能兼容 Chrome、火狐。是通用型方式。
方式三(IE独有的)
element.currentStyle.width/height
和方式二相同,但这种方式只有IE独有。获取到的即时运行完之后的宽高(三种css样式都可以获取)。
方式四
element.getBoundingClientRect().width/height
此 api 的作用是:获取一个元素的绝对位置。绝对位置是视窗 viewport 左上角的绝对位置。
此 api 可以拿到四个属性:left、top、width、height。

margin塌陷/margin重叠

1
2
3
4
5
6
7
在标准文档流中,竖直方向的margin不叠加,只取较大的值作为margin(水平方向的margin是可以叠加的,即水平方向没有塌陷现象)。
PS:如果不在标准流,比如盒子都浮动了,那么两个盒子之间是没有margin重叠的现象的。
子元素和父元素之间
如果父元素没有border属性,子元素设置了margin-top,连带父元素一起偏移;父元素设置border属性后就正常了;
margin这个属性,本质上描述的是兄弟和兄弟之间的距离; 最好不要用这个marign表达父子之间的距离。
所以,如果要表达父子之间的距离,我们一定要善于使用父亲的padding,而不是儿子的margin。

BFC(边距重叠解决方案)

BFC(Block Formatting Context):块级格式化上下文

1
2
3
4
5
6
7
8
9
10
11
BFC 的原理/BFC的布局规则
(1)BFC 内部的子元素,在垂直方向,边距会发生重叠。
(2)BFC在页面中是独立的容器,外面的元素不会影响里面的元素,反之亦然。
(3)BFC区域不与旁边的float box区域重叠。(可以用来清除浮动带来的影响)。
(4)计算BFC的高度时,浮动的子元素也参与计算。
如何生成BFC
方法1:overflow: 不为visible,可以让属性是 hidden、auto。【最常用】
方法2:浮动中:float的属性值不为none。意思是,只要设置了浮动,当前元素就创建了BFC。
方法3:定位中:只要posiiton的值不是 static或者是relative即可,可以是absolute或fixed,也就生成了一个BFC。
方法4:display为inline-block, table-cell, table-caption, flex, inline-flex

三栏布局

圣杯

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
//html
<div class='wraper'>
<div class='main'>main</div>
<div class='left'>left</div>
<div class='right'>right</div>
</div>
//css
.wraper{
padding: 0 200px;
}
.main{
float:left;
width:100%;
background:green;
height:200px;
}
.left{
width:200px;
height:200px;
background:red;
float:left;
margin-left:-100%;
position:relative;
left:-200px;
}
.right{
width:200px;
height:200px;
background:yellow;
float:left;
margin-left:-200px;
position:relative;
left:200px;
}

双飞翼

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
//html
<div class='wraper'>
<div class='main'>
<div class='content'>main</div>
</div>
<div class='left'>left</div>
<div class='right'>right</div>
</div>
//css
.wraper{
}
.main{
float:left;
width:100%;
height:200px;
background:green;
}
.content{
margin: 0 200px;
}
.left{
width:200px;
height:200px;
background:red;
float:left;
margin-left:-100%;
}
.right{
width:200px;
height:200px;
background:yellow;
float:left;
margin-left:-200px;
}

绝对定位

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<div class='wraper'>
<div class='left'>left</div>
<div class='main'>main</div>
<div class='right'>right</div>
</div>
.wraper {
position: relative;
}
.left {
position: absolute;
left: 0;
width: 200px;
height: 200px;
background: red;
}
.main {
position: absolute;
left: 200px;
right: 200px;
height: 200px;
background:green;
}
.right {
position: absolute;
right: 0;
width: 200px;
height: 200px;
background: yellow;
}

table布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div class='wraper'>
<div class='left'>left</div>
<div class='main'>main</div>
<div class='right'>right</div>
</div>
.wraper {
width: 100%;
display: table;
height: 200px;
}
.wraper div {
display: table-cell;
}
.left {
width: 200px;
background: red;
}
.main {
background: green;
}
.right {
width: 200px;
background: yellow;
}

flexbox布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<div class='wraper'>
<div class='left'>left</div>
<div class='main'>main</div>
<div class='right'>right</div>
</div>
.wraper {
display: flex;
}
.left {
width: 200px;
height: 200px;
background: red;
}
.main {
height: 200px;
flex: 1;
background:green;
}
.left {
width: 200px;
height: 200px;
background: yellow;
}

grid布局

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<div class='wraper'>
<div class='left'>left</div>
<div class='main'>main</div>
<div class='right'>right</div>
</div>
.wraper {
display: grid;
width: 100%;
grid-template-rows: 200px;
grid-template-columns: 200px 1fr 200px;
}
.left {
background: red;
}
.main {
background: green;
}
.right {
background: yellow;
}

水平垂直居中

1
2
3
4
5
6
7
8
9
10
11
12
<div class="parent">
<div class="child">DEMO</div>
</div>
.parent{
text-align:center;
display:table-cell;
vertical-align:middle;
}
.child{
display:inline-block;
}

多边形的绘制

平行四边形

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
<div class="parallelograms">
<div>二十首情诗与绝望的歌</div>
</div>
.parallelograms{
margin: 50px auto;
max-width: 200px;
padding: 10px;
line-height: 30px;
text-align: center;
color:#fff;
background-color: #58a;
transform:skew(-45deg);
}
.parallelograms div{
transform: skew(45deg);
}
<div class="parallelograms">
二十首情诗与绝望的歌
</div>
.parallelograms{
margin: 50px auto;
max-width: 200px;
padding: 10px;
line-height: 30px;
text-align: center;
color:#fff;
position: relative;
}
.parallelograms:before{
content:'';
position: absolute;
left:0;
top:0;
right:0;
bottom:0;
background-color: #58a;
transform:skew(-45deg);
z-index: -1;
}

Vue的模板

发表于 2018-10-08

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

模板渲染

Vue模板的渲染过程入下图所示,首先通过模板compiler编译器将模板编译成AST(抽象语法树),再由AST生成Vue的render函数,渲染函数结合数据再生成vnode(Virtual DOM树),对vnode进行diff和patch后生成新的UI。
'vue-template'

Virtual DOM

由于操作真实的DOM会对性能带来损失,可以通过模拟真实的DOM对象树,建立一个虚拟DOM对真实DOM 发生的变化保持追踪。
vnode包含以下属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.tag = tag //当前节点的标签名
this.data = data //当前节点的数据对象
this.children = children //数组类型,包含了当前节点的子节点
this.text = text //当前节点的文本,一般文本节点或注释节点会有该属性
this.elm = elm //当前虚拟节点对应的真实的DOM节点
this.ns = undefined //节点的namespace
this.context = context //编译作用域
this.fnContext = undefined //函数节点的上下文
this.fnOptions = undefined //函数options
this.fnScopeId = undefined //函数作用域id
this.key = data && data.key //节点的key属性,用于作为节点的标识,有利于patch的优化
this.componentOptions = componentOptions //创建组件实例时会用到的选项信息
this.componentInstance = undefined //组件实例
this.parent = undefined //组件占位节点
this.raw = false //Raw HTML
this.isStatic = false //静态节点的标识
this.isRootInsert = true //是否作为根节点插入,被<transition>包裹的节点,该属性的值为false
this.isComment = false //当前节点是否是注释节点
this.isCloned = false //当前节点是否为克隆节点
this.isOnce = false //当前节点是否有v-once指令
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false

createElement函数

createElement函数用来创建vnode,createElement接受的参数,

1
2
3
4
5
6
7
8
export function createElement (
context: Component,
tag: any, //一个 HTML 标签字符串,组件选项对象,或者一个返回值
data: any, //一个包含模板相关属性的数据对象
children: any, //子节点 (VNodes),由 `createElement()` 构建而成
normalizationType: any,
alwaysNormalize: boolean
)

render函数

Vue的模板实际是编译成了render函数,可以使用Vue.compile(template)方法编译下面的模板

1
2
3
4
5
6
7
8
9
10
11
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>

方法会返回一个对象,对象中有render和staticRenderFns两个值,下面是生成的render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function() {
with(this) {
return _c(
//创建一个div元素
'div',
[
// 静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_m(0),
// 三元表达式,判断 message 是否存在
// 如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
// 如果不存在,创建 p 元素,元素里面有文本,值为 No message.
(message)? _c('p', [ _v( _s(message))]) : _c('p', [ _v("No message.")])
]
)
}
}
其中:
_c : createElement(创建元素)
_m : renderStatic(渲染静态节点)
_v : createTextVNode(创建文本DOM)
_s : toString (转换为字符串)

staticRenderFns数组与diff算法优化相关,我们会在编译阶段给后面不会发生变化的vnode节点打上static为true的标签,那些被标记为静态节点的vnode就会单独生成staticRenderFns函数:

1
2
3
4
5
6
7
8
9
10
_m(0): function anonymous() {
with(this) {
return _c(
'header',
[
_c('h1', [ _v("I'm a template!")])
]
)
}
}

观察者(Watcher)

每个Vue组件都有一个对应的Watcher,这个Watcher会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候重新渲染组件。
上图中我们可以以render函数作为分割线,render函数左边称为编译期,将vue的模板转换为渲染函数。render函数的右边是在vue的运行时,主要是基于渲染函数生成Virtual Dom树,然后对Virtual Dom进行diff和patch。
下面图描述了vue模板渲染的流程:
vue-template-10

  • new Vue():实例化Vue
  • $mount():获取模板,并且在这过程中调用相关方法_count,new Watcher()实现数据响应式,当Watcher监听到数据变化,就会执行render函数输出一个新的vnode树形结构的数据
  • compileToFunction():将tenplate编译成render函数。首先读缓存,在compileToFunction()中,会创建一个对象,把complie编译完后的对象的render 和 staticRenderFns 两个属性分别转换成函数缓存在对象中,然后把对象存进缓存,没有缓存就调用compile 方法拿到 render 函数的字符串形式,在通过new Function的方式生成真正的渲染函数。
  • compile:将 template 编译成 render 函数的字符串形式,这个函数主要有三个步骤组成:parse,optimize和generate,最终输出一个包含 ast,render 和 staticRenderFns的对象。compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为render函数字符串,render 函数与数据通过 Watcher 产生关联。
  • update():update判断是否首次渲染,是则直接创建真实DOM,否则调用patch(),并且进行触发钩子和更新引用等其他操作。
  • patch():新旧vnode对比的diff函数,对两个树结构进行完整的diff和patch的过程,最终只有发生了变化的节点才会被更新到真实 DOM 树上。
  • destroy():完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发beforeDestroy和destroyed的钩子。在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。

Vue异步更新队列

发表于 2018-05-04

异步更新队列指的是当状态发生变化时,Vue异步执行DOM更新。我们来看个例子

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<div id="example">
<audio ref="audio" :src="url" controls="controls"></audio>
<div><button @click="changeUrl">Next</button></div>
</div>
const musicList = [
'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112003137.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112002493.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3',
'http://sc1.111ttt.cn:8282/2017/1/11m/11/304112004168.m4a?tflag=1519095601&pin=6cd414115fdb9a950d827487b16b5f97#.mp3'
];
var vm = new Vue({
el: '#example',
data: {
index: 0,
url: ''
},
methods: {
changeUrl() {
this.index = (this.index + 1) % musicList.length
this.url = musicList[this.index];
this.$refs.audio.play();
}
}
});

当我们点击Next按钮后,并不能播放下一首音乐

1
Uncaught (in promise) DOMException: The element has no supported sources.

原因就在于audio.play()是同步的,而这个时候DOM更新是异步的,src属性还没有被更新,结果播放的时候src属性为空,就报错了。解决办法就是在play的操作加上this.$nextTick()。

1
2
3
this.$nextTick(function() {
this.$refs.audio.play();
});

在Vue的官方文档中有这样的说明:
可能你还没有注意到,Vue 异步执行 DOM 更新。只要观察到数据变化,Vue 将开启一个队列,并缓冲在同一事件循环中发生的所有数据改变。如果同一个 watcher 被多次触发,只会被推入到队列中一次。这种在缓冲时去除重复数据对于避免不必要的计算和 DOM 操作上非常重要。然后,在下一个的事件循环“tick”中,Vue 刷新队列并执行实际 (已去重的) 工作。
之所以要采用异步方式更新DOM,是为了避免每次数据发生变化,就马上去重新渲染DOM,数据的状态可能会发生多次,只要计算最后一次状态的变化,这样可以大大减少性能的损耗。

Event Loop

Vue官方文档中提到在事件循环中执行DOM的刷新。我们先来了解下浏览器Event Loop机制。

浏览器Event Loop

javascript是一门单线程语言,JS的所有同步代码都在主线程(执行栈)中执行,当执行一个函数调用时,会创建一个新的执行环境并压到栈中开始执行函数中的代码,当函数中的代码执行完毕后将执行环境从栈中弹出,当栈空了,也就代表执行完毕。在主线程之外,还有一个任务队列(异步),事实上异步队列也分两种类型:微任务、宏任务。
浏览器Event Loop过程如图所示:
image

1
2
3
4
5
1. 浏览器中,先执行当前栈,执行完主线程中的任务。
2. 取出Microtask微任务队列中任务执行直到清空。
3. 取出Macrotask宏任务中 一个 任务执行。
4. 检查Microtask微任务中有没有任务,如果有任务执行直到清空。
5. 重复3和4。

属于微任务(microtask)的事件有以下几种:

1
2
3
4
1. Promise.then
2. MutationObserver
3. Object.observe(已废弃)
4. process.nextTick

属于宏任务(macrotask)的事件有以下几种:

1
2
3
4
5
6
7
1. setTimeout
2. setInterval
3. setImmediate
4. MessageChannel
5. requestAnimationFrame
6. I/O
7. UI交互事件

看下面的例子帮助我们理解这个过程

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
console.log(1)
// 栈
setTimeout(function(){
console.log(2)
// 微任务
Promise.resolve(100).then(function(){
console.log('promise')
})
})
// 栈
let promise = new Promise(function(resolve, reject){
console.log(7)
resolve(100)
}).then(function(data){
// 微任务
console.log(data)
})
// 栈
setTimeout(function(){
console.log(3)
})
//浏览器结果:1 7 5 100 2 promise 3

这是在浏览器中运行的结果。Node.js 是一个基于 Chrome V8 引擎的 JavaScript 运行环境,所以js代码还能在Node中运行,此时Event Loop机制与浏览器略有不同。在这里我们暂时不探讨Node环境的机制。

Vue异步更新队列

vue异步更新队列事件循环的tick有可能是在当前的Tick微任务执行阶段执行,也可能是在下一个Tick执行,主要取决于nextTick函数到底是使用Promise/MutationObserver(现废弃采用MessageChannel)还是setTimeout。
在Vue 2.4之前的版本中,nextTick几乎都是基于microTask实现的,但是由于microTask的执行优先级非常高,在某些场景之下它甚至要比事件冒泡还要快,就会导致一些诡异的问题;但是如果全部都改成macroTask,对一些有重绘和动画的场景也会有性能的影响。所以最终nextTick采取的策略是默认走microTask,对于一些DOM的交互事件,如v-on绑定的事件回调处理函数的处理,会强制走macroTask。

应用场景

在操作DOM节点无效的时候,就要考虑操作的实际DOM节点是否存在,或者相应的DOM是否被更新完毕。比如说,在created钩子中涉及DOM节点的操作肯定是无效的,因为此时还没有完成相关DOM的挂载。解决的方法就是在nextTick函数中去处理DOM,这样才能保证DOM被成功挂载而有效操作。还有就是在数据变化之后要执行某个操作,而这个操作需要使用随数据改变而改变的DOM时,这个操作应该放进Vue.nextTick。

css中position的探讨

发表于 2018-04-28

position属性值

在定位的时候都会接触到一个名词文档流,文档流大意就是元素在文档中的排列顺序,从左到右,从上到下排列和显示。float和 position可以使元素脱离文档流。

static(默认值)

元素按照文档流显示,不受left、right、top和bottom影响;

relative(相对定位)

相对定位是以元素原本所在位置为参考,保留原本占据的位置,不会对相邻的元素产生影响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//html
<div>
<div class='first'></div>
<div class='second'></div>
<div class='third'></div>
</div>
//css
.first{
width: 100px;
height: 100px;
background-color: green;
}
.second{
width: 100px;
height: 100px;
background-color: red;
position:relative;
top:10px;
opacity:0.3;
}
.third{
width: 100px;
height: 100px;
background-color: yellow;
}

image
从效果图看出,第二个元素position属性设置了relative,以自身原本的文字向下偏移10px,没有对第一和第三个元素产生影响。如果设置相对定位,同时对top(对象相对原位置向下偏移的距离)和bottom(对象相对原位置向上偏移的距离)赋值,只有top起作用。同理,同时对left(对象相对原位置向左偏移的距离)和right(对象相对原位置向右偏移的距离)赋值,只有left起作用。

absolute(绝对定位)

元素设置为绝对定位后会脱离文档流,相对于第一个position属性不为static的父级元素进行定位,如果所有的父级元素都未设置position属性,则以document元素进行定位。在网页中document元素指的就是html。如果设置绝对定位,同时对top(对象相对原位置向下偏移的距离)和bottom(对象相对原位置向上偏移的距离)赋值,只有top起作用。同理,同时对left(对象相对原位置向左偏移的距离)和right(对象相对原位置向右偏移的距离)赋值,只有left起作用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
//html
<body>
<div class='parent'>
<div class='first'></div>
<div class='second'></div>
<div class='third'></div>
</div>
</body>
//css
body{
width:800px;
height:500px;
border:5px solid;
}
.parent{
width: 600px;
height: 500px;
position: relative; /*absolute效果一致*/
background-color: pink;
}
.first{
width: 100px;
height: 100px;
background-color: green;
}
.second{
width: 100px;
height: 100px;
background-color: red;
position:absolute;
left:100px;
}
.third{
width: 100px;
height: 100px;
background-color: yellow;
}

image
如果父元素没有设置定位,此时是以html元素为参考定位的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
body{
width:800px;
height:500px;
border:5px solid;
margin: 50px;
}
.parent{
width: 600px;
height: 500px;
background-color: pink;
}
.first{
width: 100px;
height: 100px;
background-color: green;
}
.second{
width: 100px;
height: 100px;
background-color: red;
position:absolute;
left:100px;
}
.third{
width: 100px;
height: 100px;
background-color: yellow;
}

image

fixed(固定定位)

设置为fixed的元素会被定位于浏览器窗口的一个指定坐标,不论窗口是否滚动,它都会固定在这个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
//html
<div class='parent'>
<div class='first'></div>
<div class='second'></div>
<div class='third'></div>
</div>
//css
.parent{
width: 600px;
height: 500px;
background-color: pink;
position: relative; /*这里的定位元素不会对子元素内fixed定位元素产生影响*/
left:30px
}
.first{
width: 100px;
height: 100px;
background-color: green;
}
.second{
width: 100px;
height: 100px;
background-color: red;
position:fixed;
left:100px;
}
.third{
width: 100px;
height: 100px;
background-color: yellow;
}

image
固定定位也不是完全以浏览器窗口定位参考,在网上看到过这样一个问题,重新设置parent样式

1
2
3
4
5
6
7
.parent{
width: 600px;
height: 500px;
background-color: pink;
transform: translateX(30px);
}

image
同样将父元素向左偏移30px,使用transform实现,此时第二个子元素定位的表现和absolute类似,以父元素为参考定位。

sticky

sticky的效果就是在一个内容中,我们可以固定一个部分,然后到了另一个内容,又会固定另外一个部分

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
//html
<div class="wrap">
<div class="header">
这是头部
</div>
<div class="content">
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
这是内容部分<br>
</div>
</div>
<div class="wrap">
<div class="header">
这是另一个头部
</div>
<div class="content">
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
这是另一个内容<br>
</div>
</div>
//css
body {
margin: 0;
padding: 0;
}
.wrap {
border: 20px solid blue;
}
.header {
position: sticky;
top: 20px;
border: 20px solid red;
margin-top: 20px;
}

position与display

元素分为内联元素和区块元素,在内联元素中是不能设置width和 height样式。
1.relative:原来是什么类型的依旧是什么类型。
2.absolute | fixed : 元素就被重置为了区块元素。

position与float

一个元素若设置了 position:absolute | fixed; 则该元素就不能设置float。这是一个常识性的知识点,因为这是两个不同的流,一个是浮动流,另一个是“定位流”。但是relative却可以。因为它原本所占的空间仍然占据文档流。

vue的数据双向绑定原理

发表于 2017-12-18

vue数据双向绑定原理

目前几种主流的框架都实现了数据绑定,大致做法有:

1
2
3
发布者-订阅者模式(backbone.js)
脏值检测(angular.js)
数据劫持+发布者-订阅者模式(vue.js)

数据劫持

要实现数据劫持,我们需要了解Object.defineProperty()方法。当我们创建一个普通属性时,可以看到属性描述符的各种性质的默认值,如果需要修改这些默认值,可以使用Object.defineProperty()方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
var myObject = {
a:1
};
Object.getOwnPropertyDescriptor(myObject, "a");
// {
// value: 2,
// writable: true,
// enumerable: true,
// configurable: true
// }
Object.defineProperty( myObject, "a", {
value: 3,
writable: true,
configurable: true,
enumerable: true
} );
myObject.a //3

对象里目前存在的属性描述符有两种主要形式:数据描述符和存取描述符。数据描述符是一个具有值的属性,该值可能是可写的,也可能不是可写的。访问器描述符是由getter-setter函数对描述的属性。描述符必须是这两种形式之一;不能同时是两者。如果一个描述符不具有value,writable,get 和 set 任意一个关键字,那么它将被认为是一个数据描述符。如果一个描述符同时有(value或writable)和(get或set)关键字,将会产生一个异常。
Vue就是通过Object.defineProperty()来实现数据劫持的。看下面这段代码:设置a属性时会触发set函数,读取a属性时会触发get函数

1
2
3
4
5
6
7
8
9
10
11
12
13
var myObject = {};
var score;
Object.defineProperty(myObject,"a", {
get() {
console.log('新的分数');
},
set(val) {
score = val + '分';
console.log('分数加上单位');
}
})
myObject.a = 60; //分数加上单位
myObject.a //新的分数

思路原理

MVVM数据双向绑定大致是两方面,一是视图改变更新数据,二是数据改变更新视图。视图更新数据通过事件监听实现,比如input标签监听’input’事件,在事件触发函数内修改数据;数据更新视图就运用到上节提到的Object.defineProperty()方法,当数据变化是会触发改方法内的set函数,然后在set函数内更新视图。要实现MVVM数据绑定,需要实现以下几点:
1.实现一个数据监听器Observer,用来监听数据对象上的所有属性,一旦数据变化就通知订阅者
2.实现一个订阅者watcher,订阅并收到属性变化的通知,然后执行绑定的回调函数更新视图
3.实现一个指令解析器,对每个元素节点上的指令进行扫描和解析,根据指令模板替换数据,以及绑定响应的回调函数
'vue-wwo-way-binding'

实现Observer

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
function Observer(data) {
this.data = data;
this.walk(data);
}
Observer.prototype = {
walk(data) {
const keys = Object.keys(data);
for (let i = 0, l = keys.length; i < l; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
},
defineReactive(data, key, value) {
var dep = new Dep();
var childObj = observe(value); // 递归遍历所有子属性
Object.defineProperty(data, key, {
enumerable: true, // 可枚举
configurable: true,
get() {
Dep.depend();
return value;
},
set(newVal) {
if (val === newVal) return;
value = newVal;
console.log('监听的属性'+ key +'变化了,现在的值为'+ newVal);
Dep.notify();
}
})
}
}
function observe(data) {
if(!data || typeOf data !== 'Object') {
return;
}
const keys = Object.keys(data);
for (let i = 0, l = keys.length; i < l; i++) {
defineReactive(data, keys[i], data[keys[i]])
}
}
function Dep() {
this.subs = [];
}
Dep.prototype = {
addSub(sub) {
this.subs.push(sub);
},
removeSub(sub) {
var index = this.subs.indexOf(sub);
if (index !== -1) {
this.subs.splice(index, 1);
}
},
depend() {
if(Dep.target) {
Dep.addSub(watcher);
}
},
notify() {
const subs = this.subs.slice();
for (let i = 0, l = subs.length; i < l; i++) {
subs[i].update();
}
}
}
Dep.target = null;

实现watcher

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
function watcher(vm, exp, cb) {
this.vm = vm;
this.cb = cb;
this.exp = exp;
this.value = this.get();
}
watcher.prototype = {
update() {
this.run();// 属性值变化收到通知
},
run() {
var value = this.get();
var oldValue = this.value();
if (oldValue !== value) {
this.value = value;
this.cb.call(this.vm, value, oldVal);// 执行Compile中绑定的回调,更新视图
}
},
get() {
Dep.target = this;
var value = this.vm.data[this.exp];
Dep.target = null;
return value;
}
}

实现compile(后续补充)

Object.defineProperty的缺陷

数组漏洞

Object.defineProperty无法监听数组变化。

  1. 当你利用索引直接设置一个项时,例如:vm.items[indexOfItem] = newValue
  2. 当你修改数组的长度时,例如:vm.items.length = newLength
    作者对数组的8类方法进行了hack处理,使用它们可以实现视图更新。
    1
    2
    3
    4
    5
    6
    7
    push()
    pop()
    shift()
    unshift()
    splice()
    sort()
    reverse()

对象漏洞

Vue 不能检测对象属性的添加或删除,所以添加属性的时候需要使用推荐的vm.$set方法。数据劫持时,需要对对象的每个属性进行遍历,如果属性值也是对象那么需要深度遍历。

Proxy实现数据的双向绑定

Proxy 可以理解成,在目标对象之前架设一层“拦截”,外界对该对象的访问,都必须先通过这层拦截,因此提供了一种机制,可以对外界的访问进行过滤和改写。

Proxy可以直接监听对象而非属性

我们用Proxy改造上文中用Object.defineProperty实现的数据绑定

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
var myObject = {};
var score;
var proxy = new Proxy(myObject, {
get(target, key, receiver) {
console.log('新的分数');
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
score = value + '分';
console.log('分数加上单位');
return Reflect.set(target, key, receiver);
}
})
proxy.a = 60; //分数加上单位
proxy.a //新的分数

Proxy直接可以劫持整个对象,并返回一个新对象,不管是操作便利程度还是底层功能上都远强于Object.defineProperty。

Proxy可以直接监听数组的变化

当我们对数组进行操作(push、shift、splice等)时,会触发对应的方法名称和length的变化。

1
2
3
4
5
6
7
8
9
10
11
12
13
var arr = [1,2,3,4];
var newArr = new Proxy(arr, {
get(target, key, receiver) {
console.log(key);
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log('原始数组变了');
return Reflect.set(target, key, value, receiver);
}
})
newArr[4] = 5; //原始数组变了
arr //[1,2,3,4,5]

Proxy 支持的拦截操作有13种:

1
2
3
4
5
6
7
8
9
10
11
12
13
get(target, key, receiver):拦截对象属性的读取。
set(target, key, value, receiver):拦截对象属性的设置。
has(target, key):拦截key in proxy的操作,返回一个布尔值。
deleteProperty(target, key):拦截delete proxy[propKey]的操作,返回一个布尔值。
ownKeys(target):拦截Object.getOwnPropertyNames(proxy)、Object.getOwnPropertySymbols(proxy)、Object.keys(proxy)、for...in循环,返回一个数组。该方法返回目标对象所有自身的属性的属性名,而Object.keys()的返回结果仅包括目标对象自身的可遍历属性。
getOwnPropertyDescriptor(target,propKey):拦截Object.getOwnPropertyDescriptor(proxy, propKey),返回属性的描述对象。
defineProperty(target, propKey, propDesc):拦截Object.defineProperty(proxy, propKey, propDesc)、Object.defineProperties(proxy, propDescs),返回一个布尔值。
preventExtensions(target):拦截Object.preventExtensions(proxy),返回一个布尔值。
getPrototypeOf(target):拦截Object.getPrototypeOf(proxy),返回一个对象。
isExtensible(target):拦截Object.isExtensible(proxy),返回一个布尔值。
setPrototypeOf(target, proto):拦截Object.setPrototypeOf(proxy, proto),返回一个布尔值。如果目标对象是函数,那么还有两种额外操作可以拦截。
apply(target, object, args):拦截 Proxy 实例作为函数调用的操作,比如proxy(...args)、proxy.call(object, ...args)、proxy.apply(...)。
construct(target, args):拦截 Proxy 实例作为构造函数调用的操作,比如new proxy(...args)。

vue组件之间的数据传递

发表于 2017-11-25

父子组件的通信

在vue组件关系中最常见的就是父子组件,组件A在它的模板中使用了组件B,他们之间的通信方式是:父组件(A)通过prop给子组件(B)下发数据,子组件(B)通过事件给父组件(A)发送消息。
'vue-props-events'
prop这样的数据流称之为单向数据流,即父组件内的属性变化时,子组件相应的属性也会同步更新,而子组件属性的变化不会影响父组件的属性。这是为了防止子组件无意间修改了父组件的状态,来避免应用的数据流变得难以理解。

Prop

组件实例的作用域是孤立的,这意味不能(也不应该)在子组件的模板中直接引用父组件的数据。父组件的数据需要通过 prop 才能下发到子组件中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
<div id="app">
<child :my-name="name" :my-age="age">
</div>
<template id="child">
<div class="child">
<h3>子组件child数据</h3>
<ul>
<li>
<label>姓名</label>
<span>{{ myName }}</span>
</li>
<li>
<label>年龄</label>
<span>{{ myAge }}</span>
</li>
</ul>
</div>
</template>
let parent = new Vue({
el: "#app",
data() {
return {
name:"jack",
age: 28
}
},
components: {
"child": {
template:"#child",
props:["myName", "myAge"]
}
}
})

结果
result
在很多组件的应用中往往也会涉及到prop得到修改:
1.Prop 作为初始值传入后,子组件想把它当作局部数据来用;
2.Prop 作为原始数据传入,由子组件处理成其它数据输出。
所以当你想要实现子组件的prop修改影响父组件(虽然不推荐使用),可以使用.sync修饰符(在vue 2.3.0版本重新引入)打到prop的双向绑定,它会作为编译时的语法糖,扩展为一个自动更新父组件属性的v-on监听器。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<div id="app">
<div class="parent">
<h3>父组件Parent数据</h3>
<ul>
<li>
<label>姓名:</label>
<span>{{ name }}</span>
<input type="text" v-model="name" />
</li>
<li>
<label>年龄:</label>
<span>{{ age }}</span>
<input type="text" v-model="age" />
</li>
</ul>
</div>
<child v-bind:my-name.sync="name" v-bind:my-age.sync="age"></child>
</div>
<template id="child">
<div class="child">
<h3>子组件child数据</h3>
<ul>
<li>
<label>姓名</label>
<span>{{ myName }}</span>
<input type="text" v-model="myName" />
</li>
<li>
<label>年龄</label>
<span>{{ myAge }}</span>
<input type="text" v-model="myAge" />
</li>
</ul>
</div>
</template>
let parent = new Vue({
el: '#app',
data () {
return {
name: 'w3cplus',
age: 7
}
},
components: {
'child': {
template: '#child',
props: ['myName', 'myAge']
}
}
})

另外一种父子组件通信的方式是使用Vue.$emit自定义事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
<div id="app">
<div class="parent">
<h3>父组件Parent数据</h3>
<ul>
<li>
<label>姓名:</label>
<span>{{ name }}</span>
<input type="text" v-model="name" />
</li>
<li>
<label>年龄:</label>
<span>{{ age }}</span>
<input type="text" v-model="age" />
</li>
</ul>
</div>
<child v-bind:my-name="name" v-bind:my-age="age" @update:my-name='updateName' @update:my-age='updateAge'></child>
</div>
<template id="child">
<div class="child">
<h3>子组件child数据</h3>
<ul>
<li>
<label>姓名</label>
<span>{{ myName }}</span>
<input type="text" v-model="childMyName" />
</li>
<li>
<label>年龄</label>
<span>{{ myAge }}</span>
<input type="text" v-model="childMyAge" />
</li>
</ul>
</div>
</template>
let parent = new Vue({
el: '#app',
data () {
return {
name: 'jack',
age: 7
}
},
methods:{
updateName(val){
this.name = val;
},
updateAge(val){
this.age = val;
}
},
components: {
'child': {
template: '#child',
props: ['myName', 'myAge'],
data() {
return {
childMyName:this.myName,
childMyAge:this.myAge
}
},
watch:{
childMyName(val) {
this.$emit('update:my-name',val);
},
childMyAge(val) {
this.$emit('update:my-age',val);
}
}
}
}
})

非父子组件通信

有时候,非父子关系的两个组件也需要通信,一般也可以采用Vue.$emit事件实现。一般在项目中,组件都是分模块文件放置的,比如有两个组件componentA.js,componentB.js,两个文件需要通信时,我们一般会创建一个事件处理文件event.js。

1
2
3
import Vue from "vue";
let bus = new Vue();
export default bus;

触发组件 A 中的事件bus.$emit(‘id-selected’, “xxx”);
在组件 B 创建的钩子中监听事件
bus.$on(‘id-selected’, function (id) {
// …
})
当数据结构较为简单时,推荐使用这种方式;但项目中往往会碰到数据结构较为复杂的情况,这是可以使用状态管理模式vuex。在vuex store中的数据都是响应式的, 跟data数据里面的一样使用,一个组件变化了,另一个组件中的数据自然而然的跟着做响应了。

多层级父子组件通信

在Vue1.0中实现了$broadcast与$dispatch两个方法用来向子组件(或父组件)广播(或派发),当子组件(或父组件)上监听了事件并返回true的时候会向爷孙级组件继续广播(或派发)事件。但是这个方法在Vue2.0里面已经被移除了。在饿了么开源组件库elementUI中又重新实现了broadcast和dispatch.但是跟Vue1.0的两个实现方法略有不同。这两个方法实现了向子孙组件广播以及向多层级父组件事件派发的功能。但是并非广义上的时间广播,它需要指定一个commentName进行向指定组件名组件定向广播(派发事件)。这两个方法的实现是用到$parent和$children,用以遍历子节点或逐级向上查询父节点,访问到指定组件名的时候,调用$emit触发指定事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
function broadcast(commentName, eventName, params) {
/*遍历当前节点下的所有子组件*/
this.$children.forEach(child => {
/*获取子组件名称*/
var name = child.$options.commentName;
if (name === commentName) {
/*如果是我们需要广播到的子组件的时候调用$emit触发所需事件,在子组件中用$on监听*/
this.$emit.apply(child, [eventName].concat(params));
} else {
/*非所需子组件则递归遍历深层次子组件*/
broadcast.apply(child, [commentName, eventName].concat(params));
}
})
}
export default {
methods: {
/*对多级父组件进行事件派发*/
dispatch(commentName, eventName, params) {
/*获取父组件,如果以及是根组件,则是$root*/
var parent = this.$parent || this.$root;
/*获取父节点的组件名*/
var name = parent.optins.commentName;
while(parent && (!name || name !== commentName)) {
/*当父组件不是所需组件时继续向上寻找*/
parent = parent.$parent;
if (parent) {
name = parent.optins.commentName;
}
}
/*找到所需组件后调用$emit触发当前事件*/
if(parent) {
parent.$emit.apply(parent, [eventName].concat(params));
}
},
/*
向所有子组件进行事件广播。
这里包了一层,为了修改broadcast的this对象为当前Vue实例
*/
broadcast(commentName, eventName, params) {
broadcast.call(this, componentName, eventName, params);
}
}
}

vue computed计算属性

发表于 2017-10-15

Vue中的计算属性是所有属性的计算,而这些计算都是变向的在过滤值,通过数据的不断变化计算出来不同的值和操作不同的方法. 而在Vue中,会使用到计算属性的场景常见的有:
1.模板内的表达式
2.属性v-bind内可以进行的表达式
3.指令中可以进行的表达式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<template>
<div>
<p @click="count++">{{count+'分'}}</p>
<input v-model='message'>
<p>{{message.split('').reverse().join('') }}</p>
</div>
</template>
<script>
export default {
data () {
return {
count : 0,
message : ''
}
}
}
</script>

我们可以看到
1.在模板内使用了字符串拼接undefined分
2.通过v-bind绑定了一个click事件,button每点击一次,count值就加1,注意,这里并没有使用methods方法
3.input通过v-model进行了数据双向绑定,绑定了message,并且在p标签中对message进行了字符串反转,这里的操作语义化并不是很明显,更好的方法是用filter
filter的优势
filter给我们用于计算和过滤一些模板表达式和v-bind属性表达式一些弊端的地方进行计算,他会返回当前计算的值,可以进行传参在多地方共用这个过滤方法。
filter的劣势
如果我们要计算多个数据不同变化结合而成的地方那filter就能难过到了,本质上filter我认为就是1v1,对单个数据进行过滤,可以进行传参,同方法,但不同参,非常适用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<template>
<div>
<input v-model='message'>
<p>{{message | reverseString}}</p>
</div>
</template>
<script>
export default {
data () {
return {
message : ''
}
},
filters : {
reverseString (value) {
if(!value) return ''
value = value.split('').reverse().join('')
return value
}
}
}
</script>

computed

computed可以做哪些,又有什么应用场景呢
他规避了模板语法和filter两个所有的劣势,他的优势在于通过计算所有依赖的数据进行计算,然后返回一个值,记住可以依赖方法里所有的数据,只要一个数据发生变化,则会重新计算,来更新view的改变。
应用场景
我们在玩微博的时候,发微博会有一个字数限制,比如说只限输入160个字符。为了让用户体验更好,在文本域中输入内容的时候,同时有一个提示信息,告诉用户你还能输入多少字符。那么使用Vue来做这样的事情就会显得容易的多。比如下面这个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<template>
<div>
<textarea v-model="content" :maxlength="totalcount"></textarea>
<p>你还可以输入{{reduceCount}}字</p>
</div>
</template>
<script>
export default {
data () {
return {
totalcount : 160 , //总共只给输入160字
content : ''
}
},
computed : {
reduceCount () {
return this.totalcount - this.content.length
}
}
}
</script>

在computed创建了一个reduceCount,一直在监听文字的字符长度,来再次进行计算,返回值给视图,让视图进行变化。这也是一个很简单的示例。前面也提到过,我们可以监听多个数据,只要一个数据变了,整个方法就会重新计算,然后反馈到视图。有下面这样的场景应用,一场足球比赛的播报信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
<template>
<div>
<h1>比赛时间{{time}}s</h1>
<h2>直播播报{{result}}</h2>
<div>
<p>中国队进球数:{{team.china}}</p>
<button @click="team.china++">点击中国队进一球</button>
<p>韩国队进球数:{{team.korea}}</p>
<button @click="team.korea++">点击韩国队进一球</button>
</div>
</div>
</template>
<script>
export default {
created () {
let time = setInterval(()=>{
this.time++
if(this.time == 90){
clearInterval(time)
}
},1000)
},
data () {
return {
time : 0,
team : {
china : 0,
korea : 0
}
}
},
computed : {
result () {
if(this.time<90){
if(this.team.china>this.team.korea){
return '中国队领先'
}else if(this.team.china<this.team.korea){
return '韩国队领先'
}else{
return '双方僵持'
}
}else{
if(this.team.china>this.team.korea){
return '中国队赢'
}else if(this.team.china<this.team.korea){
return '韩国队赢'
}else{
return '平局'
}
}
}
}
}
</script>

1.比赛时间,用time维护
2.比赛双方的进球数有team维护
3.比赛的播报情况,在90分钟内,要显示中国领先或者韩国领先或者双方僵持,如果到了90分钟我们要显示中国队赢还是韩国队赢,还是平局
第三个数据我们需要监听时间和进球数来判断比赛的胜负,computed是观察一个或多个数据,当一个数据变化的时候,他就会重新计算,还有就是通过观察所有数据来维护一个状态,就是所谓的返回一个状态值。

computed vs methods

在Vue中,使用methods可以做computed同样的事情,不同的是,computed可以进行缓存。就是在上个例子中我们对比赛时间和两个球队的进球数进行了检测数据。如果随着时间的改变,但是球数没动,对于computed来说就不会再次计算球数而是进入缓存,重新计算的是这个时间,而且页面的DOM更新也会触发methods来重新计算属性。所以,如果不想让计算属性进入缓存,请使用methods,但我个人更推荐使用computed,语义化会更好一点。毕竟是什么选项里就应该做什么事,methods里面就是应该来管事件的。

computed vs watch

computed,watch都可以对数据进行实时监听,但是应用场景不同。
1.computed是适用对多数据变动进行监听,然后维护一个状态,就是返回一个状态值。
2.watch是对一个数据进行监听,在数据变化时,返回两个值,一个是value(当前值),二是oldvalue是变化前的值。
watch的应用场景主要是监听一个数据进行复杂的逻辑操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
<template>
<div>
<h1>比赛时间{{time}}s</h1>
<h2>直播播报{{result}}</h2>
<div>
<p>中国队进球数:{{team.china}}</p>
<button @click="team.china++">点击中国队进一球</button>
<p>韩国队进球数:{{team.korea}}</p>
<button @click="team.korea++">点击韩国队进一球</button>
</div>
</div>
</template>
<script>
export default {
created () {
let time = setInterval(()=>{
this.time++
if(this.time == 90){
clearInterval(time)
}
},1000)
},
data () {
return {
time : 0,
team : {
china : 0,
korea : 0
},
result : "双方僵持"
}
},
watch : {
time (value,oldval) {
if(value<90){
if(this.team.china>this.team.korea){
this.result = '中国队领先'
}else if(this.team.china<this.team.korea){
this.result = '韩国队领先'
}else{
this.result = '双方僵持'
}
}else{
if(this.team.china>this.team.korea){
this.result = '中国队赢'
}else if(this.team.china<this.team.korea){
this.result = '韩国队赢'
}else{
this.result = '平局'
}
}
},
team (value,oldval){
if(this.time<90){
if(value.china>value.korea){
this.result = '中国队领先'
}else if(value.china<value.korea){
this.result = '韩国队领先'
}else{
this.result = '双方僵持'
}
}else{
if(value.china>value.korea){
this.result = '中国队赢'
}else if(value.china<value.korea){
this.result = '韩国队赢'
}else{
this.result = '平局'
}
}
}
}
}
</script>

以上代码和computed产生的效果是一模一样,但是很明显,就像我对computed和watch阐述过了应用场景,这个场景只是维护了一个比赛状态,而不牵扯到逻辑操作。虽然也能完成,但无论从代码量的比对还是可读性,还是可维护性的都不胜于computed。但说到底谁更强大呢?我还是老实说watch更强大,虽然他也有场景的局限性,但是他可以做牵扯到计算属性的一切操作,缺点是watch只能一个一个监听。

总结

Vue官方有一句话说得很重要:

1
虽然计算属性在大多数情况下更合适,但有时也需要一个自定义的watcher。这是为什么Vue提供一个更通用的方法,使用wacth来响应数据的变化。当你想要在数据变化响应时,执行异步操作或开销较大的操作,这是很有用的。

所以根据不同的场景使用不同的方法

computed

监听一个或者多个数据维护返回一个状态值,如果其中一个或者多个数据发生变化,则会重新计算整个函数体,返回一个新的状态值

watcher

只能一个一个监听数据,数据发生变化时返回两个参数,第一个是当前值,第二个是变化前的值。每当数据变化的时候,会触发函数体的逻辑操作,根据逻辑行为进行后续的操作。

Vue的模板

发表于 2017-06-13

Vue.js 使用了基于 HTML 的模板语法,允许开发者声明式地将 DOM 绑定至底层 Vue 实例的数据。所有 Vue.js 的模板都是合法的 HTML ,所以能被遵循规范的浏览器和 HTML 解析器解析。在底层的实现上,Vue 将模板编译成虚拟 DOM 渲染函数。结合响应系统,Vue 能够智能地计算出最少需要重新渲染多少组件,并把 DOM 操作次数减到最少。

模板渲染

Vue模板的渲染过程入下图所示,首先通过模板compiler编译器将模板编译成AST(抽象语法树),再由AST生成Vue的render函数,渲染函数结合数据再生成vnode(Virtual DOM树),对vnode进行diff和patch后生成新的UI。
'vue-template'

Virtual DOM

由于操作真实的DOM会对性能带来损失,可以通过模拟真实的DOM对象树,建立一个虚拟DOM对真实DOM 发生的变化保持追踪。
vnode包含以下属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
this.tag = tag //当前节点的标签名
this.data = data //当前节点的数据对象
this.children = children //数组类型,包含了当前节点的子节点
this.text = text //当前节点的文本,一般文本节点或注释节点会有该属性
this.elm = elm //当前虚拟节点对应的真实的DOM节点
this.ns = undefined //节点的namespace
this.context = context //编译作用域
this.fnContext = undefined //函数节点的上下文
this.fnOptions = undefined //函数options
this.fnScopeId = undefined //函数作用域id
this.key = data && data.key //节点的key属性,用于作为节点的标识,有利于patch的优化
this.componentOptions = componentOptions //创建组件实例时会用到的选项信息
this.componentInstance = undefined //组件实例
this.parent = undefined //组件占位节点
this.raw = false //Raw HTML
this.isStatic = false //静态节点的标识
this.isRootInsert = true //是否作为根节点插入,被<transition>包裹的节点,该属性的值为false
this.isComment = false //当前节点是否是注释节点
this.isCloned = false //当前节点是否为克隆节点
this.isOnce = false //当前节点是否有v-once指令
this.asyncFactory = asyncFactory
this.asyncMeta = undefined
this.isAsyncPlaceholder = false

createElement函数

createElement函数用来创建vnode,createElement接受的参数,

1
2
3
4
5
6
7
8
export function createElement (
context: Component,
tag: any, //一个 HTML 标签字符串,组件选项对象,或者一个返回值
data: any, //一个包含模板相关属性的数据对象
children: any, //子节点 (VNodes),由 `createElement()` 构建而成
normalizationType: any,
alwaysNormalize: boolean
)

render函数

Vue的模板实际是编译成了render函数,可以使用Vue.compile(template)方法编译下面的模板

1
2
3
4
5
6
7
8
9
10
11
<div>
<header>
<h1>I'm a template!</h1>
</header>
<p v-if="message">
{{ message }}
</p>
<p v-else>
No message.
</p>
</div>

方法会返回一个对象,对象中有render和staticRenderFns两个值,下面是生成的render函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
function() {
with(this) {
return _c(
//创建一个div元素
'div',
[
// 静态节点 header,此处对应 staticRenderFns 数组索引为 0 的 render 函数
_m(0),
// 三元表达式,判断 message 是否存在
// 如果存在,创建 p 元素,元素里面有文本,值为 toString(message)
// 如果不存在,创建 p 元素,元素里面有文本,值为 No message.
(message)? _c('p', [ _v( _s(message))]) : _c('p', [ _v("No message.")])
]
)
}
}
其中:
_c : createElement(创建元素)
_m : renderStatic(渲染静态节点)
_v : createTextVNode(创建文本DOM)
_s : toString (转换为字符串)

staticRenderFns数组与diff算法优化相关,我们会在编译阶段给后面不会发生变化的vnode节点打上static为true的标签,那些被标记为静态节点的vnode就会单独生成staticRenderFns函数:

1
2
3
4
5
6
7
8
9
10
_m(0): function anonymous() {
with(this) {
return _c(
'header',
[
_c('h1', [ _v("I'm a template!")])
]
)
}
}

观察者(Watcher)

每个Vue组件都有一个对应的Watcher,这个Watcher会在组件render的时候收集组件所依赖的数据,并在依赖有更新的时候重新渲染组件。
上图中我们可以以render函数作为分割线,render函数左边称为编译期,将vue的模板转换为渲染函数。render函数的右边是在vue的运行时,主要是基于渲染函数生成Virtual Dom树,然后对Virtual Dom进行diff和patch。
下面图描述了vue模板渲染的流程:
vue-template-10

  • new Vue():实例化Vue
  • $mount():获取模板,并且在这过程中调用相关方法_count,new Watcher()实现数据响应式,当Watcher监听到数据变化,就会执行render函数输出一个新的vnode树形结构的数据
  • compileToFunction():将tenplate编译成render函数。首先读缓存,在compileToFunction()中,会创建一个对象,把complie编译完后的对象的render 和 staticRenderFns 两个属性分别转换成函数缓存在对象中,然后把对象存进缓存,没有缓存就调用compile 方法拿到 render 函数的字符串形式,在通过new Function的方式生成真正的渲染函数。
  • compile:将 template 编译成 render 函数的字符串形式,这个函数主要有三个步骤组成:parse,optimize和generate,最终输出一个包含 ast,render 和 staticRenderFns的对象。compile 函数主要是将 template 转换为 AST,优化 AST,再将 AST 转换为render函数字符串,render 函数与数据通过 Watcher 产生关联。
  • update():update判断是否首次渲染,是则直接创建真实DOM,否则调用patch(),并且进行触发钩子和更新引用等其他操作。
  • patch():新旧vnode对比的diff函数,对两个树结构进行完整的diff和patch的过程,最终只有发生了变化的节点才会被更新到真实 DOM 树上。
  • destroy():完全销毁一个实例。清理它与其它实例的连接,解绑它的全部指令及事件监听器。触发beforeDestroy和destroyed的钩子。在大多数场景中你不应该调用这个方法。最好使用 v-if 和 v-for 指令以数据驱动的方式控制子组件的生命周期。

Javascript继承机制

发表于 2017-06-08

在介绍Javascript的继承原理之前,我们先来看下继承方法里需要使用的方法call、apply和new。

call和apply

call方法定义
语法:Function.call([thisObj[,arg1[, arg2[, [,.argN]]]]])
定义:调用一个对象的一个方法,以另一个对象替换当前对象。
说明:
call 方法可以用来代替另一个对象调用一个方法。call方法可将一个函数的对象上下文从初始的上下文改变为由thisObj指定的新对象。如果没有提供 thisObj 参数,那么 Global对象被用作thisObj。
apply方法定义
语法:Function.apply([thisObj[,argArray]])
定义:应用某一对象的一个方法,用另一个对象替换当前对象。
说明:如果没有提供 argArray 和 thisObj任何一个参数,那么 Global 对象将被用作 thisObj,并且无法被传递任何参数。
call方法跟apply方法并没有明显区别,只是传递参数的形式不一样。
call():一个一个的传递参数;
apply():以数组的形式传递。
call模拟方法的实现分为以下几个步骤
1.this参数可以传null,当为null的时候,视为指向window
2.将函数设为对象的属性
3.给定参数并执行该函数
4.删除该函数

1
2
3
4
5
6
7
8
9
10
11
12
Funtion.prototype.call = function(context) {
context = context || window;
context.fn = this;
var args = [];
//eval函数只接受原始字符串作为参数,如果参数不是字符串,那么该方法就会不做任何改变的返回
for (var i = 1, l = arguments.length; i < l; i++) {
args.push('arguments['+ i +']');
}
var result = eval('context.fn('+ args +')');
delete context.fn;
return result;
}

apply模拟方法的实现也是类似的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Function.prototype.apply = function(context, arr) {
context = context || window;
context.fn = this;
var result;
if(!arr) {
result = context.fn();
} else {
var args = [];
for(var i =0, l = arr.length; i < l; i++) {
args.push('arguments['+ i +']');
}
result = eval('context.fn('+ args +')');
}
delete context.fn;
return result;
}

new

new模拟方法的实现
当用new实例化一个构造函数后,我们可以访问到构造函数里的属性以及构造函数原型(prototype)上的属性,所以模拟new的实现分成以下几个步骤
1.创建一个空对象
2.将空对象的原型指向构造函数原型,这样空对象就可以访问到构造函数原型上的属性
3.使用apply,将构造函数this指向改变为新建的对象,这样新建的对象就能访问构造函数上属性
4.如果构造函数中返回的值是对象,则返回这个对象;如果不是,返回新建的对象

1
2
3
4
5
6
7
function newObject() {
var obj = {};
var constructor = Array.prototype.shift.call(arguments);
obj.__proto__ = constructor.prototype;
var result = constructor.apply(obj,arguments);
return typeof result === 'object'? result : obj;
}

寄生组合式继承

在《JavaScript高级程序设计》书中介绍了几种经典的继承方法,但都存在着一些问题。
1.原型链继承:当原型中属性值是个复杂数据类型时,所有的实例都会共享这个数据,其中一个实例修改这个数据其他实例都会受到影响。
2.构造函数继承:方法都在构造函数中定义,每次创建实例都会创建一遍方法。
3.组合继承:最大的缺点是会调用两次父构造函数。一次是设置子类型实例的原型的时候,一次在创建子类型实例的时候。
在高程书的最后介绍了寄生组合式继承,代码实现如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
function object(o) {
function F(){};
F.prototype = o.prototype;
return new F();
}
function inheritPrototype(subType,superType) {
var prototype = object(superType.prototype);
prototype.consturctor = subType;
subType.prototype = prototype;
}
function SuperType(name){
this.name = name;
this.colors = {"red","blue","green"}
}
SuperType.prototype.sayName = function(){
//code
}
function SubType(name,age){
SuperType.call(this,name);
this.age = age;
}
inheritPrototype(SubType,SuperType);
var instance = new SubType();

simple-inheritance库的实现

以下是摘自别人对simple-inheritance的注释。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
(function() {
//initializing用于控制类的初始化,非常巧妙,请留意下文中使用技巧
//fnTest返回一个正则比表达式,用于检测函数中是否含有_super,这样就可以按需重写,提高效率。当然浏览器如果不支持的话就返回一个通用正则表达式,
// /xyz/.test(function() {xyz;})等价于
// /xyz/.test((function() {xyz;}).toString())
var initializing = false,
fnTest = /xyz/.test(function() {
xyz;
}) ? /\b_super\b/ : /.*/;
//所有类的基类Class,这里的this一般是window对象
this.Class = function() {};
//对基类添加extend方法,用于从基类继承
Class.extend = function(prop) {
//保存当前类的原型
var _super = this.prototype;
//创建当前类的对象,用于赋值给子类的prototype,这里非常巧妙的使用父类实例作为子类的原型,而且避免了父类的初始化(通过闭包作用域的initializing控制);
initializing = true;
var prototype = new this();
initializing = false;
//将参数prop中赋值到prototype中,这里的prop中一般是包括init函数和其他函数的对象
for (var name in prop) {
//对应重名函数,需要特殊处理,处理后可以在子函数中使用this._super()调用父类同名构造函数, 这里的fnTest很巧妙:只有子类中含有_super字样时才处理从而提高效率
prototype[name] = typeof prop[name] == "function" && typeof _super[name] == "function" && fnTest.test(prop[name]) ?
(function(name, fn) {
return function() {
//_super在这里是我们的关键字,需要暂时存储一下
var tmp = this._super;
//这里就可以通过this._super调用父类的构造函数了
this._super = _super[name];
//调用子类函数
fn.apply(this, arguments);
//复原_super,如果tmp为空就不需要复原了
tmp && (this._super = tmp);
}
})(name, prop[name]) : prop[name];
}
//当new一个对象时,实际上是调用该类原型上的init方法,注意通过new调用时传递的参数必须和init函数的参数一一对应
function Class() {
if (!initializing && this.init) {
this.init.apply(this, arguments);
}
}
//给子类设置原型
Class.prototype = prototype;
//给子类设置构造函数
Class.prototype.constructor = Class;
//设置子类的extend方法,使得子类也可以通过extend方法被继承
Class.extend = arguments.callee;
return Class;
}
})();

ES6 Class继承

ES6 的class可以看作只是一个语法糖,它的绝大部分功能,ES5都可以做到,新的class写法只是让对象原型的写法更加清晰、更像面向对象编程的语法而已。Class的基本语法如下

1
2
3
4
5
6
7
8
9
10
11
//定义类
class Point {
constructor(x, y) {
this.x = x;
this.y = y;
}
toString() {
return '(' + this.x + ', ' + this.y + ')';
}
}

类的所有方法都是定义在类的prototype属性上面,类的内部所有定义的方法,都是不可枚举的。Class 可以通过extends关键字实现继承,这比ES5的通过修改原型链实现继承,要清晰和方便很多。

1
2
3
4
5
6
7
8
9
10
class ColorPoint extends Point {
constructor(x, y, color) {
super(x, y); // 调用父类的constructor(x, y)
this.color = color;
}
toString() {
return this.color + ' ' + super.toString(); // 调用父类的toString()
}
}

定义了一个ColorPoint类,该类通过extends关键字,继承了Point类的所有属性和方法。在constructor方法和toString方法之中,都出现了super关键字,它在这里表示父类的构造函数,用来新建父类的this对象。子类必须在constructor方法中调用super方法,否则新建实例时会报错。这是因为子类没有自己的this对象,而是继承父类的this对象,然后对其进行加工。如果不调用super方法,子类就得不到this对象。ES5的继承,实质是先创造子类的实例对象this,然后再将父类的方法添加到this上面。ES6的继承机制完全不同,实质是先创造父类的实例对象this(所以必须先调用super方法),然后再用子类的构造函数修改this

Javascript实现多继承

如果有火柴人的粉丝相信对dojo不会陌生,在这个库里使用dojo.declare实现里类的定义机制,不仅可以实现如前几节介绍的单继承,还能实现多继承方式。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
dojo.declare('classX', null,
{
sayX: function(){
console.log("hi, this is X");
},
say: function() {
console.log("hi, welcome!");
}
});
dojo.declare('classY', null,
{
sayY: function(){
console.log("hi, this is Y");
},
say: function() {
console.log("hi, goodbye!");
}
});
dojo.declare('classZ', [classX,classY]);
var objZ = new classZ();
objZ.sayX(); //hi, this is X
objZ.sayY(); //hi, this is Y
objZ.say(); //hi, goodbye!
objZ instanceof classX //true
objZ instanceof Object //true
objZ instanceof classY //flase

从上面代码看到,classZ拥有classX和classY类的全部方法,一般我们会认为classZ继承了classX和classY,那么classZ instanceof classX、classZ instanceof classY应该都是true,然而在JavaScript中只有一个prototype,实际上classZ的父类是classX,classY类似于聚合类,他的属性和方法mixin到classX,达到实现继承的效果。使用ES6 Class简单模拟多继承的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
const mixinClass = (base, ...mixins) => {
const mixinProps = (dst, src) => {
Object.getOwnPropertyNames(src).forEach(prop => {
if (/^constructor$/.test(prop)) { return; }
Object.defineProperty(dst, prop, Object.getOwnPropertyDescriptor(src, prop));
});
};
let ctor;
if (base && typeof base === 'function') {
ctor = class extends base {
constructor(...props) {
super(...props);
}
};
mixins.forEach(src => {
mixinProps(ctor.prototype, src.prototype);
});
} else {
ctor = class {};
}
return ctor;
}
class A {
methodA() {
console.log('methodA');
}
}
class B {
methodB() {
console.log('methodB');
}
}
class C extends mixinClass(A, B) {
methodA() {
console.log('methodA in C');
}
methodC() {
console.log('methodC');
}
}
let classC = new C();
classC.methodA(); //methodA in C
classC.methodB(); //methodB
classC instanceof C; //true
classC instanceof A; //true
classC instanceof B; //false

12
sujinyang

sujinyang

11 日志
© 2019 sujinyang
由 Hexo 强力驱动
主题 - NexT.Pisces