前端面試之路三(javaScript高階篇)

kinshan發表於2018-12-21

原型

  • 原型的實際應用
  • 原型如何實現它的擴充套件性

原型的實際應用

jquery和zepto的簡單使用

<p>jquery test1</p>
<p>jquery test2</p>
<p>jquery test3</p>

<div id="div1">
    <p>jquery test in div</p>
</div>
複製程式碼
 var $p = $("p")
$p.css('font-size','40px')   //css是原型方法
console.log($p.html())       //html是原型方法

var $div1 = $("#div1")
$div1.css('color','blue')     //css是原型方法
console.log($div1.html())     //html是原型方法
複製程式碼

zepto如何使用原型


//空物件
var zepto = {}

zepto.init = function(){
    //原始碼中,這裡的處理情況比較複雜。但因為是本次只是針對原型,因此這裡就弱化了
    var slice = Array.prototype.slice
    var dom = slice.call(document.querySelectorAll(selector))
    return zepto.Z(dom,selector)
}

//即使用zepto時候的$
var $ = function(selector){
    return zepto.init(selector)
}
複製程式碼
//這就是建構函式
function Z(dom, selector) {
    var i, len = dom ? dom.length : 0
    for (i = 0; i < len; i++) {
        this[i] = dom[i]
    }
    this.length = len
    this.selector = selector || ''
}  

zepto.Z = function(dom, selector) {
    return new Z(dom, selector)
}
複製程式碼
$.fn = {
    constructor:zepto.Z,
    css:function(key,value){},
    html:function(value){}
}

zepto.Z.prototype = Z.prototype = $.fn 
複製程式碼

簡單的zepto實現

myZepto.js實現

(function(window) {
    var zepto = {};

    function Z(dom, selector) {
        var i, len = dom ? dom.length : 0;
        for (i = 0; i < len; i++) {
            this[i] = dom[i];
        }
        this.length = len;
        this.selector = selector || '';
    }

    zepto.Z = function(dom, selector) {
        return new Z(dom, selector);
    }

    zepto.init = function(selector) {
        var slice = Array.prototype.slice;
        var dom = slice.call(document.querySelectorAll(selector));
        return zepto.Z(dom, selector);
    }

    var $ = function(selector) {
        return zepto.init(selector);
    }

    window.$ = $;

    $.fn = {
        css: function(key, value) {
            console.log(key, value);
        },
        html: function() {
            return "html";
        }
    }

    Z.prototype = $.fn

})(window)
複製程式碼

jquery如何使用原型

var jQuery = function(selector){
    //注意new關鍵字,第一步就找到了建構函式
    return new jQuery.fn.init(selector);
}

//定義建構函式
var init =  jQuery.fn.init = function(selector){
    var slice = Array.prototype.slice;
    var dom = slice.call(document.querySelectorAll(selector));

    var i,len=dom?dom.length:0;
    for(i = 0;i<len;i++) this[i] = dom[i];
    this.length = len;
    this.selector = selector || '';
}


//初始化jQuery.fn
jQuery.fn = jQuery.prototype = {
    constructor: jQuery,
    css: function(key, value) {},
    html: function(value) {}
}

//定義原型
init.prototype = jQuery.fn
複製程式碼

原型的擴充套件性

如何體現原型的擴充套件性

  • 總結zeptojquery原型的使用
  • 外掛機制

為什麼要把原型放在$.fn

init.prototype = jQuery.fn;

zepto.Z.prototype = Z.prototype = $.fn

因為要擴充套件外掛

//做一個簡單的外掛
$.fn.getNodeName = function(){
    return this[0].nodeName
}
複製程式碼

好處

  • 只有$會暴露在window全域性變數上
  • 將外掛擴充套件統一到$.fn.xxx這一介面,方便使用

非同步

什麼是單執行緒,和非同步有什麼關係

  • 單執行緒:只有一個執行緒,同一時間只能做一件事,兩段JS不能同時執行
  • 原因:避免DOM渲染的衝突
  • 解決方案:非同步

為什麼js只有一個執行緒:避免DOM渲染衝突

  • 瀏覽器需要渲染DOM
  • JS可以修改DOM結構
  • JS執行的時候,瀏覽器DOM渲染會暫停
  • 兩端JS也不能同時執行(都修改DOM就衝突了)
  • webworker支援多執行緒,但是不能訪問DOM

 

什麼是event-loop

  • 事件輪詢,JS實現非同步的具體解決方案
  • 同步程式碼,直接執行(主執行緒)
  • 非同步函式先放在非同步佇列中(任務佇列)
  • 待同步函式執行完畢,輪詢執行非同步佇列的函式
setTimeout(function(){
	console.log(1);
},100);              //100ms之後才放入非同步佇列中,目前非同步佇列是空的
setTimeout(function(){
	console.log(2);  //直接放入非同步佇列
})
console.log(3)       //直接執行

//執行3之後,非同步佇列中只有2,把2拿到主執行緒執行
//2執行完之後,非同步佇列中並沒有任務,所以一直輪詢非同步佇列
//直到100ms之後1放入非同步佇列,將1拿到主執行緒中執行
複製程式碼
$.ajax({
	url:'./data.json',
	success:function(){        //網路請求成功就把success放入非同步佇列
		console.log('a');
	}
})

setTimeout(function(){
	console.log('b')
},100)

setTimeout(function(){
	console.log('c');
})
console.log('d')

//列印結果:
//d    //d   
//c    //c  
//a    //b   
//b    //a   

//真實環境不會出現dacb
複製程式碼

解決方案存在的問題

  • 問題一:沒按照書寫方式執行,可讀性差
  • 問題二:callback中不容易模組化

是否用過jQuery的Deferred

  • jQuery1.5的變化
  • 使用jQuery Deferred
  • 初步引入Promise概念

 

jQuery1.5之前

var ajax = $.ajax({
	url:'./data.json',
	success:function(){
		console.log('success1');
		console.log('success2');
		console.log('success3');
	},
	error:function(){
		console.log('error');
	}
})
console.log(ajax); //返回一個XHR物件
複製程式碼

 

jQuery1.5之後

第一種寫法:

var ajax = $.ajax('./data.json');
ajax.done(function(){
	console.log('success1')
})
.fai(function(){
	console.log('fail')
})
.done(function(){
	console.log('success2');
})
console.log(ajax); //deferred物件
複製程式碼

第二種寫法:

var ajax = $.ajax('./data.json');
ajax.then(function(){
	console.log('success1')
},function(){
	console.log('error1');
})
.then(function(){
	console.log('success2');
},function(){
	console.log('error');
})

複製程式碼
  • 無法改變JS非同步和單執行緒的本質
  • 只能從寫法上杜絕callback這種形式
  • 它是一種語法糖,但是解耦了程式碼
  • 很好的提現:開放封閉原則(對擴充套件開放對修改封閉)

使用jQuery Deferred

給出一段非常簡單的程式碼,使用setTimeout函式

var wait = function(){
	var task = function(){
		console.log('執行完成');
	}
	setTimeout(task,2000)
}

wait();
複製程式碼

新增需求:要在執行完成之後進行某些特別複雜的操作,程式碼可能會很多,而且分好幾步

function waitHandle(){
	var dtd = $.Deferred();         //建立一個deferred物件
	
	var wait = function(dtd){       // 要求傳入一個deferred物件
		var task = function(){
			console.log("執行完成");
			dtd.resolve();          //表示非同步任務已完成
			//dtd.reject()          // 表示非同步任務失敗或者出錯
		};
		setTimeout(task,2000);
		return dtd;
	}
	//注意,這裡已經要有返回值
	return wait(dtd);
}


//使用
var w = waithandle()
w.then(function(){
	console.log('ok1');
},function(){
	console.log('err2');
})
.then(function(){
	console.log('ok2');
},function(){
	console.log('err2');
})
//還有w.wait w.fail
複製程式碼

總結:dtdAPI可分成兩類,用意不同

  • 第一類:dtd.resolvedtd.reject
  • 第二類:dtd.thendtd.donedtd.fail
  • 這兩類應該分開,否則後果嚴重! 可以在上面程式碼中最後執行dtd.reject()試一下後果

使用dtd.promise()

function waitHandle(){
	var dtd = $.Deferred();
	var wait = function(){
		var task = function(){
			console.log('執行完成');
			dtd.resolve();
		}
		setTimeout(task,2000)
		return dtd.promise();  //注意這裡返回的是promise,而不是直接返回deferred物件
	}
	return wait(dtd)
}


var w = waitHandle();   //promise物件
$.when(w).then(function(){
	console.log('ok1');
},function(){
	console.log('err1');
})

//w.reject()  //w.reject is not a function
複製程式碼

監聽式呼叫:只能被動監聽,不能干預promise的成功和失敗

  • 可以jQuery1.5對ajax的改變舉例
  • 說明如何簡單的封裝、使用deferred
  • 說明promise和Defrred的區別

要想深入瞭解它,就需要知道它的前世今生

Promise的基本使用和原理

基本語法

fucntion loadImg() {
    var promise = new Promise(function(resolve,reject) {
        var img = document.getElementById("img")
        img.onload = function(){
            resolve(img)
        }
        img.onerror  = function(){
            reject()
        }
    })
    return promise
}

var src = ""
var result = loadImg(src)
result.then(function() {
    console.log(1, img.width)
    return img
}, fucntion() {
    console.log('error 1')
}).then(function(img) {
    console.log(1, img.width)
})
複製程式碼

異常捕獲

規定

  • then只接受一個函式(只接受一個成功的回撥函式),最後統一用catch捕獲異常

兩種異常情況:

  • 程式邏輯異常
  • 業務之內,reject()等情況

處理方案 在reject("圖片路徑錯誤")中把資訊傳出去,可以通過'.catch()'捕獲

var src = ""
var result = loadImg(src)
result.then(function() {
    console.log(1, img.width)
    return img
}).then(function(img) {
    console.log(1, img.width)
}).catch(function(ex) {
    //最後統一捕獲異常
    console.log(ex)
})



複製程式碼

多個串聯

var scr1 = 'https://www.imooc.com/static/img/index/logo_new.png';
var result1 = loadImg(src1);
var src2 = 'https://www.imooc.com/static/img/index/logo_new1.png';
var result2 = loadImg(src2);


result1.then(function(img1) {
    console.log('第一個圖片載入完成', img1.width);
    return result2;   //重要
}).then(function(img2) {
    console.log('第二個圖片載入完成', img2.width);
}).catch(function(ex) {
    console.log(ex);
})
複製程式碼

Promise.allPromise.race

  • Promise.all接收一個promise物件的陣列
  • 待全部完成後,統一執行success
Promise.all([result1, result2]).then(datas => {
        //接收到的datas是一個陣列,依次包含了多個promise返回的內容
        console.log(datas[0]);
        console.log(datas[1]);
})
複製程式碼
  • Promise.race接收一個包含多個promise物件的陣列
  • 只要有一個完成,就執行success
Promise.race([result1, result2]).then(data => {
    //data即最先執行完成的promise的返回值
    console.log(data);
})
複製程式碼

Promise標準

  • 狀態
  • 三種狀態:pending,fulfilled,rejected
    • 初始狀態:pending
    • pending變為fulfilled,或者pending變為rejected
    • 狀態變化不可逆
  • then
    • promise必須實現then這個方法
    • then()必須接收兩個函式作為標準
    • then返回的必須是一個promise例項

沒有return就是預設返回的result

Promise總結

  • 基本語法
  • 如何異常捕獲(Errorreject)
  • 多個串聯-鏈式執行的好處
  • Promise.allPromise.race
  • Promise標準 - 狀態變化、then函式

介紹一下async/await(和Promise的區別、聯絡)

es7提案中,用的比較多,babel也已支援

koa框架 async/await替換generator

解決的是非同步執行和編寫邏輯

then只是將callback拆分了

var w = watiHandle()
w.then(function(){...},function(){...})
.then(function(){...},function(){...})

複製程式碼

async/await是最直接的同步寫法

const load = async function(){
    const result1 = await loadImg(src1)
    console.log(result1)
    const result2 = await loadImg(src2)
    console.log(result2)
}
load()
複製程式碼

用法

  • 使用await,函式後面必須用async標識
  • await後面跟的是一個Promise例項
  • 需要balbel-polyfill(相容)

特點

  • 使用了Promise,並沒有和Promise衝突
  • 完全是同步的寫法,再也沒有回撥函式
  • 但是:改變不了JS單執行緒、非同步的本質

總結一下當前JS非同步的方案

  • jQuery Defered
  • promise
  • async/await
  • generator(能解決非同步並不是為了解決非同步)

虛擬DOM

vdom 是什麼?為何會存在 vdom?

什麼是vdom

  • virtual dom,虛擬DOM
  • 用JS模擬DOM結構
  • DOM變化的對比,放在JS層來做(圖靈完備語言:能實現各種邏輯的語言)
  • 提高重繪效能

DOM

<ul id="list">
    <li class="item">Item 1</li>
    <li class="item">Item 2</li>
</ul>
複製程式碼

虛擬DOM

{
    tag: 'ul',
    attrs: {
        id: 'list'
    },
    children: [{
            tag: 'li',
            attrs: { className: 'item' },
            children: ['item1']
        },
        {
            tag: 'li',
            attrs: { className: 'item' },
            children: ['item2']
        }
    ]
}

//className代替class,因為class是js的保留字
複製程式碼

瀏覽器最耗費效能就是DOM操作,DOM操作非常昂貴
現在瀏覽器執行JS速度非常快
這就是vdom存在的原因

一個需求場景

//將該資料展示成一個表格
//隨便修改一個資訊,表格也隨著修改
[
    {
        name: 'zhangsan',
        age: 20,
        address: 'beijing'
    },
    {
        name: 'lisi',
        age: 21,
        address: 'shanghai'
    },
    {
        name: 'wangwu',
        age: 22,
        address: 'guangzhou'
    }
]
複製程式碼

jQery實現

//渲染函式
funciton render(data) {
    //此處省略n行
}

//修改資訊
$('#btn-change').click(function(){
    data[1].age = 30;
    data[2].address = 'shenzhen';
    render(data);
})


//初始化時渲染
render(data)
複製程式碼
//render函式具體寫法
function render(data) {
    $container = $("#container");
    //清空現有內容
    $container.html('');
    //拼接table
    var $table = $('<table>')
    $table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
    data.forEach(item => {
        $table.append($('<tr><td>' + item.name + '</td><td>' + item.age + '</td><td>' + item.address + '</td></tr>'))
        $container.append($table)
    });

}

//只執行了一次渲染,相對來說還是比較高效的
//DOM渲染是最昂貴的,只能儘量避免渲染
複製程式碼

遇到的問題

  • DOM操作是“昂貴”的,JS執行效率高
  • 儘量減少DOM操作,而不是"推倒重來"(清空重置)
  • 專案越複雜,影響就越嚴重
  • vdom可解決這個問題
var div = document.createElement('div');
var item,result = '';
for(item in div){
	result += '|' + item;
}
console.log(result);

//瀏覽器預設建立出來的DOM節點,屬性是非常多的

//result
|align|title|lang|translate|dir|dataset|hidden|tabIndex|accessKey|draggable|spellcheck|autocapitalize|contentEditable|isContentEditable|inputMode|offsetParent|offsetTop|offsetLeft|offsetWidth|offsetHeight|style|innerText|outerText|onabort|onblur|oncancel|oncanplay|oncanplaythrough|onchange|onclick|onclose|oncontextmenu|oncuechange|ondblclick|ondrag|ondragend|ondragenter|ondragleave|ondragover|ondragstart|ondrop|ondurationchange|onemptied|onended|onerror|onfocus|oninput|oninvalid|onkeydown|onkeypress|onkeyup|onload|onloadeddata|onloadedmetadata|onloadstart|onmousedown|onmouseenter|onmouseleave|onmousemove|onmouseout|onmouseover|onmouseup|onmousewheel|onpause|onplay|onplaying|onprogress|onratechange|onreset|onresize|onscroll|onseeked|onseeking|onselect|onstalled|onsubmit|onsuspend|ontimeupdate|ontoggle|onvolumechange|onwaiting|onwheel|onauxclick|ongotpointercapture|onlostpointercapture|onpointerdown|onpointermove|onpointerup|onpointercancel|onpointerover|onpointerout|onpointerenter|onpointerleave|nonce|click|focus|blur|namespaceURI|prefix|localName|tagName|id|className|classList|slot|attributes|shadowRoot|assignedSlot|innerHTML|outerHTML|scrollTop|scrollLeft|scrollWidth|scrollHeight|clientTop|clientLeft|clientWidth|clientHeight|attributeStyleMap|onbeforecopy|onbeforecut|onbeforepaste|oncopy|oncut|onpaste|onsearch|onselectstart|previousElementSibling|nextElementSibling|children|firstElementChild|lastElementChild|childElementCount|onwebkitfullscreenchange|onwebkitfullscreenerror|setPointerCapture|releasePointerCapture|hasPointerCapture|hasAttributes|getAttributeNames|getAttribute|getAttributeNS|setAttribute|setAttributeNS|removeAttribute|removeAttributeNS|hasAttribute|hasAttributeNS|toggleAttribute|getAttributeNode|getAttributeNodeNS|setAttributeNode|setAttributeNodeNS|removeAttributeNode|closest|matches|webkitMatchesSelector|attachShadow|getElementsByTagName|getElementsByTagNameNS|getElementsByClassName|insertAdjacentElement|insertAdjacentText|insertAdjacentHTML|requestPointerLock|getClientRects|getBoundingClientRect|scrollIntoView|scrollIntoViewIfNeeded|animate|computedStyleMap|before|after|replaceWith|remove|prepend|append|querySelector|querySelectorAll|webkitRequestFullScreen|webkitRequestFullscreen|scroll|scrollTo|scrollBy|createShadowRoot|getDestinationInsertionPoints|ELEMENT_NODE|ATTRIBUTE_NODE|TEXT_NODE|CDATA_SECTION_NODE|ENTITY_REFERENCE_NODE|ENTITY_NODE|PROCESSING_INSTRUCTION_NODE|COMMENT_NODE|DOCUMENT_NODE|DOCUMENT_TYPE_NODE|DOCUMENT_FRAGMENT_NODE|NOTATION_NODE|DOCUMENT_POSITION_DISCONNECTED|DOCUMENT_POSITION_PRECEDING|DOCUMENT_POSITION_FOLLOWING|DOCUMENT_POSITION_CONTAINS|DOCUMENT_POSITION_CONTAINED_BY|DOCUMENT_POSITION_IMPLEMENTATION_SPECIFIC|nodeType|nodeName|baseURI|isConnected|ownerDocument|parentNode|parentElement|childNodes|firstChild|lastChild|previousSibling|nextSibling|nodeValue|textContent|hasChildNodes|getRootNode|normalize|cloneNode|isEqualNode|isSameNode|compareDocumentPosition|contains|lookupPrefix|lookupNamespaceURI|isDefaultNamespace|insertBefore|appendChild|replaceChild|removeChild|addEventListener|removeEventListener|dispatchEvent
複製程式碼

vdom如何應用,核心API是什麼

介紹snabbdom

一個實現vdom的庫,vue升級2.0借鑑了snabbdom的演算法

var container = document.getElementById('container')

var vnode = h('div#container.two.classes', { on: { click: someFn } }, [
    h('span', { style: { fontWeight: 'bold' }, 'This is bold' }),
    'and this is just normal text',
    h('a', { props: { href: '/foo' } }, 'I\'ll take you places')
])

//patch into empty DOM element - this modifies the DOM as a side effect
patch(container, vnode)

var newVnode = h('div#container.two.classes', { on: { click: anotherEventHandle } }, [
        h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'this is now italic type'),
        'and this is still just normal text',
        h('a', { props: { href: '/bar' } }, 'I\'ll take you places')
    ])
    //send `patch` invocation

patch(vnode, newVnode); //Snabbdom efficiently updates the old view to the new state
複製程式碼

h函式

{
    tar: 'ul',
    attrs: {
        id: 'list'
    },
    children: [
    {
        tag: 'li',
        attrs: {
            className: 'item',
            children: ['item1']
        }
    },
    {
        tag: 'li',
        attrs: {
            className: 'item'
        },
        children: ['item2']
        }
    ]
}
複製程式碼

對應的vnode

var vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item1'),
    h('li.item', {}, 'Item')
])

複製程式碼

patch函式

var vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item1'),
    h('li.item', {}, 'Item2')
])

var container = document.getElementById('container')
patch(container, vnode)

//模擬改變
var btnChange = document.getElementById('btn-change')
btnChange.addEventListener('click', function() {
    var newVnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 111'),
        h('li.item', {}, 'Item 222'),
        h('li.item', {}, 'Item 333')
    ])
    patch(vnode, newVnode)
})
複製程式碼
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.2/h.js"></script>

複製程式碼
  <script type="text/javascript">
    var snabbdom = window.snabbdom

    //定義patch
    var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ])
    //定義h函式
    var h = snabbdom.h

    var container = document.getElementById('container')

    //生成vnode
    var vnode = h('ul#list',{},[
        h('li.list',{},'Item1'),
        h('li.list',{},'Item2'),

    ])
    patch(container,vnode)


    document.getElementById('btn-change').addEventListener('click',function(){
        //生成newVode
        var newVnode = h('ul#list',{},[
            h('li.list',{},'Item1'),
            h('li.item',{},'Item B'),
            h('li.item',{},'Item 3')
        ])
        patch(vnode,newVnode)
    })


    </script>
複製程式碼

重新實現前面的demo(用snabbdom實現)

 var snabbdom = window.snabbdom
    var patch = snabbdom.init([
        snabbdom_class,
        snabbdom_props,
        snabbdom_style,
        snabbdom_eventlisteners
    ])
    var h = snabbdom.h
    var container = document.getElementById('container')
    var btnChange = document.getElementById('btn-change')
    var vnode
    var data = [{
            name: 'zhangsan',
            age: 20,
            address: 'beijing'
        },
        {
            name: 'zhangsan',
            age: 20,
            address: 'shanghai'
        },
        {
            name: 'zhangsan',
            age: 20,
            address: 'shenzhen'
        }
    ]
    
    data.unshift({
        name:'姓名:',
        age:'年齡:',
        address:'地址:'
    })


    render(data);

    function render(data){
        var newVnode = h('table',{},data.map(function(item){
            var tds = [],i
            for(i in item){
                if(item.hasOwnProperty(i)){
                    tds.push(h('td',{},item[i]+''))
                }
            }
            return h('tr',{},tds)
        }))
        if(vnode){
            patch(vnode,newVnode)
        }else{
            patch(container,newVnode)
        }
        vnode = newVnode  //儲存當前vnode結果

    }

  
    btnChange.addEventListener('click',function(){
        data[1].age = 30
        data[2].address = '深圳'
        //re-render
        render(data)
    })
複製程式碼

核心API

  • h函式的用法
 h('<標籤名>',{...屬性...},[...子元素...])
 
 h('<標籤名>',{...屬性...},'...')
複製程式碼
  • patch函式用法
patch(container,vnode) 

patch(vnode,newVnode)  //rerender

複製程式碼

介紹一下diff演算法

什麼是diff演算法

linux中的diff:找出兩個檔案中的不同:

diff log1.txt log2.txt
複製程式碼

git diff:修改之前和修改之後版本的差異

git diff xxxx.js
複製程式碼

網上的一些線上差異化網站

http://tool.oschina.net/diff/
複製程式碼

diff演算法並不是vdom提出的概念,一直存在
現在應用到vdom中,對比的是兩個虛擬dom

去繁就簡

  • diff演算法非常複雜,實現難度很大,原始碼量很大
  • 去繁就簡,講明白核心流程,不關心細節
  • 面試官大部分也不清楚細節,但是很關心核心流程
  • 去繁就簡之後,依然具有很大的挑戰性

vdom為何要使用diff

  • DOM操作是“昂貴”的,因此儘量減少DOM操作
  • 找出本次DOM必須更新的節點來更新,其他的不更新
  • “找出”的過程,就需要diff演算法

diff演算法實現 diff實現的過程

  • patch(container,vnode)

如何用vnode生成真實的dom節點

 {
    tag: 'ul',
    attrs: {
        id: 'list'
    },
    children: [
        {
            tag: 'li',
            attrs: {
                className: 'item'
            },
            children:['item 1']
        }
    ]
}
複製程式碼
<ul id = "list">
    <li class='item'>Item 1</li>
</ul>
複製程式碼

簡單實現演算法

function createElement(vnode) {
    var tag = vnode.tag;
    var attrs = vnode.attrs || {};
    var children = vnode.children || [];
    if (!tag) {
        return null
    }
    //建立元素
    var elem = document.createElement(tag);
        //屬性
    var attrName;
    for (attrName in atts) {
        if (attrs.hasOwnProperty(attrName)) {
            elem.setAttribute(attrName, attrs[attrName])
        }
    }
    //子元素
    children.array.forEach(childVnode => {
        elem.appendChild(createElement(childVnode))
    });
    return elem;
}
 
複製程式碼
  • patch(vnode,newVnode)
 {
    tag: 'ul',
    attrs: { id: 'list' },
    children: [{
            tag: 'li',
            attrs: { className: 'item' },
            children: ["Item 1"]
        },
        {
            tag: 'li',
            attrs: {
                className: 'item',
                children: ['Item 2']
            }
        }
    ]
}
複製程式碼

對比:

{
    tag: 'ul',
    attrs: { id: 'list' },
    children: [{
            tag: 'li',
            attrs: { className: 'item' },
            children: ["Item 1"]
        },
        {
            tag: 'li',
            attrs: {
                className: 'item',
                children: ['Item 222']
            }
        },
        {
            tag: 'li',
            attrs: {
                className: 'item',
                children: ['Item 3']
            }
        }
    ]
}
 
複製程式碼

前端面試之路三(javaScript高階篇)

簡單實現

 function updateChildren(vnode, newVnode) {
    var children = vnode.children || [];
    var newChildren = newVnode.children || [];

    //遍歷現有的children
    children.forEach((child, index) => {
        var newChild = newChildren[index];
        if (newChild == null) {
            return;
        }
        if (child.tag === newChild.tag) {
            updateChildren(child, newChild)
        } else {
            replaceNode(child, newChild)
        }
    });
}
複製程式碼
  • 節點新增和刪除
  • 節點重新排序
  • 節點屬性、樣式、事件繫結
  • 如何積極壓榨效能

MVVMVue

使用jQuery和使用框架的區別

資料與檢視的分離,解耦(封閉開放原則)

  • jquery中資料與檢視混合在一起了,不符合開放封閉原則
  • vue:通過Vue物件將資料和View完全分離開來了

以資料驅動檢視

  • jquery完全違背了這種理念,jquery直接修改檢視,直接操作DOM
  • vue對資料進行操作不再需要引用相應的DOM物件,通過Vue物件這個vm實現相互的繫結。以資料驅動檢視,只關心資料變化,DOM操作被封裝

MVVM的理解

MVC

前端面試之路三(javaScript高階篇)
前端面試之路三(javaScript高階篇)

MVVM

  • Model:資料,模型
  • View:檢視、模板(檢視和模型是分離的)
  • ViewModel:連線ModelView
    前端面試之路三(javaScript高階篇)

前端面試之路三(javaScript高階篇)

MVVM不算是創新,ViewModel算是一種微創新
是從MVC發展而來,結合前端場景的創新

如何實現MVVM

Vue三要素

  • 響應式vue如何監聽到data的每個屬性變化
  • 模板引擎:vue的模板如何被解析,指令如何處理
  • 渲染vue的模板如何渲染成html,以及渲染過程

vue中如何實現響應式

什麼是響應式

  • 修改data屬性之後,vue立刻監聽到(然後立刻渲染頁面)
  • data屬性被代理到vm

Object.defineProperty

ES5中加入的API,所以Vue不支援低版本瀏覽器(因為低版本瀏覽器不支援這個屬性)

var obj = {};
    var _name = 'zhangsan';

    Object.defineProperty(obj,"_name",{
        get:function(){
            console.log("get");
            return _name;
        },
        set:function(newVal){
            console.log(newVal);
            _name = newVal;
        }
    });

    console.log(obj.name) //可以監聽到
    obj.name = 'list'
複製程式碼

模擬實現

var vm = new Vue({
    el: '#app',
    data: {
        price: 100,
        name: 'zhangsan'
    }
})
複製程式碼
var vm = {}
var data = {
    name: 'zhangsan',
    price: 100
}

var key, value
for (key in data) {
    //命中閉包。新建一個函式,保證key的獨立的作用域
    (function(key) {
        Object.defineProperty(vm, key, {
            get: function() {
                console.log('get')
                return data[key]
            },
            set: function(newVal) {
                console.log('set')
                data[key] = newVal
            }
        })
    })(key)
}

複製程式碼

vue如何解析模板

模板是什麼

  • 本質:字串;有邏輯,如v-if,if-else-if,嵌入JS變數...
  • html格式很像,但有很大區別(靜態),最終還是要轉化為html顯示
  • 模板最終必須轉換成JS程式碼
    • 1、因為有邏輯(v-for,v-if),必須用JS才能實現(圖靈完備)
    • 2、轉換為html渲染頁面,必須用JS才能實現
    • 因此,模板最重要轉換成一個JS函式(render函式)

render函式

先了解with()的使用

function fn1() {
    with(obj) {
        console.log(name);
        console.log(age);
        getAddress()
    }
}

複製程式碼

最簡單的一個示例

<div id="app">
    <p>{{price}}</p>
</div>
複製程式碼
with(this) {    //this:vm
    return _c(
        'div', 
        {
            attrs: { "id": "app" }
        }, 
        [
            _c('p',[_v(_s(price))]     )  //price代理到了vm上
        ]
    )
}
複製程式碼
//vm._c
ƒ (a, b, c, d) { return createElement(vm, a, b, c, d, false); }

//vm._v
ƒ createTextVNode (val) {
  return new VNode(undefined, undefined, undefined, String(val))
}

//vm._s
ƒ toString (val) {
  return val == null? '': typeof val === 'object'? JSON.stringify(val, null,2): String(val)
}
複製程式碼
  • 模板中所有資訊都包含在了render函式中
  • thisvm
  • pricethis.price,即data中的price
  • _cthis._cvm._c

更復雜的一個例子

    <div id="app">
        <div>
            <input v-model="title">
            <button v-on:click="add">submit</button>
        </div>
        <div>
            <ul>
                <li v-for="item in list">{{item}}</li>
            </ul>
        </div>
    </div>
複製程式碼

如何尋找render函式:code.render

模板如何生成render函式:

vue2.0開始就支援預編譯,我們在開發環境下寫模板,經過編譯打包,產生生產環境的render函式(JS程式碼)

with(this){  // this 就是 vm
            return _c(
                'div',
                {
                    attrs:{"id":"app"}
                },
                [
                    _c(
                        'div',
                        [
                            _c(
                                'input',
                                {
                                    directives:[
                                        {
                                            name:"model",
                                            rawName:"v-model",
                                            value:(title),
                                            expression:"title"
                                        }
                                    ],
                                    domProps:{
                                        "value":(title)
                                    },
                                    on:{
                                        "input":function($event){
                                          if($event.target.composing)return;
                                            title=$event.target.value
                                        }
                                    }
                                }
                            ),
                            _v(" "),
                            _c(
                                'button',
                                {
                                    on:{
                                        "click":add
                                    }
                                },
                                [_v("submit")]
                            )
                        ]
                    ),
                    _v(" "),
                    _c('div',
                        [
                            _c(
                                'ul',
                                _l((list),function(item){return _c('li',[_v(_s(item))])})
                            )
                        ]
                    )
                ]
            )
        }

複製程式碼
//vm._l
 function renderList(val,render) {
        var ret, i, l, keys, key;
        if (Array.isArray(val) || typeof val === 'string') {
            ret = new Array(val.length);
            for (i = 0, l = val.length; i < l; i++) {
                ret[i] = render(val[i], i);
            }
        } else if (typeof val === 'number') {
            ret = new Array(val);
            for (i = 0; i < val; i++) {
                ret[i] = render(i + 1, i);
            }
        } else if (isObject(val)) {
            keys = Object.keys(val);
            ret = new Array(keys.length);
            for (i = 0, l = keys.length; i < l; i++) {
                key = keys[i];
                ret[i] = render(val[key], key, i);
            }
        }
        if (isDef(ret)) {
            (ret)._isVList = true;
        }
        return ret
    }

複製程式碼
  • v-model是怎麼實現的?
  • v-on:click是怎麼實現的
  • v-for是怎麼實現的

render函式與DOM

  • 已經解決了模板中"邏輯"(v-for,v-if)的問題
  • 還剩下模板生成html的問題
  • 另外,vm_c是什麼?render函式返回了什麼
    • vm._c其實就相當於snabbdom中的h函式
    • render函式執行之後,返回的是vnode
vm._update(vnode) {
    const prevVnode = vm._vnode
    vm._vnode = vnode
    if (!prevVnode) {
        vm.$sel = vm.__patch__(vm.$sel, vnode)    //與snabbdom中的patch函式差不多
    } else {
        vm.$sel = vm.__patch__(prevVnode, vnode)
    }
}

funciton updateComponent() {
    //vm._render即上面的render函式,返回vnode
    vm._update(vm._render())
}
複製程式碼
  • updateComponent中實現了vdompatch
  • 頁面首次渲染執行updateComponent
  • data中每次修改屬性,執行updataCommponent

vue的實現流程

第一步:解析模板成render函式

  • with的用法
  • 模板中所有的資訊都被render函式包含
  • 模板中用到的data中的屬性,都變成了js變數
  • 模板中的v-modelv-ifv-on都變成了js邏輯
  • render函式返回vnode

第二步:響應式監聽

  • Object.defineProperty
  • data屬性代理到vm

第三步:首次渲染,顯示頁面,且繫結依賴

  • 初次渲染,執行updateaComponent,執行vm._render()
  • 執行render函式,會訪問到vm.listvm.title
  • 會被響應式的get方法監聽到(為什麼監聽get?直接監聽set不就行了嗎?)
    • data中有很多屬性,有些會被用到,有些可能不會被用到
    • 被用到的會走到get,不被用到的不會走get
    • 未走到get中的屬性,set的時候我們也無需關係
    • 避免不必要的重複渲染
  • 執行updateComponent,會走到vdompatch方法
  • patchvnode渲染成DOM,初次渲染完成

第四步:data屬性變化,觸發rerender

  • 屬性修改,被響應式的set監聽到
  • set中執行updateaComponetn
  • updateComponent重新執行vm._render()
  • 生成的vnodeprevVnode,通過patch進行對比
  • 渲染到html

元件化和React

  • JSX和vdom的關係
  • 說一下setState的過程
  • 闡述自己對React和Vue的認識

說一下對元件化的理解

todolist Demo1

命令列

  • npm i create-react-app -g
  • create-react-app react-test
  • npm start
  • app.js
import Todo from './components/todo/index.js';

class App extends Component {
  render() {
    return (
      <div className="App">
        <Todo></Todo>
      </div>
    );
  }
}
export default App;

複製程式碼
  • src => components => todo
import React, { Component } from 'react'

class Todo extends Component {
    constructor(props) {
        super(props)
    }
    render() {
        return ( 
            <p> this is todo component </p>
        )
    }
}

export default Todo

複製程式碼

todolist Demo2

list => todo => index.js

import React, { Component } from 'react'

class List extends Component{
    constructor(props){
        super(props)
    }
    render() {
        const list = this.props.data
        return (
            <ul>
                {
                    list.map((item,index) =>{
                        return <li key={index}>{item}</li>  
                    })
                }
            </ul>
        )
    }
}

export default List
複製程式碼

必須加key

todo.js

import React, { Component } from 'react'
import List from './list/index.js';
import Input from './input/index.js';

class Todo extends Component {
    constructor(props) {
        super(props)
        this.state = {
            list:[]
        }
    }
    render() {
        return ( 
            <div> 
                <Input addTitle={this.addTitle.bind(this)}/>
                <List data={this.state.list} />
            </div>
        )
    }

    //主動新增list
    addTitle(title){ 
        const currentList = this.state.list   //獲取當前list
        this.setState(
            {
                list:currentList.concat(title)
            }
        )
    }
}

export default Todo

複製程式碼

list => input => index.js

import React, { Component } from "react";

class Input extends Component {
  constructor(props) {
    super(props);
    this.state = {
      title: ""
    };
  }
  render() {
      return(
        <div>
            <input value={this.state.title} onChange={this.changeHandle.bind(this)} />
            <button onClick={this.clickHandle.bind(this)}>submit</button>
        </div>  
      )
   
  }
  changeHandle(event) {
    this.setState({
      title: event.target.value
    });
  }
  clickHandle() {
    const title = this.state.title;
    //把title新增進列表
    const addTitle = this.props.addTitle;
    addTitle(title);
    //清空
    this.setState({
        title: ""
    });
  }
}

export default Input
複製程式碼

說一下對元件化的理解

元件的封裝

  • 檢視
  • 資料
  • 變化邏輯(資料驅動檢視)

元件的複用

  • props傳遞
  • 複用

JSX本質是什麼

JSX語法

  • html形式
  • 引入JS變數和表示式
  • if...else...
  • 迴圈
  • style和className
  • 事件

JSX解析成JS

  • JSX其實是語法糖
  • 開發環境將JSX編譯成JS程式碼
  • JSX寫法大大降低了學習成本和編碼工作量
  • 同時,JSX也會增加debug成本
/*JSX程式碼*/
var profile = <div>
    <img src="avater.png" className="profile"/>
    <h3>{[user.firstName,user.lastName].join('')}</h3>
</div>

----------------------------------------------------------------------------

//最終解析成
var profile = React.createElement("div",null,
  React.createElement("img",{src:"avater.png",className:"profile"}),
  React.createElement("h3",null,[user.firstName,user.lastName].join("")))
複製程式碼

React.createElement引數說明

React.createElement('div',{'id':'div1'},child1,child2,child3)
React.createElement('div',{'id':'div1'},[...])
複製程式碼
  • vdomh()
  • vue_c()
  • reactcreateElement
render(){
        const list = this.props.data
        return{
            <ul>
                {
                    list.map((list,index)=>{
                        return <li key={index}>{item}</li>>
                    })
                }
            </ul>
        }
    }
    
----------------------------------------------------------------------------
function render(){
    const list = this.props.data;
    return React.createElement(
        'ul',
        'null',
        list.map((item,index) => {
            return React.createElement(
                "li",
                {key:index},
                item
            );
        })
    );
}
複製程式碼

JSX獨立的標準

  • JSXReact引入的,但不是React獨有的
  • React已經將它作為一個獨立標準開放,其他專案也可用
  • React.createElement是可以自定義修改的
  • 說明:本身功能已經完備;和其他標準相容和擴充套件性沒問題

Hybrid

  • 移動端佔大部分流量,已經遠遠超過pc
  • 一線網際網路公司都有自己的APP
  • 這些APP中有很大比例的前端程式碼,拿微信舉例,你每天瀏覽微信的內容,很多都是前端

hybrid是什麼,為何用hybrid,如何實現

hybrid文字解釋

  • hybrid即“混合”,即前端和客戶端的混合開發
  • 需前端開發人員和客戶端人員配合完成
  • 某些環節也可能涉及到server

存在價值,為何會用hybrid

  • 可以快速迭代更新【關鍵】(無須app稽核)
  • 體驗流暢(和NA的體驗基本類似)
  • 減少開發和溝通成本,雙端公用一套程式碼

webview

  • 是app中的一個元件(app可以有webview,也可以沒有)
  • 用於載入h5頁面,即一個小型的瀏覽器核心

file://協議

  • 其實在一開始接觸html開發,已經使用file協議了
    • 載入本地檔案,快
      前端面試之路三(javaScript高階篇)
      前端面試之路三(javaScript高階篇)
    • 網路載入,慢

前端面試之路三(javaScript高階篇)

  • “協議”、“標準的重要性”

要做到和原生一樣的體驗,就必須要求載入速度特別的快,變態的快,和客戶端幾乎一樣的快

hybrid適用場景

  • 使用NA:體驗要求極致,變化不頻繁(如頭條的首頁)
  • 使用hybrid:體驗要求高,變化頻繁(如頭條的新聞詳情頁)
  • 使用h5:體驗無要求,不常用(如舉報,反饋等頁面)

hybrid具體實現

  • 前端做好靜態頁面(html js css),將檔案交給客戶端
  • 客戶端拿到前端靜態頁面,以檔案的形式儲存在app中
  • 客戶端在一個webview中
  • 使用file協議載入靜態頁面

前端面試之路三(javaScript高階篇)

app釋出之後,如何實時更新

前端面試之路三(javaScript高階篇)

分析

  • 要替換每個客戶端的靜態檔案
  • 只能客戶端來做(客戶端是我們開發的)
  • 客戶端去serve下載最新的靜態檔案
  • 我們維護server的靜態檔案

前端面試之路三(javaScript高階篇)

具體實現

  • 分版本有版本號,如201803211015
  • 將靜態檔案壓縮成zip包,上傳到服務端
  • 客戶端每次啟動,都去服務端檢查版本號
  • 如果服務端版本號大於客戶端版本號,就去下載最新的zip包
  • 下載完之後解壓包,然後將現有檔案覆蓋

hybridh5區別

優點:

  • 體驗更好,跟NA體驗基本一致
  • 可快速迭代,無須app稽核【關鍵】

缺點:

  • 開發成本高。聯調、測試、查bug都比較麻煩
  • 運維成本高。(參考此前講過的更新上線的流程)

適用場景:

  • hybrid:產品的穩定功能,體驗要求高,迭代頻繁
  • h5:單次的運營活動(如xx紅包)或不常用功能
  • hybrid適合產品型,h5適合運營型

JS和客戶端通訊

前端如何獲取內容

  • 新聞詳情頁適用hybrid,前端如何獲取新聞內容
  • 不能用ajax獲取。第一跨域(ajaxhttp協議),第二速度慢。
  • 客戶端獲取新聞內容,然後JS通訊拿到內容,再渲染。

JS和客戶端通訊的基本形式

前端面試之路三(javaScript高階篇)

schema協議簡介和使用

  • 之前介紹了http(s)和file協議
  • schema協議--前端和客戶端通訊的約定

前端面試之路三(javaScript高階篇)

前端面試之路三(javaScript高階篇)

前端面試之路三(javaScript高階篇)

function invokScan() {
    window['_invok_scan_callback'] = function(result){
        alert(result)
    }
    var iframe = document.createElement('iframe')
    iframe.style.display = 'none'
    iframe.src = 'weixin://dl/scan?k1=v1&k1=v2&callback=_invok_scan_callback' //重要
    var body = document.body
    body.appendChild(iframe)
    setTimeout(() => {
        body.removeChild(iframe)
        iframe = none
    });
}
document.getElementById('btn').addEventListener('click', function() {
    invokScan();
})
複製程式碼

schema使用的封裝

我們希望封裝後達到的效果:

/*傻瓜式呼叫,而且不用再自己定義全域性函式 */
window.invok.share({title:'xxxx',content:'xxx'},funciton(result){
    if(result.error === 0){
        alert('分享成功')
    }else{
        //分享失敗
        alert(result.message)
    }
})
複製程式碼
//分享
function invokeShare(data,callback){
    _invoke('share',data,callback)
}
//登入
function invokeLogin(data,callback){
    _invoke('login',data,callback)
}
//開啟掃一掃
function invokeScan(data,callback){
    _invoke('scan',data,callback)
}
複製程式碼
//暴露給全域性
window.invoke = {
    share:invokeShare,
    login:invokeLogin,
    scan:invokeScan
}
複製程式碼
function _invoke(action,data,callback){
    //拼接schema協議
    var schema = 'myapp://utils';
    scheam += '/' + action;
    scheam += '?a=a';
    var key;
    for(key in data){
        if(data.hasOwnProperty(key)){
            schema += '&' + key + '=' +data[key]
        }
    }
}
複製程式碼
//處理callback
var callbackName = ''
if(typeof callback == 'string'){
    callbackName = callback
}else{
    callbackName = action + Date.now()
    window[callbackName] = callback
}

schema += '&callback' + callbackName
複製程式碼

(function(window, undefined) {

    //呼叫schema封裝
    function _invoke(action, data, callback) {
        //拼裝schema協議
        var schema = 'myapp://utils/' + action;
        //拼接引數
        schema += '?a=a';
        var key;
        for (key in data) {
            if (data.hasOwnProperty(key)) {
                schema += '&' + key + '=' + data[key];
            }
        }
        //處理callback
        var callbackName = '';
        if (typeof callback === 'string') {
            callbackName = callback
        } else {
            callbackName = action + Date.now()
            window[callbackName] = callback
        }
        schema += 'allback = callbackName'
    }

    //暴露到全域性變數
    window.invoke = {
        share: function(data, callback) {
            _invoke('share', data, callback)
        },
        scan: function(data, callback) {
            _invoke('scan', data, callback)
        },
        login: function(data, callback) {
            _invoke('login', data, callback)
        }
    }
})(window)
複製程式碼

內建上線

  • 將以上封裝的程式碼打包,叫做invoke.js,內建到客戶端
  • 客戶端每次啟動webview,都預設執行invoke.js
  • 本地載入,沒有網路請求,黑客看到不到schema協議,更安全

總結

  • 通訊的基本形式:呼叫能力,傳遞引數,監聽回撥
  • 對schema協議的理解和使用
  • 呼叫schema程式碼的封裝
  • 內建上線:更快更安全

相關文章