js面试课程

2019-12-18  本文已影响0人  在路上919

一、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

说明:

  1. babel-core 作为babel的核心存在,babel的核心api都在这个模块里面,所以使用Babel这个依赖是首先要安装的
  2. babel-preset-es2015 是指 ES2015 / ES6 插件集合,把与es6转成es5相关的几十个插件全部封装到这个包里,省去了我们配置插件的麻烦。作用就是把es6转换成es5。其他的es(2016,2017...)作用类似
  3. 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
使用方法:

Ⅱ、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 上扩展插件,有什么好处?
好处:

  1. 只有 $ 会暴露在window全局变量,其他的如 init, Z在外面取不到
  2. 将插件扩展统一到 $.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 渲染的冲突呢?

  1. 浏览器需要渲染 DOM
  2. 而js可以修改 DOM结构
  3. 所以js执行的时候,浏览器 DOM 渲染会暂停
  4. 两段(句)js 代码也不能同时执行(都修改 DOM 会有冲突)
  5. webworker支持多线程,但是不能访问 DOM

问题:异步解决方案的问题

  1. 没按照书写顺序执行,可读性差
  2. callback 中不容易模块化

Ⅱ、event-loop

event-loop: 异步的实现方式
event-loop的文字解释:

用到异步的场景:

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)

解释:

  1. 代码执行,首先执行主队列的代码,主队列代码执行完毕,去异步队列去找异步代码,若有异步代码,将异步代码转到主队列执行。然后浏览器一直在主队列和异步队列循环,一旦发现异步队列有代码,就转到主队列执行。这就是事件轮询。
  2. 当异步队列有多组代码时,异步队列遵循先进先出的数据结构
// 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变化:

2. jQuery Deferred的使用(封装)

常规写法:

// 如果异步函数task逻辑非常复杂,代码容易耦合度高,不利于修改维护
var wait = function () {
    var task = function () {
        console.log('执行完成')
    }
    setTimeout(task, 2000) // 异步,2s后执行task
}
wait()

不足:

使用 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')
})

分析:

注意:

第一类:dtd.resolve, dtd.reject (主动执行的)
第二类:dtd.then, dtd.done, dtd.fail (被动监听的)

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

分析:

Ⅳ、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 捕获异常
两方面的异常:

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的使用:

四、虚拟 DOM

Ⅰ、vdom(virtual dom)基本认识

1. 什么是 vdom?

定义:

原代码:


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)

打印出来:

image.png
上面我们可以得知:浏览器默认创建的 DOM 节点是非常复杂的,节点属性非常之多,从侧面可以看出进行 DOM 操作是非常耗性能的。而在 js 层面模拟的 vdom 就相当简洁,而且浏览器执行 js 是非常高效的,所以在复杂页面上,vdom 能够大大提高性能。
总结:

Ⅱ、vdom 的使用

1. snabbdom的介绍和使用

定义:
snabbdom: 是一个开源的 vdom 库。虚拟 DOM 其实类似于 MVC, MVVM,是一类技术实现,能够实现 vdom 的库也有很多,不过 snabbdom 使用量还是很多的,而且vue2.0也是借用了 snabbdom, 所以我们要借用 snabbdom 来学习虚拟 DOM。

image.png image.png image.png
说明:
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 算法?

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>

区别:

Ⅱ、MVC 和 MVVM

1. mvc

<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

mvvm 是 mvc 结合前端应用场景做的一次升级。


image.png image.png

mvvm 框架的三要素:

  1. 响应式:vue 如何监听到 data 的每个属性变化?
  2. 模板引擎:vue 的模板如何被解析,指令如何处理?
  3. 渲染:vue 的模板如何被渲染成 html?以及渲染过程

Ⅲ、响应式

1. 什么是响应式?

我们在使用 vue 时,当我们修改了 data 的属性值之后,立刻可以在页面中渲染出来。所以 vue 为什么可以监听到 data 属性值的变化呢?而且 data 中的属性直接可以通过 Vue 实例调用,不必再通过 data ,比如:vm.name, vm.age, 这是怎么实现的?
响应式总结:

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>

模板:

由上面模板解析来看,模板最终要转换成 html 渲染到页面上,怎么转换?
模板最终要转换成 js 代码,因为:

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

问题:

  1. input中 v-model 如何实现的双向绑定?
    在input中监听了一个 input 事件,当在 input 框中输入值时,会自动触发这个事件,事件函数中有 title=$event.target.value,把输入框输入的值赋值给了 data 中的title;当我们改变 data 中的title时,创造的 input 中有 domProps:{"value":(title)},把 data 中的 title 赋值给了 input 框的 value。这就是双向绑定。
  2. v-on:click 是如何实现的?
    解析模板,把模板转化为render函数时,在创建 button 元素节点时,监听了一个 click 事件,click 事件函数就是 methods 中定义好的方法。
  3. 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())
}

分析:

Ⅴ、Vue 的整个实现流程 (总结)

流程:

1. 第一步:模板解析成 render 函数

问题:为什么是第一步呢?
其实,模板转化为 render 函数的具体过程我们是不必去关心的。我们甚至还可以采取预编译,也就是说我们编译完成后,模板已经自动转化为了 render 函数,从这里我们就知道这个是第一步执行。响应式是在 js 执行的时候才会运行的。

要点:

2. 第二步:响应式开始监听

要点:

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

要点:

问题:为何要监听 get ,直接监听 set 不行吗?

image.png
解答:

比如上面的title,list,在模板中使用了,在 render 函数渲染时调用 vm.title, vm.list, 经过了响应式的 get 方法,被 get 监听到,当我们修改这些属性时,会触发响应式的 set 方法,set 方法的监听代码中的 updateComponent 函数就会执行,把改动后的属性值渲染到页面上。
如果没有在模板中使用的 data 属性,那么页面上就不会渲染出这个属性。如果属性值改变,也能被 set 方法监听到,那么经过 updateComponent 函数重新渲染,页面上也没有什么变化,只是白白浪费性能,所以要用 get 监听做一下筛选。

4. 第四步:data 属性变化,触发 rerender

image.png image.png
要点:

五、hybrid的了解

Ⅰ、hybrid 的实现流程

1. hybrid 的文字解释

2. hybrid 的存在价值

原因:无需 app 审核。
因为 app 开发的内容每一次上线更新都要在应用商店里面进行审核(比如苹果应用商店审核大概一周,国内的比如华为,小米,vivo等大概一两天),因为安卓 app 开发的语言是 Java,app有很大的权限,可以获取手机的定位,摄像头,通讯录等,所以必须要审核。
如果采用 hybrid 开发页面,采用纯前端的方式,那么上面那些权限就获取不到,所以就无需审核。所以用 hybrid 开发的页面可以无限次上线更新,节约了大量的审核时间。

3. webview

image.png
image.png

4. file 协议

5. hybrid 的具体实现

image.png

注意:不是所有场景都适合使用 hybrid

6. 总结

Ⅱ、hybrid 的更新上线流程

1. 回顾 hybrid 的实现流程

image.png

2. hybrid 的实现方法

image.png

说明:

具体实现流程:


image.png

说明:

3. 总结

掌握流程图

Ⅲ、hybrid 和 h5的区别

优点:

缺点:

使用的场景:

Ⅳ、前端和客户端通信

1. 遗留问题

  1. app 发布后,静态文件如何进行实时更新?(上面分析过)
  2. 静态页面如何获取内容?

下面主要分析第二点。
那么新闻详情页使用 hybrid,前端如何获取新闻内容呢?

  1. 跨域:Ajax请求肯定请求的是线上的一个 http(s) 的 API 地址,而 hybrid 是通过file 协议获取内容,协议不一样,肯定跨域(这个可以解决)
  2. 速度慢:正常的页面访问是页面加载完成后,解析 js,js通过Ajax获取内容,然后在解析,其实就是 h5 的加载方式

其实,客户端获取内容与js获取内容速度差不多,不过客户端可以预加载,提前就把静态文件的内容加载了过来

2. JS 和客户端通讯的基本形式

image.png

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>

分析:

上一篇下一篇

猜你喜欢

热点阅读