js面试课程
一、ES6学习
Ⅰ、Babel的使用
Babel是一个JavaScript编译器,主要用于将ECMAScript 2015+版本的代码转换为向后兼容的 JavaScript 代码,以便能够运行到当前以及和旧版本的浏览器或其他环境中。参考文章:Babel配置
1. npm init
初始化
保证电脑在node环境下,因为我们要使用npm安装,所以先用 npm init
初始化一下
2. 下载
npm i --save-dev babel-core babel-preset-es2015 babel-preset-latest
说明:
babel-core
作为babel的核心存在,babel的核心api都在这个模块里面,所以使用Babel这个依赖是首先要安装的babel-preset-es2015
是指 ES2015 / ES6 插件集合,把与es6转成es5相关的几十个插件全部封装到这个包里,省去了我们配置插件的麻烦。作用就是把es6转换成es5。其他的es(2016,2017...)作用类似babel-preset-latest
支持现有所有ECMAScript版本的新特性,包括处于stage 4里的特性(已经确定的规范,将被添加到下个年度的)
3. 配置
建立 .babelrc
文件,babel所有的操作基本都会来读取这个配置文件,除了一些在回调函数中设置options参数的,如果没有这个配置文件,会从package.json文件的babel属性中读取配置。
{
"presets": ["es2015", "latest"], // 预设
"plugins": [] // 插件
}
现在更为推荐的preset:babel-preset-env
这款preset能灵活决定加载哪些插件和polyfill,不过还是得开发者手动进行一些配置
参考文章:babel-preset-env
{
"presets": [
["env", {
"modules": false,
"targets": { // 指定要转义到哪个环境
"browsers": ["> 1%", "last 2 versions", "not ie <= 8"]
// 浏览器环境,支持市场份额超过1%,支持每个浏览器最近的两个版本,ie大于8的浏览器
"node": "current" // node 环境,支持的是当前运行版本的nodejs
}
}],
"stage-2"
],
"plugins": ["transform-vue-jsx", "transform-runtime"]
}
4. 在命令行中对js文件进行转码
babel-cli
工具能够实现这个功能
npm i -g babel-cli
使用方法:
- 直接在命令行输出转译后的代码
babel script.js - 指定输出文件
babel script.js --out-file build.js
或者是
babel script.js -o build.js
Ⅱ、webpack的使用
webpack是一个模块打包工具。Babel解决的是语法层面的问题,而webpack是可以集合各种工具的自动化打包工具。
1. 安装
npm i webpack --save-dev
2. 配置
配置文件:webpack.config.js
module.exports = {
entry: "./src/index.js",
output: {
path: __dirname,
filename: "build/bundle.js"
},
module: {
rules: [{
test: /\.js?$/,
exclude: /(node_modules)/,
loader: "babel-loader"
}]
},
plugins: []
}
3. 启动
在 package.json
文件的 scripts
下配置
"scripts": {
"start": "webpack"
},
启动:npm start
或者 npm run start
二、原型
Ⅰ、原型的实际应用
1. zepto如何使用原型?
代码如下:
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>prototype</title>
</head>
<body>
<p>jquery test 1</p>
<p>jquery test 2</p>
<p>jquery test 3</p>
<div id="div">
<p>jquery test in div</p>
</div>
<script src="./my-zepto.js"></script>
<script>
var $p = $('p')
$p.css('font-size', '40px')
console.log($p.html()) // 调用一个函数时,该函数没有返回值,会返回return
var $div = $('#div')
$div.css('color', 'blue') // css 原型方法
console.log($div.html()) // html 原型方法
</script>
</body>
</html>
js:
(function (window) {
// 空对象
var zepto = {}
// 构造函数
function Z (dom, selector) {
var i, len = dom ? dom.length : 0
for (let i=0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
zepto.Z = function (dom, selector) {
// 注意: 出现了 new 关键字
return new Z(dom, selector)
}
zepto.init = function (selector) {
var slice = Array.prototype.slice
var dom = slice.call(document.querySelectorAll(selector)) // 将类数组转化为数组
// console.log(document.querySelectorAll(selector))
// console.log(dom)
return zepto.Z(dom, selector)
}
// 这里的 $ 就是使用 zepto 时 的 $
var $ = function (selector) {
return zepto.init(selector)
}
$.fn = {
css: function (key, value) {
console.log('css')
},
html: function (value) {
return "这是一个模拟的 html 函数"
}
}
// 定义原型
Z.prototype = $.fn
window.$ = $
})(window)
问题:为什么每个节点对象都可以使用css, html, append, remove...等这些方法呢?这些方法定义在哪里呢?
解答:当我们获取节点对象时 var $p = $('p')
,首先调用$
函数,然后进入zepto.init
函数,进入 zepto.Z
函数,而 zepto.Z
函数返回一个 new Z(dom, selector)
,可以看出Z是一个构造函数。也就是说,我们获取的节点对象 $p
就是构造函数 Z
的一个实例对象。
所以构造函数 Z
的prototype中定义的方法和属性,实例对象 $p
都能够使用
Z.prototype = $.fn = {
css: function(key, value){
},
html: function(){},
append: function(){},
remove: function(){}
......
}
2. jQuery如何使用原型?
代码如下:
html:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>prototype</title>
</head>
<body>
<p>jquery test 1</p>
<p>jquery test 2</p>
<p>jquery test 3</p>
<div id="div">
<p>jquery test in div</p>
</div>
<script src="./my-jquery.js"></script>
<script>
var $p = $('p')
$p.css('font-size', '40px')
console.log($p.html())
var $div = $('#div')
$div.css('color', 'blue')
console.log($div.html())
</script>
</body>
</html>
js:
(function (window) {
// 在jQuery库中,jQuery === $
var jQuery = function (selector) {
// 注意 new 关键字,第一步就找到了构造函数
return new jQuery.fn.init(selector)
}
jQuery.fn = {
css: function (key, value) {
console.log('css')
},
html: function (value) {
return 'html'
}
}
// 定义构造函数
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 (let i = 0; i < len; i++) {
this[i] = dom[i]
}
this.length = len
this.selector = selector || ''
}
init.prototype = jQuery.fn
window.$ = jQuery
})(window)
原理与 zepto 类似,不再解释
Ⅱ、原型的扩展
问题:在上面两个原型应用中,定义原型时 init.prototype = jQuery.fn = {}, Z.prototype = $.fn = {}
,都使用了 $.fn
中转,为什么要使用中转,直接将原型等于那个对象不就行了吗?
解答:因为要扩展插件, 比如: $.fn.getNodeName = function(){}
问题:我们为什么非要在 $.fn
上扩展插件,有什么好处?
好处:
- 只有
$
会暴露在window全局变量,其他的如init, Z
在外面取不到 - 将插件扩展统一到
$.fn.xxx
这一个接口,方便使用
实例代码:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>prototype</title>
</head>
<body>
<p>jquery test 1</p>
<p>jquery test 2</p>
<p>jquery test 3</p>
<div id="div">
<p>jquery test in div</p>
</div>
<script src="./jquery.js"></script>
<script>
// 插件扩展
$.fn.getNodeName = function () {
console.log(this[0].nodeName)
}
</script>
<script>
// 验证
var $p = $('p')
// console.log($p)
$p.getNodeName() // P
var $div = $('#div')
// console.log($div)
$div.getNodeName() // DIV
</script>
</body>
</html>
三、异步
Ⅰ、单线程
单线程:只有一个线程,同一时间只能做一样事情。
原因:避免 DOM 渲染冲突
解决方案:异步
代码:
console.log('start')
var i, sum=0
for(i = 0; i < 1000000000; i++){
sum++
} // 代码要在这里执行一会儿,才能进行下一步
console.log(sum)
console.log('start')
alert('pending')
console.log('end')
// 本例中异步是在 1s 后执行,若是同步任务执行事件小于 1s,那么异步任务会在 1s 时执行,如果同步任务大于 1s,
//那么异步任务会在同步任务执行完之后执行,所以这里的 1s 后执行并不绝对。
console.log(1)
setTimeout(function(){
console.log(2)
}, 1000) // 这里的1s并不是1s后肯定会执行,它是异步,肯定同步执行完之后执行
console.log(3)
var sum = 0
for(let i = 0; i < 1000000000; i++){
sum++
}
console.log(4)
问题:为什么单线程能够避免 DOM 渲染的冲突呢?
- 浏览器需要渲染 DOM
- 而js可以修改 DOM结构
- 所以js执行的时候,浏览器 DOM 渲染会暂停
- 两段(句)js 代码也不能同时执行(都修改 DOM 会有冲突)
- webworker支持多线程,但是不能访问 DOM
问题:异步解决方案的问题
- 没按照书写顺序执行,可读性差
- callback 中不容易模块化
Ⅱ、event-loop
event-loop: 异步的实现方式
event-loop的文字解释:
- 事件轮询,js实现异步的具体解决方案
- 同步代码,直接执行
- 异步代码,先放在异步队列中
- 待同步代码执行完毕,轮询执行 异步队列的函数
用到异步的场景:
- 定时器:setTimeout, setInterval等
- 网络请求:Ajax,img,script,form等
- 事件绑定(click, load, done...)
代码演示:
console.log(1)
$.ajax({
url: './data.json',
success: function () {
console.log(2)
} // 当网络请求成功的时候,放入异步队列
})
setTimeout(function(){
console.log(3)
}) // 立即放入异步队列
setTimeout(()=>{
console.log(4)
},1000) // 1000ms 放入异步队列
console.log(5)
解释:
- 代码执行,首先执行主队列的代码,主队列代码执行完毕,去异步队列去找异步代码,若有异步代码,将异步代码转到主队列执行。然后浏览器一直在主队列和异步队列循环,一旦发现异步队列有代码,就转到主队列执行。这就是事件轮询。
- 当异步队列有多组代码时,异步队列遵循先进先出的数据结构
// event-loop 异步队列是一个先进先出的数据结构
console.log(1)
setTimeout(function(){
console.log(3)
})
setTimeout(()=>{
console.log(2)
})
console.log(4)
Ⅲ、jQuery中异步解决方法-deferred
1. jQuery1.5前后变化
jQuery1.5之前使用回调函数解决异步
$.ajax({
url: './data.json',
success: function () {
console.log('success 1')
console.log('success 2')
console.log('success 3')
},
error: function () {
console.log('error')
}
})
jquery1.5之后开始使用 deferred
// done, fail 方法
var ajax = $.ajax('./data.json') // ajax 是一个 deferred 对象
ajax.done(function(){
console.log('success a')
}).fail(function(){
console.log('error a')
}).done(function(){
console.log('success b')
}).fail(function(){
console.log('error b')
}).done(function(){
console.log('success c')
}).fail(function(){
console.log('error c')
})
// then方法
// 很像 promise 的写法
var ajax = $.ajax('./data.json') // ajax 是一个 deferred 对象
ajax.then(function(){
console.log('success 100')
}, function(){
console.log('error 100')
}).then(function(){
console.log('success 200')
}, function(){
console.log('error 200')
}).then(function(){
console.log('success 300')
}, function(){
console.log('error 300')
})
jQuery1.5变化:
- 无法改变 js 异步和单线程的本质
- 只能从写法上杜绝 callback 这种形式
- 它是一种语法糖形式,但是解耦了代码
- 很好的体现:开放封闭原则-对扩展开放,对修改封闭
2. jQuery Deferred的使用(封装)
常规写法:
// 如果异步函数task逻辑非常复杂,代码容易耦合度高,不利于修改维护
var wait = function () {
var task = function () {
console.log('执行完成')
}
setTimeout(task, 2000) // 异步,2s后执行task
}
wait()
不足:
- 如果 task 函数里面还有一系列复杂操作,那么大量的代码写在一个函数里,不利于阅读和维护。
- task 回调函数可能还有其他的回调函数,容易形成回调地狱,那么多层嵌套的回调使代码显得臃肿和难以维护。
- 对测试代码不利,一旦我们要修改代码,就要在 task 里面修改,那么整个 task 就要重新测试,浪费时间和精力。
- 违反了开放封闭原则。所以的代码全部写在 task 里面,没有办法扩展,只能再 task 里面修改。
使用 jQuery Deferred 封装:
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 // 要求返回 deferred 对象
}
// 注意:这里一定要有返回值
return wait(dtd)
}
var w = waitHandle()
// w.reject()
w.then(function(){
console.log('success 1')
}, function(){
console.log('error 1')
}).then(function(){
console.log('success 2')
}, function(){
console.log('error 2')
}).then(function(){
console.log('success 3')
}, function(){
console.log('error 3')
})
分析:
- 使用 jQuery Deferred 封装后,调用 waitHandle() ,返回值是 dtd,任然是一个 deferred 对象
- 封装时对 dtd 做的操作是,异步任务执行成功时,执行
dtd.resolve()
;异步任务失败或出错,执行dtd.reject()
- 封装后就解决了常规写法中出现的那些问题。
注意:
- dtd 的 API 可以分成两类,用意不同,这两类应该分开使用,否则后果很严重。
第一类:dtd.resolve, dtd.reject (主动执行的)
第二类:dtd.then, dtd.done, dtd.fail (被动监听的)
- 比如:在上面代码 w 下面,执行
w.reject()
,那么下面 then 中的结果就变啦,怎么解决?看下面
3. dtd.promise()
代码如下:
function waitHandle () {
var dtd = $.Deferred()
var wait = function (dtd) {
var task = function () {
console.log('执行完成')
// 成功
dtd.resolve()
// 失败
// dtd.reject()
}
setTimeout(task, 2000)
return dtd.promise()
}
return wait(dtd)
}
var w = waitHandle()
// w.reject()
$.when(w).then(function(){
console.log('success 1')
}, function(){
console.log('error 1')
})
分析:
- 经过上面改动,此时的 w 变成了一个 promise 对象
- 此时再添加
w.reject()
,会直接报错
Ⅳ、promise
1. promise基本使用
常规代码:
function LoadImg(src){
var img = document.createElement('img')
img.src = src
img.onload = function () {
console.log('加载成功')
}
img.onerror = function () {
console.log('加载失败')
}
}
var src = "https://www.baidu.com/img/bd_logo1.png?where=super"
LoadImg(src)
promise 封装:
function LoadImg (src) {
var promise = new Promise(function(resolve, reject){
var img = document.createElement('img')
img.src = src
// throw new Error('自定义错误')
img.onload = function () {
console.log('加载成功')
resolve(img)
}
img.onerror = function () {
console.log('加载失败')
reject()
}
})
return promise
}
var src = "https://www.baidu.com/img/bd_logo1.png?where=super"
LoadImg(src).then(function(img){
console.log('ok')
console.log(1, img.width)
return img
}, function () {
console.log('fail')
}).then(function(img){
console.log('ok 1')
console.log(2, img.height)
}, function () {
console.log('fail 1')
})
2. promise 异常捕获
规定:then 只接受一个参数,最后统一用 catch 捕获异常
两方面的异常:
- 代码逻辑之外的,语法方面的错误 (Error)
- 代码逻辑之内的错误 (reject)
var src = "https://www.baidu.com/img/bd_logo1.png?where=super"
LoadImg(src).then(function(img){
console.log(1, img.width)
return img
}).then(function(img){
console.log(2, img.height)
}).catch(function (ex) {
// 统一异常捕获
console.log(ex)
})
3. 代码串联(链式操作)
需求:
有时候我们需要先执行一段代码,等这段代码执行完毕,再执行下一段代码,有先后顺序。比如,我们首先去获取某个人的信息,成功后再去获取好友的信息...。
代码如下:
var src1 = "https://www.baidu.com/img/bd_logo1.png?where=super"
var result1 = LoadImg(src1)
var src2 = "https://upload.jianshu.io/users/upload_avatars/7182212/aa3cd65c-dedf-45ea
-9708-0d68ffaceedc.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"
var result2 = LoadImg(src2)
result1.then(function(img1){
console.log('图片一加载完成', img1.width)
return result2 // 重要
// return result2, img1 // 那么此时 img1 将作为下一个 then 的参数
}).then(function (img2) {
console.log('图片二加载完成', img2.width)
}).catch(function (ex) {
console.log(ex)
})
4. Promise.all() & Promise.race()
var src1 = "https://www.baidu.com/img/bd_logo1.png?where=super"
var result1 = LoadImg(src1)
var src2 = "https://upload.jianshu.io/users/upload_avatars/7182212/aa3cd65c-dedf-45ea-9708-
0d68ffaceedc.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"
var result2 = LoadImg(src2)
Promise.all([result1, result2]).then(function (datas) {
console.log('all', datas[0])
console.log('all', datas[1])
})
Promise.race([result1, result2]).then(function (data) {
console.log('race', data)
})
image.png
5. async/await的使用
promise中的 then 方法只是将 callback 拆分了,但是 then 中还是使用了回调函数;async/await 可以将异步的操作用完全同步的方式写出来
代码如下:
<script>
function LoadImg (src) {
var promise = new Promise(function(resolve, reject){
var img = document.createElement('img')
img.src = src
img.onload = function () {
resolve(img)
}
img.onerror = function () {
reject()
}
})
return promise
}
var src1 = "https://www.baidu.com/img/bd_logo1.png?where=super"
var src2 = "https://upload.jianshu.io/users/upload_avatars/7182212/aa3cd65c-dedf-45ea-9708-0d68ffaceedc.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"
const load = async function () {
var img1 = await LoadImg(src1)
console.log(img1.width)
var img2 = await LoadImg(src2)
console.log(img2.width)
}
load()
</script>
// 与promise对比
var src1 = "https://www.baidu.com/img/bd_logo1.png?where=super"
var result1 = LoadImg(src1)
var src2 = "https://upload.jianshu.io/users/upload_avatars/7182212/aa3cd65c-dedf-45ea-9708-0d68ffaceedc.jpg?imageMogr2/auto-orient/strip|imageView2/1/w/240/h/240"
var result2 = LoadImg(src2)
result1.then(function(img1){
console.log('图片一加载完成', img1.width)
return result2
}).then(function (x) {
console.log('图片二加载完成', x)
}).catch(function (ex) {
console.log(ex)
})
async/await的使用:
- 使用 await ,函数必须要用 async 标识。
- await 后面跟的是一个 Promise 实例,使用了 Promise,与promise并不冲突。
- 需要 babel-polyfill (用来解析async/await),不过现在不用好像也没关系,最好用上。
四、虚拟 DOM
Ⅰ、vdom(virtual dom)基本认识
1. 什么是 vdom?
定义:
- virtual dom,虚拟 DOM
- 用 js 来模拟 DOM 结构
- DOM 变化的对比,放在 js 里面做(图灵完备语言--具有完整的架构逻辑)
- 提高重绘性能
原代码:
image.png
解析成 vdom:
image.png
下面代码没有使用 vdom ,用 jQuery 进行的常规操作
代码如下:将 data 中的数据展示成表格,随便修改一个信息,表格也跟着改变
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<script src="https://cdn.bootcss.com/jquery/3.4.1/jquery.min.js"></script>
<script>
// 再浏览器中 dom 渲染是最耗性能的
var data = [
{
name: "张三",
age: "20",
address: "北京"
},
{
name: "李四",
age: "18",
address: "上海"
},
{
name: "王二",
age: "22",
address: "郑州"
}
]
// 渲染函数
function render () {
var $container = $('#container')
// 清空容器,重要!!!
$container.html('')
// 拼接表格
var $table = $('<table>')
$table.append($('<tr><td>name</td><td>age</td><td>address</td></tr>'))
data.forEach(function (item) {
$table.append($('<tr><td>'+ item.name +'</td><td>'+ item.age +'</td><td>'+ item.address +'</td></tr>'))
})
// 渲染到页面
$container.append($table)
}
$('#btn-change').click(function () {
data[1].age = 30
data[2].address = "深圳"
// re-render
render(data)
})
//页面加载完,立即进行初次渲染
render(data)
</script>
</body>
分析:
以上代码,当我们修改表格中任何一个值时,DOM 渲染时都会把以前的值清空,把变化后的值重新渲染上。这样一来很多没有发生变化的 DOM 节点也会重新渲染,浪费性能。
2. 为何使用 vdom ?
代码:
var div = document.createElement('div')
var item, result = ''
for (item in div) {
result += '|' + item
}
console.log(result)
打印出来:
上面我们可以得知:浏览器默认创建的 DOM 节点是非常复杂的,节点属性非常之多,从侧面可以看出进行 DOM 操作是非常耗性能的。而在 js 层面模拟的 vdom 就相当简洁,而且浏览器执行 js 是非常高效的,所以在复杂页面上,vdom 能够大大提高性能。
总结:
- DOM 操作是 “昂贵” 的,而 js 运行效率高
- 尽量减少 DOM 操作,而不是 “推倒重来”
- 项目越复杂,影响越严重
- vdom 即可以解决这个问题
Ⅱ、vdom 的使用
1. snabbdom的介绍和使用
定义:
snabbdom: 是一个开源的 vdom 库。虚拟 DOM 其实类似于 MVC, MVVM,是一类技术实现,能够实现 vdom 的库也有很多,不过 snabbdom 使用量还是很多的,而且vue2.0也是借用了 snabbdom, 所以我们要借用 snabbdom 来学习虚拟 DOM。
说明:
snabbdom 有两个关键函数,h 函数用来创造 虚拟节点 的,而 patch 函数是用来把虚拟节点渲染出来的。
代码演示:
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<!-- 下面版本要一致 -->
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
var snabbdom = window.snabbdom
var container = document.getElementById('container')
// 定义 patch 函数
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h 函数
var h = snabbdom.h
// 生成 vnode
var vnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item 2')
])
document.getElementById('btn-change').addEventListener('click', function () {
var newVnode = h('ul#list', {}, [
h('li.item', {}, 'Item 1'),
h('li.item', {}, 'Item b'),
h('li.item', {}, 'Item 3')
])
patch(vnode, newVnode)
})
patch(container, vnode)
</script>
</body>
说明:
通过上面代码的演示,我们可以发现,当我们修改列表中节点值时,就只会被改变的节点发生变化,其他的值不在发生变化,这就大大减少了代码渲染的工作量。(代码的改变可以通过浏览器控制台中代码的闪烁看出)。
下面将jQuery编写的列表通过 snabbdom 的方法渲染出来:
代码如下:
<body>
<div id="container"></div>
<button id="btn-change">change</button>
<!-- 下面版本要一致 -->
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-class.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-props.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-style.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/snabbdom-eventlisteners.js"></script>
<script src="https://cdn.bootcss.com/snabbdom/0.7.3/h.js"></script>
<script>
var snabbdom = window.snabbdom
var data = [
{
name: "张三",
age: "20",
address: "北京"
},
{
name: "李四",
age: "18",
address: "上海"
},
{
name: "王二",
age: "22",
address: "郑州"
}
]
data.unshift({
name: '姓名',
age: '年龄',
address: '地址'
})
// 定义 patch 函数
var patch = snabbdom.init([
snabbdom_class,
snabbdom_props,
snabbdom_style,
snabbdom_eventlisteners
])
// 定义 h 函数
var h = snabbdom.h
var container = document.getElementById('container')
var btnChange = document.getElementById('btn-change')
var vnode
function render () {
var newVnode = h('table', {}, data.map(function(item){
var tds = []
var key
for(key in item){
if(item.hasOwnProperty(key)){
tds.push(h('td', {}, item[key] + ''))
}
}
return h('tr', {}, tds)
}))
if(vnode){
patch(vnode, newVnode)
} else {
// 初次渲染
patch(container, newVnode)
}
vnode = newVnode
}
btnChange.addEventListener('click', function () {
data[1].age = '30'
data[2].address = '深圳'
// re-render
render(data)
})
// 初次渲染
render(data)
</script>
</body>
Ⅲ、diff 算法的简单了解
1. 什么时 diff 算法?
diff 算法:是 Linux 的一种基础命令,用来找出不同文件之间的差异。例如:在 git 操作时,有一个 git diff 命令,就是用来查看文件修改前后所改变的内容。
2. vdom 为何使用 diff 算法?
- DOM 操作是昂贵的,所以要尽量减少 DOM 操作。
- 找出本次 DOM 必须更新的节点来更新,其他的不更新。
-
这个 “找出” 的过程,就需要使用 diff 算法。
image.png
image.png
3. diff 简单实现过程
由于 diff 算法是十分复杂的,所以本次实现也是最基本的,最简单情况的实现,通过 snabbdom 中 patch 函数的简单实现讲解 diff 算法的实现。
代码如下:
// patch(container, vnode) 的执行流程
function createElement (vnode) {
var tag = vnode.tag
var attrs = vnode.attrs || {}
var children = vnode.children || []
if(!vnode){
return null
}
// 创建真实的 DOM 元素
var elem = document.createElement(tag)
// 为元素添加 属性
var attrName
for(attrName in attrs){
if(attrs.hasOwnProperty(attrName)){
elem.setAttribute(attrName, attrs[attrName])
}
}
// 为元素添加 子元素
children.forEach(function(childVnode){
// 创建子元素
elem.appendChild(createElement(childVnode)) // 递归
});
// 返回真实的 DOM 元素
return elem
}
// patch(vnode, newVnode) 的实现逻辑
function updateChildren (vnode, newVnode) {
var children = vnode.children
var newChildren = newVnode.children
children.forEach(function (child, index) {
var newChild = newChildren[index]
if(newChild == null){
return
}
if(child.tag === newChild.tag){
updateChildren(child, newChild)
} else {
replaceNode(child, newChild)
}
});
}
function replaceNode () {
// ......
}
注意:
其实 diff 算法的实现不仅仅只是上述的子节点不同的形式,还有很多形式
- 节点新增和删除
- 节点重新排序
- 节点属性、样式、事件变化
- 如何极致压榨性能
- ......
四、MVVM 和 Vue
Ⅰ、jQuery 和 Vue 的区别
通过两种方式写一个简单的 todolist 做对比:
jQuery 代码:
<body>
<div id="app">
<div>
<input class="text-title" type="text">
<button class="confirm">确认</button>
</div>
<ul class="ul-list">
</ul>
</div>
<script src="./jquery.js"></script>
<script>
$('.confirm').click(function () {
var title = $('.text-title').val()
var $li = `<li>${title}</li>`
$('.ul-list').append($li)
$('.text-title').val('')
})
</script>
</body>
Vue 代码:
<body>
<div id="app">
<div>
<input type="text" v-model="title">
<button @click="add">确定</button>
</div>
<ul>
<li v-for="(item, index) in list" @click = "deleteItem(index)">{{item}}</li>
</ul>
</div>
<script src="./vue.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
title: '',
list: []
},
methods: {
add () {
this.list.push(this.title)
this.title = ''
},
deleteItem (index) {
// console.log(this.list.slice(index, index+1)) // slice 用于截取目标数组的一部分,返回被截取的元素数组,原数组不变
this.list.splice(index, 1) // splice用于删除原数组的一部分成员,返回被删除的元素数组,原数组改变
// console.log(index)
}
}
})
</script>
</body>
区别:
- 数据与视图的分离,解耦(开放封闭原则
- 以数据驱动视图,只关心数据的变化,DOM操作被封装
Ⅱ、MVC 和 MVVM
1. mvc
- m - Model - 数据层
- v - View - 视图层
-
c - Controller - 控制器(逻辑层)
image.png
一般是 view 有什么命令,让控制器去执行,控制器去改变了 model 中的数据,然后再渲染到 view 上。
代码如下:
<body>
<div id="app">
<div>
<input class="text-title" type="text">
<button class="confirm">确认</button>
</div>
<ul class="ul-list">
</ul>
</div>
<script src="./jquery.js"></script>
<script>
var view = $("#app")
// var model = {
// title: '',
// list: []
// }
var controller = {
view: null,
model: null,
init: function (view) {
this.view = view
// this.model = model
this.bindEvent()
},
bindEvent: function () {
$('.confirm').click(function () {
var title = $('.text-title').val()
var $li = `<li>${title}</li>`
$('.ul-list').append($li)
$('.text-title').val('')
})
}
}
controller.init(view)
</script>
</body>
分析:
view,model,controller 是三个模块,当然上面代码没有使用 model,当然也是可以使用的,灵活应用即可。使用 mvc ,特别是功能模块复杂时,会使得代码更有条理,易于修改和维护。
2. mvvm
- Model - 模型、数据
- View - 视图、模板(视图和模型是分离的)
- ViewModel - 连接 Model 和 View
mvvm 是 mvc 结合前端应用场景做的一次升级。
image.png image.png
mvvm 框架的三要素:
- 响应式:vue 如何监听到 data 的每个属性变化?
- 模板引擎:vue 的模板如何被解析,指令如何处理?
- 渲染:vue 的模板如何被渲染成 html?以及渲染过程
Ⅲ、响应式
1. 什么是响应式?
我们在使用 vue 时,当我们修改了 data
的属性值之后,立刻可以在页面中渲染出来。所以 vue 为什么可以监听到 data
属性值的变化呢?而且 data
中的属性直接可以通过 Vue
实例调用,不必再通过 data ,比如:vm.name, vm.age
, 这是怎么实现的?
响应式总结:
- 当我们修改了 data 属性之后,vue 立刻可以监听到
-
data
属性被代理到vm
上
2. Vue 中如何实现响应式?
核心函数:Object.defineProperty
常规代码:
var obj = {// obj 里面是一些静态的属性,没有什么逻辑变化,所以是无法监听的
name: "zhangsan",
age: 20
}
Object.defineProperty
的使用:
var obj = {}
var _name = "zhangsan"
Object.defineProperty(obj, "name", {
get: function () {
console.log("get", _name) // 监听代码,在这里可以写监听代码的一些逻辑
return _name
},
set: function (newVal) {
console.log("set", newVal) // 监听代码
return newVal
}
})
将 data
的属性值绑定到 vm 上:
var vm = {}
var data = {
name: 'zhangsan',
age: 20
}
var key
for(key in data){
(function(key){
Object.defineProperty(vm, key, {
get: function () {
// console.log(key)
return data[key]
},
set: function (newVal) {
data[key] = newVal
}
})
})(key)
}
Ⅳ、模板解析(vue)
1. 模板是什么?
vue 中的模板
<div id="app">
<div>
<input type="text" v-model="title">
<button @click="add">确定</button>
</div>
<ul>
<li v-for="(item, index) in list" @click = "deleteItem(index)">{{item}}</li>
</ul>
</div>
模板:
- 本质:字符串
- 有逻辑,
v-for, v-if, v-model.....
- 与 html 格式很像,但是有很大区别。(html 是静态的,写几个标签就显示几个标签;vue 模板是动态的,比如:
v-for
循环显示多个标签,v-if
可以控制标签的显示隐藏等) - 最终还是要转换成 html 来显示
由上面模板解析来看,模板最终要转换成 html 渲染到页面上,怎么转换?
模板最终要转换成 js 代码,因为:
- 转换成 html 渲染页面,必须用 js 才能实现(三门语言中,js 才能动态的修改 html 结构)
- 模板有逻辑,必须用 js 来实现 (图灵完备语言)
- 所以,模板最终要转换成一个 js 函数 (render 函数)
2. with 语法
注意:自己日常开始时不要使用 with 语法,容易出问题
代码如下:
<script>
var obj = {
name: 'zhangsan',
age: 20,
getAddress: function () {
alert('beijing')
}
}
// function fn () {
// alert(obj.name)
// alert(obj.age)
// obj.getAddress()
// }
// fn()
function fn1 () {
with(obj){
alert(name)
alert(age)
getAddress()
}
}
fn1()
</script>
3. render 函数
上面提到,要把模板内容渲染出来,必须把模板转化成 render 函数进行解析
render 函数的写法如下:
<body>
<!--模板-->
<div id="app">
<p>{{price}}</p>
</div>
<script src="./vue-2.5.13.js"></script>
<script>
var vm = new Vue({
el: '#app',
data: {
price: 100
}
})
// 以下是手写的 ppt 中的手写 render 函数
function render () {
with(this){ // this 就是 vm
return _c(
'div',
{
attrs:{"id": "app"}
},
[
_c('p', [_v(_s(price))])
//_c 创建元素节点
//_v 创建文本节点
//_s toString,转化为文本
])
}
}
// 这种写法与上面是等效的
function render1 () {
return vm._c(
'div',
{
attrs:{"id": "app"}
},
[
vm._c('p', [vm._v(vm._s(vm.price))])
]
)
}
</script>
</body>
image.png
如果模板中有逻辑,转化为 render 函数是什么样的呢?
// 模板
<div id="app">
<div>
<input type="text" v-model="title">
<button @click="add">确定</button>
</div>
<ul>
<li v-for="(item, index) in list" @click = "deleteItem(index)">{{item}}</li>
</ul>
</div>
// 转化为 render 函数
with(this){
return _c(
'div',
{
attrs:{"id":"app"}
},
[
_c(
'div',
[
_c(
'input',
{
directives:[
{
name:"model",
rawName:"v-model",
value:(title),
expression:"title"
}
],
attrs:{"type":"text"},
domProps:{"value":(title)},
on:{"input":function($event){
if($event.target.composing)
return;
title=$event.target.value
}
}
}
),
_v(" "), // 回车文本
_c(
'button',
{
on:{"click":add}
},
[_v("确定")]
)
]
),
_v(" "),
_c(
'ul',
_l( // 创造一个数组,作用类似于数组中的 map 函数
(list),
function(item,index){
return _c(
'li',
{on:{"click":function($event){deleteItem(index)}}},[_v(_s(item))]
)
}
)
)
]
)
}
问题:
- input中
v-model
如何实现的双向绑定?
在input中监听了一个input
事件,当在 input 框中输入值时,会自动触发这个事件,事件函数中有title=$event.target.value
,把输入框输入的值赋值给了 data 中的title;当我们改变 data 中的title时,创造的 input 中有domProps:{"value":(title)}
,把 data 中的 title 赋值给了 input 框的 value。这就是双向绑定。 -
v-on:click
是如何实现的?
解析模板,把模板转化为render函数时,在创建 button 元素节点时,监听了一个 click 事件,click 事件函数就是 methods 中定义好的方法。 -
v-for
是如何实现的?
_l((list), function(item, index){return _c('li', [_v(_s(item))])})
,_l是用来创造一个数组的,_l里面通过循环遍历 list, 返回了一个个 li 标签,通过_l
形成一个由一个个 li 标签组成的数组。
上面已经解决了模板中“逻辑”的问题,通过将模板转化为 render 函数,在 render 函数中将 逻辑 通过js代码的方式写出来。那么我们如何将已经转化为 render 函数的模板生成 html 呢?另外,vm._c 是什么?render 函数返回了什么?
image.png
其实 vue 中的 vnode 就是借助于虚拟DOM库 snabbdom 来实现的,vm._c其实就相当于 snabbdom 中的 h
函数,render 函数执行后,返回的是 vnode。
snabbdom 渲染的两个关键函数是 h
函数和 patch
函数,那么 vue 模板转化为 html 渲染同样要借助与这两个函数。
vm._update(vnode){
const preVnode = vm._vnode
vm._vnode = vnode
if(!preVnode){
vm.$el = vm.__patch__(vm.$el, vnode) // 首次渲染
} else {
vm.$el = vm.__patch__(preVnode, vnode)
}
}
function updateComponent () {
// vm._render 即 render 函数,返回 vnode
vm._update(vm._render())
}
分析:
-
updateComponent
函数中实现了 vnode 的 patch - 页面首次渲染执行
updateComponent
- data 中每次修改属性,执行
updateComponent
(怎么执行?在响应式里面,Object.defineProperty
的 set 函数的监听代码里可以写上updateComponent
函数)
Ⅴ、Vue 的整个实现流程 (总结)
流程:
- 第一步:解析模板成 render 函数
- 第二步:响应式开始监听
- 第三步:首次渲染,显示页面,且绑定依赖
- 第四步:data 属性变化,触发 rerender
1. 第一步:模板解析成 render 函数
问题:为什么是第一步呢?
其实,模板转化为 render 函数的具体过程我们是不必去关心的。我们甚至还可以采取预编译,也就是说我们编译完成后,模板已经自动转化为了 render 函数,从这里我们就知道这个是第一步执行。响应式是在 js 执行的时候才会运行的。
要点:
- with 的用法
- 模板中的所有信息都要被 render 函数所包含
- 模板中用到的 data 中的属性,都变成了 js 变量
- 模板中的
v-model, v-for, v-on, v-if
都变成了 js 逻辑 - render 函数返回 vnode
2. 第二步:响应式开始监听
要点:
- 核心函数
Object.defineProperty
- 将 data 属性代理到 vm 上(这样第一步中转化成的 render 函数才可以顺利执行)
3. 首次渲染,显示页面,且绑定依赖
vm._update(vnode){
const preVnode = vm._vnode
vm._vnode = vnode
if(!preVnode){
vm.$el = vm.__patch__(vm.$el, vnode) // 首次渲染
} else {
vm.$el = vm.__patch__(preVnode, vnode)
}
}
function updateComponent () {
// vm._render 即 render 函数,返回 vnode
vm._update(vm._render())
}
要点:
- 初次渲染,执行
updateComponent
函数,执行vm._render()
- 执行 render 函数, 会访问到 vm.list, vm.title
- 会被响应式的 get 方法监听到
- 执行
updateComponent
函数, 会走到 vdom 的 patch 方法 - patch 将 vnode 渲染成 DOM,初次渲染完成。
问题:为何要监听 get ,直接监听 set 不行吗?
解答:
- data 中有很多属性,有些用的到,有些用不到
- 被用到的会走到 get ,不被用到的不会走 get
- 未走到 get 的属性,那么 set 也不会理会
- 避免不必要的重复渲染
比如上面的title,list,在模板中使用了,在 render 函数渲染时调用 vm.title, vm.list
, 经过了响应式的 get 方法,被 get 监听到,当我们修改这些属性时,会触发响应式的 set 方法,set 方法的监听代码中的 updateComponent
函数就会执行,把改动后的属性值渲染到页面上。
如果没有在模板中使用的 data 属性,那么页面上就不会渲染出这个属性。如果属性值改变,也能被 set 方法监听到,那么经过 updateComponent
函数重新渲染,页面上也没有什么变化,只是白白浪费性能,所以要用 get 监听做一下筛选。
4. 第四步:data 属性变化,触发 rerender
image.png image.png要点:
- 修改属性,被响应式的 set 监听到
- set 中执行
updateComponent
-
updateComponent
重新执行vm._render()
- 生成的 vnode 和 preVnode,通过 patch 进行对比
- 渲染到 html 中
五、hybrid的了解
Ⅰ、hybrid 的实现流程
1. hybrid 的文字解释
- hybrid 即 “混合”,就是前端与客户端的混合开发
- 需要前端与客户端开发人员配合完成
- 某些环节也可能涉及到 server 端
image.png
分析: - 比如上面 app 右面的详情页是 hybrid 做的
- 那么详情页 topBar, bottomBar 是客户端,中间的新闻就是 hybrid 做的
2. hybrid 的存在价值
- 可以快速迭代更新(关键)
原因:无需 app 审核。
因为 app 开发的内容每一次上线更新都要在应用商店里面进行审核(比如苹果应用商店审核大概一周,国内的比如华为,小米,vivo等大概一两天),因为安卓 app 开发的语言是 Java,app有很大的权限,可以获取手机的定位,摄像头,通讯录等,所以必须要审核。
如果采用 hybrid 开发页面,采用纯前端的方式,那么上面那些权限就获取不到,所以就无需审核。所以用 hybrid 开发的页面可以无限次上线更新,节约了大量的审核时间。
- 体验流畅(和 NA(native 客户端) 的体验基本类似)
- 减少开发和沟通成本,双端公用一套代码(不一定)
3. webview
- 是 app 中的一个组件(app 中可以有webview组件,也可以没有)
- 用于加载 h5 页面,即一个小型的浏览器内核
- webview 是一类工具的统称
image.png
4. file 协议
- file 协议:本地加载,快
- http(s) 协议:网络加载,慢
- file 协议:组成
file://
+ 本地文件的绝对路径
5. hybrid 的具体实现
- 前端做好静态页面(html, css, js),把页面文件交给客户端
- 客户端拿到静态页面,以文件的形式存储在 app 中
- 客户端在一个 webview 中
- 使用 file 协议加载静态页面
注意:不是所有场景都适合使用 hybrid
- 使用 NA:体验要求极致,变化不频繁(如头条的首页)
- 使用 hybrid:体验要求高,变化频繁(如头条的新闻详情页)
- 使用 H5:体验无要求,不常用(如举报,反馈等页面)
6. 总结
- hybrid 是客户端和前端的混合开发
- hybrid 存在的核心意义在于快速迭代,无需审核
- hybrid 的实现流程(图),以及 webview 和 file 协议
Ⅱ、hybrid 的更新上线流程
1. 回顾 hybrid 的实现流程
image.png2. hybrid 的实现方法
image.png说明:
- 我们要替换每个客户端的静态文件
- 只能由客户端来做(客户端是由我们开发的)
- 客户端去 server 下载最新的静态文件
- 我们维护 server 的静态文件
具体实现流程:
image.png
说明:
- 分版本,有版本号,比如 201908061011
- 将静态文件压缩成 zip 包,上传到 server 端
- 客户端每次启动或刷新,都去服务端检查最新的版本号
- 如果服务端版本号大于客户端版本号,客户端就会下载最新的 zip 包
- 下载完之后解压包,然后将现有文件覆盖
3. 总结
掌握流程图
- 要点一:服务端的版本和 zip 包的维护
- 要点二:更新 zip 之前,先对比版本号
- 要点三:zip 下载和覆盖
Ⅲ、hybrid 和 h5的区别
优点:
- 体验更好,跟 NA 体验基本一致
- 可快速迭代,无需 app 审核【关键】
缺点:
- 开发成本高。联调、测试、查 bug 都比较麻烦
- 运维成本高。(更新上线的流程复杂,环节多)
使用的场景:
- hybrid:产品的稳定功能,体验要求高,迭代频繁
- h5:单次的运营活动(如xx红包)或不常用功能
Ⅳ、前端和客户端通信
1. 遗留问题
- app 发布后,静态文件如何进行实时更新?(上面分析过)
- 静态页面如何获取内容?
下面主要分析第二点。
那么新闻详情页使用 hybrid,前端如何获取新闻内容呢?
- 不能用 Ajax 获取,第一跨域,第二速度慢
- 跨域:Ajax请求肯定请求的是线上的一个 http(s) 的 API 地址,而 hybrid 是通过file 协议获取内容,协议不一样,肯定跨域(这个可以解决)
- 速度慢:正常的页面访问是页面加载完成后,解析 js,js通过Ajax获取内容,然后在解析,其实就是 h5 的加载方式
- 客户端获取新闻内容,然后 js 通讯拿到内容,再渲染
其实,客户端获取内容与js获取内容速度差不多,不过客户端可以预加载,提前就把静态文件的内容加载了过来
2. JS 和客户端通讯的基本形式
- js 访问客户端能力,传递参数和回调函数
- 客户端通过回调函数返回内容
3. schema协议简介和使用
schema 协议:前端与客户端通讯的约定
image.png
schema 协议代码演示
// 以下是演示,无法正常运行,微信有严格的权限验证,外部页面不能随意使用 schema
<body>
<button class="btn">click</button>
<script>
function invokeScan () {
window['_invoke_scan_callback_'] = function (result) { // 全局的回调
alert(result)
}
var iframe = document.createElement('iframe')
iframe.style.display = 'none'
// iframe.src = 'weixin://dl/scan' // iframe 访问 schema
iframe.src = 'weixin://dl/scan?a=1&b=2&c=3&callback=_invoke_scan_callback_'
var body = document.body
body.appendChild(iframe)
setTimeout(function(){
body.removeChild(iframe) // 销毁 schema
iframe = null
})
}
document.getElementByClassName('btn').addEventListener('click', function(){
invokeScan()
})
</script>
</body>
从上面我们可以看出,使用 schema 协议进行通讯很繁琐
将 schema 协议进行封装
// 封装代码 schema封装.js
(function (window, undefined) {
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 += callbackName
}
// 调用
var iframe = document.createElement('iframe')
iframe.style.display = 'none'
iframe.src = schema
var body = document.body
body.appendChild(iframe)
setTimeout(function(){
body.removeChild(iframe)
iframe = null
})
// 暴露到 全局变量
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)
// 调用
<body>
<button class="btn1">扫一扫</button>
<button class="btn2">分享</button>
<script src="./schema封装.js"></script>
<script>
document.getElementByClassName('btn1').addEventListener('click', function(){
window.invoke.scan({}, function () {})
})
document.getElementByClassName('btn2').addEventListener('click', function(){
window.invoke.share({
title: 'xxx',
content: 'yyy'
}, function (result) {
if(result.errno === 0){
alert('分享成功')
} else {
alert(result.message)
}
})
})
</script>
</body>
分析:
- 将以上封装的代码打包,叫做 invoke.js,内置到客户端
- 客户端每次执行 webview,都默认执行 invoke.js
- 本地加载,免去网络加载的时间,更快
- 本地加载,没有网络请求,黑客看不到 schema 协议,更安全