前端進階篇二

 

1、JavaScript進階

#1 內置類型

  • JS 中分爲七種內置類型,七種內置類型又分爲兩大類型:基本類型和對象(Object)。
  • 基本類型有六種: nullundefinedboolean,numberstringsymbol
  • 其中 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 

#2 Typeof

typeof 對於基本類型,除了 null 均可以顯示正確的類型css

typeof 1 // 'number' typeof '1' // 'string' typeof undefined // 'undefined' typeof true // 'boolean' typeof Symbol() // 'symbol' typeof b // b 沒有聲明,可是還會顯示 undefined 

typeof 對於對象,除了函數都會顯示 objecthtml

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 

#3 類型轉換

轉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 字符索引來比較

#4 原型

  • 每一個函數都有 prototype 屬性,除了 Function.prototype.bind(),該屬性指向原型。
  • 每一個對象都有 __proto__屬性,指向了建立該對象的構造函數的原型。其實這個屬性指向了 [[prototype]],可是 [[prototype]] 是內部屬性,咱們並不能訪問到,因此使用 _proto_ 來訪問。
  • 對象能夠經過__proto__ 來尋找不屬於該對象的屬性,__proto__ 將對象鏈接起來組成了原型鏈

#5 new

  • 新生成了一個對象
  • 連接到原型
  • 綁定 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 } 

#6 instanceof

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__ } } 

#7 this

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 一旦綁定了上下文,就不會被任何代碼改變

#8 執行上下文

當執行 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中引入了 letlet不能在聲明前使用,可是這並非常說的 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 

#9 閉包

閉包的定義很簡單:函數 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 } ... } 

#10 深淺拷貝

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"} 
  • 你會發如今上述狀況中,該方法會忽略掉函數和`undefined。
  • 可是在一般狀況下,複雜數據都是能夠序列化的,因此這個函數能夠解決大部分問題,而且該函數是內置函數中處理深拷貝性能最快的。固然若是你的數據中含有以上三種狀況下,能夠使用 lodash 的深拷貝函數。

#11 模塊化

在有 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() }) 

#12 防抖

你是否在平常開發中遇到一個問題,在滾動事件中須要作個複雜計算或者實現一個按鈕的防二次點擊操做。

  • 這些需求均可以經過函數防抖動來實現。尤爲是第一個需求,若是在頻繁的事件回調中作複雜計算,頗有可能致使頁面卡頓,不如將屢次計算合併爲一次計算,只在一個精確點作操做
  • PS:防抖和節流的做用都是防止函數屢次調用。區別在於,假設一個用戶一直觸發這個函數,且每次觸發函數的間隔小於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,若是是延遲調用就調用函數

#13 節流

防抖動和節流本質是不同的。防抖動是將屢次執行變爲最後一次執行,節流是將屢次執行變成每隔一段時間執行

/** * 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; }; }; 

#14 繼承

在 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 底層的這個限制

#15 call, apply, bind

  • 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']) 

#16 Promise 實現

  • 能夠把 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); } } 

#17 Generator 實現

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(); } } }); } 

#18 Proxy

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 

#2、瀏覽器

#1 事件機制

事件觸發三階段

  • 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> 

事件代理的方式相對於直接給目標註冊事件來講,有如下優勢

  • 節省內存
  • 不須要給子節點註銷事件

#2 跨域

由於瀏覽器出於安全考慮,有同源策略。也就是說,若是協議、域名或者端口有一個不一樣就是跨域,Ajax 請求會失敗

JSONP

JSONP 的原理很簡單,就是利用 <script> 標籤沒有跨域限制的漏洞。經過 <script>標籤指向一個須要訪問的地址並提供一個回調函數來接收數據當須要通信時

<script src="http://domain/api?param1=a&param2=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('驗證經過') } }); 

#3 Event loop

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 順序是這樣的

  • 執行同步代碼,這屬於宏任務
  • 執行棧爲空,查詢是否有微任務須要執行
  • 執行全部微任務
  • 必要的話渲染 UI
  • 而後開始下一輪 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
  • 一個 timer 指定的時間並非準確時間,而是在達到這個時間後儘快執行回調,可能會由於系統正在執行別的事務而延遲

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 

#4 Service Worker

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 中讀取的

#5 渲染機制

瀏覽器的渲染機制通常分爲如下幾個步驟

  • 處理 HTML 並構建 DOM 樹。
  • 處理 CSS 構建 CSSOM 樹。
  • 將 DOM 與 CSSOM 合併成一個渲染樹。
  • 根據渲染樹來佈局,計算每一個節點的位置。
  • 調用 GPU 繪製,合成圖層,顯示在屏幕上

  • 在構建 CSSOM 樹時,會阻塞渲染,直至 CSSOM 樹構建完成。而且構建 CSSOM 樹是一個十分消耗性能的過程,因此應該儘可能保證層級扁平,減小過分層疊,越是具體的 CSS 選擇器,執行速度越慢
  • 當 HTML 解析到 script 標籤時,會暫停構建 DOM,完成後纔會從暫停的地方從新開始。也就是說,若是你想首屏渲染的越快,就越不該該在首屏就加載 JS 文件。而且 CSS 也會影響 JS 的執行,只有當解析完樣式表纔會執行 JS,因此也能夠認爲這種狀況下,CSS 也會暫停構建 DOM

圖層

通常來講,能夠把普通文檔流當作一個圖層。特定的屬性能夠生成一個新的圖層。不一樣的圖層渲染互不影響,因此對於某些頻繁須要渲染的建議單獨生成一個新圖層,提升性能。但也不能生成過多的圖層,會引發副作用

  • 經過如下幾個經常使用屬性能夠生成新圖層
    • 3D變換:translate3dtranslateZ
    • will-change
    • videoiframe 標籤
    • 經過動畫實現的 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標籤,瀏覽器會自動將該節點變爲圖層

#3、性能

#1 DNS 預解析

  • DNS 解析也是須要時間的,能夠經過預解析的方式來預先得到域名所對應的 IP
<link rel="dns-prefetch" href="//blog.poetries.top"> 

#2 緩存

  • 緩存對於前端性能優化來講是個很重要的點,良好的緩存策略能夠下降資源的重複加載提升網頁的總體加載速度
  • 一般瀏覽器緩存策略分爲兩種:強緩存和協商緩存

強緩存

實現強緩存能夠經過兩種響應頭實現: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 秒後過時,須要再次請求

協商緩存

  • 若是緩存過時了,咱們就能夠使用協商緩存來解決問題。協商緩存須要請求,若是緩存有效會返回 304
  • 協商緩存須要客戶端和服務端共同實現,和強緩存同樣,也有兩種實現方式

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 並配合策略緩存使用,而後對文件進行指紋處理,一旦文件名變更就會馬上下載新的文件

#3 使用 HTTP / 2.0

  • 由於瀏覽器會有併發請求限制,在 HTTP / 1.1 時代,每一個請求都須要創建和斷開,消耗了好幾個 RTT 時間,而且因爲 TCP 慢啓動的緣由,加載體積大的文件會須要更多的時間
  • 在 HTTP / 2.0 中引入了多路複用,可以讓多個請求使用同一個 TCP 連接,極大的加快了網頁的加載速度。而且還支持 Header 壓縮,進一步的減小了請求的數據大小

#4 預加載

  • 在開發中,可能會遇到這樣的狀況。有些資源不須要立刻用到,可是但願儘早獲取,這時候就能夠使用預加載
  • 預加載實際上是聲明式的 fetch ,強制瀏覽器請求資源,而且不會阻塞 onload 事件,能夠使用如下代碼開啓預加載
<link rel="preload" href="http://example.com"> 

預加載能夠必定程度上下降首屏的加載時間,由於能夠將一些不影響首屏但重要的文件延後加載,惟一缺點就是兼容性很差

#5 預渲染

能夠經過預渲染將下載的文件預先在後臺渲染,能夠使用如下代碼開啓預渲染

<link rel="prerender" href="http://poetries.com"> 
  • 預渲染雖然能夠提升頁面的加載速度,可是要確保該頁面百分百會被用戶在以後打開,不然就白白浪費資源去渲染

#6 懶執行與懶加載

懶執行

  • 懶執行就是將某些邏輯延遲到使用時再計算。該技術能夠用於首屏優化,對於某些耗時邏輯並不須要在首屏就使用的,就能夠使用懶執行。懶執行須要喚醒,通常能夠經過定時器或者事件的調用來喚醒

懶加載

  • 懶加載就是將不關鍵的資源延後加載

懶加載的原理就是隻加載自定義區域(一般是可視區域,但也能夠是即將進入可視區域)內須要加載的東西。對於圖片來講,先設置圖片標籤的 src 屬性爲一張佔位圖,將真實的圖片資源放入一個自定義屬性中,當進入自定義區域時,就將自定義屬性替換爲 src屬性,這樣圖片就會去下載資源,實現了圖片懶加載

  • 懶加載不只能夠用於圖片,也能夠使用在別的資源上。好比進入可視區域纔開始播放視頻等

#7 文件優化

圖片優化

對於如何優化圖片,有 2 個思路

  • 減小像素點
  • 減小每一個像素點可以顯示的顏色

圖片加載優化

  • 不用圖片。不少時候會使用到不少修飾類圖片,其實這類修飾圖片徹底能夠用 CSS 去代替。
  • 對於移動端來講,屏幕寬度就那麼點,徹底沒有必要去加載原圖浪費帶寬。通常圖片都用 CDN 加載,能夠計算出適配屏幕的寬度,而後去請求相應裁剪好的圖片
  • 小圖使用 base64格式
  • 將多個圖標文件整合到一張圖片中(雪碧圖)
  • 選擇正確的圖片格式:
    • 對於可以顯示 WebP 格式的瀏覽器儘可能使用 WebP 格式。由於 WebP 格式具備更好的圖像數據壓縮算法,能帶來更小的圖片體積,並且擁有肉眼識別無差別的圖像質量,缺點就是兼容性並很差
    • 小圖使用 PNG,其實對於大部分圖標這類圖片,徹底能夠使用 SVG 代替
    • 照片使用 JPEG

其餘文件優化

  • CSS文件放在 head 中
  • 服務端開啓文件壓縮功能
  • 將 script 標籤放在 body 底部,由於 JS 文件執行會阻塞渲染。固然也能夠把 script 標籤放在任意位置而後加上 defer ,表示該文件會並行下載,可是會放到 HTML 解析完成後順序執行。對於沒有任何依賴的 JS文件能夠加上 async ,表示加載和渲染後續文檔元素的過程將和 JS 文件的加載與執行並行無序進行。 執行 JS代碼過長會卡住渲染,對於須要不少時間計算的代碼
  • 能夠考慮使用 WebworkerWebworker可讓咱們另開一個線程執行腳本而不影響渲染。

CDN

靜態資源儘可能使用 CDN 加載,因爲瀏覽器對於單個域名有併發請求上限,能夠考慮使用多個 CDN 域名。對於 CDN 加載靜態資源須要注意 CDN 域名要與主站不一樣,不然每次請求都會帶上主站的 Cookie

#8 其餘

使用 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發起一個請求

#4、安全

#1 XSS

跨網站指令碼(英語: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, "&amp;"); str = str.replace(/</g, "&lt;"); str = str.replace(/>/g, "&gt;"); 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> 變成

// -> &lt;script&gt;alert(1)&lt;&##x2F;script&gt; 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>&lt;script&gt;alert("xss");&lt;/script&gt; console.log(html); 

以上示例使用了 js-xss來實現。能夠看到在輸出中保留了 h1 標籤且過濾了 script標籤

#2 CSRF

跨站請求僞造(英語: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

#3 密碼安全

加鹽

對於密碼存儲來講,必然是不能明文存儲在數據庫中的,不然一旦數據庫泄露,會對用戶形成很大的損失。而且不建議只對密碼單純經過加密算法加密,由於存在彩虹表的關係

  • 一般須要對密碼加鹽,而後進行幾回不一樣加密算法的加密
// 加鹽也就是給原密碼添加字符串,增長原密碼長度 sha256(sha1(md5(salt + password + salt))) 

可是加鹽並不能阻止別人盜取帳號,只能確保即便數據庫泄露,也不會暴露用戶的真實密碼。一旦攻擊者獲得了用戶的帳號,能夠經過暴力破解的方式破解密碼。對於這種狀況,一般使用驗證碼增長延時或者限制嘗試次數的方式。而且一旦用戶輸入了錯誤的密碼,也不能直接提示用戶輸錯密碼,而應該提示帳號或密碼錯誤

前端加密

雖然前端加密對於安全防禦來講意義不大,可是在遇到中間人攻擊的狀況下,能夠避免明文密碼被第三方獲取

#5、小程序

#1 登陸

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 的概念, 登陸態必須緩存在本地,所以強烈建議爲登陸態設置過時時間
  • 值得一提的是若是須要支持風控安全校驗,多平臺登陸等功能,可能須要加入一些公共參數,例如platformchanneldeviceParam等參數。在和服務端肯定方案時,做爲前端同窗應該及時提出這些合理的建議,設計合理的系統。
  • openid , unionid 不要在接口中明文傳輸,這是一種危險的行爲,同時也很不專業

#2 圖片導出

這是一種常見的引流方式,通常同時會在圖片中附加一個小程序二維碼。

基本原理

  • 藉助 canvas 元素,將須要導出的樣式首先在 canvas 畫布上繪製出來 (api基本和h5保持一致,但有輕微差別,使用時注意便可
  • 藉助微信提供的 canvasToTempFilePath 導出圖片,最後再使用 saveImageToPhotosAlbum (須要受權)保存圖片到本地

如何優雅實現

  • 繪製出須要的樣式這一步是省略不掉的。可是咱們能夠封裝一個繪製庫,包含常見圖形的繪製,例如矩形,圓角矩形,圓, 扇形, 三角形, 文字,圖片減小繪製代碼,只須要提煉出樣式信息,即可以輕鬆的繪製,最後導出圖片存入相冊。筆者以爲如下這種方式繪製更爲優雅清晰一些,其實也能夠使用加入一個type參數來指定繪製類型,傳入的一個是樣式數組,實現繪製。
  • 結合上一步的實現,若是對於同一類型的卡片有屢次導出需求的場景,也能夠使用自定義組件的方式,封裝同一類型的卡片爲一個通用組件,在須要導出圖片功能的地方,引入該組件便可。
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可見字符)限制,能夠藉助服務端生成 短連接 的方式來解決

#3 數據統計

數據統計做爲目前一種經常使用的分析用戶行爲的方式,小程序端也是必不可少的。小程序採起的曝光,點擊數據埋點其實和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並非多處調用的,只會在某一個特定的頁面調用,經過這個思路咱們能夠分析出,該接口被請求,則這個行爲被觸發了,則徹底能夠經過服務端日誌得出埋點數據,可是這種方式侷限性較大,並且屬於分析結果得出過程,可能存在偏差,但能夠做爲一種思路瞭解一下。

微信自定義數據分析

微信自己提供的數據分析能力,微信自己提供了常規分析和自定義分析兩種數據分析方式,在小程序後臺配置便可。藉助小程序數據助手這款小程序能夠很方便的查看

#4 工程化

工程化作什麼

目前的前端開發過程,工程化是必不可少的一環,那小程序工程化都須要作些什麼呢,先看下目前小程序開發當中存在哪些問題須要解決:

  • 不支持 css預編譯器,做爲一種主流的 css解決方案,不管是 less,sass,stylus 均可以提高css效率
  • 不支持引入npm包 (這一條,從微信公開課中聽聞,微信準備支持)
  • 不支持ES7等後續的js特性,好用的async await等特性都沒法使用
  • 不支持引入外部字體文件,只支持base64
  • 沒有 eslint 等代碼檢查工具

方案選型

對於目前經常使用的工程化方案,webpackrollupparcel等來看,都經常使用與單頁應用的打包和處理,而小程序天生是 「多頁應用」 而且存在一些特定的配置。根據要解決的問題來看,無非是文件的編譯,修改,拷貝這些處理,對於這些需求,咱們想到基於流的 gulp很是的適合處理,而且相對於webpack配置多頁應用更加簡單。因此小程序工程化方案推薦使用 gulp

具體開發思路

經過 gulp 的 task 實現:

  • 實時編譯 less 文件至相應目錄
  • 引入支持asyncawait的運行時文件
  • 編譯字體文件爲base64 並生成相應css文件,方便使用
  • 依賴分析哪些地方引用了npm包,將npm包打成一個文件,拷貝至相應目錄
  • 檢查代碼規範

#5 小程序架構

微信小程序的框架包含兩部分 View 視圖層、App Service邏輯層。View 層用來渲染頁面結構,AppService 層用來邏輯處理、數據請求、接口調用。

它們在兩個線程裏運行。

視圖層和邏輯層經過系統層的 JSBridage 進行通訊,邏輯層把數據變化通知到視圖層,觸發視圖層頁面更新,視圖層把觸發的事件通知到邏輯層進行業務處理

  • 視圖層使用 WebView 渲染,iOS中使用自帶 WKWebView,在 Android 使用騰訊的 x5內核(基於 Blink)運行。
  • 邏輯層使用在 iOS 中使用自帶的 JSCore 運行,在 Android中使用騰訊的 x5 內核(基於 Blink)運行。
  • 開發工具使用 nw.js 同時提供了視圖層和邏輯層的運行環境。

#6 WXML && WXSS

WXML

  • 支持數據綁定
  • 支持邏輯算術、運算
  • 支持模板、引用
  • 支持添加事件(bindtap
  • Wxml編譯器:Wcc 把 Wxml文件 轉爲 JS
  • 執行方式:Wcc index.wxml
  • 使用 Virtual DOM,進行局部更新

WXSS

  • 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 中相同的選擇器

#7 小程序的問題

  • 小程序仍然使用 WebView 渲染,並不是原生渲染。(部分原生)
  • 服務端接口返回的頭沒法執行,好比:Set-Cookie
  • 依賴瀏覽器環境的 JS庫不能使用。
  • 不能使用 npm,可是能夠自搭構建工具或者使用 mpvue。(將來官方有計劃支持)
  • 不能使用 ES7,能夠本身用babel+webpack自搭或者使用 mpvue
  • 不支持使用本身的字體(將來官方計劃支持)。
  • 能夠用 base64 的方式來使用 iconfont
  • 小程序不能發朋友圈(能夠經過保存圖片到本地,發圖片到朋友前。二維碼能夠使用B接口)。
  • 獲取二維碼/小程序接口的限制
  • 程序推送只能使用「服務通知」 並且須要用戶主動觸發提交 formIdformId 只有7天有效期。(如今的作法是在每一個頁面都放入form而且隱藏以此獲取更多的 formId。後端使用原則爲:優先使用有效期最短的)
  • 小程序大小限制 2M,分包總計不超過 8M
  • 轉發(分享)小程序不能拿到成功結果,原來能夠。連接(小遊戲造的孽)
  • 拿到相同的 unionId 必須綁在同一個開放平臺下。開放平臺綁定限制:
    • 50個移動應用
    • 10個網站
    • 50個同主體公衆號
    • 5個不一樣主體公衆號
    • 50個同主體小程序
    • 5個不一樣主體小程序
  • 公衆號關聯小程序
    • 全部公衆號均可以關聯小程序。
    • 一個公衆號可關聯10個同主體的小程序,3個不一樣主體的小程序。
    • 一個小程序可關聯500個公衆號。
    • 公衆號一個月可新增關聯小程序13次,小程序一個月可新增關聯500次。
  • 一個公衆號關聯的10個同主體小程序和3個非同主體小程序能夠互相跳轉
  • 品牌搜索不支持金融、醫療
  • 小程序受權須要用戶主動點擊
  • 小程序不提供測試 access_token
  • 安卓系統下,小程序受權獲取用戶信息以後,刪除小程序再從新獲取,並從新受權,獲得舊簽名,致使第一次受權失敗
  • 開發者工具上,受權獲取用戶信息以後,若是清緩存選擇所有清除,則即便使用了wx.checkSession,而且在session_key有效期內,受權獲取用戶信息也會獲得新的session_key

#8 受權獲取用戶信息流程

  • 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,從而再一次致使用戶信息解密失敗

#9 性能優化

咱們知道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(主包)內

引用原則

  • ``packageA沒法require packageB JS 文件,但能夠require app、本身package內的JS` 文件
  • ``packageA沒法import packageBtemplate,但能夠require app、本身package內的template`
  • ``packageA 沒法使用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)替代

使用自定義組件

在須要頻繁更新的場景下,自定義組件的更新只在組件內部進行,不受頁面其餘部份內容複雜性影響

#10 wepy vs mpvue

數據流管理

相比傳統的小程序框架,這個一直是咱們做爲資深開發者比較指望去解決的,在 Web 開發中,隨着 FluxRedux、Vuex 等多個數據流工具出現,咱們也指望在業務複雜的小程序中使用

  • WePY 默認支持 Redux,在腳手架生成項目的時候能夠內置
  • Mpvue 做爲 Vue 的移植版本,固然支持 Vuex,一樣在腳手架生成項目的時候能夠內置

組件化

  • WePY 相似 Vue實現了單文件組件,最大的差異是文件後綴 .wpy,只是寫法上會有差別
export default class Index extends wepy.page {} 
  • Mpvue 做爲 Vue 的移植版本,支持單文件組件,templatescript 和 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
    • 可是配置方式仍是相似,分環境配置文件,最終都會編譯成小程序支持的目錄結構和文件後綴

#11 mpvue

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.jsweex平臺見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,找出 diffpatch完了以後的 diff 怎麼更新視圖,也就是如何給這些 DOM 加入 attrclassstyle等 DOM 屬性呢? Vue中有 nextTick 的概念用以更新視圖,mpvue這塊對於小程序的 setData 應該怎麼處理呢?
  • 另外個問題在於小程序的 Virtual DOM 怎麼生成?也就是怎麼將 template編譯成render function。這當中還涉及到運行時-編譯器-vs-只包含運行時,顯然若是要提升性能、減小包大小、輸出 wxmlmpvue 也要提供預編譯的能力。由於要預輸出 wxml 且無法動態改變 DOM,因此動態組件,自定義 render,和<script type="text/x-template">字符串模版等都不支持

另外還有一些其餘問題,最後總結一下

  • 1.如何預編譯生成render function
  • 2.如何預編譯生成 wxmlwxsswxs
  • 3.如何 patch 出 diff
  • 4.如何更新視圖
  • 5.如何創建小程序事件代理機制,在事件代理函數中觸發與之對應的vue組件事件響應
  • 6.如何創建vue實例與小程序 Page實例關聯
  • 7.如何創建小程序和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' } } }; 

#6、React

#1 React 中 keys 的做用是什麼?

Keys是 React 用於追蹤哪些列表中元素被修改、被添加或者被移除的輔助標識

  • 在開發過程當中,咱們須要保證某個元素的 key 在其同級元素中具備惟一性。在 React Diff 算法中React 會藉助元素的 Key 值來判斷該元素是新近建立的仍是被移動而來的元素,從而減小沒必要要的元素重渲染。此外,React 還須要藉助 Key 值來判斷元素與本地狀態的關聯關係,所以咱們毫不可忽視轉換函數中 Key 的重要性

#2 傳入 setState 函數的第二個參數的做用是什麼?

該函數會在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 } }) 

#3 React 中 refs 的做用是什麼

  • Refs 是 React 提供給咱們的安全訪問 DOM元素或者某個組件實例的句柄
  • 能夠爲元素添加ref屬性而後在回調函數中接受該元素在 DOM 樹中的句柄,該值會做爲回調函數的第一個參數返回

#4 在生命週期中的哪一步你應該發起 AJAX 請求

咱們應當將AJAX 請求放到 componentDidMount 函數中執行,主要緣由有下

  • React 下一代調和算法 Fiber 會經過開始或中止渲染的方式優化應用性能,其會影響到 componentWillMount 的觸發次數。對於 componentWillMount 這個生命週期函數的調用次數會變得不肯定,React 可能會屢次頻繁調用 componentWillMount。若是咱們將 AJAX 請求放到 componentWillMount 函數中,那麼顯而易見其會被觸發屢次,天然也就不是好的選擇。
  • 若是咱們將AJAX 請求放置在生命週期的其餘函數中,咱們並不能保證請求僅在組件掛載完畢後纔會要求響應。若是咱們的數據請求在組件掛載以前就完成,而且調用了setState函數將數據添加到組件狀態中,對於未掛載的組件則會報錯。而在 componentDidMount 函數中進行 AJAX 請求則能有效避免這個問題

#5 shouldComponentUpdate 的做用

shouldComponentUpdate 容許咱們手動地判斷是否要進行組件更新,根據組件的應用場景設置函數的合理返回值可以幫咱們避免沒必要要的更新

#6 如何告訴 React 它應該編譯生產環境版

一般狀況下咱們會使用 Webpack 的 DefinePlugin 方法來將 NODE_ENV 變量值設置爲 production。編譯版本中 React會忽略 propType 驗證以及其餘的告警信息,同時還會下降代碼庫的大小,React 使用了 Uglify 插件來移除生產環境下沒必要要的註釋等信息

#7 概述下 React 中的事件處理邏輯

爲了解決跨瀏覽器兼容性問題,React 會將瀏覽器原生事件(Browser Native Event)封裝爲合成事件(SyntheticEvent)傳入設置的事件處理器中。這裏的合成事件提供了與原生事件相同的接口,不過它們屏蔽了底層瀏覽器的細節差別,保證了行爲的一致性。另外有意思的是,React 並無直接將事件附着到子元素上,而是以單一事件監聽器的方式將全部的事件發送到頂層進行處理。這樣 React 在更新 DOM 的時候就不須要考慮如何去處理附着在 DOM 上的事件監聽器,最終達到優化性能的目的

#8 createElement 與 cloneElement 的區別是什麼

createElement 函數是 JSX 編譯以後使用的建立 React Element 的函數,而 cloneElement 則是用於複製某個元素並傳入新的 Props

#9 redux中間件

中間件提供第三方插件的模式,自定義攔截 action -> reducer 的過程。變爲 action-> middlewares -> reducer。這種機制可讓咱們改變數據流,實現如異步actionaction 過濾,日誌輸出,異常報告等功能

  • redux-logger:提供日誌輸出
  • redux-thunk:處理異步操做
  • redux-promise:處理異步操做,actionCreator的返回值是promise

#10 redux有什麼缺點

  • 一個組件所須要的數據,必須由父組件傳過來,而不能像flux中直接從store取。
  • 當一個組件相關數據更新時,即便父組件不須要用到這個組件,父組件仍是會從新render,可能會有效率影響,或者須要寫複雜的shouldComponentUpdate進行判斷。

#11 react組件的劃分業務組件技術組件?

  • 根據組件的職責一般把組件分爲UI組件和容器組件。
  • UI 組件負責 UI 的呈現,容器組件負責管理數據和邏輯。
  • 二者經過React-Redux 提供connect方法聯繫起來

#12 react生命週期函數

初始化階段

  • getDefaultProps:獲取實例的默認屬性
  • getInitialState:獲取每一個實例的初始化狀態
  • componentWillMount:組件即將被裝載、渲染到頁面上
  • render:組件在這裏生成虛擬的DOM節點
  • omponentDidMount:組件真正在被裝載以後

運行中狀態

  • componentWillReceiveProps:組件將要接收到屬性的時候調用
  • shouldComponentUpdate:組件接受到新屬性或者新狀態的時候(能夠返回false,接收數據後不更新,阻止render調用,後面的函數不會被繼續執行了)
  • componentWillUpdate:組件即將更新不能修改屬性和狀態
  • render:組件從新描繪
  • componentDidUpdate:組件已經更新

銷燬階段

  • componentWillUnmount:組件即將銷燬

#13 react性能優化是哪一個周期函數

shouldComponentUpdate 這個方法用來判斷是否須要調用render方法從新描繪dom。由於dom的描繪很是消耗性能,若是咱們能在shouldComponentUpdate方法中可以寫出更優化的dom diff算法,能夠極大的提升性能

#14 爲何虛擬dom會提升性能

虛擬dom至關於在js和真實dom中間加了一個緩存,利用dom diff算法避免了沒有必要的dom操做,從而提升性能

具體實現步驟以下

  • 用 JavaScript 對象結構表示 DOM 樹的結構;而後用這個樹構建一個真正的 DOM 樹,插到文檔當中
  • 當狀態變動的時候,從新構造一棵新的對象樹。而後用新的樹和舊的樹進行比較,記錄兩棵樹差別
  • 把2所記錄的差別應用到步驟1所構建的真正的DOM樹上,視圖就更新

#15 diff算法?

  • 把樹形結構按照層級分解,只比較同級元素。
  • 給列表結構的每一個單元添加惟一的key屬性,方便比較。
  • React 只會匹配相同 class 的 component(這裏面的class指的是組件的名字)
  • 合併操做,調用 component 的 setState 方法的時候, React 將其標記爲 - dirty.到每個事件循環結束, React 檢查全部標記 dirty的 component從新繪製.
  • 選擇性子樹渲染。開發人員能夠重寫shouldComponentUpdate提升diff的性能

#16 react性能優化方案

  • 重寫shouldComponentUpdate來避免沒必要要的dom操做
  • 使用 production 版本的react.js
  • 使用key來幫助React識別列表中全部子組件的最小變化

#16 簡述flux 思想

Flux 的最大特色,就是數據的"單向流動"。

  • 用戶訪問 View
  • View發出用戶的 Action
  • Dispatcher 收到Action,要求 Store 進行相應的更新
  • Store 更新後,發出一個"change"事件
  • View 收到"change"事件後,更新頁面

#17 說說你用react有什麼坑點?

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 進行傳入

#18 我如今有一個button,要用react在上面綁定點擊事件,要怎麼作?

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> } 

#19 react 的虛擬dom是怎麼實現的

首先說說爲何要使用Virturl DOM,由於操做真實DOM的耗費的性能代價過高,因此react內部使用js實現了一套dom結構,在每次操做在和真實dom以前,使用實現好的diff算法,對虛擬dom進行比較,遞歸找出有變化的dom節點,而後對其進行更新操做。爲了實現虛擬DOM,咱們須要把每一種節點類型抽象成對象,每一種節點類型有本身的屬性,也就是prop,每次進行diff的時候,react會先比較該節點類型,假如節點類型不同,那麼react會直接刪除該節點,而後直接建立新的節點插入到其中,假如節點類型同樣,那麼會比較prop是否有更新,假若有prop不同,那麼react會斷定該節點有更新,那麼重渲染該節點,而後在對其子節點進行比較,一層一層往下,直到沒有子節點

#20 react 的渲染過程當中,兄弟節點之間是怎麼處理的?也就是key值不同的時候

一般咱們輸出節點的時候都是map一個數組而後返回一個ReactNode,爲了方便react內部進行優化,咱們必須給每個reactNode添加key,這個key prop在設計值處不是給開發者用的,而是給react用的,大概的做用就是給每個reactNode添加一個身份標識,方便react進行識別,在重渲染過程當中,若是key同樣,若組件屬性有所變化,則react只更新組件對應的屬性;沒有變化則不更新,若是key不同,則react先銷燬該組件,而後從新建立該組件

#21 那給我介紹一下react

  1. 之前咱們沒有jquery的時候,咱們大概的流程是從後端經過ajax獲取到數據而後使用jquery生成dom結果真後更新到頁面當中,可是隨着業務發展,咱們的項目可能會愈來愈複雜,咱們每次請求到數據,或則數據有更改的時候,咱們又須要從新組裝一次dom結構,而後更新頁面,這樣咱們手動同步dom和數據的成本就愈來愈高,並且頻繁的操做dom,也使我咱們頁面的性能慢慢的下降。
  2. 這個時候mvvm出現了,mvvm的雙向數據綁定可讓咱們在數據修改的同時同步dom的更新,dom的更新也能夠直接同步咱們數據的更改,這個特定能夠大大下降咱們手動去維護dom更新的成本,mvvm爲react的特性之一,雖然react屬於單項數據流,須要咱們手動實現雙向數據綁定。
  3. 有了mvvm還不夠,由於若是每次有數據作了更改,而後咱們都全量更新dom結構的話,也沒辦法解決咱們頻繁操做dom結構(下降了頁面性能)的問題,爲了解決這個問題,react內部實現了一套虛擬dom結構,也就是用js實現的一套dom結構,他的做用是講真實dom在js中作一套緩存,每次有數據更改的時候,react內部先使用算法,也就是鼎鼎有名的diff算法對dom結構進行對比,找到那些咱們須要新增、更新、刪除的dom節點,而後一次性對真實DOM進行更新,這樣就大大下降了操做dom的次數。 那麼diff算法是怎麼運做的呢,首先,diff針對類型不一樣的節點,會直接斷定原來節點須要卸載而且用新的節點來裝載卸載的節點的位置;針對於節點類型相同的節點,會對比這個節點的全部屬性,若是節點的全部屬性相同,那麼斷定這個節點不須要更新,若是節點屬性不相同,那麼會斷定這個節點須要更新,react會更新並重渲染這個節點。
  4. react設計之初是主要負責UI層的渲染,雖然每一個組件有本身的state,state表示組件的狀態,當狀態須要變化的時候,須要使用setState更新咱們的組件,可是,咱們想經過一個組件重渲染它的兄弟組件,咱們就須要將組件的狀態提高到父組件當中,讓父組件的狀態來控制這兩個組件的重渲染,當咱們組件的層次愈來愈深的時候,狀態須要一直往下傳,無疑加大了咱們代碼的複雜度,咱們須要一個狀態管理中心,來幫咱們管理咱們狀態state。
  5. 這個時候,redux出現了,咱們能夠將全部的state交給redux去管理,當咱們的某一個state有變化的時候,依賴到這個state的組件就會進行一次重渲染,這樣就解決了咱們的咱們須要一直把state往下傳的問題。redux有action、reducer的概念,action爲惟一修改state的來源,reducer爲惟一肯定state如何變化的入口,這使得redux的數據流很是規範,同時也暴露出了redux代碼的複雜,原本那麼簡單的功能,卻須要完成那麼多的代碼。
  6. 後來,社區就出現了另一套解決方案,也就是mobx,它推崇代碼簡約易懂,只須要定義一個可觀測的對象,而後哪一個組價使用到這個可觀測的對象,而且這個對象的數據有更改,那麼這個組件就會重渲染,並且mobx內部也作好了是否重渲染組件的生命週期shouldUpdateComponent,不建議開發者進行更改,這使得咱們使用mobx開發項目的時候能夠簡單快速的完成不少功能,連redux的做者也推薦使用mobx進行項目開發。可是,隨着項目的不斷變大,mobx也不斷暴露出了它的缺點,就是數據流太隨意,出了bug以後很差追溯數據的流向,這個缺點正好體現出了redux的優勢所在,因此針對於小項目來講,社區推薦使用mobx,對大項目推薦使用redux

#7、Vue

#1 對於MVVM的理解

MVVM 是 Model-View-ViewModel 的縮寫

  • Model 表明數據模型,也能夠在Model中定義數據修改和操做的業務邏輯。
  • View 表明UI 組件,它負責將數據模型轉化成UI 展示出來。
  • ViewModel 監聽模型數據的改變和控制視圖行爲、處理用戶交互,簡單理解就是一個同步View 和 Model的對象,鏈接ModelView
  • MVVM架構下,View和 Model 之間並無直接的聯繫,而是經過ViewModel進行交互,Model和 ViewModel 之間的交互是雙向的, 所以View 數據的變化會同步到Model中,而Model 數據的變化也會當即反應到View 上。
  • ViewModel 經過雙向數據綁定把 View 層和 Model層鏈接了起來,而View和 Model 之間的同步工做徹底是自動的,無需人爲干涉,所以開發者只需關注業務邏輯,不須要手動操做DOM,不須要關注數據狀態的同步問題,複雜的數據狀態維護徹底由 MVVM 來統一管理

#2 請詳細說下你對vue生命週期的理解

答:總共分爲8個階段建立前/後,載入前/後,更新前/後,銷燬前/後

  • 建立前/後: 在beforeCreate階段,vue實例的掛載元素el和數據對象data都爲undefined,還未初始化。在created階段,vue實例的數據對象data有了,el尚未
  • 載入前/後:在beforeMount階段,vue實例的$eldata都初始化了,但仍是掛載以前爲虛擬的dom節點,data.message還未替換。在mounted階段,vue實例掛載完成,data.message成功渲染。
  • 更新前/後:當data變化時,會觸發beforeUpdateupdated方法
  • 銷燬前/後:在執行destroy方法後,對data的改變不會再觸發周期函數,說明此時vue實例已經解除了事件監聽以及和dom的綁定,可是dom結構依然存在

什麼是vue生命週期?

  • 答: Vue 實例從建立到銷燬的過程,就是生命週期。從開始建立、初始化數據、編譯模板、掛載Dom→渲染、更新→渲染、銷燬等一系列過程,稱之爲 Vue 的生命週期。

vue生命週期的做用是什麼?

  • 答:它的生命週期中有多個事件鉤子,讓咱們在控制整個Vue實例的過程時更容易造成好的邏輯。

vue生命週期總共有幾個階段?

  • 答:它能夠總共分爲8個階段:建立前/後、載入前/後、更新前/後、銷燬前/銷燬後。

第一次頁面加載會觸發哪幾個鉤子?

  • 答:會觸發下面這幾個beforeCreatecreatedbeforeMountmounted 。

DOM 渲染在哪一個週期中就已經完成?

  • 答:DOM 渲染在 mounted 中就已經完成了

#3 Vue實現數據雙向綁定的原理:Object.defineProperty()

  • vue實現數據雙向綁定主要是:採用數據劫持結合發佈者-訂閱者模式的方式,經過 Object.defineProperty() 來劫持各個屬性的settergetter,在數據變更時發佈消息給訂閱者,觸發相應監聽回調。當把一個普通 Javascript 對象傳給 Vue 實例來做爲它的 data 選項時,Vue 將遍歷它的屬性,用 Object.defineProperty() 將它們轉爲 getter/setter。用戶看不到 getter/setter,可是在內部它們讓 Vue追蹤依賴,在屬性被訪問和修改時通知變化。
  • vue的數據雙向綁定 將MVVM做爲數據綁定的入口,整合ObserverCompileWatcher三者,經過Observer來監聽本身的model的數據變化,經過Compile來解析編譯模板指令(vue中是用來解析 {{}}),最終利用watcher搭起observerCompile之間的通訊橋樑,達到數據變化 —>視圖更新;視圖交互變化(input)—>數據model變動雙向綁定效果。

#4 Vue組件間的參數傳遞

父組件與子組件傳值

父組件傳給子組件:子組件經過props方法接受數據;

  • 子組件傳給父組件: $emit 方法傳遞參數

非父子組件間的數據傳遞,兄弟組件傳值

eventBus,就是建立一個事件中心,至關於中轉站,能夠用它來傳遞事件和接收事件。項目比較小時,用這個比較合適(雖然也有很多人推薦直接用VUEX,具體來講看需求)

#5 Vue的路由實現:hash模式 和 history模式

  • hash模式:在瀏覽器中符號「#」,#以及#後面的字符稱之爲hash,用 window.location.hash 讀取。特色:hash雖然在URL中,但不被包括在HTTP請求中;用來指導瀏覽器動做,對服務端安全無用,hash不會重加載頁面。
  • history模式:history採用HTML5的新特性;且提供了兩個新方法: pushState(), replaceState()能夠對瀏覽器歷史記錄棧進行修改,以及popState事件的監聽到狀態變動

#5 vue路由的鉤子函數

首頁能夠控制導航跳轉,beforeEachafterEach等,通常用於頁面title的修改。一些須要登陸才能調整頁面的重定向功能。

  • beforeEach主要有3個參數tofromnext
  • toroute即將進入的目標路由對象。
  • fromroute當前導航正要離開的路由。
  • nextfunction必定要調用該方法resolve這個鉤子。執行效果依賴next方法的調用參數。能夠控制網頁的跳轉

#6 vuex是什麼?怎麼使用?哪一種功能場景使用它?

  • 只用來讀取的狀態集中放在store中; 改變狀態的方式是提交mutations,這是個同步的事物; 異步邏輯應該封裝在action中。
  • main.js引入store,注入。新建了一個目錄store… export
  • 場景有:單頁應用中,組件之間的狀態、音樂播放、登陸狀態、加入購物車

vuex

  • stateVuex 使用單一狀態樹,即每一個應用將僅僅包含一個store 實例,但單一狀態樹和模塊化並不衝突。存放的數據狀態,不能夠直接修改裏面的數據。
  • mutationsmutations定義的方法動態修改Vuex 的 store 中的狀態或數據
  • getters:相似vue的計算屬性,主要用來過濾一些數據。
  • actionactions能夠理解爲經過將mutations裏面處裏數據的方法變成可異步的處理數據的方法,簡單的說就是異步操做數據。view 層經過 store.dispath 來分發 action

image.png

modules:項目特別複雜的時候,可讓每個模塊擁有本身的statemutationactiongetters,使得結構很是清晰,方便管理

image.png

#7 v-if 和 v-show 區別

  • 答:v-if按照條件是否渲染,v-showdisplayblocknone

#$route$router的區別

  • $route是「路由信息對象」,包括pathparamshashqueryfullPathmatchedname等路由信息參數。
  • $router是「路由實例」對象包括了路由的跳轉方法,鉤子函數等

#9 如何讓CSS只在當前組件中起做用?

將當前組件的<style>修改成<style scoped>

#10 <keep-alive></keep-alive>的做用是什麼?

  • <keep-alive></keep-alive> 包裹動態組件時,會緩存不活動的組件實例,主要用於保留組件狀態或避免從新渲染

好比有一個列表和一個詳情,那麼用戶就會常常執行打開詳情=>返回列表=>打開詳情…這樣的話列表和詳情都是一個頻率很高的頁面,那麼就能夠對列表組件使用<keep-alive></keep-alive>進行緩存,這樣用戶每次返回列表的時候,都能從緩存中快速渲染,而不是從新渲染

#11 指令v-el的做用是什麼?

提供一個在頁面上已存在的 DOM元素做爲 Vue實例的掛載目標.能夠是 CSS 選擇器,也能夠是一個 HTMLElement 實例,

#12 在Vue中使用插件的步驟

  • 採用ES6import ... from ...語法或CommonJSrequire()方法引入插件
  • 使用全局方法Vue.use( plugin )使用插件,能夠傳入一個選項對象Vue.use(MyPlugin, { someOption: true })

#13 請列舉出3個Vue中經常使用的生命週期鉤子函數?

  • created: 實例已經建立完成以後調用,在這一步,實例已經完成數據觀測, 屬性和方法的運算, watch/event事件回調. 然而, 掛載階段尚未開始, $el屬性目前還不可見
  • mountedel被新建立的 vm.$el 替換,並掛載到實例上去以後調用該鉤子。若是 root 實例掛載了一個文檔內元素,當 mounted被調用時 vm.$el 也在文檔內。
  • activatedkeep-alive組件激活時調用

#14 vue-cli 工程技術集合介紹

問題一:構建的 vue-cli 工程都到了哪些技術,它們的做用分別是什麼?

  • vue.jsvue-cli工程的核心,主要特色是 雙向數據綁定 和 組件系統。
  • vue-routervue官方推薦使用的路由框架。
  • 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.jsmanifest.jsvendor.js 文件裏面所包含代碼的頁面。能夠具此優化 vue-cli 生產環境部署的靜態資源,提高 頁面 的加載速度

#15 NextTick

nextTick可讓咱們在下次 DOM 更新循環結束以後執行延遲迴調,用於得到更新後的 DOM

#16 vue的優勢是什麼?

  • 低耦合。視圖(View)能夠獨立於Model變化和修改,一個ViewModel能夠綁定到不一樣的"View"上,當View變化的時候Model能夠不變,當Model變化的時候View也能夠不變
  • 可重用性。你能夠把一些視圖邏輯放在一個ViewModel裏面,讓不少view重用這段視圖邏輯
  • 可測試。界面素來是比較難於測試的,而如今測試能夠針對ViewModel來寫

#17 路由之間跳轉?

聲明式(標籤跳轉)

<router-link :to="index">

編程式( js跳轉)

router.push('index')

#18 實現 Vue SSR

其基本實現原理

  • 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 的組件輸出成一個完整 HTMLvue-server-renderer 就是幹這事的

  • Vue SSR須要作的事多點(輸出完整 HTML),除了complier -> vnode,還需如數據獲取填充至 HTML、客戶端混合(hydration)、緩存等等。 相比於其餘模板引擎(ejsjade 等),最終要實現的目的是同樣的,性能上可能要差點

#19 Vue 組件 data 爲何必須是函數

  • 每一個組件都是 Vue 的實例。
  • 組件共享 data 屬性,當 data 的值是同一個引用類型的值時,改變其中一個會影響其餘

#20 Vue computed 實現

  • 創建與其餘屬性(如:data、 Store)的聯繫;
  • 屬性改變後,通知計算屬性從新計算

實現時,主要以下

  • 初始化 data, 使用 Object.defineProperty 把這些屬性所有轉爲 getter/setter
  • 初始化 computed, 遍歷 computed 裏的每一個屬性,每一個 computed 屬性都是一個 watch 實例。每一個屬性提供的函數做爲屬性的 getter,使用 Object.defineProperty 轉化。
  • Object.defineProperty getter 依賴收集。用於依賴發生變化時,觸發屬性從新計算。
  • 若出現當前 computed 計算屬性嵌套其餘 computed 計算屬性時,先進行其餘的依賴收集

#21 Vue complier 實現

  • 模板解析這種事,本質是將數據轉化爲一段 html ,最開始出如今後端,通過各類處理吐給前端。隨着各類 mv* 的興起,模板解析交由前端處理。
  • 總的來講,Vue complier 是將 template 轉化成一個 render 字符串。

能夠簡單理解成如下步驟:

  • parse 過程,將 template 利用正則轉化成AST 抽象語法樹。
  • optimize 過程,標記靜態節點,後 diff 過程跳過靜態節點,提高性能。
  • generate 過程,生成 render 字符串

#22 怎麼快速定位哪一個組件出現性能問題

用 timeline 工具。 大意是經過 timeline 來查看每一個函數的調用時常,定位出哪一個函數的問題,從而能判斷哪一個組件出了問題

#8、框架通識

#1 MVVM

MVVM 由如下三個內容組成

  • View:界面
  • Model:數據模型
  • ViewModel:做爲橋樑負責溝通 View 和 Model

在 JQuery 時期,若是須要刷新 UI 時,須要先取到對應的 DOM 再更新 UI,這樣數據和業務的邏輯就和頁面有強耦合。

MVVM

在 MVVM 中,UI 是經過數據驅動的,數據一旦改變就會相應的刷新對應的 UIUI 若是改變,也會改變對應的數據。這種方式就能夠在業務處理中只關心數據的流轉,而無需直接和頁面打交道。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 

#2 路由原理

前端路由實現起來其實很簡單,本質就是監聽 URL 的變化,而後匹配路由規則,顯示相應的頁面,而且無須刷新。目前單頁面使用的路由就只有兩種實現方式

  • hash 模式
  • history 模式

www.test.com/##/ 就是 Hash URL,當 ## 後面的哈希值發生變化時,不會向服務器請求數據,能夠經過 hashchange 事件來監聽到 URL 的變化,從而進行跳轉頁面。

History模式是 HTML5 新推出的功能,比之 Hash URL 更加美觀

#3 Virtual Dom

爲何須要 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)
相關文章
相關標籤/搜索