【1】而实现 mvvm 的双向绑定有以下几点:
- 1、vue会将 data 初始化为一个 Observer 监听器,并会用 Object.defineProperty() 进行数据响应式的绑定,而通过递归遍历(new Observer(val) -递归子属性), 将对象中的每个值的属性转换成 getter 和 setter的形式来侦测变化,而 data 中的每个 key,都拥有一个独立的
消息订阅器 Dep(依赖收集器)
,用来收集订阅者,是一个封装好的 Dep 类,专门管理收集到的依赖; - 2、在读取对象属性时,会触发 get() 函数,同时判断是否要添加订阅者,需要则向消息订阅器 Dep添加当前的依赖,收集到的当前的订阅者,也就是 Watcher,最后把Watcher订阅对象到数组里(Dep.target.addDep());
- 3、而在
mount 挂载函数时
,会初始化一个 Watcher(订阅者),而它会在自身实例化时往 消息订阅器Dep 中添加自己,也就是将当前收集器的目标 Dep.target 指向自己,同时触发 get()函数,从而将当前的 Wathcer 与 Dep 关联起来,完成依赖收集
; - 4、在 data 值发生变更时,触发set,调用发布订阅中心的notify,触发了依赖收集器 Dep 类,来遍历当前这个数据的订阅数组,执行里面所有的watcher,
触发 Watcher.update 从而进行视图更新
;
const Observer = function(data) {
for (let key in data) { //===> 循环修改为data中每个属性添加 get set
defineReactive(data, key);
}
}
const defineReactive = function(obj, key) {
const dep = new Dep(); // 局部变量dep,用于 get set 内部调用
// 获取当前值
let val = obj[key];
Object.defineProperty(obj, key, {
enumerable: true, // 设置当前描述属性为可被循环
configurable: true, // 设置当前描述属性可被修改
get() {
console.log('in get');
// 调用依赖收集器中的addSub,用于收集当前属性与Watcher中的依赖关系
dep.depend();
return val;
},
set(newVal) {
if (newVal === val) return;
val = newVal;
//===> 当值发生变更时,通知依赖收集器,更新每个需要更新的Watcher
dep.notify(); // 这里每个需要更新通过什么断定?dep.subs
}
});
}
const observe = function(data) { return new Observer(data); }
const Vue = function(options) {
const self = this;
// 将 data 赋值给 this._data,源码这部分用的 Proxy 所以我们用最简单的方式临时实现
if (options && typeof options.data === 'function') {
this._data = options.data.apply(this);
}
//===> 挂载函数(初始化)
this.mount = function() {
new Watcher(self, self.render);
}
// 渲染函数
this.render = function() {
with(self) {
_data.text;
}
}
observe(this._data); // 监听 this._data
}
const Watcher = function(vm, fn) {
const self = this;
this.vm = vm;
Dep.target = this; // 实例化时(初始化),将当前收集器的目标 Dep.target 指向当前 watcher(自己),缓存自己
this.addDep = function(dep) { //===> 向 Dep 方法添加当前 Wathcer 到 订阅者数组内,我这个Watcher要被塞到Dep里去了
dep.addSub(self);
}
this.update = function() { //===> 更新方法,用于触发 vm._render,Dep通知我更新
console.log('in watcher update');
fn();
}
// 将自己添加到订阅器的操作,这里会首次调用 vm._render,从而触发 text 的 get
// 从而将当前的 Wathcer 与 Dep 关联起来
this.value = fn();
//===> 这里null 是清空了 Dep.target,为了防止notify触发时,不停的绑定Watcher与Dep,造成代码死循环
Dep.target = null; // // 添加完毕,重置,释放自己
}
const Dep = function() {
const self = this;
// 收集目标
this.target = null;
// 存储收集器中需要通知的Watcher
this.subs = [];
//===> 当有目标时,绑定 Dep 与 Wathcer 的关系
this.depend = function() {
if (Dep.target) { // 为了让Watcher初始化进行触发,判断是否需要添加订阅者(初始化)
// 可以直接写 self.addSub(Dep.target),
// 没有这么写因为想还原源码的过程。
Dep.target.addDep(self);
}
}
this.addSub = function(watcher) { //===> 为当前收集器添加 Watcher
self.subs.push(watcher);
}
this.notify = function() { //===> 通知收集器中所的所有Wathcer,调用其update方法
for (let i = 0; i < self.subs.length; i += 1) { // 遍历 订阅者
self.subs[i].update();
}
}
}
const vue = new Vue({
data() { return { text: 'hello world' }; }
})
vue.mount(); // in get
vue._data.text = '123'; // in watcher update /n in get
Observer
负责将数据转换成getter/setter形式;Dep
负责管理数据的依赖列表,是一个发布订阅模式,上游对接Observer
,下游对接Watcher
,而 Watcher 是实际上的数据依赖,负责将数据的变化转发到外界(渲染、回调);- 首先将 data 传入Observer 转成 getter/setter形式;当Watcher实例读取数据时,会触发getter,被收集到Dep仓库中;当数据更新时,触发setter,通知Dep仓库中的所有Watcher实例更新,Watcher实例负责通知外界;
- vue 的diff 算法是 深度优先遍历 还是 广度优先算法? - 在
sameVnode()
会判断是否是相同的节点,然后在patchVnode()
过程中会调用 updateChildren,所以 vue 的 diff 算法是个深度优先算法
。 - Vue3 - API !!
- 正则 match() 方法
可在字符串内检索指定的值,或找到一个或多个正则表达式的匹配,返回一个数组;
方法:
- split():
就是将一字符串以特定的字符分割成多个字符串,join
-方法将数组作为字符串返回;- indexOf():
,可以判断一个元素是否
在数组中存在,或者判断一个字符是否在字符串中存在,如果存在返回该元素或字符在 “第一次出现的” 位置的索引(下标)
, 不存在返回 -1; exmyth 链接 !!!- hasOwnProperty():
检测一个属性是否是对象的自有属性。也是 Javascript 中唯一一个处理对象属性 而不会 往上遍历原型链的。 - JSON.parse(JSON.stringify()) !!!- filter():
和 map() 类似,它也接收一个函数。和 map() 不同的是,它把传入的函数依次作用于每个元素,然后根据返回值是 true 还是 false决定
保留 还是 丢弃 该元素。- 多个字段同时重复 !!!
// 切割域名,获取域名后的地址路径
private getRouterUrl() {
const url = document.location.toString();
const arrUrl = url.split("//"); // ['https:', 'www.baidu.com/index']
const start = arrUrl[1].indexOf("/"); // 下标位置,14
let relUrl = arrUrl[1].substring(start); // stop省略,截取从start开始到结尾的所有字符
// 判断是否有传参,split 切割 ?后的参数
if (relUrl.indexOf("?") != -1) {
relUrl = relUrl.split("?")[0];
}
// console.log(arrUrl);
return relUrl
}
var obj1 = {
name: '孙悟空',
age: 500
};
// 使用for...in遍历对象时,不仅会遍历自身属性,也会遍历原型链上的属性
for(key in obj1) {
if(obj1.hasOwnProperty(key)){
console.log(key) // 打印出: name age
}
}
// trim() 函数去掉字符串首尾空白字符
let arr = ['A', '', 'B', null, undefined, 'C', ' '];
let r = arr.filter((el, index, self) => {
return el && el.trim(); // 注意:IE9 以下的版本没有 trim()方法
});
arr; // ['A', 'B', 'C']
// indexOf() 总是返回第一个元素的位置,后续的重复元素位置与indexOf返回的位置不相等,
// 因此被filter滤掉了,所以重复的元素仅会保留第一个位置的元素
let r,
arr = ['apple', 'strawberry', 'banana', 'pear', 'apple', 'orange', 'orange', 'strawberry'];
r = arr.filter((el, index, self) => {
return self.indexOf(el) === index;
});
alert(r.toString());
// reduce() 还可以将多维数组转化为一维
let newobj = {};
person = person.reduce((cur, next) => {
newobj[next.id] ? "" : newobj[next.id] = true && cur.push(next);
return cur;
}, []) //设置cur默认类型为数组,并且初始值为空的数组
console.log(person);
// '字符串' 反转
arr.split('').reverse().join('');
【1】- 递归
思想:
在这个过程中充分反应了 “传递”(询问)
和 “回归”(反馈)
的思想。同时 递归算法运行效率低、性能消耗大,递归深度较大时慎用(等不到结果),还可能会导致栈溢出。
书写“套路”:
- 1、找到终止条件,写给 if;
- 2、找到函数的等价关系式,写给 return;
// 每隔一秒输出 1,2,3,4,5
const recursiveTest = function(){
console.time("递归时间")
const test = function (i) {
setTimeout(function () {
i = i + 1
console.log('递归输出:' + i)
if(i < 5){
test(i)
} else {
console.timeEnd("递归时间")
}
}, 1000)
}
test(0)
}
recursiveTest();
// 递归求和
function factorial(num) {
// 满足条件退出递归;
if(num <= 2) return num;
// 将递推关系的结构转换为递归体;
return num * factorial(num - 1);
}
// 循环算法,解决了 递归 消耗系统性能的问题,可以计算任意数值
function fn(n) {
// 满足条件退出递归
if(n === 1 || n === 2) return n;
let left = 1; // 左边的数据
let right = 2; // 右边的数据
let sum = 0;
for(var i = 3 ; i <= n ; i++) { // 循环从第3项开始
sum = left + right; // 计算前一次左右数据的和
left = right; // 把前一次的right赋值给下一次的left
right = sum; // 把前一次的和赋值给下一次的right
}
return sum;
}
// 递归实现,https://juejin.cn/post/6844903591090601997#heading-0
let result = '';
const recursion = (cityData, id) => {
// cityData数据为空的时候直接返回
if (!cityData || !cityData.length) return;
// 常规循环cityData
for (let i = 0, len = cityData.length; i < len; i++) {
const childs = cityData[i].children;
// 如果匹配到id的话,就是我们要的结果
if (cityData[i].id === id) result = cityData[i].name
// 如果还有子节点,执行递归
if(childs && childs.length > 0){
recursion(childs, id);
}
}
return result
};
const r = recursion(cityData, 11112);
console.log(r) // 灵芝
出栈与入栈
【2】深浅拷贝-最根本的区别在于 是否真正获取一个对象的复制实体,而不是引用
浅拷贝:一个值 赋给 另一个值,当原先的值不改变地址的情况下改变数据,另一个值跟着变;(会引用同一块内存区域,拷贝的深度不够
)
深拷贝:一个值 赋给 另一个值,当原先的值不改变地址的情况下改变数据,另一个值不变;
- 记区别:
- 基本数据类型的特点:直接存储在 “栈(stack)中” 的数据;
- 引用数据类型的特点:存储的是该对象 “在栈中引用”,真实的数据存放在 “堆内存里”;
- 引用类型在赋值时,赋的值地址;(地址类型,先在堆中申请空间
存
放数据 ,再把堆区的地址赋
给变量) 【— 深入理解】 - ||==》
- 浅拷贝(shallowCopy)只是增加了
一个指针指向 “已存在” 的内存地址
!!! - 深拷贝(deepCopy)是增加了
一个指针并且申请了一个新的内存
,使这个增加的指针指向这个“新的内存”
!!!(获得了实体)
注意 : 原先的值不改变地址的情况下改变数据,如果是改了地址的改变,那将用另一个值的改变与否 判断
深拷贝 浅拷贝 毫无意义。
- Object.getOwnPropertyNames()
- 可以拿到不可遍历属性名
: [‘name’, ‘age’, ‘null_keys’, ‘arr’, ‘foo’];- Object.hasOwnProperty()
- 返回一个布尔值,判断对象是否包含特定的自身(非继承)属性;(忽略“继承属性”)- Object.constructor()
- 返回对创建此对象的数组函数的引用;- Object.instanceof()
- 运算符用于检测构造函数的 prototype 属性
是否出现在某个实例对象的原型链上;- 数组的 typeof 也是 ‘object’,所以用
Array.isArray(obj)
;
let testObj = {
name: "張三",
age: 20,
null_keys: null,
arr: [1, 2, 3, 4],
foo() {
console.log("name")
}
};
// 递归深拷贝 不考虑集合映射
function deepCloneFoo(obj, newsObj = {}) {
// 【1】遍历拿到所有的键名 getOwnPropertyNames()
Object.getOwnPropertyNames(obj).forEach(key => {
// console.log(obj[key]);
// 【2】判断是否为复杂数据类型(引用类型) 以及防止null
if (typeof obj[key] == "object" && obj[key]) {
// 【3】判断原型 数组给初始化空数组
if (obj[key] instanceof Array) {
newsObj[key] = [];
} else {
newsObj[key] = {}; // 对象给初始化空对象
}
cloneFoo(obj[key], newsObj[key]) // 递归函数
} else if (obj[key] instanceof Function) { // 【4】检测 Function 是否存在于参数obj的原型链上
newsObj[key] = obj[key]
} else {
newsObj[key] = obj[key]; // 【5】一般数据类型直接赋值
}
})
return newsObj
}
function deepClone(origin) {
if(origin === null) return null
if(typeof origin !== 'object') return origin;
if(origin.constructor === Date) return new Date(origin);
// 接受两个参数,origin 是原对象
var _target = origin.constructor(); // 保持继承链
// 循环 origin
for(var key in origin) {
// 不遍历其原型链上的属性
if (origin.hasOwnProperty(key)) {
// 如果 origin[key] 是一个引用类型的值,则进入递归逻辑
if (typeof origin[key] === 'object' && origin[key] !== null) {
// 进入递归,此时原始值就是 origin[key],被赋值的对象是 _target[key]
// 注意,上述第一次声明的 _target 将会贯穿整个递归,后续所有的赋值,都将会被 return 到 _target
_target[key] = deepClone(origin[key])
} else {
// 如果不是对象或数组,则进入此逻辑,直接赋值给 _target[key]
_target[key] = origin[key]
}
}
}
return _target // for...in 循环结束后,return 当前上下文的 _target 值
}
【3】map() 数组 去重
- Array.filter()
- 可以看成是一个过滤函数,返回符合条件的元素的数组;- filter() 方法传入一个回调函数,这个回调函数会携带一个参数,参数为当前迭代的项(我们叫它 item )。回调函数
返回 true
的项会保留在数组中,返回 false
的项会被过滤出数组。 - Array.has()
- 在数组中寻找是否包含有匹配的值,如果存在则跳过;是作用于它的后代元素中- ES6在原型上新增了
Array.Includes()
方法(找到返true,否则返false),取代传统IndexOf()
查找字符串的方法(返回某个指定的字符串值在字符串中首次出现的位置
,没找到返-1); - Array.indexOf()
- 如果当前数组的第 i 项
在当前数组中第一次出现的位置不是 i;那么表示第 i 项
是重复的,忽略掉。否则存入结果数组。
// parseInt() 默认有两个参数,第二个参数是进制数
function returnInt(element){
return parseInt(element, 10);
}
["1", "2", "3"].map(returnInt); // 1,2,3
let arr = [1, 2, 3, 4, 5];
let arr2 = arr.map(num => num * 2).filter(num => num > 5); // [6, 8, 10]
// 循环对数组中的元素调用callback 函数, 如果返回true 保留,如果返回false 过滤掉, 返回新数组,老数组不变
let arr_unique = arr.filter(function(item, idx, array){
return idx === array.indexOf(item);
})
【4】reduce() - 会把所有元素按照聚合函数聚合成一个结果
-
1、
previousValue(pre):
“累计器” — 累加器累加回调函数的返回值,Array类型
(,[] 初始)。(上一次调用回调返回的值,或者是提供的初始值(initialValue))
2、currentValue(cur):
数组中当前被处理的元素;
3、index :
当前元素在数组中的索引;
4、array :
调用 reduce 的数组 ; -
Array.Includes()
方法(找到返true,否则返false)
reduce()
- 如何在具有映射键值对的 JS 中将数组转换为对象?,如图:
--->
JS 对象与数组相互转化 ====> 方法2 ====>“ ES10 -Object.fromEntries() + map() + Object.values ”
let add = [
{key: 'a', name: 'John'},
{key: 'b', name: 'Doe'},
];
// js 对象与数组相互转化,对象赋值
let arr = add.reduce((els, cur) => {
els[cur.key] = cur.name
return els
}, {});
// 扁平一个二维数组
var arr = [[1, 2, 8], [3, 4, 9], [5, 6, 10]];
var res = arr.reduce((x, y) => x.concat(y), []);
let arr = [
{'id': 1, 'name': 2}, {'id': 1, 'name': 1}, {'id': 2, 'name': 22}, {'id': 1, 'name': 'f2d1'}, {'id': 2, 'name': 'f21'}, ];
function deteleObject(obj) {
let uniques = [];
let stringify = {};
// 排序
obj.sort(function (a, b, id='id') {
return Number(a[id]) - Number(b[id]);
});
for (let i = 0; i < obj.length; i++) {
if(typeof obj[i] == "object") {
let keys = Object.keys(obj[i]);
let str = "";
for (let j = 0; j < keys.length; j++) { // 遍历对象的键名(keys[j])和值(obj[i][keys[j]])
str += JSON.stringify(keys[j]);
str += JSON.stringify(obj[i][keys[j]]);
// console.log(str); // 此时输出 "id"1"name"1
}
if (!stringify.hasOwnProperty(str)) { // 忽略重复对象
uniques.push(obj[i]);
stringify[str] = true;
}
} else {
if (!stringify.hasOwnProperty(obj[i])) { // 纯数组模式
uniques.push(obj[i]);
stringify[obj[i]] = true;
}
}
}
return uniques
}
console.log(deteleObject(arr));
// 去重
let arr = [1,2,3,4,4,1]
let newArr = arr.reduce((pre, cur) => {
if(!pre.includes(cur)) {
return pre.concat(cur)
} else {
return pre
}
}, []); // [1, 2, 3, 4]
const FTisES6ReduceObjectNum = (arr, newobj = {}) => arr.reduce((preVal, curVal) => {
// newobj[curVal.id] 查看是否有 id 属性值相同的一项,没有则将当前项 curVal 加入数组 preVal ,
// 然后返回修改后数组,作为下一次的初始值进行遍历
newobj[curVal.id] ? '' : newobj[curVal.id] = true && preVal.push(curVal);
return preVal
}, []);
Array.concat()
- 用于连接两个或多个数组;Array.isArray()
- 用于判断一个值是否为数组;
// 求乘积
let arr = [1, 2, 3, 4];
let sum = arr.reduce((x, y) => x + y, 0);
// 将二维数组转化为一维
let arr = [[0, 1], [2, 3], [4, 5]]
let newArr = arr.reduce((pre,cur)=>{
return pre.concat(cur)
},[])
console.log(newArr); // [0, 1, 2, 3, 4, 5]
// 将多维数组转化为一维
let arr = [[0, 1], [2, 3], [4,[5,6,7]]]
const newArr = function(arr) {
return arr.reduce((pre, cur) => pre.concat( Array.isArray(cur) ? newArr(cur) : cur ), [])
}
console.log(newArr(arr)); //[0, 1, 2, 3, 4, 5, 6, 7]