前端笔记(Vue原理重点)

314 阅读7分钟

组件化基础

‘很久以前’就有组件化

  • asp jsp php 已经有组件化了
  • nodejs中也有类似的组件化

数据驱动视图(MVVM,setState)

  • 传统组件,只是静态渲染,更新还要依赖于操作DOM
  • 数据驱动视图 - Vue MvvM
  • 数据驱动视图 - React setState

mvvm.png

在很早以前我们就有了“组件化”的思想,也就是同一个模板,根据后台的数据渲染不同的内容,但这个过程是通过dom去静态渲染的,所以当时jquery很流行。后来,在这个基础上,逐渐发展成“数据驱动视图”,也就是我们说的MVVM模型。

MVVMModel - View - ViewModel的简写,数据和视图之间变更是通过ViewModel去实现的,数据更新时,通过ViewModel去通知视图更新;当视图更新时,也是通过ViewModel去修改数据,开发者不再将重心放在dom的操作上,而是让开发者更关注于数据和业务,dom操作则由ViewModel去实现,也就是VueReact框架帮我们做的事。

但是VueReact又不是完全遵循MVVM模型的,因为它们允许用户通过ref去获取dom,并进行对应的dom操作,这又与MVVM是相悖的,所以说它们不是完全遵循的。

Vue响应式

  • 组件data 的数据一旦变化,立刻触发视图更新
  • 实现数据驱动视图的第一步
  • 核心API - Object.defineProperty
  • Object.defineProperty 的一些缺点(Vue3.0启用Proxy)
  • Proxy 兼容性不好,且无法使用polyfill

Object.defineProperty 基本用法

/**
* Object.defineProperty 基本用法
*/
const data = {}
const name = 'zhangsan'
Object.defineProperty(data,"name",{
    get:function(){
        console.log('get')
        return name
    }
    set:function(newValue){
        console.log('set')
        name = newValue
    }
})

//测试
console.log(data.name) //get zhangsan
data.name = 'lisi' //set

深度监听,对象、数组

// 触发更新视图
function updateView() {
    console.log('视图更新')
}

// 重新定义数组原型 用于将数组进行监听
const oldArrayProperty = Array.prototype
// 创建新对象,原型指向 oldArrayProperty ,再扩展新的方法不会影响原型
const arrProto = Object.create(oldArrayProperty);
['push', 'pop', 'shift', 'unshift', 'splice'].forEach(methodName => {
    arrProto[methodName] = function () {
        oldArrayProperty[methodName].call(this, ...arguments)
        // Array.prototype.push.call(this, ...arguments)
        updateView() // 触发视图更新
    }
})

// 重新定义属性,监听起来
function defineReactive(target, key, value) {
    // 深度监听
    observer(value)

    // 核心 API
    Object.defineProperty(target, key, {
        get() {
            return value
        },
        set(newValue) {
            if (newValue !== value) {
                // 深度监听
                observer(newValue)

                // 设置新值
                // 注意,value 一直在闭包中,此处设置完之后,再 get 时也是会获取最新的值
                value = newValue

                // 触发更新视图
                updateView()
            }
        }
    })
}

// 监听对象属性
function observer(target) {
    if (typeof target !== 'object' || target === null) {
        // 不是对象或数组
        return target
    }

    // 污染全局的 Array 原型
    // Array.prototype.push = function () {
    //     updateView()
    //     ...
    // }

    if (Array.isArray(target)) {
        target.__proto__ = arrProto
    }

    // 重新定义各个属性(for in 也可以遍历数组)
    for (let key in target) {
        defineReactive(target, key, target[key])
    }
}

// 准备数据
const data = {
    name: 'zhangsan',
    age: 20,
    info: {
        address: '北京' // 需要深度监听
    },
    nums: [10, 20, 30]
}

// 监听数据
observer(data)

// 测试
 data.name = 'lisi'
 data.age = 21
 console.log('age', data.age)
 data.x = '100' // 新增属性,监听不到 —— 所以有 Vue.set
 delete data.name // 删除属性,监听不到 —— 所有已 Vue.delete
 data.info.address = '上海' // 深度监听
data.nums.push(4) // 监听数组

Object.defineProperty缺点

  • 深度监听,需要递归到底,一次性计算量大
  • 无法监听新增属性、删除属性(Vue.set Vue.delete)
  • 无法原生监听数组,需要特殊处理

虚拟DOM(Virtual DOM) 和 diff

  • vdom 是实现vue和React的重要基石
  • diff算法是vdom中最核心、最关键的部分
  • vdom- 是用JS模拟DOM结构,计算出最小的变更,操作DOM
  • 数据驱动视图的模式下,有效控制DOM操作

用JS(vnode)模拟DOM结构

<div id="div1" class="container">
    <p>vdom</p>
    <ul style="font-size:20px">
        <li>a</li>
    </ul>
</div>
{
    tag:"div",
    props:{
        className:"container",
        id:"div1"
    },
    children: [
    {
        tag:"p",
        children: "vdom"
    },{
        tag: 'ul',
        props:{style:'font-size:20px'}
        children:[
            {
                tag:li,
                children: a
            }
            // ...
        ]
    }
    ]
}

通过 snabbdom 学习vdom

  • 简单强大的vdom 库,易学易用
  • Vue参考它实现的vdomdiff
  • Vue3.0重写了vdom的代码,优化了性能
  • React vdom 具体实现和Vue也不同,但不妨碍统一学习
import { init } from 'snabbdom/init'
import { classModule } from 'snabbdom/modules/class'
import { propsModule } from 'snabbdom/modules/props'
import { styleModule } from 'snabbdom/modules/style'
import { eventListenersModule } from 'snabbdom/modules/eventlisteners'
import { h } from 'snabbdom/h' // helper function for creating vnodes

const patch = init([ // Init patch function with chosen modules
  classModule, // makes it easy to toggle classes
  propsModule, // for setting properties on DOM elements
  styleModule, // handles styling on elements with support for animations
  eventListenersModule, // attaches event listeners
])

const container = document.getElementById('container')

const vnode = h('div#container.two.classes', { on: { click: someFn } }, [
  h('span', { style: { fontWeight: 'bold' } }, 'This is bold'),
  ' and this is just normal text',
  h('a', { props: { href: '/foo' } }, 'I\'ll take you places!')
])
// Patch into empty DOM element – this modifies the DOM as a side effect
patch(container, vnode)

const newVnode = h('div#container.two.classes', { on: { click: anotherEventHandler } }, [
  h('span', { style: { fontWeight: 'normal', fontStyle: 'italic' } }, 'This is now italic type'),
  ' and this is still just normal text',
  h('a', { props: { href: '/bar' } }, 'I\'ll take you places!')
])
// Second `patch` invocation
patch(vnode, newVnode) // Snabbdom efficiently updates the old view to the new state

github

diff算法

  • diff算法vdom中最核心,最关键的部分
  • diff算法能在日常中使用vue React 中体现出来(如Key)
  • diff算法是前端热门话题,面试'宠儿
  • diff即对比,是一个广泛的概念,如linux diff命令。git diff
  • 两个js对象也可以做diff,如 github.com/cujojs/jiff
  • 两棵树做diff,如这里的vdom diff

diff算法概述

树diff的时间复杂度O(n^3)

  • 第一,遍历tree1;第二,遍历tree2
  • 第三,排序
  • 1000个节点,要计算1亿次。算法不可用

优化时间复杂度到O(n)

  • 只比较同一层级,不跨级比较
  • tag不相同,则直接删掉重建,不再深度比较
  • tag和key,两者都相同,则认为是相同节点,不再深度比较

msedge_N03rOHqRE3.png

msedge_uYV5vxQOSh.png

模板编译

  • Vue模板不是html,有指令、有插值、JS表达式
  • 是“组件渲染和更新过程”中的一部分
  • 前置知识:JS的with语法
  • Vue template complier 将模板编译为render 函数
  • 执行render函数生成vnode

with语法

const obj = {a:100,b:200}
console.log(obj.a)
console.log(obj.b)
console.log(obj.c) //undefined
// 使用 with , 能改变 {} 内自由变量的查找方式
// 将 {} 内自由变量,当做 obj 的属性来查找
with(obj) {
    console.log(a)
    console.log(b)
    console.log(c) // 会报错!!
}
  • 改变{}内自由变量的查找规则,当做obj属性来查找
  • 如果找不到匹配的obj属性,就会报错
  • with要慎用,它打破了作用域的规则,易读性变差

编译模板

模板不是html,有指令、插值、js表达式,能实现判断、循环 html是标签语言,只有JS才能实现判断、循环(图灵完备)

const compiler = require('vue-template-compiler')

// 插值
 const template = `<p>{{message}}</p>`
// with(this){return createElement('p',[createTextVNode(toString(message))])}
 h -> vnode
 createElement -> vnode

 // 表达式
 const template = `<p>{{flag ? message : 'no message found'}}</p>`
 // with(this){return _c('p',[_v(_s(flag ? message : 'no message found'))])}

// // 属性和动态属性
 const template = `
     <div id="div1" class="container">
         <img :src="imgUrl"/>
     </div>
 `
// with(this){return _c('div',
//      {staticClass:"container",attrs:{"id":"div1"}},
//      [
//          _c('img',{attrs:{"src":imgUrl}})])}

 // 条件
 const template = `
     <div>
         <p v-if="flag === 'a'">A</p>
         <p v-else>B</p>
     </div>
 `
// with(this){return _c('div',[(flag === 'a')?_c('p',[_v("A")]):_c('p',[_v("B")])])}

// 循环
 const template = `
     <ul>
         <li v-for="item in list" :key="item.id">{{item.title}}</li>
     </ul>
 `
// with(this){return _c('ul',_l((list),function(item){return _c('li',{key:item.id},[_v(_s(item.title))])}),0)}

// 事件
 const template = `
     <button @click="clickHandler">submit</button>
 `
// with(this){return _c('button',{on:{"click":clickHandler}},[_v("submit")])}

// v-model
const template = `<input type="text" v-model="name">`
// 主要看 input 事件
/* with(this){return _c('input',{directives:[{name:"model",rawName:"v-model",value:
(name),expression:"name"}],attrs:{"type":"text"},domProps:{"value":(name)},on:
{"input":function($event{if($event.target.composing)return;name=$event.target.value}}})}*/

// render 函数
// 返回 vnode
// patch

// 编译
const res = compiler.compile(template)
console.log(res.render)

// ---------------分割线--------------

// // 从 vue 源码中找到缩写函数的含义
// function installRenderHelpers (target) {
//     target._o = markOnce;
//     target._n = toNumber;
//     target._s = toString;
//     target._l = renderList;
//     target._t = renderSlot;
//     target._q = looseEqual;
//     target._i = looseIndexOf;
//     target._m = renderStatic;
//     target._f = resolveFilter;
//     target._k = checkKeyCodes;
//     target._b = bindObjectProps;
//     target._v = createTextVNode;
//     target._e = createEmptyVNode;
//     target._u = resolveScopedSlots;
//     target._g = bindObjectListeners;
//     target._d = bindDynamicKeys;
//     target._p = prependModifier;
// }

  • 模板编译为render函数,执行render函数返回vnode
  • 基于vnode再执行patch和diff
  • 使用webpack vue-loader,会在开发环境下编译模板(重要)

扩展Vue组件中使用render代替template

Vue.component('heading',{
    //template: `xxx`,
    render:function (createElement){
        return creatElement(
            'h' + this.level,
            [
                createElement('a', {
                    attrs:{
                        name: 'headerId',
                        href: '#' + 'headerId'
                    }
                },'this is a tag')
            ]
        )
    }
- })
  • 但容易出现语意不明。
  • 在有些复杂的情况中,不能用template,可以考虑用render
  • React一直都用render(没有模板),和这里一样

小结

  • 模板到render函数,再到vnode,再到渲染和更新
  • vue组件可以用render 替代 template

组件渲染/更新的过程

  • 一个组件渲染到页面,修改data触发更新(数据驱动视图)
  • 响应式:监听 data 属性 getter setter (包括数组)
  • 模板编译:模板到render函数,再到vnode
  • vdom:patch(elem,vnode)和patch(vnode,newVnode)

更新过程

初次渲染过程

  • 解析模板为render函数(或在开发环境已完成,vue-loader
  • 触发响应式,监听data属性getter setter
  • 执行render函数,生成vnode,patch(elem,vnode)

更新过程

  • 修改data,触发stter(此前在getter中已被监听)
  • 重新执行render函数,生成newVnode
  • patch(vnode,newVnode)

msedge_jLWPTHaGf8.png

异步渲染

  • 回顾$nextTick
  • 汇总data的更改,一次性更新视图
  • 减少DOM操作次数,提高性能

前端路由的原理

  • 稍微复杂一点的SPA,都需要路由
  • vue-router也是vue全家桶的标配之一

Vue-router路由模式

  • hash
  • H5 history

网页url组成部分

msedge_37URt8mMoz.png

hash的特点

  • hash变化会触发网页跳转,即浏览器的前进、后退
  • hash变化不会刷新页面,SPA必需的特点
  • hash永远不会提交到server端(自生自灭)
<!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>hash test</title>
</head>
<body>
    <p>hash test</p>
    <button id="btn1">修改 hash</button>

    <script>
        // hash 变化,包括:
        // a. JS 修改 url
        // b. 手动修改 url 的 hash
        // c. 浏览器前进、后退
        window.onhashchange = (event) => {
            console.log('old url', event.oldURL)
            console.log('new url', event.newURL)

            console.log('hash:', location.hash)
        }

        // 页面初次加载,获取 hash
        document.addEventListener('DOMContentLoaded', () => {
            console.log('hash:', location.hash)
        })

        // JS 修改 url
        document.getElementById('btn1').addEventListener('click', () => {
            location.href = '#/user'
        })
    </script>
</body>
</html>

H5 history

  • 用 url 规范的路由,但跳转时不刷新页面
  • history.pushState

正常页面浏览 如图: msedge_3XHyoDUS4G.png 改造成H5 history模式 如图: msedge_e6iEKFlUyM.png

<!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>history API test</title>
</head>
<body>
    <p>history API test</p>
    <button id="btn1">修改 url</button>

    <script>
        // 页面初次加载,获取 path
        document.addEventListener('DOMContentLoaded', () => {
            console.log('load', location.pathname)
        })

        // 打开一个新的路由
        // 【注意】用 pushState 方式,浏览器不会刷新页面
        document.getElementById('btn1').addEventListener('click', () => {
            const state = { name: 'page1' }
            console.log('切换路由到', 'page1')
            history.pushState(state, '', 'page1') // 重要!!
        })

        // 监听浏览器前进、后退
        window.onpopstate = (event) => { // 重要!!
            console.log('onpopstate', event.state, location.pathname)
        }

        // 需要 server 端配合,可参考
        // https://router.vuejs.org/zh/guide/essentials/history-mode.html#%E5%90%8E%E7%AB%AF%E9%85%8D%E7%BD%AE%E4%BE%8B%E5%AD%90
    </script>
</body>
</html>

小结:

  • hash - window.onhashchange
  • h5 history - history.pushStatewindow.onpopstate
  • history 需要后端支持 两者如何选择
  • to B 的系统推荐使用hash,简单易用,对url规范不敏感
  • to C 的系统,可以考虑选择H5 history, 但需要服务端支持
  • 能选择简单的,就别用复杂的,要考虑成本和收益