美文网首页
js面试课程

js面试课程

作者: 在路上919 | 来源:发表于2019-12-18 17:17 被阅读0次

    一、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
    使用方法:

    • 直接在命令行输出转译后的代码
      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 上扩展插件,有什么好处?
    好处:

    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的文字解释:

    • 事件轮询,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)
    

    解释:

    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变化:

    • 无法改变 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)
    

    打印出来:

    image.png
    上面我们可以得知:浏览器默认创建的 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。

    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 算法?

    • 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 框架的三要素:

    1. 响应式:vue 如何监听到 data 的每个属性变化?
    2. 模板引擎:vue 的模板如何被解析,指令如何处理?
    3. 渲染: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))]
                                    )
                        }
                    )
                )
            ]
        )
    }
    

    问题:

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

    分析:

    • 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 不行吗?

    image.png
    解答:
    • 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
    image.png

    4. file 协议

    • file 协议:本地加载,快
    • http(s) 协议:网络加载,慢
    • file 协议:组成 file:// + 本地文件的绝对路径

    5. hybrid 的具体实现

    • 前端做好静态页面(html, css, js),把页面文件交给客户端
    • 客户端拿到静态页面,以文件的形式存储在 app 中
    • 客户端在一个 webview 中
    • 使用 file 协议加载静态页面
    image.png

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

    • 使用 NA:体验要求极致,变化不频繁(如头条的首页)
    • 使用 hybrid:体验要求高,变化频繁(如头条的新闻详情页)
    • 使用 H5:体验无要求,不常用(如举报,反馈等页面)

    6. 总结

    • hybrid 是客户端和前端的混合开发
    • hybrid 存在的核心意义在于快速迭代,无需审核
    • hybrid 的实现流程(图),以及 webview 和 file 协议

    Ⅱ、hybrid 的更新上线流程

    1. 回顾 hybrid 的实现流程

    image.png

    2. hybrid 的实现方法

    image.png

    说明:

    • 我们要替换每个客户端的静态文件
    • 只能由客户端来做(客户端是由我们开发的)
    • 客户端去 server 下载最新的静态文件
    • 我们维护 server 的静态文件

    具体实现流程:


    image.png

    说明:

    • 分版本,有版本号,比如 201908061011
    • 将静态文件压缩成 zip 包,上传到 server 端
    • 客户端每次启动或刷新,都去服务端检查最新的版本号
    • 如果服务端版本号大于客户端版本号,客户端就会下载最新的 zip 包
    • 下载完之后解压包,然后将现有文件覆盖

    3. 总结

    掌握流程图

    • 要点一:服务端的版本和 zip 包的维护
    • 要点二:更新 zip 之前,先对比版本号
    • 要点三:zip 下载和覆盖

    Ⅲ、hybrid 和 h5的区别

    优点:

    • 体验更好,跟 NA 体验基本一致
    • 可快速迭代,无需 app 审核【关键】

    缺点:

    • 开发成本高。联调、测试、查 bug 都比较麻烦
    • 运维成本高。(更新上线的流程复杂,环节多)

    使用的场景:

    • hybrid:产品的稳定功能,体验要求高,迭代频繁
    • h5:单次的运营活动(如xx红包)或不常用功能

    Ⅳ、前端和客户端通信

    1. 遗留问题

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

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

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

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

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

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

    分析:

    • 将以上封装的代码打包,叫做 invoke.js,内置到客户端
    • 客户端每次执行 webview,都默认执行 invoke.js
    • 本地加载,免去网络加载的时间,更快
    • 本地加载,没有网络请求,黑客看不到 schema 协议,更安全

    相关文章

      网友评论

          本文标题:js面试课程

          本文链接:https://www.haomeiwen.com/subject/ierhkctx.html