JS
中分为七种内置类型,七种内置类型又分为两大类型:基本类型和对象(Object
)。null
,undefined
,boolea
n,number
,string
,symbol
。JS
的数字类型是浮点类型的,没有整型。而且浮点类型基于 IEEE 754
标准实现,在使用中会遇到某些 Bug。NaN
也属于 number
类型,而且 NaN
不等于自身。let a = 111 // 这只是字面量,不是 number 类型 a.toString() // 使用时候才会转换为对象类型
对象(
Object
)是引用类型,在使用过程当中会遇到浅拷贝和深拷贝的问题。javascript
let a = { name: 'FE' } let b = a b.name = 'EF' console.log(a.name) // EF
typeof
对于基本类型,除了null
均可以显示正确的类型css
typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 没有声明,可是还会显示 undefined
typeof
对于对象,除了函数都会显示object
html
typeof [] // 'object' typeof {} // 'object' typeof console.log // 'function'
对于
null
来讲,虽然它是基本类型,可是会显示object
,这是一个存在好久了的Bug
前端
typeof null // 'object'
PS:为何会出现这种状况呢?由于在
JS
的最第一版本中,使用的是32
位系统,为了性能考虑使用低位存储了变量的类型信息,000
开头表明是对象,然而null
表示为全零,因此将它错误的判断为object
。虽然如今的内部类型判断代码已经改变了,可是对于这个Bug
倒是一直流传下来。vue
Object.prototype.toString.call(xx)
。这样咱们就能够得到相似 [object Type]
的字符串let a // 咱们也能够这样判断 undefined a === undefined // 可是 undefined 不是保留字,可以在低版本浏览器被赋值 let undefined = 1 // 这样判断就会出错 // 因此能够用下面的方式来判断,而且代码量更少 // 由于 void 后面随便跟上一个组成表达式 // 返回就是 undefined a === void 0
转Booleanjava
在条件判断时,除了
undefined
,null
,false
,NaN
,''
,0
,-0
,其余全部值都转为true
,包括全部对象node
对象转基本类型react
对象在转换基本类型时,首先会调用
valueOf
而后调用toString
。而且这两个方法你是能够重写的jquery
let a = { valueOf() { return 0 } }
四则运算符webpack
只有当加法运算时,其中一方是字符串类型,就会把另外一个也转为字符串类型。其余运算只要其中一方是数字,那么另外一方就转为数字。而且加法运算会触发三种类型转换:将值转换为原始值,转换为数字,转换为字符串
1 + '1' // '11' 2 * '2' // 4 [1, 2] + [2, 1] // '1,22,1' // [1, 2].toString() -> '1,2' // [2, 1].toString() -> '2,1' // '1,2' + '2,1' = '1,22,1'
对于加号须要注意这个表达式
'a' + + 'b'
'a' + + 'b' // -> "aNaN" // 由于 + 'b' -> NaN // 你也许在一些代码中看到过 + '1' -> 1
== 操做符
这里来解析一道题目
[] == ![] // -> true
,下面是这个表达式为什么为true
的步骤
// [] 转成 true,而后取反变成 false [] == false // 根据第 8 条得出 [] == ToNumber(false) [] == 0 // 根据第 10 条得出 ToPrimitive([]) == 0 // [].toString() -> '' '' == 0 // 根据第 6 条得出 0 == 0 // -> true
比较运算符
toPrimitive
转换对象unicode
字符索引来比较prototype
属性,除了 Function.prototype.bind()
,该属性指向原型。__proto__
属性,指向了建立该对象的构造函数的原型。其实这个属性指向了 [[prototype]]
,可是 [[prototype]]
是内部属性,咱们并不能访问到,因此使用 _proto_
来访问。__proto__
来寻找不属于该对象的属性,__proto__
将对象链接起来组成了原型链this
在调用 new 的过程当中会发生以上四件事情,咱们也能够试着来本身实现一个 new
function create() { // 建立一个空的对象 let obj = new Object() // 得到构造函数 let Con = [].shift.call(arguments) // 连接到原型 obj.__proto__ = Con.prototype // 绑定 this,执行构造函数 let result = Con.apply(obj, arguments) // 确保 new 出来的是个对象 return typeof result === 'object' ? result : obj }
instanceof
能够正确的判断对象的类型,由于内部机制是经过判断对象的原型链中是否是能找到类型的prototype
咱们也能够试着实现一下
instanceof
function instanceof(left, right) { // 得到类型的原型 let prototype = right.prototype // 得到对象的原型 left = left.__proto__ // 判断对象的类型是否等于类型的原型 while (true) { if (left === null) return false if (prototype === left) return true left = left.__proto__ } }
function foo() { console.log(this.a) } var a = 1 foo() var obj = { a: 2, foo: foo } obj.foo() // 以上二者状况 `this` 只依赖于调用函数前的对象,优先级是第二个状况大于第一个状况 // 如下状况是优先级最高的,`this` 只会绑定在 `c` 上,不会被任何方式修改 `this` 指向 var c = new foo() c.a = 3 console.log(c.a) // 还有种就是利用 call,apply,bind 改变 this,这个优先级仅次于 new
看看箭头函数中的
this
function a() { return () => { return () => { console.log(this) } } } console.log(a()()())
箭头函数实际上是没有
this
的,这个函数中的this
只取决于他外面的第一个不是箭头函数的函数的this
。在这个例子中,由于调用a
符合前面代码中的第一个状况,因此this
是window
。而且 this 一旦绑定了上下文,就不会被任何代码改变
当执行 JS 代码时,会产生三种执行上下文
eval
执行上下文每一个执行上下文中都有三个重要的属性
VO
),包含变量、函数声明和函数的形参,该属性只能在全局上下文中访问JS
采用词法做用域,也就是说变量的做用域是在定义时就决定了)this
var a = 10 function foo(i) { var b = 20 } foo()
对于上述代码,执行栈中有两个上下文:全局上下文和函数 foo 上下文。
stack = [ globalContext, fooContext ]
对于全局上下文来讲,
VO
大概是这样的
globalContext.VO === globe globalContext.VO = { a: undefined, foo: <Function>, }
对于函数
foo
来讲,VO
不能访问,只能访问到活动对象(AO
)
fooContext.VO === foo.AO fooContext.AO { i: undefined, b: undefined, arguments: <> } // arguments 是函数独有的对象(箭头函数没有) // 该对象是一个伪数组,有 `length` 属性且能够经过下标访问元素 // 该对象中的 `callee` 属性表明函数自己 // `caller` 属性表明函数的调用者
对于做用域链,能够把它理解成包含自身变量对象和上级变量对象的列表,经过
[[Scope]]
属性查找上级变量
fooContext.[[Scope]] = [ globalContext.VO ] fooContext.Scope = fooContext.[[Scope]] + fooContext.VO fooContext.Scope = [ fooContext.VO, globalContext.VO ]
接下来让咱们看一个老生常谈的例子,
var
b() // call b console.log(a) // undefined var a = 'Hello world' function b() { console.log('call b') }
想必以上的输出你们确定都已经明白了,这是由于函数和变量提高的缘由。一般提高的解释是说将声明的代码移动到了顶部,这其实没有什么错误,便于你们理解。可是更准确的解释应该是:在生成执行上下文时,会有两个阶段。第一个阶段是建立的阶段(具体步骤是建立
VO
),JS
解释器会找出须要提高的变量和函数,而且给他们提早在内存中开辟好空间,函数的话会将整个函数存入内存中,变量只声明而且赋值为undefined
,因此在第二个阶段,也就是代码执行阶段,咱们能够直接提早使用。
b() // call b second function b() { console.log('call b fist') } function b() { console.log('call b second') } var b = 'Hello world'
var
会产生不少错误,因此在ES6
中引入了let
。let
不能在声明前使用,可是这并非常说的let
不会提高,let
提高了声明但没有赋值,由于临时死区致使了并不能在声明前使用。
var foo = 1 (function foo() { foo = 10 console.log(foo) }()) // -> ƒ foo() { foo = 10 ; console.log(foo) }
由于当
JS
解释器在遇到非匿名的当即执行函数时,会建立一个辅助的特定对象,而后将函数名称做为这个对象的属性,所以函数内部才能够访问到foo
,可是这个值又是只读的,因此对它的赋值并不生效,因此打印的结果仍是这个函数,而且外部的值也没有发生更改。
specialObject = {}; Scope = specialObject + Scope; foo = new FunctionExpression; foo.[[Scope]] = Scope; specialObject.foo = foo; // {DontDelete}, {ReadOnly} delete Scope[0]; // remove specialObject from the front of scope chain
闭包的定义很简单:函数 A 返回了一个函数 B,而且函数 B 中使用了函数 A 的变量,函数 B 就被称为闭包。
function A() { let a = 1 function B() { console.log(a) } return B }
你是否会疑惑,为何函数
A
已经弹出调用栈了,为何函数B
还能引用到函数A
中的变量。由于函数A
中的变量这时候是存储在堆上的。如今的JS
引擎能够经过逃逸分析辨别出哪些变量须要存储在堆上,哪些须要存储在栈上。
经典面试题,循环中使用闭包解决 var 定义函数的问题
for ( var i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
setTimeout
是个异步函数,全部会先把循环所有执行完毕,这时候 i
就是 6
了,因此会输出一堆 6
。for (var i = 1; i <= 5; i++) { (function(j) { setTimeout(function timer() { console.log(j); }, j * 1000); })(i); }
setTimeout
的第三个参数for ( var i=1; i<=5; i++) { setTimeout( function timer(j) { console.log( j ); }, i*1000, i); }
第三种就是使用
let
定义i
了
for ( let i=1; i<=5; i++) { setTimeout( function timer() { console.log( i ); }, i*1000 ); }
由于对于
let
来讲,他会建立一个块级做用域,至关于
{ // 造成块级做用域 let i = 0 { let ii = i setTimeout( function timer() { console.log( i ); }, i*1000 ); } i++ { let ii = i } i++ { let ii = i } ... }
letet a a = { age : 1 } let b = a a.age = 2 console.log(b.age) // 2
浅拷贝
首先能够经过
Object.assign
来解决这个问题
let a = { age: 1 } let b = Object.assign({}, a) a.age = 2 console.log(b.age) // 1
固然咱们也能够经过展开运算符
(…)
来解决
let a = { age: 1 } let b = {...a} a.age = 2 console.log(b.age) // 1
一般浅拷贝就能解决大部分问题了,可是当咱们遇到以下状况就须要使用到深拷贝了
let a = { age: 1, jobs: { first: 'FE' } } let b = {...a} a.jobs.first = 'native' console.log(b.jobs.first) // native
浅拷贝只解决了第一层的问题,若是接下去的值中还有对象的话,那么就又回到刚开始的话题了,二者享有相同的引用。要解决这个问题,咱们须要引入深拷
深拷贝
这个问题一般能够经过
JSON.parse(JSON.stringify(object))
来解决
let a = { age: 1, jobs: { first: 'FE' } } let b = JSON.parse(JSON.stringify(a)) a.jobs.first = 'native' console.log(b.jobs.first) // FE
可是该方法也是有局限性的:
undefined
let obj = { a: 1, b: { c: 2, d: 3, }, } obj.c = obj.b obj.e = obj.a obj.b.c = obj.c obj.b.d = obj.b obj.b.e = obj.b.c let newObj = JSON.parse(JSON.stringify(obj)) console.log(newObj)
若是你有这么一个循环引用对象,你会发现你不能经过该方法深拷贝
undefined
的时候,该对象也不能正常的序列化let a = { age: undefined, jobs: function() {}, name: 'poetries' } let b = JSON.parse(JSON.stringify(a)) console.log(b) // {name: "poetries"}
lodash
的深拷贝函数。在有
Babel
的状况下,咱们能够直接使用ES6
的模块化
// file a.js export function a() {} export function b() {} // file b.js export default function() {} import {a, b} from './a.js' import XXX from './b.js'
CommonJS
CommonJs
是Node
独有的规范,浏览器中使用就须要用到Browserify
解析了。
// a.js module.exports = { a: 1 } // or exports.a = 1 // b.js var module = require('./a.js') module.a // -> log 1
在上述代码中,
module.exports
和exports
很容易混淆,让咱们来看看大体内部实现
var module = require('./a.js') module.a // 这里其实就是包装了一层当即执行函数,这样就不会污染全局变量了, // 重要的是 module 这里,module 是 Node 独有的一个变量 module.exports = { a: 1 } // 基本实现 var module = { exports: {} // exports 就是个空对象 } // 这个是为何 exports 和 module.exports 用法类似的缘由 var exports = module.exports var load = function (module) { // 导出的东西 var a = 1 module.exports = a return module.exports };
再来讲说
module.exports
和exports
,用法实际上是类似的,可是不能对exports
直接赋值,不会有任何效果。
对于
CommonJS
和ES6
中的模块化的二者区别是:
require(${path}/xx.js)
,后者目前不支持,可是已有提案,前者是同步导入,由于用于服务端,文件都在本地,同步导入即便卡住主线程影响也不大。require/exports
来执行的AMD
AMD
是由RequireJS
提出的
// AMD define(['./a', './b'], function(a, b) { a.do() b.do() }) define(function(require, exports, module) { var a = require('./a') a.doSomething() var b = require('./b') b.doSomething() })
你是否在平常开发中遇到一个问题,在滚动事件中须要作个复杂计算或者实现一个按钮的防二次点击操做。
wait
,防抖的状况下只会调用一次,而节流的 状况会每隔必定时间(参数wait
)调用函数// 这个是用来获取当前时间戳的 function now() { return +new Date() } /** * 防抖函数,返回函数连续调用时,空闲时间必须大于或等于 wait,func 才会执行 * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {boolean} immediate 设置为ture时,是否当即调用函数 * @return {function} 返回客户调用函数 */ function debounce (func, wait = 50, immediate = true) { let timer, context, args // 延迟执行函数 const later = () => setTimeout(() => { // 延迟函数执行完毕,清空缓存的定时器序号 timer = null // 延迟执行的状况下,函数会在延迟函数中执行 // 使用到以前缓存的参数和上下文 if (!immediate) { func.apply(context, args) context = args = null } }, wait) // 这里返回的函数是每次实际调用的函数 return function(...params) { // 若是没有建立延迟执行函数(later),就建立一个 if (!timer) { timer = later() // 若是是当即执行,调用函数 // 不然缓存参数和调用上下文 if (immediate) { func.apply(this, params) } else { context = this args = params } // 若是已有延迟执行函数(later),调用的时候清除原来的并从新设定一个 // 这样作延迟函数会从新计时 } else { clearTimeout(timer) timer = later() } } }
null
,就能够再次点击了。ID
,若是是延迟调用就调用函数防抖动和节流本质是不同的。防抖动是将屡次执行变为最后一次执行,节流是将屡次执行变成每隔一段时间执行
/** * underscore 节流函数,返回函数连续调用时,func 执行频率限定为 次 / wait * * @param {function} func 回调函数 * @param {number} wait 表示时间窗口的间隔 * @param {object} options 若是想忽略开始函数的的调用,传入{leading: false}。 * 若是想忽略结尾函数的调用,传入{trailing: false} * 二者不能共存,不然函数不能执行 * @return {function} 返回客户调用函数 */ _.throttle = function(func, wait, options) { var context, args, result; var timeout = null; // 以前的时间戳 var previous = 0; // 若是 options 没传则设为空对象 if (!options) options = {}; // 定时器回调函数 var later = function() { // 若是设置了 leading,就将 previous 设为 0 // 用于下面函数的第一个 if 判断 previous = options.leading === false ? 0 : _.now(); // 置空一是为了防止内存泄漏,二是为了下面的定时器判断 timeout = null; result = func.apply(context, args); if (!timeout) context = args = null; }; return function() { // 得到当前时间戳 var now = _.now(); // 首次进入前者确定为 true // 若是须要第一次不执行函数 // 就将上次时间戳设为当前的 // 这样在接下来计算 remaining 的值时会大于0 if (!previous && options.leading === false) previous = now; // 计算剩余时间 var remaining = wait - (now - previous); context = this; args = arguments; // 若是当前调用已经大于上次调用时间 + wait // 或者用户手动调了时间 // 若是设置了 trailing,只会进入这个条件 // 若是没有设置 leading,那么第一次会进入这个条件 // 还有一点,你可能会以为开启了定时器那么应该不会进入这个 if 条件了 // 其实仍是会进入的,由于定时器的延时 // 并非准确的时间,极可能你设置了2秒 // 可是他须要2.2秒才触发,这时候就会进入这个条件 if (remaining <= 0 || remaining > wait) { // 若是存在定时器就清理掉不然会调用二次回调 if (timeout) { clearTimeout(timeout); timeout = null; } previous = now; result = func.apply(context, args); if (!timeout) context = args = null; } else if (!timeout && options.trailing !== false) { // 判断是否设置了定时器和 trailing // 没有的话就开启一个定时器 // 而且不能不能同时设置 leading 和 trailing timeout = setTimeout(later, remaining); } return result; }; };
在 ES5 中,咱们能够使用以下方式解决继承的问题
function Super() {} Super.prototype.getNumber = function() { return 1 } function Sub() {} let s = new Sub() Sub.prototype = Object.create(Super.prototype, { constructor: { value: Sub, enumerable: false, writable: true, configurable: true } })
ES6
中,咱们能够经过 class
语法轻松解决这个问题class MyDate extends Date { test() { return this.getTime() } } let myDate = new MyDate() myDate.test()
ES6
不是全部浏览器都兼容,因此咱们须要使用 Babel
来编译这段代码。myDate.test()
你会惊奇地发现出现了报错由于在
JS
底层有限制,若是不是由Date
构造出来的实例的话,是不能调用Date
里的函数的。因此这也侧面的说明了:ES6
中的class
继承与ES5
中的通常继承写法是不一样的。
Date
构造出来,那么咱们能够改变下思路实现继承function MyData() { } MyData.prototype.test = function () { return this.getTime() } let d = new Date() Object.setPrototypeOf(d, MyData.prototype) Object.setPrototypeOf(MyData.prototype, Date.prototype)
_proto__
转而链接到子类的 prototype
=> 子类的 prototype
的 __proto__
改成父类的 prototype
。JS
底层的这个限制call
和 apply
都是为了解决改变 this
的指向。做用都是相同的,只是传参的方式不一样。call
能够接收一个参数列表,apply
只接受一个参数数组let a = { value: 1 } function getValue(name, age) { console.log(name) console.log(age) console.log(this.value) } getValue.call(a, 'yck', '24') getValue.apply(a, ['yck', '24'])
Promise
当作一个状态机。初始是 pending
状态,能够经过函数 resolve
和 reject
,将状态转变为 resolved
或者 rejected
状态,状态一旦改变就不能再次变化。then
函数会返回一个 Promise
实例,而且该返回值是一个新的实例而不是以前的实例。由于 Promise
规范规定除了 pending
状态,其余状态是不能够改变的,若是返回的是一个相同实例的话,多个 then
调用就失去意义了。then
来讲,本质上能够把它当作是 flatMap
// 三种状态 const PENDING = "pending"; const RESOLVED = "resolved"; const REJECTED = "rejected"; // promise 接收一个函数参数,该函数会当即执行 function MyPromise(fn) { let _this = this; _this.currentState = PENDING; _this.value = undefined; // 用于保存 then 中的回调,只有当 promise // 状态为 pending 时才会缓存,而且每一个实例至多缓存一个 _this.resolvedCallbacks = []; _this.rejectedCallbacks = []; _this.resolve = function (value) { if (value instanceof MyPromise) { // 若是 value 是个 Promise,递归执行 return value.then(_this.resolve, _this.reject) } setTimeout(() => { // 异步执行,保证执行顺序 if (_this.currentState === PENDING) { _this.currentState = RESOLVED; _this.value = value; _this.resolvedCallbacks.forEach(cb => cb()); } }) }; _this.reject = function (reason) { setTimeout(() => { // 异步执行,保证执行顺序 if (_this.currentState === PENDING) { _this.currentState = REJECTED; _this.value = reason; _this.rejectedCallbacks.forEach(cb => cb()); } }) } // 用于解决如下问题 // new Promise(() => throw Error('error)) try { fn(_this.resolve, _this.reject); } catch (e) { _this.reject(e); } } MyPromise.prototype.then = function (onResolved, onRejected) { var self = this; // 规范 2.2.7,then 必须返回一个新的 promise var promise2; // 规范 2.2.onResolved 和 onRejected 都为可选参数 // 若是类型不是函数须要忽略,同时也实现了透传 // Promise.resolve(4).then().then((value) => console.log(value)) onResolved = typeof onResolved === 'function' ? onResolved : v => v; onRejected = typeof onRejected === 'function' ? onRejected : r => throw r; if (self.currentState === RESOLVED) { return (promise2 = new MyPromise(function (resolve, reject) { // 规范 2.2.4,保证 onFulfilled,onRjected 异步执行 // 因此用了 setTimeout 包裹下 setTimeout(function () { try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === REJECTED) { return (promise2 = new MyPromise(function (resolve, reject) { setTimeout(function () { // 异步执行onRejected try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (reason) { reject(reason); } }); })); } if (self.currentState === PENDING) { return (promise2 = new MyPromise(function (resolve, reject) { self.resolvedCallbacks.push(function () { // 考虑到可能会有报错,因此使用 try/catch 包裹 try { var x = onResolved(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); self.rejectedCallbacks.push(function () { try { var x = onRejected(self.value); resolutionProcedure(promise2, x, resolve, reject); } catch (r) { reject(r); } }); })); } }; // 规范 2.3 function resolutionProcedure(promise2, x, resolve, reject) { // 规范 2.3.1,x 不能和 promise2 相同,避免循环引用 if (promise2 === x) { return reject(new TypeError("Error")); } // 规范 2.3.2 // 若是 x 为 Promise,状态为 pending 须要继续等待不然执行 if (x instanceof MyPromise) { if (x.currentState === PENDING) { x.then(function (value) { // 再次调用该函数是为了确认 x resolve 的 // 参数是什么类型,若是是基本类型就再次 resolve // 把值传给下个 then resolutionProcedure(promise2, value, resolve, reject); }, reject); } else { x.then(resolve, reject); } return; } // 规范 2.3.3.3.3 // reject 或者 resolve 其中一个执行过得话,忽略其余的 let called = false; // 规范 2.3.3,判断 x 是否为对象或者函数 if (x !== null && (typeof x === "object" || typeof x === "function")) { // 规范 2.3.3.2,若是不能取出 then,就 reject try { // 规范 2.3.3.1 let then = x.then; // 若是 then 是函数,调用 x.then if (typeof then === "function") { // 规范 2.3.3.3 then.call( x, y => { if (called) return; called = true; // 规范 2.3.3.3.1 resolutionProcedure(promise2, y, resolve, reject); }, e => { if (called) return; called = true; reject(e); } ); } else { // 规范 2.3.3.4 resolve(x); } } catch (e) { if (called) return; called = true; reject(e); } } else { // 规范 2.3.4,x 为基本类型 resolve(x); } }
Generator
是ES6
中新增的语法,和Promise
同样,均可以用来异步编程
// 使用 * 表示这是一个 Generator 函数 // 内部能够经过 yield 暂停代码 // 经过调用 next 恢复执行 function* test() { let a = 1 + 2; yield 2; yield 3; } let b = test(); console.log(b.next()); // > { value: 2, done: false } console.log(b.next()); // > { value: 3, done: false } console.log(b.next()); // > { value: undefined, done: true }
从以上代码能够发现,加上
*
的函数执行后拥有了next
函数,也就是说函数执行后返回了一个对象。每次调用next
函数能够继续执行被暂停的代码。如下是Generator
函数的简单实现
// cb 也就是编译过的 test 函数 function generator(cb) { return (function() { var object = { next: 0, stop: function() {} }; return { next: function() { var ret = cb(object); if (ret === undefined) return { value: undefined, done: true }; return { value: ret, done: false }; } }; })(); } // 若是你使用 babel 编译后能够发现 test 函数变成了这样 function test() { var a; return generator(function(_context) { while (1) { switch ((_context.prev = _context.next)) { // 能够发现经过 yield 将代码分割成几块 // 每次执行 next 函数就执行一块代码 // 而且代表下次须要执行哪块代码 case 0: a = 1 + 2; _context.next = 4; return 2; case 4: _context.next = 6; return 3; // 执行完毕 case 6: case "end": return _context.stop(); } } }); }
Proxy
是ES6
中新增的功能,能够用来自定义对象中的操做
let p = new Proxy(target, handler); // `target` 表明须要添加代理的对象 // `handler` 用来自定义对象中的操做 能够很方便的使用 Proxy 来实现一个数据绑定和监听 let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { setBind(value); return Reflect.set(target, property, value); } }; return new Proxy(obj, handler); }; let obj = { a: 1 } let value let p = onWatch(obj, (v) => { value = v }, (target, property) => { console.log(`Get '${property}' = ${target[property]}`); }) p.a = 2 // bind `value` to `2` p.a // -> Get 'a' = 2
事件触发三阶段
document
往事件触发处传播,遇到注册的捕获事件会触发document
传播,遇到注册的冒泡事件会触发事件触发通常来讲会按照上面的顺序进行,可是也有特例,若是给一个目标节点同时注册冒泡和捕获事件,事件触发会按照注册的顺序执行
// 如下会先打印冒泡而后是捕获 node.addEventListener('click',(event) =>{ console.log('冒泡') },false); node.addEventListener('click',(event) =>{ console.log('捕获 ') },true)
注册事件
addEventListener
注册事件,该函数的第三个参数能够是布尔值,也能够是对象。对于布尔值 useCapture
参数来讲,该参数默认值为 false
。useCapture
决定了注册的事件是捕获事件仍是冒泡事件stopPropagation
来阻止事件的进一步传播。一般咱们认为 stopPropagation
是用来阻止事件冒泡的,其实该函数也能够阻止捕获事件。stopImmediatePropagation
一样也能实现阻止事件,可是还能阻止该事件目标执行别的注册事件node.addEventListener('click',(event) =>{ event.stopImmediatePropagation() console.log('冒泡') },false); // 点击 node 只会执行上面的函数,该函数不会执行 node.addEventListener('click',(event) => { console.log('捕获 ') },true)
事件代理
若是一个节点中的子节点是动态生成的,那么子节点须要注册事件的话应该注册在父节点上
<ul id="ul"> <li>1</li> <li>2</li> <li>3</li> <li>4</li> <li>5</li> </ul> <script> let ul = document.querySelector('##ul') ul.addEventListener('click', (event) => { console.log(event.target); }) </script>
事件代理的方式相对于直接给目标注册事件来讲,有如下优势
由于浏览器出于安全考虑,有同源策略。也就是说,若是协议、域名或者端口有一个不一样就是跨域,
Ajax
请求会失败
JSONP
JSONP
的原理很简单,就是利用<script>
标签没有跨域限制的漏洞。经过<script>
标签指向一个须要访问的地址并提供一个回调函数来接收数据当须要通信时
<script src="http://domain/api?param1=a¶m2=b&callback=jsonp"></script> <script> function jsonp(data) { console.log(data) } </script>
JSONP
使用简单且兼容性不错,可是只限于 get
请求CORS
CORS
须要浏览器和后端同时支持CORS
通讯,实现CORS
通讯的关键是后端。只要后端实现了 CORS
,就实现了跨域。Access-Control-Allow-Origin
就能够开启 CORS
。 该属性表示哪些域名能够访问资源,若是设置通配符则表示全部网站均可以访问资源document.domain
a.test.com
和 b.test.com
适用于该方式。document.domain = 'test.com'
表示二级域名都相同就能够实现跨域postMessage
这种方式一般用于获取嵌入页面中的第三方页面数据。一个页面发送消息,另外一个页面判断来源并接收消息
// 发送消息端 window.parent.postMessage('message', 'http://blog.poetries.com'); // 接收消息端 var mc = new MessageChannel(); mc.addEventListener('message', (event) => { var origin = event.origin || event.originalEvent.origin; if (origin === 'http://blog.poetries.com') { console.log('验证经过') } });
JS中的event loop
众所周知
JS
是门非阻塞单线程语言,由于在最初JS
就是为了和浏览器交互而诞生的。若是JS
是门多线程的语言话,咱们在多个线程中处理DOM
就可能会发生问题(一个线程中新加节点,另外一个线程中删除节点)
JS
在执行的过程当中会产生执行环境,这些执行环境会被顺序的加入到执行栈中。若是遇到异步的代码,会被挂起并加入到 Task
(有多种 task
) 队列中。一旦执行栈为空,Event
Loop
就会从 Task
队列中拿出须要执行的代码并放入执行栈中执行,因此本质上来讲 JS
中的异步仍是同步行为console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); console.log('script end');
不一样的任务源会被分配到不一样的
Task
队列中,任务源能够分为 微任务(microtask
) 和 宏任务(macrotask
)。在ES6
规范中,microtask
称为 jobs,macrotask 称为 task
console.log('script start'); setTimeout(function() { console.log('setTimeout'); }, 0); new Promise((resolve) => { console.log('Promise') resolve() }).then(function() { console.log('promise1'); }).then(function() { console.log('promise2'); }); console.log('script end'); // script start => Promise => script end => promise1 => promise2 => setTimeout
以上代码虽然
setTimeout
写在Promise
以前,可是由于Promise
属于微任务而setTimeout
属于宏任务
微任务
process.nextTick
promise
Object.observe
MutationObserver
宏任务
script
setTimeout
setInterval
setImmediate
I/O
UI rendering
宏任务中包括了
script
,浏览器会先执行一个宏任务,接下来有异步代码的话就先执行微任务
因此正确的一次 Event loop 顺序是这样的
Event loop
,执行宏任务中的异步代码经过上述的
Event loop
顺序可知,若是宏任务中的异步代码有大量的计算而且须要操做DOM
的话,为了更快的响应界面响应,咱们能够把操做DOM
放入微任务中
Node 中的 Event loop
Node
中的 Event loop
和浏览器中的不相同。Node
的 Event loop
分为6
个阶段,它们会按照顺序反复运行┌───────────────────────┐ ┌─>│ timers │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ I/O callbacks │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ │ │ idle, prepare │ │ └──────────┬────────────┘ ┌───────────────┐ │ ┌──────────┴────────────┐ │ incoming: │ │ │ poll │<──connections─── │ │ └──────────┬────────────┘ │ data, etc. │ │ ┌──────────┴────────────┐ └───────────────┘ │ │ check │ │ └──────────┬────────────┘ │ ┌──────────┴────────────┐ └──┤ close callbacks │ └───────────────────────┘
timer
timers
阶段会执行 setTimeout
和 setInterval
I/O
I/O
阶段会执行除了 close
事件,定时器和 setImmediate
的回调poll
poll
阶段很重要,这一阶段中,系统会作两件事情
poll
队列中的事件而且当 poll
中没有定时器的状况下,会发现如下两件事情
poll
队列不为空,会遍历回调队列并同步执行,直到队列为空或者系统限制poll
队列为空,会有两件事发生setImmediate
须要执行,poll
阶段会中止而且进入到 check
阶段执行 setImmediate
setImmediate
须要执行,会等待回调被加入到队列中并当即执行回调timer
阶段执行回调。check
check
阶段执行 setImmediate
close callbacks
close callbacks
阶段执行 close
事件Node
中,有些状况下的定时器执行顺序是随机的setTimeout(() => { console.log('setTimeout'); }, 0); setImmediate(() => { console.log('setImmediate'); }) // 这里可能会输出 setTimeout,setImmediate // 可能也会相反的输出,这取决于性能 // 由于可能进入 event loop 用了不到 1 毫秒,这时候会执行 setImmediate // 不然会执行 setTimeout
上面介绍的都是
macrotask
的执行状况,microtask
会在以上每一个阶段完成后当即执行
setTimeout(()=>{ console.log('timer1') Promise.resolve().then(function() { console.log('promise1') }) }, 0) setTimeout(()=>{ console.log('timer2') Promise.resolve().then(function() { console.log('promise2') }) }, 0) // 以上代码在浏览器和 node 中打印状况是不一样的 // 浏览器中必定打印 timer1, promise1, timer2, promise2 // node 中可能打印 timer1, timer2, promise1, promise2 // 也可能打印 timer1, promise1, timer2, promise2
Node
中的process.nextTick
会先于其余microtask
执行
setTimeout(() => { console.log("timer1"); Promise.resolve().then(function() { console.log("promise1"); }); }, 0); process.nextTick(() => { console.log("nextTick"); }); // nextTick, timer1, promise1
Service workers
本质上充当Web应用程序与浏览器之间的代理服务器,也能够在网络可用时做为浏览器和网络间的代理。它们旨在(除其余以外)使得可以建立有效的离线体验,拦截网络请求并基于网络是否可用以及更新的资源是否驻留在服务器上来采起适当的动做。他们还容许访问推送通知和后台同步API
目前该技术一般用来作缓存文件,提升首屏速度
// index.js if (navigator.serviceWorker) { navigator.serviceWorker .register("sw.js") .then(function(registration) { console.log("service worker 注册成功"); }) .catch(function(err) { console.log("servcie worker 注册失败"); }); } // sw.js // 监听 `install` 事件,回调中缓存所需文件 self.addEventListener("install", e => { e.waitUntil( caches.open("my-cache").then(function(cache) { return cache.addAll(["./index.html", "./index.js"]); }) ); }); // 拦截全部请求事件 // 若是缓存中已经有请求的数据就直接用缓存,不然去请求数据 self.addEventListener("fetch", e => { e.respondWith( caches.match(e.request).then(function(response) { if (response) { return response; } console.log("fetch source"); }) ); });
打开页面,能够在开发者工具中的
Application
看到Service Worker
已经启动了
在 Cache 中也能够发现咱们所需的文件已被缓存
当咱们从新刷新页面能够发现咱们缓存的数据是从
Service
Worker
中读取的
浏览器的渲染机制通常分为如下几个步骤
HTML
并构建 DOM
树。CSS
构建 CSSOM
树。DOM
与 CSSOM
合并成一个渲染树。GPU
绘制,合成图层,显示在屏幕上图层
通常来讲,能够把普通文档流当作一个图层。特定的属性能够生成一个新的图层。不一样的图层渲染互不影响,因此对于某些频繁须要渲染的建议单独生成一个新图层,提升性能。但也不能生成过多的图层,会引发副作用
3D
变换:translate3d
、translateZ
will-change
video
、iframe
标签opacity
动画转换position: fixed
重绘(Repaint)和回流(Reflow)
color
就叫称为重绘回流一定会发生重绘,重绘不必定会引起回流。回流所需的成本比重绘高的多,改变深层次的节点极可能致使父节点的一系列回流
window
大小不少人不知道的是,重绘和回流其实和 Event loop
有关
Event loop
执行完 Microtasks
后,会判断 document
是否须要更新。由于浏览器是 60Hz
的刷新率,每 16ms
才会更新一次。resize
或者 scroll
,有的话会去触发事件,因此 resize
和 scroll
事件也是至少 16ms
才会触发一次,而且自带节流功能。media query
requestAnimationFrame
回调IntersectionObserver
回调,该方法用于判断元素是否可见,能够用于懒加载上,可是兼容性很差requestIdleCallback
回调减小重绘和回流
translate
替代 top
visibility
替换display: none
,由于前者只会引发重绘,后者会引起回流(改变了布局)table
布局,可能很小的一个小改动会形成整个 table 的从新布局requestAnimationFrame
CSS
选择符从右往左匹配查找,避免 DOM
深度过深video
标签,浏览器会自动将该节点变为图层DNS
解析也是须要时间的,能够经过预解析的方式来预先得到域名所对应的 IP
<link rel="dns-prefetch" href="//blog.poetries.top">
强缓存
实现强缓存能够经过两种响应头实现:
Expires
和Cache-Control
。强缓存表示在缓存期间不须要请求,state code
为200
Expires: Wed, 22 Oct 2018 08:41:00 GMT
Expires
是HTTP / 1.0
的产物,表示资源会在Wed, 22 Oct 2018 08:41:00 GMT
后过时,须要再次请求。而且Expires
受限于本地时间,若是修改了本地时间,可能会形成缓存失效
Cache-control: max-age=30
Cache-Control
出现于HTTP / 1.1
,优先级高于Expires
。该属性表示资源会在30
秒后过时,须要再次请求
协商缓存
Last-Modified
和 If-Modified-Since
Last-Modified
表示本地文件最后修改日期,If-Modified-Since
会将 Last-Modified
的值发送给服务器,询问服务器在该日期后资源是否有更新,有更新的话就会将新的资源发送回来Last-Modified
被修改,因此在 HTTP / 1.1
出现了 ETag
ETag
和 If-None-Match
ETag
相似于文件指纹,If-None-Match
会将当前 ETag
发送给服务器,询问该资源 ETag 是否变更,有变更的话就将新的资源发送回来。而且 ETag
优先级比 Last-Modified
高选择合适的缓存策略
对于大部分的场景均可以使用强缓存配合协商缓存解决,可是在一些特殊的地方可能须要选择特殊的缓存策略
Cache-control: no-store
,表示该资源不须要缓存Cache-Control: no-cache
并配合 ETag
使用,表示该资源已被缓存,可是每次都会发送请求询问资源是否更新。Cache-Control: max-age=31536000
并配合策略缓存使用,而后对文件进行指纹处理,一旦文件名变更就会马上下载新的文件HTTP / 1.1
时代,每一个请求都须要创建和断开,消耗了好几个 RTT
时间,而且因为 TCP
慢启动的缘由,加载体积大的文件会须要更多的时间HTTP / 2.0
中引入了多路复用,可以让多个请求使用同一个 TCP
连接,极大的加快了网页的加载速度。而且还支持 Header
压缩,进一步的减小了请求的数据大小fetch
,强制浏览器请求资源,而且不会阻塞 onload
事件,能够使用如下代码开启预加载<link rel="preload" href="http://example.com">
预加载能够必定程度上下降首屏的加载时间,由于能够将一些不影响首屏但重要的文件延后加载,惟一缺点就是兼容性很差
能够经过预渲染将下载的文件预先在后台渲染,能够使用如下代码开启预渲染
<link rel="prerender" href="http://poetries.com">
懒执行
懒加载
懒加载的原理就是只加载自定义区域(一般是可视区域,但也能够是即将进入可视区域)内须要加载的东西。对于图片来讲,先设置图片标签的
src
属性为一张占位图,将真实的图片资源放入一个自定义属性中,当进入自定义区域时,就将自定义属性替换为src
属性,这样图片就会去下载资源,实现了图片懒加载
图片优化
对于如何优化图片,有 2 个思路
图片加载优化
CSS
去代替。base64
格式WebP
格式的浏览器尽可能使用 WebP
格式。由于 WebP
格式具备更好的图像数据压缩算法,能带来更小的图片体积,并且拥有肉眼识别无差别的图像质量,缺点就是兼容性并很差PNG
,其实对于大部分图标这类图片,彻底能够使用 SVG
代替JPEG
其余文件优化
CSS
文件放在 head
中script
标签放在 body
底部,由于 JS
文件执行会阻塞渲染。固然也能够把 script
标签放在任意位置而后加上 defer
,表示该文件会并行下载,可是会放到 HTML
解析完成后顺序执行。对于没有任何依赖的 JS
文件能够加上 async
,表示加载和渲染后续文档元素的过程将和 JS
文件的加载与执行并行无序进行。 执行 JS
代码过长会卡住渲染,对于须要不少时间计算的代码Webworker
。Webworker
可让咱们另开一个线程执行脚本而不影响渲染。CDN
静态资源尽可能使用
CDN
加载,因为浏览器对于单个域名有并发请求上限,能够考虑使用多个CDN
域名。对于CDN
加载静态资源须要注意CDN
域名要与主站不一样,不然每次请求都会带上主站的Cookie
使用 Webpack 优化项目
Webpack4
,打包项目使用 production
模式,这样会自动开启代码压缩ES6
模块来开启 tree shaking
,这个技术能够移除没有使用的代码base64
的方式写入文件中监控
对于代码运行错误,一般的办法是使用
window.onerror
拦截报错。该方法能拦截到大部分的详细报错信息,可是也有例外
Script error
. 对于这种状况咱们须要给 script
标签添加 crossorigin
属性arguments.callee.caller
来作栈递归catch
的方式捕获错误。好比 Promise
能够直接使用 catch 函数,async await
能够使用 try catch
sourceMap
文件便于 debug
。img
标签的 src
发起一个请求跨网站指令码(英语:
Cross-site scripting
,一般简称为:XSS
)是一种网站应用程式的安全漏洞攻击,是代码注入的一种。它容许恶意使用者将程式码注入到网页上,其余使用者在观看网页时就会受到影响。这类攻击一般包含了HTML
以及使用者端脚本语言
XSS
分为三种:反射型,存储型和DOM-based
如何攻击
XSS
经过修改 HTML
节点或者执行 JS
代码来攻击网站。URL
获取某些参数<!-- http://www.domain.com?name=<script>alert(1)</script> --> <div>{{name}}</div>
上述
URL
输入可能会将HTML
改成<div><script>alert(1)</script></div>
,这样页面中就凭空多了一段可执行脚本。这种攻击类型是反射型攻击,也能够说是DOM-based
攻击
如何防护
最广泛的作法是转义输入输出的内容,对于引号,尖括号,斜杠进行转义
function escape(str) { str = str.replace(/&/g, "&"); str = str.replace(/</g, "<"); str = str.replace(/>/g, ">"); str = str.replace(/"/g, "&quto;"); str = str.replace(/'/g, "&##39;"); str = str.replace(/`/g, "&##96;"); str = str.replace(/\//g, "&##x2F;"); return str }
经过转义能够将攻击代码
<script>alert(1)</script>
变成
// -> <script>alert(1)<&##x2F;script> escape('<script>alert(1)</script>')
对于显示富文原本说,不能经过上面的办法来转义全部字符,由于这样会把须要的格式也过滤掉。这种状况一般采用白名单过滤的办法,固然也能够经过黑名单过滤,可是考虑到须要过滤的标签和标签属性实在太多,更加推荐使用白名单的方式
var xss = require("xss"); var html = xss('<h1 id="title">XSS Demo</h1><script>alert("xss");</script>'); // -> <h1>XSS Demo</h1><script>alert("xss");</script> console.log(html);
以上示例使用了
js-xss
来实现。能够看到在输出中保留了h1
标签且过滤了script
标签
跨站请求伪造(英语:
Cross-site request forgery
),也被称为one-click attack
或者session riding
,一般缩写为CSRF
或者XSRF
, 是一种挟制用户在当前已登陆的Web
应用程序上执行非本意的操做的攻击方法
CSRF
就是利用用户的登陆态发起恶意请求
如何攻击
假设网站中有一个经过 Get 请求提交用户评论的接口,那么攻击者就能够在钓鱼网站中加入一个图片,图片的地址就是评论接口
<img src="http://www.domain.com/xxx?comment='attack'"/>
如何防护
Get
请求不对数据进行修改Cookie
token
加盐
对于密码存储来讲,必然是不能明文存储在数据库中的,不然一旦数据库泄露,会对用户形成很大的损失。而且不建议只对密码单纯经过加密算法加密,由于存在彩虹表的关系
// 加盐也就是给原密码添加字符串,增长原密码长度 sha256(sha1(md5(salt + password + salt)))
可是加盐并不能阻止别人盗取帐号,只能确保即便数据库泄露,也不会暴露用户的真实密码。一旦攻击者获得了用户的帐号,能够经过暴力破解的方式破解密码。对于这种状况,一般使用验证码增长延时或者限制尝试次数的方式。而且一旦用户输入了错误的密码,也不能直接提示用户输错密码,而应该提示帐号或密码错误
前端加密
虽然前端加密对于安全防御来讲意义不大,可是在遇到中间人攻击的状况下,能够避免明文密码被第三方获取
unionid和openid
了解小程序登录以前,咱们写了解下小程序/公众号登陆涉及到两个最关键的用户标识:
OpenId
是一个用户对于一个小程序/公众号的标识,开发者能够经过这个标识识别出用户。UnionId
是一个用户对于同主体微信小程序/公众号/APP
的标识,开发者须要在微信开放平台下绑定相同帐号的主体。开发者可经过UnionId
,实现多个小程序、公众号、甚至APP 之间的数据互通了。关键Api
wx.login
官方提供的登陆能力wx.checkSession
校验用户当前的session_key
是否有效wx.authorize
提早向用户发起受权请求wx.getUserInfo
获取用户基本信息登陆流程设计
直接复用现有系统的登陆体系,只须要在小程序端设计用户名,密码/验证码输入页面,即可以简便的实现登陆,只须要保持良好的用户体验便可
OpenId
建立用户体系
OpenId
是一个小程序对于一个用户的标识,利用这一点咱们能够轻松的实现一套基于小程序的用户体系,值得一提的是这种用户体系对用户的打扰最低,能够实现静默登陆。具体步骤以下
wx.login
获取 code
code
向服务端,服务端拿到 code 调用微信登陆凭证校验接口,微信服务器返回 openid
和会话密钥 session_key
,此时开发者服务端即可以利用 openid
生成用户入库,再向小程序客户端返回自定义登陆态storage
)自定义登陆态(token
),后续调用接口时携带该登陆态做为用户身份标识便可利用 Unionid 建立用户体系
若是想实现多个小程序,公众号,已有登陆系统的数据互通,能够经过获取到用户
unionid
的方式创建用户体系。由于unionid
在同一开放平台下的所全部应用都是相同的,经过unionid
创建的用户体系便可实现全平台数据的互通,更方便的接入原有的功能,那如何获取unionid
呢,有如下两种方式
App
、公众号上进行过微信登陆受权,经过 wx.login
能够直接获取 到 unionid
wx.getUserInfo
和 <button open-type="getUserInfo"><button/>
这两种方式引导用户主动受权,主动受权后经过返回的信息和服务端交互 (这里有一步须要服务端解密数据的过程,很简单,微信提供了示例代码) 便可拿到 unionid
创建用户体系, 而后由服务端返回登陆态,本地记录便可实现登陆,附上微信提供的最佳实践
wx.login
获取 code
,而后从微信后端换取到 session_key
,用于解密 getUserInfo
返回的敏感数据wx.getSetting
获取用户的受权状况
API
wx.getUserInfo
获取用户最新的信息;注意事项
unionid
形式的登陆体系,在之前(18年4月以前)是经过如下这种方式来实现,但后续微信作了调整(由于一进入小程序,主动弹起各类受权弹窗的这种形式,比较容易致使用户流失),调整为必须使用按钮引导用户主动受权的方式,此次调整对开发者影响较大,开发者须要注意遵照微信的规则,并及时和业务方沟通业务形式,不要存在侥幸心理,以防形成小程序不过审等状况wx.login(获取code) ===> wx.getUserInfo(用户受权) ===> 获取 unionid
cookie
的概念, 登陆态必须缓存在本地,所以强烈建议为登陆态设置过时时间platform
,channel
,deviceParam
等参数。在和服务端肯定方案时,做为前端同窗应该及时提出这些合理的建议,设计合理的系统。openid
, unionid
不要在接口中明文传输,这是一种危险的行为,同时也很不专业这是一种常见的引流方式,通常同时会在图片中附加一个小程序二维码。
基本原理
canvas
元素,将须要导出的样式首先在 canvas
画布上绘制出来 (api
基本和h5
保持一致,但有轻微差别,使用时注意便可canvasToTempFilePath
导出图片,最后再使用 saveImageToPhotosAlbum
(须要受权)保存图片到本地如何优雅实现
class CanvasKit { constructor() { } drawImg(option = {}) { ... return this } drawRect(option = {}) { return this } drawText(option = {}) { ... return this } static exportImg(option = {}) { ... } } let drawer = new CanvasKit('canvasId').drawImg(styleObj1).drawText(styleObj2) drawer.exportImg()
注意事项
canvas
上,须要经过downLoadFile
先下载图片到本地临时文件才能够绘制32
可见字符)限制,能够借助服务端生成 短连接 的方式来解决数据统计做为目前一种经常使用的分析用户行为的方式,小程序端也是必不可少的。小程序采起的曝光,点击数据埋点其实和h5原理是同样的。可是埋点做为一个和业务逻辑不相关的需求,咱们若是在每个点击事件,每个生命周期加入各类埋点代码,则会干扰正常的业务逻辑,和使代码变的臃肿,笔者提供如下几种思路来解决数据埋点
设计一个埋点sdk
小程序的代码结构是,每个
Page
中都有一个Page
方法,接受一个包含生命周期函数,数据的 业务逻辑对象 包装这层数据,借助小程序的底层逻辑实现页面的业务逻辑。经过这个咱们能够想到思路,对Page
进行一次包装,篡改它的生命周期和点击事件,混入埋点代码,不干扰业务逻辑,只要作一些简单的配置便可埋点,简单的代码实现以下
// 代码仅供理解思路 page = function(params) { let keys = params.keys() keys.forEach(v => { if (v === 'onLoad') { params[v] = function(options) { stat() //曝光埋点代码 params[v].call(this, options) } } else if (v.includes('click')) { params[v] = funciton(event) { let data = event.dataset.config stat(data) // 点击埋点 param[v].call(this) } } }) }
这种思路不光适用于埋点,也能够用来做全局异常处理,请求的统一处理等场景。
分析接口
对于特殊的一些业务,咱们能够采起 接口埋点,什么叫接口埋点呢?不少状况下,咱们有的
api
并非多处调用的,只会在某一个特定的页面调用,经过这个思路咱们能够分析出,该接口被请求,则这个行为被触发了,则彻底能够经过服务端日志得出埋点数据,可是这种方式局限性较大,并且属于分析结果得出过程,可能存在偏差,但能够做为一种思路了解一下。
微信自定义数据分析
微信自己提供的数据分析能力,微信自己提供了常规分析和自定义分析两种数据分析方式,在小程序后台配置便可。借助小程序数据助手这款小程序能够很方便的查看
工程化作什么
目前的前端开发过程,工程化是必不可少的一环,那小程序工程化都须要作些什么呢,先看下目前小程序开发当中存在哪些问题须要解决:
css
预编译器,做为一种主流的 css
解决方案,不管是 less
,sass
,stylus
均可以提高css
效率ES7
等后续的js
特性,好用的async await
等特性都没法使用base64
eslint
等代码检查工具方案选型
对于目前经常使用的工程化方案,
webpack
,rollup
,parcel
等来看,都经常使用与单页应用的打包和处理,而小程序天生是 “多页应用” 而且存在一些特定的配置。根据要解决的问题来看,无非是文件的编译,修改,拷贝这些处理,对于这些需求,咱们想到基于流的gulp
很是的适合处理,而且相对于webpack
配置多页应用更加简单。因此小程序工程化方案推荐使用gulp
具体开发思路
经过
gulp
的task
实现:
less
文件至相应目录async
,await
的运行时文件base64
并生成相应css
文件,方便使用npm
包,将npm
包打成一个文件,拷贝至相应目录微信小程序的框架包含两部分
View
视图层、App Service
逻辑层。View
层用来渲染页面结构,AppService
层用来逻辑处理、数据请求、接口调用。
它们在两个线程里运行。
视图层和逻辑层经过系统层的
JSBridage
进行通讯,逻辑层把数据变化通知到视图层,触发视图层页面更新,视图层把触发的事件通知到逻辑层进行业务处理
WebView
渲染,iOS
中使用自带 WKWebView
,在 Android
使用腾讯的 x5
内核(基于 Blink
)运行。iOS
中使用自带的 JSCore
运行,在 Android
中使用腾讯的 x5
内核(基于 Blink
)运行。nw.js
同时提供了视图层和逻辑层的运行环境。WXML
bindtap
)Wxml
编译器:Wcc
把 Wxml
文件 转为 JS
Wcc index.wxml
Virtual DOM
,进行局部更新WXSS
wcsc
把wxss
文件转化为 js
wcsc index.wxss
尺寸单位 rpx
rpx(responsive pixel
): 能够根据屏幕宽度进行自适应。规定屏幕宽为750rpx
。公式:
const dsWidth = 750 export const screenHeightOfRpx = function () { return 750 / env.screenWidth * env.screenHeight } export const rpxToPx = function (rpx) { return env.screenWidth / 750 * rpx } export const pxToRpx = function (px) { return 750 / env.screenWidth * px }
样式导入
使用
@import
语句能够导入外联样式表,@import
后跟须要导入的外联样式表的相对路径,用;
表示语句结束
内联样式
静态的样式统一写到
class
中。style
接收动态的样式,在运行时会进行解析,请尽可能避免将静态的样式写进style
中,以避免影响渲染速度
全局样式与局部样式
定义在
app.wxss
中的样式为全局样式,做用于每个页面。在page
的wxss
文件中定义的样式为局部样式,只做用在对应的页面,并会覆盖app.wxss
中相同的选择器
WebView
渲染,并不是原生渲染。(部分原生)Set-Cookie
。JS
库不能使用。npm
,可是能够自搭构建工具或者使用 mpvue
。(将来官方有计划支持)ES7
,能够本身用babel+webpack
自搭或者使用 mpvue
。base64
的方式来使用 iconfont
。formId
,formId
只有7天有效期。(如今的作法是在每一个页面都放入form
而且隐藏以此获取更多的 formId
。后端使用原则为:优先使用有效期最短的)unionId
必须绑在同一个开放平台下。开放平台绑定限制:
50
个移动应用10
个网站50
个同主体公众号5
个不一样主体公众号50
个同主体小程序5
个不一样主体小程序access_token
wx.checkSession
,而且在session_key
有效期内,受权获取用户信息也会获得新的session_key
session_key
有有效期,有效期并无被告知开发者,只知道用户越频繁使用小程序,session_key
有效期越长wx.login
时会直接更新 session_key
,致使旧 session_key
失效wx.checkSession
检查登陆态,并保证没有过时的 session_key
不会被更新,再调用 wx.login
获取 code
。接着用户受权小程序获取用户信息,小程序拿到加密后的用户数据,把加密数据和 code
传给后端服务。后端经过 code
拿到 session_key
并解密数据,将解密后的用户信息返回给小程序面试题:先受权获取用户信息再 login 会发生什么?
session_key
对用户信息进行加密。调用 wx.login
从新登陆,会刷新 session_key
,这时后端服务从开放平台获取到新 session_key
,可是没法对老 session_key
加密过的数据解密,用户信息获取失败wx.checkSession
呢?wx.checkSession
检查登陆态,而且保证 wx.login 不会刷新 session_key
,从而让后端服务正确解密数据。可是这里存在一个问题,若是小程序较长时间不用致使 session_key
过时,则 wx.login
一定会从新生成 session_key
,从而再一次致使用户信息解密失败咱们知道
view
部分是运行在webview
上的,因此前端领域的大多数优化方式都有用
加载优化
代码包的大小是最直接影响小程序加载启动速度的因素。代码包越大不只下载速度时间长,业务代码注入时间也会变长。因此最好的优化方式就是减小代码包的大小
小程序加载的三个阶段的表示
优化方式
首屏加载的体验优化建议
storage API
对异步请求数据进行缓存,二次启动时先利用缓存数据渲染页面,在进行后台更新。使用分包加载优化
TabBar
页面,以及一些全部分包都需用到公共资源/JS
脚本,而分包则是根据开发者的配置进行划分优势:
限制
8M
2M
├── app.js ├── app.json ├── app.wxss ├── packageA │ └── pages │ ├── cat │ └── dog ├── packageB │ └── pages │ ├── apple │ └── banana ├── pages │ ├── index │ └── logs └── utils
开发者经过在
app.json
subPackages
字段声明项目分包结构
{ "pages":[ "pages/index", "pages/logs" ], "subPackages": [ { "root": "packageA", "pages": [ "pages/cat", "pages/dog" ] }, { "root": "packageB", "pages": [ "pages/apple", "pages/banana" ] } ] }
分包原则
subPackages
后,将按 subPackages
配置路径进行打包,subPackages
配置路径外的目录将被打包到 app
(主包) 中app
(主包)也能够有本身的 pages
(即最外层的 pages
字段subPackage
的根目录不能是另一个 subPackage
内的子目录TAB
页面必须在 app
(主包)内引用原则
没法
require packageB JS 文件,但能够
require app、本身
package内的
JS` 文件没法
import packageB的
template,但能够
require app、本身
package内的
template`没法使用
packageB的资源,但能够使用
app、本身
package` 内的资源官方即将推出 分包预加载
独立分包
渲染性能优化
setData
的调用都是一次进程间通讯过程,通讯开销与 setData
的数据量正相关。setData
会引起视图层页面内容的更新,这一耗时操做必定时间中会阻塞用户交互。setData
是小程序开发使用最频繁,也是最容易引起性能问题的避免不当使用 setData
data
在方法间共享数据,可能增长 setData
传输的数据量。。data
应仅包括与页面渲染相关的数据。setData
传输大量数据,通信耗时与数据正相关,页面更新延迟可能形成页面更新开销增长。仅传输页面中发生变化的数据,使用 setData
的特殊 key
实现局部更新。setData
,操做卡顿,交互延迟,阻塞通讯,页面渲染延迟。避免没必要要的 setData
,对连续的setData
调用进行合并。setData
,抢占前台页面的渲染资源。页面切入后台后的 setData
调用,延迟到页面从新展现时执行。避免不当使用onPageScroll
pageScroll
事件。不监听,则不会派发。onPageScroll
中执行复杂逻辑onPageScroll
中频繁调用 setData
SelectQuery
)用以判断是否显示,部分场景建议使用节点布局橡胶状态监听(inersectionObserver
)替代使用自定义组件
在须要频繁更新的场景下,自定义组件的更新只在组件内部进行,不受页面其余部份内容复杂性影响
数据流管理
相比传统的小程序框架,这个一直是咱们做为资深开发者比较指望去解决的,在
Web
开发中,随着Flux
、Redu
x、Vuex
等多个数据流工具出现,咱们也指望在业务复杂的小程序中使用
WePY
默认支持 Redux
,在脚手架生成项目的时候能够内置Mpvue
做为 Vue
的移植版本,固然支持 Vuex
,一样在脚手架生成项目的时候能够内置组件化
WePY
相似 Vue
实现了单文件组件,最大的差异是文件后缀 .wpy
,只是写法上会有差别export default class Index extends wepy.page {}
Mpvue
做为 Vue
的移植版本,支持单文件组件,template
、script
和 style
都在一个 .vue
文件中,和 vue
的写法相似,因此对 Vue
开发熟悉的同窗会比较适应工程化
全部的小程序开发依赖官方提供的开发者工具。开发者工具简单直观,对调试小程序颇有帮助,如今也支持腾讯云(目前咱们尚未使用,可是对新的一些开发者仍是有帮助的),能够申请测试报告查看小程序在真实的移动设备上运行性能和运行效果,可是它自己没有相似前端工程化中的概念和工具
wepy
内置了构建,经过 wepy init
命令初始化项目,大体流程以下:
wepy-cli
会判断模版是在远程仓库仍是在本地,若是在本地则会当即跳到第 3
步,反之继续进行。Project name
等问题,依据开发者的回答,建立项目mpvue
沿用了 vue
中推崇的 webpack
做为构建工具,但同时提供了一些本身的插件以及配置文件的一些修改,好比
html-webpack-plugin
webpack-dev-middleware
修改为 webpack-dev-middleware-hard-disk
webpack-loader
修改为 mpvue-loader
mpvue
Vue.js
小程序版, fork
自 vuejs/vue@2.4.1
,保留了 vue runtime
能力,添加了小程序平台的支持。 mpvue
是一个使用 Vue.js
开发小程序的前端框架。框架基于 Vue.js
核心,mpvue
修改了 Vue.js
的 runtime
和 compiler
实现,使其能够运行在小程序环境中,从而为小程序开发引入了整套 Vue.js
开发体验
框架原理
两个大方向
mpvue
提供 mp
的 runtime
适配小程序mpvue-loader
产出微信小程序所须要的文件结构和模块内容七个具体问题
mpvue
原理必然要了解 Vue
原理,这是大前提如今假设您对 Vue 原理有个大概的了解
Vue
使用了 Virtual DOM
,因此 Virtual DOM
能够在任何支持 JavaScript
语言的平台上操做,譬如说目前 Vue
支持浏览器平台或 weex
,也能够是 mp
(小程序)。那么最后 Virtual DOM
如何映射到真实的 DOM
节点上呢?vue
为平台作了一层适配层,浏览器平台见 runtime/node-ops.js
、weex
平台见runtime/node-ops.js
,小程序见runtime/node-ops.js
。不一样平台之间经过适配层对外提供相同的接口,Virtual DOM
进行操做Real DOM
节点的时候,只须要调用这些适配层的接口便可,而内部实现则不须要关心,它会根据平台的改变而改变mp
平台的 runtime
方向走。但问题是小程序不能操做 DOM
,因此 mp
下的node-ops.js
里面的实现都是直接 return obj
Virtual DOM
和旧 Virtual DOM
之间须要作一个 patch
,找出 diff
。patch
完了以后的 diff
怎么更新视图,也就是如何给这些 DOM
加入 attr
、class
、style
等 DOM 属性呢? Vue
中有 nextTick
的概念用以更新视图,mpvue
这块对于小程序的 setData
应该怎么处理呢?Virtual DOM
怎么生成?也就是怎么将 template
编译成render function
。这当中还涉及到运行时-编译器-vs-只包含运行时,显然若是要提升性能、减小包大小、输出 wxml
、mpvue
也要提供预编译的能力。由于要预输出 wxml
且无法动态改变 DOM
,因此动态组件,自定义 render
,和<script type="text/x-template">
字符串模版等都不支持另外还有一些其余问题,最后总结一下
render function
wxml
,wxss
,wxs
atch
出 diff
vue
组件事件响应vue
实例与小程序 Page
实例关联vue
生命周期映射关系,能在小程序生命周期中触发vue
生命周期
platform/mp
的目录结构
. ├── compiler //解决问题1,mpvue-template-compiler源码部分 ├── runtime //解决问题3 4 5 6 7 ├── util //工具方法 ├── entry-compiler.js //mpvue-template-compiler的入口。package.json相关命令会自动生成mpvue-template-compiler这个package。 ├── entry-runtime.js //对外提供Vue对象,固然是mpvue └── join-code-in-build.js //编译出SDK时的修复
mpvue-loader
mpvue-loader
是vue-loader
的一个扩展延伸版,相似于超集的关系,除了vue-loader
自己所具有的能力以外,它还会利用mpvue-template-compiler
生成render function
entry
webpack
的配置中的 entry
开始,分析依赖模块,并分别打包。在entry
中 app
属性及其内容会被打包为微信小程序所须要的 app.js/app.json/app.wxss
,其他的会生成对应的page.js
/page.json
/page.wxml
/page.wxss
,如示例的 entry
将会生成以下这些文件,文件内容下文慢慢讲来:// webpack.config.js { // ... entry: { app: resolve('./src/main.js'), // app 字段被识别为 app 类型 index: resolve('./src/pages/index/main.js'), // 其他字段被识别为 page 类型 'news/home': resolve('./src/pages/news/home/index.js') } } // 产出文件的结构 . ├── app.js ├── app.json ├──· app.wxss ├── components │ ├── card$74bfae61.wxml │ ├── index$023eef02.wxml │ └── news$0699930b.wxml ├── news │ ├── home.js │ ├── home.wxml │ └── home.wxss ├── pages │ └── index │ ├── index.js │ ├── index.wxml │ └── index.wxss └── static ├── css │ ├── app.wxss │ ├── index.wxss │ └── news │ └── home.wxss └── js ├── app.js ├── index.js ├── manifest.js ├── news │ └── home.js └── vendor.js
wxml
每个.vue
的组件都会被生成为一个wxml
规范的template
,而后经过wxml
规范的import
语法来达到一个复用,同时组件若是涉及到props
的data
数据,咱们也会作相应的处理,举个实际的例子:
<template> <div class="my-component" @click="test"> <h1>{{msg}}</h1> <other-component :msg="msg"></other-component> </div> </template> <script> import otherComponent from './otherComponent.vue' export default { components: { otherComponent }, data () { return { msg: 'Hello Vue.js!' } }, methods: { test() {} } } </script>
这样一个
Vue
的组件的模版部分会生成相应的wxml
<import src="components/other-component$hash.wxml" /> <template name="component$hash"> <view class="my-component" bindtap="handleProxy"> <view class="_h1">{{msg}}</view> <template is="other-component$hash" wx:if="{{ $c[0] }}" data="{{ ...$c[0] }}"></template> </view> </template>
可能已经注意到了
other-component(:msg="msg")
被转化成了 。mpvue
在运行时会从根组件开始把全部的组件实例数据合并成一个树形的数据,而后经过setData
到appData
,$c
是$children
的缩写。至于那个0
则是咱们的compiler
处理事后的一个标记,会为每个子组件打一个特定的不重复的标记。 树形数据结构以下
// 这儿数据结构是一个数组,index 是动态的 { $child: { '0'{ // ... root data $child: { '0': { // ... data msg: 'Hello Vue.js!', $child: { // ...data } } } } } }
wxss
这个部分的处理同
web
的处理差别不大,惟一不一样在于经过配置生成.css
为.wxss
,其中的对于css
的若干处理,在postcss-mpvue-wxss
和px2rpx-loader
这两部分的文档中又详细的介绍
app.json/page.json
放到页面入口处,使用 copy-webpack-plugin
copy
到对应的生成位置。这部份内容来源于
app
和page
的entry
文件,一般习惯是main.js
,你须要在你的入口文件中export default { config: {} }
,这才能被咱们的loader
识别为这是一个配置,须要写成json
文件
import Vue from 'vue'; import App from './app'; const vueApp = new Vue(App); vueApp.$mount(); // 这个是咱们约定的额外的配置 export default { // 这个字段下的数据会被填充到 app.json / page.json config: { pages: ['static/calendar/calendar', '^pages/list/list'], // Will be filled in webpack window: { backgroundTextStyle: 'light', navigationBarBackgroundColor: '##455A73', navigationBarTitleText: '美团汽车票', navigationBarTextStyle: '##fff' } } };
Keys
是React
用于追踪哪些列表中元素被修改、被添加或者被移除的辅助标识
key
在其同级元素中具备惟一性。在 React Diff
算法中React
会借助元素的 Key
值来判断该元素是新近建立的仍是被移动而来的元素,从而减小没必要要的元素重渲染。此外,React 还须要借助 Key
值来判断元素与本地状态的关联关系,所以咱们毫不可忽视转换函数中 Key
的重要性该函数会在
setState
函数调用完成而且组件开始重渲染的时候被调用,咱们能够用该函数来监听渲染是否完成:
this.setState( { username: 'tylermcginnis33' }, () => console.log('setState has finished and the component has re-rendered.') )
this.setState((prevState, props) => { return { streak: prevState.streak + props.count } })
Refs
是 React
提供给咱们的安全访问 DOM
元素或者某个组件实例的句柄ref
属性而后在回调函数中接受该元素在 DOM
树中的句柄,该值会做为回调函数的第一个参数返回咱们应当将AJAX 请求放到
componentDidMount
函数中执行,主要缘由有下
React
下一代调和算法 Fiber
会经过开始或中止渲染的方式优化应用性能,其会影响到 componentWillMount
的触发次数。对于 componentWillMount
这个生命周期函数的调用次数会变得不肯定,React
可能会屡次频繁调用 componentWillMount
。若是咱们将 AJAX
请求放到 componentWillMount
函数中,那么显而易见其会被触发屡次,天然也就不是好的选择。AJAX
请求放置在生命周期的其余函数中,咱们并不能保证请求仅在组件挂载完毕后才会要求响应。若是咱们的数据请求在组件挂载以前就完成,而且调用了setState
函数将数据添加到组件状态中,对于未挂载的组件则会报错。而在 componentDidMount
函数中进行 AJAX
请求则能有效避免这个问题
shouldComponentUpdate
容许咱们手动地判断是否要进行组件更新,根据组件的应用场景设置函数的合理返回值可以帮咱们避免没必要要的更新
一般状况下咱们会使用
Webpack
的DefinePlugin
方法来将NODE_ENV
变量值设置为production
。编译版本中React
会忽略propType
验证以及其余的告警信息,同时还会下降代码库的大小,React
使用了Uglify
插件来移除生产环境下没必要要的注释等信息
为了解决跨浏览器兼容性问题,
React
会将浏览器原生事件(Browser Native Event
)封装为合成事件(SyntheticEvent
)传入设置的事件处理器中。这里的合成事件提供了与原生事件相同的接口,不过它们屏蔽了底层浏览器的细节差别,保证了行为的一致性。另外有意思的是,React
并无直接将事件附着到子元素上,而是以单一事件监听器的方式将全部的事件发送到顶层进行处理。这样React
在更新DOM
的时候就不须要考虑如何去处理附着在DOM
上的事件监听器,最终达到优化性能的目的
createElement
函数是 JSX 编译以后使用的建立React Element
的函数,而cloneElement
则是用于复制某个元素并传入新的Props
中间件提供第三方插件的模式,自定义拦截
action
->reducer
的过程。变为action
->middlewares
->reducer
。这种机制可让咱们改变数据流,实现如异步action
,action
过滤,日志输出,异常报告等功能
redux-logger
:提供日志输出redux-thunk
:处理异步操做redux-promise
:处理异步操做,actionCreator
的返回值是promise
flux
中直接从store
取。render
,可能会有效率影响,或者须要写复杂的shouldComponentUpdate
进行判断。React-Redux
提供connect
方法联系起来初始化阶段
getDefaultProps
:获取实例的默认属性getInitialState
:获取每一个实例的初始化状态componentWillMount
:组件即将被装载、渲染到页面上render
:组件在这里生成虚拟的DOM
节点omponentDidMount
:组件真正在被装载以后运行中状态
componentWillReceiveProps
:组件将要接收到属性的时候调用shouldComponentUpdate
:组件接受到新属性或者新状态的时候(能够返回false,接收数据后不更新,阻止render
调用,后面的函数不会被继续执行了)componentWillUpdate
:组件即将更新不能修改属性和状态render
:组件从新描绘componentDidUpdate
:组件已经更新销毁阶段
componentWillUnmount
:组件即将销毁
shouldComponentUpdate
这个方法用来判断是否须要调用render方法从新描绘dom。由于dom的描绘很是消耗性能,若是咱们能在shouldComponentUpdate方
法中可以写出更优化的dom diff
算法,能够极大的提升性能
虚拟
dom
至关于在js
和真实dom
中间加了一个缓存,利用dom diff
算法避免了没有必要的dom
操做,从而提升性能
具体实现步骤以下
JavaScript
对象结构表示 DOM 树的结构;而后用这个树构建一个真正的 DOM
树,插到文档当中DOM
树上,视图就更新key
属性,方便比较。React
只会匹配相同 class
的 component
(这里面的class
指的是组件的名字)component
的 setState
方法的时候, React
将其标记为 - dirty
.到每个事件循环结束, React
检查全部标记 dirty
的 component
从新绘制.shouldComponentUpdate
提升diff
的性能shouldComponentUpdate
来避免没必要要的dom操做production
版本的react.js
key
来帮助React
识别列表中全部子组件的最小变化
Flux
的最大特色,就是数据的"单向流动"。
View
View
发出用户的 Action
Dispatcher
收到Action
,要求 Store
进行相应的更新Store
更新后,发出一个"change"
事件View
收到"change"
事件后,更新页面1. JSX作表达式判断时候,须要强转为boolean类型
若是不使用
!!b
进行强转数据类型,会在页面里面输出0
。
render() { const b = 0; return <div> { !!b && <div>这是一段文本</div> } </div> }
2. 尽可能不要在 componentWillReviceProps 里使用 setState,若是必定要使用,那么须要判断结束条件,否则会出现无限重渲染,致使页面崩溃
3. 给组件添加ref时候,尽可能不要使用匿名函数,由于当组件更新的时候,匿名函数会被当作新的prop处理,让ref属性接受到新函数的时候,react内部会先清空ref,也就是会以null为回调参数先执行一次ref这个props,而后在以该组件的实例执行一次ref,因此用匿名函数作ref的时候,有的时候去ref赋值后的属性会取到null
4. 遍历子节点的时候,不要用 index 做为组件的 key 进行传入
class Demo { render() { return <button onClick={(e) => { alert('我点击了按钮') }}> 按钮 </button> } }
你以为你这样设置点击事件会有什么问题吗?
因为
onClick
使用的是匿名函数,全部每次重渲染的时候,会把该onClick
当作一个新的prop
来处理,会将内部缓存的onClick
事件进行从新赋值,因此相对直接使用函数来讲,可能有一点的性能降低
修改
class Demo { onClick = (e) => { alert('我点击了按钮') } render() { return <button onClick={this.onClick}> 按钮 </button> }
首先说说为何要使用
Virturl DOM
,由于操做真实DOM
的耗费的性能代价过高,因此react
内部使用js
实现了一套dom结构,在每次操做在和真实dom以前,使用实现好的diff算法,对虚拟dom进行比较,递归找出有变化的dom节点,而后对其进行更新操做。为了实现虚拟DOM
,咱们须要把每一种节点类型抽象成对象,每一种节点类型有本身的属性,也就是prop,每次进行diff
的时候,react
会先比较该节点类型,假如节点类型不同,那么react
会直接删除该节点,而后直接建立新的节点插入到其中,假如节点类型同样,那么会比较prop
是否有更新,假若有prop
不同,那么react
会断定该节点有更新,那么重渲染该节点,而后在对其子节点进行比较,一层一层往下,直到没有子节点
一般咱们输出节点的时候都是map一个数组而后返回一个
ReactNode
,为了方便react
内部进行优化,咱们必须给每个reactNode
添加key
,这个key prop
在设计值处不是给开发者用的,而是给react用的,大概的做用就是给每个reactNode
添加一个身份标识,方便react进行识别,在重渲染过程当中,若是key同样,若组件属性有所变化,则react
只更新组件对应的属性;没有变化则不更新,若是key不同,则react先销毁该组件,而后从新建立该组件
MVVM
是Model-View-ViewModel
的缩写
Model
表明数据模型,也能够在Model
中定义数据修改和操做的业务逻辑。View
表明UI
组件,它负责将数据模型转化成UI
展示出来。ViewModel
监听模型数据的改变和控制视图行为、处理用户交互,简单理解就是一个同步View 和 Model
的对象,链接Model
和View
- 在
MVVM
架构下,View
和Model
之间并无直接的联系,而是经过ViewModel
进行交互,Model
和ViewModel
之间的交互是双向的, 所以View
数据的变化会同步到Model中,而Model 数据的变化也会当即反应到View
上。ViewModel
经过双向数据绑定把View
层和Model
层链接了起来,而View
和Model
之间的同步工做彻底是自动的,无需人为干涉,所以开发者只需关注业务逻辑,不须要手动操做DOM,不须要关注数据状态的同步问题,复杂的数据状态维护彻底由MVVM
来统一管理
答:总共分为8个阶段建立前/后,载入前/后,更新前/后,销毁前/后
beforeCreate
阶段,vue
实例的挂载元素el
和数据对象data
都为undefined
,还未初始化。在created
阶段,vue
实例的数据对象data
有了,el尚未beforeMount
阶段,vue
实例的$el
和data
都初始化了,但仍是挂载以前为虚拟的dom
节点,data.message
还未替换。在mounted
阶段,vue
实例挂载完成,data.message
成功渲染。data
变化时,会触发beforeUpdate
和updated
方法destroy
方法后,对data
的改变不会再触发周期函数,说明此时vue
实例已经解除了事件监听以及和dom
的绑定,可是dom
结构依然存在什么是vue生命周期?
vue生命周期的做用是什么?
vue生命周期总共有几个阶段?
8
个阶段:建立前/后、载入前/后、更新前/后、销毁前/销毁后。第一次页面加载会触发哪几个钩子?
beforeCreate
、created
、beforeMount
、mounted
。DOM 渲染在哪一个周期中就已经完成?
DOM
渲染在 mounted
中就已经完成了vue
实现数据双向绑定主要是:采用数据劫持结合发布者-订阅者模式的方式,经过 Object.defineProperty()
来劫持各个属性的setter
,getter
,在数据变更时发布消息给订阅者,触发相应监听回调。当把一个普通 Javascript
对象传给 Vue 实例来做为它的 data
选项时,Vue 将遍历它的属性,用 Object.defineProperty()
将它们转为 getter/setter
。用户看不到 getter/setter
,可是在内部它们让 Vue
追踪依赖,在属性被访问和修改时通知变化。MVVM
做为数据绑定的入口,整合Observer
,Compile
和Watcher
三者,经过Observer
来监听本身的model
的数据变化,经过Compile
来解析编译模板指令(vue
中是用来解析 {{}}
),最终利用watcher
搭起observer
和Compile
之间的通讯桥梁,达到数据变化 —>视图更新;视图交互变化(input
)—>数据model
变动双向绑定效果。父组件与子组件传值
父组件传给子组件:子组件经过
props
方法接受数据;
$emit
方法传递参数非父子组件间的数据传递,兄弟组件传值
eventBus
,就是建立一个事件中心,至关于中转站,能够用它来传递事件和接收事件。项目比较小时,用这个比较合适(虽然也有很多人推荐直接用VUEX
,具体来讲看需求)
hash
模式:在浏览器中符号“#”
,#以及#后面的字符称之为hash
,用 window.location.hash
读取。特色:hash
虽然在URL
中,但不被包括在HTTP
请求中;用来指导浏览器动做,对服务端安全无用,hash
不会重加载页面。history
模式:history
采用HTML5
的新特性;且提供了两个新方法: pushState()
, replaceState()
能够对浏览器历史记录栈进行修改,以及popState
事件的监听到状态变动首页能够控制导航跳转,
beforeEach
,afterEach
等,通常用于页面title
的修改。一些须要登陆才能调整页面的重定向功能。
beforeEach
主要有3个参数to
,from
,next
。to
:route
即将进入的目标路由对象。from
:route
当前导航正要离开的路由。next
:function
必定要调用该方法resolve
这个钩子。执行效果依赖next
方法的调用参数。能够控制网页的跳转store
中; 改变状态的方式是提交mutations
,这是个同步的事物; 异步逻辑应该封装在action
中。main.js
引入store
,注入。新建了一个目录store
,… export
state
:Vuex
使用单一状态树,即每一个应用将仅仅包含一个store
实例,但单一状态树和模块化并不冲突。存放的数据状态,不能够直接修改里面的数据。mutations
:mutations
定义的方法动态修改Vuex
的 store
中的状态或数据getters
:相似vue
的计算属性,主要用来过滤一些数据。action
:actions
能够理解为经过将mutations
里面处里数据的方法变成可异步的处理数据的方法,简单的说就是异步操做数据。view
层经过 store.dispath
来分发 action
modules
:项目特别复杂的时候,可让每个模块拥有本身的state
、mutation
、action
、getters
,使得结构很是清晰,方便管理
v-if
按照条件是否渲染,v-show
是display
的block
或none
;$route
和$router
的区别$route
是“路由信息对象”,包括path
,params
,hash
,query
,fullPath
,matched
,name
等路由信息参数。$router
是“路由实例”对象包括了路由的跳转方法,钩子函数等将当前组件的
<style>
修改成<style scoped>
<keep-alive></keep-alive>
的做用是什么?<keep-alive></keep-alive>
包裹动态组件时,会缓存不活动的组件实例,主要用于保留组件状态或避免从新渲染好比有一个列表和一个详情,那么用户就会常常执行打开详情=>返回列表=>打开详情…这样的话列表和详情都是一个频率很高的页面,那么就能够对列表组件使用
<keep-alive></keep-alive>
进行缓存,这样用户每次返回列表的时候,都能从缓存中快速渲染,而不是从新渲染
提供一个在页面上已存在的
DOM
元素做为Vue
实例的挂载目标.能够是 CSS 选择器,也能够是一个HTMLElement
实例,
ES6
的import ... from ...
语法或CommonJS
的require()
方法引入插件Vue.use( plugin )
使用插件,能够传入一个选项对象Vue.use(MyPlugin, { someOption: true })
created
: 实例已经建立完成以后调用,在这一步,实例已经完成数据观测, 属性和方法的运算, watch/event
事件回调. 然而, 挂载阶段尚未开始, $el
属性目前还不可见mounted
: el
被新建立的 vm.$el
替换,并挂载到实例上去以后调用该钩子。若是 root
实例挂载了一个文档内元素,当 mounted
被调用时 vm.$el
也在文档内。activated
: keep-alive
组件激活时调用问题一:构建的 vue-cli 工程都到了哪些技术,它们的做用分别是什么?
vue.js
:vue-cli
工程的核心,主要特色是 双向数据绑定 和 组件系统。vue-router
:vue
官方推荐使用的路由框架。vuex
:专为 Vue.js
应用项目开发的状态管理器,主要用于维护vue
组件间共用的一些 变量 和 方法。axios
( 或者 fetch
、ajax
):用于发起 GET
、或 POST
等 http
请求,基于 Promise
设计。vuex
等:一个专为vue
设计的移动端UI
组件库。emit.js
文件,用于vue
事件机制的管理。webpack
:模块加载和vue-cli
工程打包器。问题二:vue-cli 工程经常使用的 npm 命令有哪些?
node_modules
资源包的命令:npm install
vue-cli
开发环境的 npm命令:npm run dev
vue-cli
生成 生产环境部署资源 的 npm
命令:npm run build
vue-cli
生产环境部署资源文件大小的 npm
命令:npm run build --report
在浏览器上自动弹出一个 展现
vue-cli
工程打包后app.js
、manifest.js
、vendor.js
文件里面所包含代码的页面。能够具此优化vue-cli
生产环境部署的静态资源,提高 页面 的加载速度
nextTick
可让咱们在下次 DOM 更新循环结束以后执行延迟回调,用于得到更新后的DOM
View
)能够独立于Model
变化和修改,一个ViewModel
能够绑定到不一样的"View"
上,当View变化的时候Model能够不变,当Model
变化的时候View
也能够不变ViewModel
里面,让不少view
重用这段视图逻辑ViewModel
来写声明式(标签跳转)
<router-link :to="index">
编程式( js跳转)
router.push('index')
其基本实现原理
app.js
做为客户端与服务端的公用入口,导出 Vue
根实例,供客户端 entry
与服务端 entry
使用。客户端 entry
主要做用挂载到 DOM
上,服务端 entry
除了建立和返回实例,还进行路由匹配与数据预获取。webpack
为客服端打包一个 Client Bundle
,为服务端打包一个 Server Bundle
。url
,加载相应组件,获取和解析异步数据,建立一个读取 Server Bundle
的 BundleRenderer
,而后生成 html
发送给客户端。DOM
与本身的生成的 DOM 进行对比,把不相同的 DOM
激活,使其能够可以响应后续变化,这个过程称为客户端激活 。为确保混合成功,客户端与服务器端须要共享同一套数据。在服务端,能够在渲染以前获取数据,填充到 stroe
里,这样,在客户端挂载到 DOM
以前,能够直接从 store
里取数据。首屏的动态数据经过 window.__INITIAL_STATE__
发送到客户端
Vue SSR
的实现,主要就是把Vue
的组件输出成一个完整HTML
,vue-server-renderer
就是干这事的
Vue SSR
须要作的事多点(输出完整 HTML),除了complier -> vnode
,还需如数据获取填充至 HTML
、客户端混合(hydration
)、缓存等等。 相比于其余模板引擎(ejs
, jade
等),最终要实现的目的是同样的,性能上可能要差点Vue
的实例。data
属性,当 data
的值是同一个引用类型的值时,改变其中一个会影响其余data
、 Store
)的联系;实现时,主要以下
data
, 使用 Object.defineProperty
把这些属性所有转为 getter/setter
。computed
, 遍历 computed
里的每一个属性,每一个 computed
属性都是一个 watch
实例。每一个属性提供的函数做为属性的 getter
,使用 Object.defineProperty
转化。Object.defineProperty getter
依赖收集。用于依赖发生变化时,触发属性从新计算。computed
计算属性嵌套其余 computed
计算属性时,先进行其余的依赖收集html
,最开始出如今后端,通过各类处理吐给前端。随着各类 mv*
的兴起,模板解析交由前端处理。Vue complier
是将 template
转化成一个 render
字符串。能够简单理解成如下步骤:
parse
过程,将 template
利用正则转化成AST
抽象语法树。optimize
过程,标记静态节点,后 diff
过程跳过静态节点,提高性能。generate
过程,生成 render
字符串用
timeline
工具。 大意是经过timeline
来查看每一个函数的调用时常,定位出哪一个函数的问题,从而能判断哪一个组件出了问题
MVVM
由如下三个内容组成
View
:界面Model
:数据模型ViewModel
:做为桥梁负责沟通 View
和 Model
在
JQuery
时期,若是须要刷新UI
时,须要先取到对应的DOM
再更新UI
,这样数据和业务的逻辑就和页面有强耦合。
MVVM
在 MVVM
中,UI
是经过数据驱动的,数据一旦改变就会相应的刷新对应的 UI
,UI
若是改变,也会改变对应的数据。这种方式就能够在业务处理中只关心数据的流转,而无需直接和页面打交道。ViewModel
只关心数据和业务的处理,不关心 View
如何处理数据,在这种状况下,View
和 Model
均可以独立出来,任何一方改变了也不必定须要改变另外一方,而且能够将一些可复用的逻辑放在一个 ViewModel
中,让多个 View
复用这个 ViewModel
。
MVVM
中,最核心的也就是数据双向绑定,例如 Angluar
的脏数据检测,Vue
中的数据劫持。脏数据检测
当触发了指定事件后会进入脏数据检测,这时会调用
$digest
循环遍历全部的数据观察者,判断当前值是否和先前的值有区别,若是检测到变化的话,会调用$watch
函数,而后再次调用$digest
循环直到发现没有变化。循环至少为二次 ,至多为十次。
脏数据检测虽然存在低效的问题,可是不关心数据是经过什么方式改变的,均可以完成任务,可是这在
Vue
中的双向绑定是存在问题的。而且脏数据检测能够实现批量检测出更新的值,再去统一更新UI
,大大减小了操做DOM
的次数。因此低效也是相对的,这就仁者见仁智者见智了。
数据劫持
Vue
内部使用了Object.defineProperty()
来实现双向绑定,经过这个函数能够监听到set
和get
的事件。
var data = { name: 'yck' } observe(data) let name = data.name // -> get value data.name = 'yyy' // -> change value function observe(obj) { // 判断类型 if (!obj || typeof obj !== 'object') { return } Object.keys(obj).forEach(key => { defineReactive(obj, key, obj[key]) }) } function defineReactive(obj, key, val) { // 递归子属性 observe(val) Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { console.log('get value') return val }, set: function reactiveSetter(newVal) { console.log('change value') val = newVal } }) }
以上代码简单的实现了如何监听数据的
set
和get
的事件,可是仅仅如此是不够的,还须要在适当的时候给属性添加发布订阅
<div> {{name}} </div>
在解析如上模板代码时,遇到 就会给属性
name
添加发布订阅。
// 经过 Dep 解耦 class Dep { constructor() { this.subs = [] } addSub(sub) { // sub 是 Watcher 实例 this.subs.push(sub) } notify() { this.subs.forEach(sub => { sub.update() }) } } // 全局属性,经过该属性配置 Watcher Dep.target = null function update(value) { document.querySelector('div').innerText = value } class Watcher { constructor(obj, key, cb) { // 将 Dep.target 指向本身 // 而后触发属性的 getter 添加监听 // 最后将 Dep.target 置空 Dep.target = this this.cb = cb this.obj = obj this.key = key this.value = obj[key] Dep.target = null } update() { // 得到新值 this.value = this.obj[this.key] // 调用 update 方法更新 Dom this.cb(this.value) } } var data = { name: 'yck' } observe(data) // 模拟解析到 `{{name}}` 触发的操做 new Watcher(data, 'name', update) // update Dom innerText data.name = 'yyy'
接下来,对
defineReactive
函数进行改造
function defineReactive(obj, key, val) { // 递归子属性 observe(val) let dp = new Dep() Object.defineProperty(obj, key, { enumerable: true, configurable: true, get: function reactiveGetter() { console.log('get value') // 将 Watcher 添加到订阅 if (Dep.target) { dp.addSub(Dep.target) } return val }, set: function reactiveSetter(newVal) { console.log('change value') val = newVal // 执行 watcher 的 update 方法 dp.notify() } }) }
以上实现了一个简易的双向绑定,核心思路就是手动触发一次属性的
getter
来实现发布订阅的添加
Proxy 与 Object.defineProperty 对比
Object.defineProperty
虽然已经可以实现双向绑定了,可是他仍是有缺陷的。
Vue
中确实能检测到数组数据的变化,可是实际上是使用了 hack
的办法,而且也是有缺陷的。const arrayProto = Array.prototype export const arrayMethods = Object.create(arrayProto) // hack 如下几个函数 const methodsToPatch = [ 'push', 'pop', 'shift', 'unshift', 'splice', 'sort', 'reverse' ] methodsToPatch.forEach(function (method) { // 得到原生函数 const original = arrayProto[method] def(arrayMethods, method, function mutator (...args) { // 调用原生函数 const result = original.apply(this, args) const ob = this.__ob__ let inserted switch (method) { case 'push': case 'unshift': inserted = args break case 'splice': inserted = args.slice(2) break } if (inserted) ob.observeArray(inserted) // 触发更新 ob.dep.notify() return result }) })
反观
Proxy
就没以上的问题,原生支持监听数组变化,而且能够直接对整个对象进行拦截,因此Vue
也将在下个大版本中使用Proxy
替换Object.defineProperty
let onWatch = (obj, setBind, getLogger) => { let handler = { get(target, property, receiver) { getLogger(target, property) return Reflect.get(target, property, receiver); }, set(target, property, value, receiver) { setBind(value); return Reflect.set(target, property, value); } }; return new Proxy(obj, handler); }; let obj = { a: 1 } let value let p = onWatch(obj, (v) => { value = v }, (target, property) => { console.log(`Get '${property}' = ${target[property]}`); }) p.a = 2 // bind `value` to `2` p.a // -> Get 'a' = 2
前端路由实现起来其实很简单,本质就是监听
URL
的变化,而后匹配路由规则,显示相应的页面,而且无须刷新。目前单页面使用的路由就只有两种实现方式
hash
模式history
模式
www.test.com/##/
就是Hash URL
,当##
后面的哈希值发生变化时,不会向服务器请求数据,能够经过hashchange
事件来监听到URL
的变化,从而进行跳转页面。
History
模式是HTML5
新推出的功能,比之Hash URL
更加美观
为何须要 Virtual Dom
众所周知,操做
DOM
是很耗费性能的一件事情,既然如此,咱们能够考虑经过JS
对象来模拟DOM
对象,毕竟操做JS
对象比操做DOM
省时的多
// 假设这里模拟一个 ul,其中包含了 5 个 li [1, 2, 3, 4, 5] // 这里替换上面的 li [1, 2, 5, 4]
从上述例子中,咱们一眼就能够看出先前的
ul
中的第三个li
被移除了,四五替换了位置。
DOM
中,那么就是如下代码// 删除第三个 li ul.childNodes[2].remove() // 将第四个 li 和第五个交换位置 let fromNode = ul.childNodes[4] let toNode = node.childNodes[3] let cloneFromNode = fromNode.cloneNode(true) let cloenToNode = toNode.cloneNode(true) ul.replaceChild(cloneFromNode, toNode) ul.replaceChild(cloenToNode, fromNode)
固然在实际操做中,咱们还须要给每一个节点一个标识,做为判断是同一个节点的依据。因此这也是
Vue
和React
中官方推荐列表里的节点使用惟一的key
来保证性能。
DOM
对象能够经过 JS
对象来模拟,反之也能够经过 JS
对象来渲染出对应的 DOM
JS
对象模拟 DOM
对象的简单实现export default class Element { /** * @param {String} tag 'div' * @param {Object} props { class: 'item' } * @param {Array} children [ Element1, 'text'] * @param {String} key option */ constructor(tag, props, children, key) { this.tag = tag this.props = props if (Array.isArray(children)) { this.children = children } else if (isString(children)) { this.key = children this.children = null } if (key) this.key = key } // 渲染 render() { let root = this._createElement( this.tag, this.props, this.children, this.key ) document.body.appendChild(root) return root } create() { return this._createElement(this.tag, this.props, this.children, this.key) } // 建立节点 _createElement(tag, props, child, key) { // 经过 tag 建立节点 let el = document.createElement(tag) // 设置节点属性 for (const key in props) { if (props.hasOwnProperty(key)) { const value = props[key] el.setAttribute(key, value) } } if (key) { el.setAttribute('key', key) } // 递归添加子节点 if (child) { child.forEach(element => { let child if (element instanceof Element) { child = this._createElement( element.tag, element.props, element.children, element.key ) } else { child = document.createTextNode(element) } el.appendChild(child) }) } return el } }
Virtual Dom 算法简述
JS
来模拟实现了 DOM
,那么接下来的难点就在于如何判断旧的对象和新的对象之间的差别。DOM
是多叉树的结构,若是须要完整的对比两颗树的差别,那么须要的时间复杂度会是 O(n ^ 3)
,这个复杂度确定是不能接受的。因而 React
团队优化了算法,实现了 O(n)
的复杂度来对比差别。O(n)
复杂度的关键就是只对比同层的节点,而不是跨层对比,这也是考虑到在实际业务中不多会去跨层的移动 DOM
元素因此判断差别的算法就分为了两步
Virtual Dom 算法实现
树的递归
tagName
或者 key
和旧的不一样,这种状况表明须要替换旧的节点,而且也再也不须要遍历新旧节点的子元素了,由于整个旧节点都被删掉了tagName
和 key
(可能都没有)和旧的相同,开始遍历子树import { StateEnums, isString, move } from './util' import Element from './element' export default function diff(oldDomTree, newDomTree) { // 用于记录差别 let pathchs = {} // 一开始的索引为 0 dfs(oldDomTree, newDomTree, 0, pathchs) return pathchs } function dfs(oldNode, newNode, index, patches) { // 用于保存子树的更改 let curPatches = [] // 须要判断三种状况 // 1.没有新的节点,那么什么都不用作 // 2.新的节点的 tagName 和 `key` 和旧的不一样,就替换 // 3.新的节点的 tagName 和 key(可能都没有) 和旧的相同,开始遍历子树 if (!newNode) { } else if (newNode.tag === oldNode.tag && newNode.key === oldNode.key) { // 判断属性是否变动 let props = diffProps(oldNode.props, newNode.props) if (props.length) curPatches.push({ type: StateEnums.ChangeProps, props }) // 遍历子树 diffChildren(oldNode.children, newNode.children, index, patches) } else { // 节点不一样,须要替换 curPatches.push({ type: StateEnums.Replace, node: newNode }) } if (curPatches.length) { if (patches[index]) { patches[index] = patches[index].concat(curPatches) } else { patches[index] = curPatches } } }
判断属性的更改
判断属性的更改也分三个步骤
function diffProps(oldProps, newProps) { // 判断 Props 分如下三步骤 // 先遍历 oldProps 查看是否存在删除的属性 // 而后遍历 newProps 查看是否有属性值被修改 // 最后查看是否有属性新增 let change = [] for (const key in oldProps) { if (oldProps.hasOwnProperty(key) && !newProps[key]) { change.push({ prop: key }) } } for (const key in newProps) { if (newProps.hasOwnProperty(key)) { const prop = newProps[key] if (oldProps[key] && oldProps[key] !== newProps[key]) { change.push({ prop: key, value: newProps[key] }) } else if (!oldProps[key]) { change.push({ prop: key, value: newProps[key] }) } } } return change }
判断列表差别算法实现
这个算法是整个
Virtual Dom
中最核心的算法,且让我一一为你道来。 这里的主要步骤其实和判断属性差别是相似的,也是分为三步
PS:该算法只对有
key
的节点作处理
function listDiff(oldList, newList, index, patches) { // 为了遍历方便,先取出两个 list 的全部 keys let oldKeys = getKeys(oldList) let newKeys = getKeys(newList) let changes = [] // 用于保存变动后的节点数据 // 使用该数组保存有如下好处 // 1.能够正确得到被删除节点索引 // 2.交换节点位置只须要操做一遍 DOM // 3.用于 `diffChildren` 函数中的判断,只须要遍历 // 两个树中都存在的节点,而对于新增或者删除的节点来讲,彻底不必 // 再去判断一遍 let list = [] oldList && oldList.forEach(item => { let key = item.key if (isString(item)) { key = item } // 寻找新的 children 中是否含有当前节点 // 没有的话须要删除 let index = newKeys.indexOf(key) if (index === -1) { list.push(null) } else list.push(key) }) // 遍历变动后的数组 let length = list.length // 由于删除数组元素是会更改索引的 // 全部从后往前删能够保证索引不变 for (let i = length - 1; i >= 0; i--) { // 判断当前元素是否为空,为空表示须要删除 if (!list[i]) { list.splice(i, 1) changes.push({ type: StateEnums.Remove, index: i }) } } // 遍历新的 list,判断是否有节点新增或移动 // 同时也对 `list` 作节点新增和移动节点的操做 newList && newList.forEach((item, i) => { let key = item.key if (isString(item)) { key = item } // 寻找旧的 children 中是否含有当前节点 let index = list.indexOf(key) // 没找到表明新节点,须要插入 if (index === -1 || key == null) { changes.push({ type: StateEnums.Insert, node: item, index: i }) list.splice(i, 0, key) } else { // 找到了,须要判断是否须要移动 if (index !== i) { changes.push({ type: StateEnums.Move, from: index, to: i }) move(list, index, i) } } }) return { changes, list } } function getKeys(list) { let keys = [] let text list && list.forEach(item => { let key if (isString(item)) { key = [item] } else if (item instanceof Element) { key = item.key } keys.push(key) }) return keys }
遍历子元素打标识
对于这个函数来讲,主要功能就两个
function diffChildren(oldChild, newChild, index, patches) { let { changes, list } = listDiff(oldChild, newChild, index, patches) if (changes.length) { if (patches[index]) { patches[index] = patches[index].concat(changes) } else { patches[index] = changes } } // 记录上一个遍历过的节点 let last = null oldChild && oldChild.forEach((item, i) => { let child = item && item.children if (child) { index = last && last.children ? index + last.children.length + 1 : index + 1 let keyIndex = list.indexOf(item.key) let node = newChild[keyIndex] // 只遍历新旧中都存在的节点,其余新增或者删除的不必遍历 if (node) { dfs(item, node, index, patches) } } else index += 1 last = item }) }
渲染差别
经过以前的算法,咱们已经能够得出两个树的差别了。既然知道了差别,就须要局部去更新
DOM
了,下面就让咱们来看看Virtual Dom
算法的最后一步骤
这个函数主要两个功能
DOM
let index = 0 export default function patch(node, patchs) { let changes = patchs[index] let childNodes = node && node.childNodes // 这里的深度遍历和 diff 中是同样的 if (!childNodes) index += 1 if (changes && changes.length && patchs[index]) { changeDom(node, changes) } let last = null if (childNodes && childNodes.length) { childNodes.forEach((item, i) => { index = last && last.children ? index + last.children.length + 1 : index + 1 patch(item, patchs) last = item }) } } function changeDom(node, changes, noChild) { changes && changes.forEach(change => { let { type } = change switch (type) { case StateEnums.ChangeProps: let { props } = change props.forEach(item => { if (item.value) { node.setAttribute(item.prop, item.value) } else { node.removeAttribute(item.prop) } }) break case StateEnums.Remove: node.childNodes[change.index].remove() break case StateEnums.Insert: let dom if (isString(change.node)) { dom = document.createTextNode(change.node) } else if (change.node instanceof Element) { dom = change.node.create() } node.insertBefore(dom, node.childNodes[change.index]) break case StateEnums.Replace: node.parentNode.replaceChild(change.node.create(), node) break case StateEnums.Move: let fromNode = node.childNodes[change.from] let toNode = node.childNodes[change.to] let cloneFromNode = fromNode.cloneNode(true) let cloenToNode = toNode.cloneNode(true) node.replaceChild(cloneFromNode, toNode) node.replaceChild(cloenToNode, fromNode) break default: break } }) }
Virtual Dom 算法的实现也就是如下三步
JS
来模拟建立 DOM
对象let test4 = new Element('div', { class: 'my-div' }, ['test4']) let test5 = new Element('ul', { class: 'my-div' }, ['test5']) let test1 = new Element('div', { class: 'my-div' }, [test4]) let test2 = new Element('div', { id: '11' }, [test5, test4]) let root = test1.render() let pathchs = diff(test1, test2) console.log(pathchs) setTimeout(() => { console.log('开始更新') patch(root, pathchs) console.log('结束更新') }, 1000)