前端框架面试之 Vue 的使用 和 Vue 的原理万字长文总结

一、 Vue 的使用

  1. vue 基本知识点,如下所示:
  • 插值、表达式,指令、动态属性,v-html 会有 XSS 风险,会覆盖子组件,代码如下所示:
<template>
    <div>
        <p>文本插值 {{message}}</p>
        <p>JS 表达式 {{ flag ? 'yes' : 'no' }} (只能是表达式,不能是 js 语句)</p>

        <p :id="dynamicId">动态属性 id</p>

        <hr/>
        <p v-html="rawHtml">
            <span>有 xss 风险</span>
            <span>【注意】使用 v-html 之后,将会覆盖子元素</span>
        </p>
        <!-- 其他常用指令后面讲 -->
    </div>
</template>

<script>
export default {
    data() {
        return {
            message: 'hello vue',
            flag: true,
            rawHtml: '指令 - 原始 html <b>加粗</b> <i>斜体</i>',
            dynamicId: `id-${Date.now()}`
        }
    }
}
</script>

  • computedwatchcomputed 有缓存,data 不变则不会重新计算。watch 可以进行深度监听,watch 监听引用类型,拿不到 oldVal,代码如下所示:
    • computed 的代码:
      <template>
          <div>
              <p>num {{num}}</p>
              <p>double1 {{double1}}</p>
              <input v-model="double2"/>
          </div>
      </template>
      
      <script>
      export default {
          data() {
              return {
                  num: 20
              }
          },
          computed: {
              double1() {
                  return this.num * 2
              },
              double2: {
                  get() {
                      return this.num * 2
                  },
                  set(val) {
                      this.num = val/2
                  }
              }
          }
      }
      </script>
      
      • watch 的代码:
      <template>
          <div>
              <input v-model="name"/>
              <input v-model="info.city"/>
          </div>
      </template>
      
      <script>
      export default {
          data() {
              return {
                  name: '张三',
                  info: {
                      city: '上海'
                  }
              }
          },
          watch: {
              name(oldVal, val) {
                  // eslint-disable-next-line
                  console.log('watch name', oldVal, val) // 值类型,可正常拿到 oldVal 和 val
              },
              info: {
                  handler(oldVal, val) {
                      // eslint-disable-next-line
                      console.log('watch info', oldVal, val) // 引用类型,拿不到 oldVal 。因为指针相同,此时已经指向了新的 val
                  },
                  deep: true // 深度监听
              }
          }
      }
      </script>
      
  • classstyle,使用动态属性,使用驼峰式写法,代码如下所示:
<template>
    <div>
        <p :class="{ black: isBlack, yellow: isYellow }">使用 class</p>
        <p :class="[black, yellow]">使用 class (数组)</p>
        <p :style="styleData">使用 style</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            isBlack: true,
            isYellow: true,

            black: 'black',
            yellow: 'yellow',

            styleData: {
                fontSize: '40px', // 转换为驼峰式
                color: 'red',
                backgroundColor: '#ccc' // 转换为驼峰式
            }
        }
    }
}
</script>

<style scoped>
    .black {
        background-color: #999;
    }
    .yellow {
        color: yellow;
    }
</style>
  • 条件渲染,v-ifv-else 的用法,可使用变量,也可以使用 === 表达式,需要区分 v-ifv-show 的区别和使用场景,代码如下所示:
<template>
    <div>
        <p v-if="type === 'a'">A</p>
        <p v-else-if="type === 'b'">B</p>
        <p v-else>other</p>

        <p v-show="type === 'a'">A by v-show</p>
        <p v-show="type === 'b'">B by v-show</p>
    </div>
</template>

<script>
export default {
    data() {
        return {
            type: 'a'
        }
    }
}
</script>
  • 循环列表渲染,遍历对象可以使用 v-forkey 也很重要,不能够乱写。注意的是,v-forv-if 是不能一起使用的,代码如下所示:
<template>
    <div>
        <p>遍历数组</p>
        <ul>
            <li v-for="(item, index) in listArr" :key="item.id">
                {{index}} - {{item.id}} - {{item.title}}
            </li>
        </ul>

        <p>遍历对象</p>
        <ul >
            <li v-for="(val, key, index) in listObj" :key="key">
                {{index}} - {{key}} -  {{val.title}}
            </li>
        </ul>
    </div>
</template>

<script>
export default {
    data() {
        return {
            flag: false,
            listArr: [
                { id: 'a', title: '标题1' }, // 数据结构中,最好有 id ,方便使用 key
                { id: 'b', title: '标题2' },
                { id: 'c', title: '标题3' }
            ],
            listObj: {
                a: { title: '标题1' },
                b: { title: '标题2' },
                c: { title: '标题3' },
            }
        }
    }
}
</script>
  • 事件,event 参数,自定义参数。事件修饰符,按键修饰符,观察事件被绑定到哪里,代码如下所示:
<template>
    <div>
        <p>{{num}}</p>
        <button @click="increment1">+1</button>
        <button @click="increment2(2, $event)">+2</button>
    </div>
</template>

<script>
export default {
    data() {
        return {
            num: 0
        }
    },
    methods: {
        increment1(event) {
            // eslint-disable-next-line
            console.log('event', event, event.__proto__.constructor) // 是原生的 event 对象
            // eslint-disable-next-line
            console.log(event.target)
            // eslint-disable-next-line
            console.log(event.currentTarget) // 注意,事件是被注册到当前元素的,和 React 不一样
            this.num++

            // 1. event 是原生的
            // 2. 事件被挂载到当前元素
            // 和 DOM 事件一样
        },
        increment2(val, event) {
            // eslint-disable-next-line
            console.log(event.target)
            this.num = this.num + val
        },
        loadHandler() {
            // do some thing
        }
    },
    mounted() {
        window.addEventListener('load', this.loadHandler)
    },
    beforeDestroy() {
        //【注意】用 vue 绑定的事件,组建销毁时会自动被解绑
        // 自己绑定的事件,需要自己销毁!!!
        window.removeEventListener('load', this.loadHandler)
    }
}
</script>
  • 表单,v-model。常见的表单项 textarea、checkbox、radio、select,修饰符 lazy、number、trim,代码如下所示:
<template>
    <div>
        <p>输入框: {{name}}</p>
        <input type="text" v-model.trim="name"/>
        <input type="text" v-model.lazy="name"/>
        <input type="text" v-model.number="age"/>

        <p>多行文本: {{desc}}</p>
        <textarea v-model="desc"></textarea>
        <!-- 注意,<textarea>{{desc}}</textarea> 是不允许的!!! -->

        <p>复选框 {{checked}}</p>
        <input type="checkbox" v-model="checked"/>

        <p>多个复选框 {{checkedNames}}</p>
        <input type="checkbox" id="jack" value="Jack" v-model="checkedNames">
        <label for="jack">Jack</label>
        <input type="checkbox" id="john" value="John" v-model="checkedNames">
        <label for="john">John</label>
        <input type="checkbox" id="mike" value="Mike" v-model="checkedNames">
        <label for="mike">Mike</label>

        <p>单选 {{gender}}</p>
        <input type="radio" id="male" value="male" v-model="gender"/>
        <label for="male"></label>
        <input type="radio" id="female" value="female" v-model="gender"/>
        <label for="female"></label>

        <p>下拉列表选择 {{selected}}</p>
        <select v-model="selected">
            <option disabled value="">请选择</option>
            <option>A</option>
            <option>B</option>
            <option>C</option>
        </select>

        <p>下拉列表选择(多选) {{selectedList}}</p>
        <select v-model="selectedList" multiple>
            <option disabled value="">请选择</option>
            <option>A</option>
            <option>B</option>
            <option>C</option>
        </select>
    </div>
</template>

<script>
export default {
    data() {
        return {
            name: '张三',
            age: 18,
            desc: '自我介绍',

            checked: true,
            checkedNames: [],

            gender: 'male',

            selected: '',
            selectedList: []
        }
    }
}
</script>
  1. 对于 vue 组件的使用,props$emit,组件间通讯,自定义事件,组件生命周期,代码如下所示:
  • index.vue 如下:
<template>
    <div>
        <Input @add="addHandler"/>
        <List :list="list" @delete="deleteHandler"/>
    </div>
</template>

<script>
import Input from './Input'
import List from './List'

export default {
    components: {
        Input,
        List
    },
    data() {
        return {
            list: [
                {
                    id: 'id-1',
                    title: '标题1'
                },
                {
                    id: 'id-2',
                    title: '标题2'
                }
            ]
        }
    },
    methods: {
        addHandler(title) {
            this.list.push({
                id: `id-${Date.now()}`,
                title
            })
        },
        deleteHandler(id) {
            this.list = this.list.filter(item => item.id !== id)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('index created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('index mounted')
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('index before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('index updated')
    },
}
</script>
  • Input.vue 如下:
<template>
    <div>
        <input type="text" v-model="title"/>
        <button @click="addTitle">add</button>
    </div>
</template>

<script>
import event from './event'

export default {
    data() {
        return {
            title: ''
        }
    },
    methods: {
        addTitle() {
            // 调用父组件的事件
            this.$emit('add', this.title)

            // 调用自定义事件
            event.$emit('onAddTitle', this.title)

            this.title = ''
        }
    }
}
</script>
  • List.vue 如下:
<template>
    <div>
        <ul>
            <li v-for="item in list" :key="item.id">
                {{item.title}}

                <button @click="deleteItem(item.id)">删除</button>
            </li>
        </ul>
    </div>
</template>

<script>
import event from './event'

export default {
    // props: ['list']
    props: {
        // prop 类型和默认值
        list: {
            type: Array,
            default() {
                return []
            }
        }
    },
    data() {
        return {

        }
    },
    methods: {
        deleteItem(id) {
            this.$emit('delete', id)
        },
        addTitleHandler(title) {
            // eslint-disable-next-line
            console.log('on add title', title)
        }
    },
    created() {
        // eslint-disable-next-line
        console.log('list created')
    },
    mounted() {
        // eslint-disable-next-line
        console.log('list mounted')

        // 绑定自定义事件
        event.$on('onAddTitle', this.addTitleHandler)
    },
    beforeUpdate() {
        // eslint-disable-next-line
        console.log('list before update')
    },
    updated() {
        // eslint-disable-next-line
        console.log('list updated')
    },
    beforeDestroy() {
        // 及时销毁,否则可能造成内存泄露
        event.$off('onAddTitle', this.addTitleHandler)
    }
}
</script>
  • event.js 如下:
import Vue from 'vue'

export default new Vue()

  1. Vue 高级特性,自定义 v-model、$nextTick、slot、动态和异步组件、keep-alive、mixin,如下所示:
  • 自定义 v-model,代码如下所示:
<template>
    <!-- 例如:vue 颜色选择 -->
    <input type="text"
        :value="text1"
        @input="$emit('change1', $event.target.value)"
    >
    <!--
        1. 上面的 input 使用了 :value 而不是 v-model
        2. 上面的 change1 和 model.event1 要对应起来
        3. text1 属性对应起来
    -->
</template>

<script>
export default {
    model: {
        prop: 'text1', // 对应 props text1
        event: 'change1'
    },
    props: {
        text1: String,
        default() {
            return ''
        }
    }
}
</script>
  • $nextTickVue 是异步渲染,data 改变之后,DOM 不会立刻渲染,$nextTick 会在 DOM 渲染之后被触发,以获取最新的 DOM 节点,代码如下所示:
<template>
  <div id="app">
    <ul ref="ul1">
        <li v-for="(item, index) in list" :key="index">
            {{item}}
        </li>
    </ul>
    <button @click="addItem">添加一项</button>
  </div>
</template>

<script>
export default {
  name: 'app',
  data() {
      return {
        list: ['a', 'b', 'c']
      }
  },
  methods: {
    addItem() {
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)
        this.list.push(`${Date.now()}`)

        // 1. 异步渲染,$nextTick 待 DOM 渲染完再回调
        // 2. 页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次
        this.$nextTick(() => {
          // 获取 DOM 元素
          const ulElem = this.$refs.ul1
          // eslint-disable-next-line
          console.log( ulElem.childNodes.length )
        })
    }
  }
}
</script>


  • slot,插槽的基本使用,作用域插槽,具名插槽,代码如下所示:

    • slot.vue,如下
    <template>
        <a :href="url">
            <slot>
                默认内容,即父组件没设置内容时,这里显示
            </slot>
        </a>
    </template>
    
    <script>
    export default {
        props: ['url'],
        data() {
            return {}
        }
    }
    </script>
    
    • scopedSlot.vue,如下
    <template>
        <a :href="url">
            <slot :slotData="website">
                {{website.subTitle}} <!-- 默认值显示 subTitle ,即父组件不传内容时 -->
            </slot>
        </a>
    </template>
    
    <script>
    export default {
        props: ['url'],
        data() {
            return {
                website: {
                    url: 'http://wangEditor.com/',
                    title: 'wangEditor',
                    subTitle: '轻量级富文本编辑器'
                }
            }
        }
    }
    </script>
    
  • 动态组件,:is = "component-name" 用法,需要根据数据,动态渲染的常见,即组件类型不确定。异步组件,import() 函数,按需加载,异步加载大组件,代码如下所示:

<template>
    <div>
        <p>vue 高级特性</p>
        <hr>

        <!-- 自定义 v-model -->
        <!-- <p>{{name}}</p>
        <CustomVModel v-model="name"/> -->

        <!-- nextTick -->
        <!-- <NextTick/> -->

        <!-- slot -->
        <!-- <SlotDemo :url="website.url">
            {{website.title}}
        </SlotDemo> -->
        <!-- <ScopedSlotDemo :url="website.url">
            <template v-slot="slotProps">
                {{slotProps.slotData.title}}
            </template>
        </ScopedSlotDemo> -->

        <!-- 动态组件 -->
        <!-- <component :is="NextTickName"/> -->
        
        <!-- 异步组件 -->
        <!-- <FormDemo v-if="showFormDemo"/>
        <button @click="showFormDemo = true">show form demo</button> -->

        <!-- keep-alive -->
        <!-- <KeepAlive/> -->

        <!-- mixin -->
        <MixinDemo/>
    </div>
</template>

<script>
// import CustomVModel from './CustomVModel'
// import NextTick from './NextTick'
// import SlotDemo from './SlotDemo'
// import ScopedSlotDemo from './ScopedSlotDemo'
// import KeepAlive from './KeepAlive'
import MixinDemo from './MixinDemo'

export default {
    components: {
        // CustomVModel
        // NextTick
        // SlotDemo,
        // ScopedSlotDemo,
        // FormDemo: () => import('../BaseUse/FormDemo'),
        // KeepAlive
        MixinDemo
    },
    data() {
        return {
            name: '张三',
            website: {
                url: 'http://baidu.com/',
                title: '百度',
                subTitle: '百度前端'
            },
            // NextTickName: "NextTick",
            showFormDemo: false
        }
    }
}
</script>
  • keep-alive,缓存组件,频繁切换,不需要重复渲染,Vue 的常见性能优化,代码如下所示:

    • keepAlive.vue
          <template>
          <div>
              <button @click="changeState('A')">A</button>
              <button @click="changeState('B')">B</button>
              <button @click="changeState('C')">C</button>
      
              <keep-alive> <!-- tab 切换 -->
                  <KeepAliveStageA v-if="state === 'A'"/> <!-- v-show -->
                  <KeepAliveStageB v-if="state === 'B'"/>
                  <KeepAliveStageC v-if="state === 'C'"/>
              </keep-alive>
          </div>
      </template>
      
      <script>
      import KeepAliveStageA from './KeepAliveStateA'
      import KeepAliveStageB from './KeepAliveStateB'
      import KeepAliveStageC from './KeepAliveStateC'
      
      export default {
          components: {
              KeepAliveStageA,
              KeepAliveStageB,
              KeepAliveStageC
          },
          data() {
              return {
                  state: 'A'
              }
          },
          methods: {
              changeState(state) {
                  this.state = state
              }
          }
      }
      </script>
      
    • keepAliveStateA.vue
      <template>
          <p>state A</p>
      </template>
      
      <script>
      export default {
          mounted() {
              // eslint-disable-next-line
              console.log('A mounted')
          },
          destroyed() {
              // eslint-disable-next-line
              console.log('A destroyed')
          }
      }
      </script>
      
    • keepAliveStateB.vue
        <template>
            <p>state B</p>
        </template>
        
        <script>
        export default {
            mounted() {
                // eslint-disable-next-line
                console.log('B mounted')
            },
            destroyed() {
                // eslint-disable-next-line
                console.log('B destroyed')
            }
        }
        </script>
      
    • keepAliveStateC.vue
      <template>
          <p>state C</p>
      </template>
      
      <script>
      export default {
          mounted() {
              // eslint-disable-next-line
              console.log('C mounted')
          },
          destroyed() {
              // eslint-disable-next-line
              console.log('C destroyed')
          }
      }
      </script>
      
  • mixin,多个组件有相同的逻辑,抽离出来。mixin 并不是完美的解决方案,会有一些问题,Vue3 提出的 Composition API 旨在解决这些问题。同样的,mixin 也会存在一些问题,变量来源不明确,不利于阅读,多 mixin 可能会造成命名冲突,mixin 和组件可能出现多对多的关系,复杂度较高,代码如下所示:

    • Mixin.vue
     <template>
        <div>
            <p>{{name}} {{major}} {{city}}</p>
            <button @click="showName">显示姓名</button>
        </div>
    </template>
    
    <script>
    import myMixin from './mixin'
    
    export default {
        mixins: [myMixin], // 可以添加多个,会自动合并起来
        data() {
            return {
                name: '张三',
                major: 'web 前端'
            }
        },
        methods: {
        },
        mounted() {
            // eslint-disable-next-line
            console.log('component mounted', this.name)
        }
    }
    </script>
    
    • mixin.js
      export default {
          data() {
              return {
                  city: '上海'
              }
          },
          methods: {
              showName() {
                  // eslint-disable-next-line
                  console.log(this.name)
              }
          },
          mounted() {
              // eslint-disable-next-line
              console.log('mixin mounted', this.name)
          }
      }
      
      
  1. 对于用于 Vue 组件,dispatch、commit、mapState、mapGetters、mapActions 和 mapMutations 也需要熟系。
  2. 对于 vue-router,需要熟系路由模式,hashH5 history,路由配置,动态路由和懒加载。对于 vue-router 路由模式,hash 模式是默认的,如 http://abc.com/#/user/10。H5 history 模式,如 http://abc.com/user/20,后者需要 server 端支持,因此无特殊需求可选择前者。

二、 Vue 的原理

  1. 对于 vue 的原理,从组件化、响应式、vdomdiff、模版编译、渲染过程、前端路由几个方面。
  2. 组件化,很久之前就已经存在了,在 asp、jsp、php、nodejs 都有类似的组件化。数据驱动视图,MVVM、setState。传统组件,只是静态渲染,更新依赖于操作 DOM,数据驱动视图,vue MVVM,数据驱动视图,react setState
  3. vue 的响应式,组件 data 的数据一旦变化,立即触发视图的更新,实现数据驱动视图的第一步。vue 的响应式,核心 APIObject.defineProperty。对于 Object.defineProperty 也存在一些缺点,Vue3.0启用 ProxyProxy 也存在兼容性的问题,兼容性不好,无法 polyfillObject.defineProperty 实现响应式,监听对象,监听数组,复杂对象,深度监听。Object.defineProperty 的缺点,深度监听,需要递归到底,一次性计算量大,无法监听新增属性和删除属性,Vue.setVue.delete,无法原生监听数组,需要特殊处理,代码如下所示:
// 触发更新视图
function updateView() {
    console.log('视图更新')
}

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

// 重新定义属性,监听起来
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) // 监听数组

  1. 虚拟 DOM( Virtual DOM)diffvdom 是实现 vuereact 的重要基石,diff 算法是 vdom 中最核心、最关键的部分,如下所示:
  • DOM 操作非常耗费性能,以前用 jQuery,可以自行控制 DOM 操作的时机,手动调整。VueReact 是数据驱动视图,如何有效的控制 DOM 操作。
  • 解决方案就可以用 vdom,有了一定复杂度,想减少计算次数比较难。将计算转移为 JS 计算,JS 执行速度很快。vdomJS 模拟 DOM 结构,计算出最小的变更,操作 DOM
  • 通过 snabbdom 学习 vdom,简洁强大的 vdom 库,易学易用,Vue 参考它实现的 vdomdiff,地址为 https://github.com/snabbdom/snabbdom。在 Vue3.0 重写了 vdom 的代码,优化了性能
  • diff 算法是 vdom 中最核心、最关键的部分,在日常使用 vuereact 中可以体现出来,如 key
  • diff 即对比,是一个广泛的概念,如 linux diff 命令、git diff 等。两个 JS 对象也可以做 diff,两棵树做 diff,如 vdom diff 等等。
  • diff 的时间复杂度 O(n^3),第一,遍历 tree 1;第二,遍历 tree2;第三,排序。1000个节点,要计算 1 亿次,算法不可用
  • 优化时间复杂度到 O(n),只比较同一层级,不跨级比较。tag 不相同,则直接删掉重建,不再深度比较。tagkey,两者都相同,则认为是相同节点,不再深度比较。
  1. 对于 diff 算法的总结,patchVnode、addVnodes removeVnodes、updateChildrenkey 的重要性。vdom 核心概念很重要,h、vnode、patch、diff、key等等。vdom 存在的价值更加重要,数据驱动视图,控制 DOM 操作。snabbdom 的简单实现,代码如下所示:
const snabbdom = window.snabbdom

// 定义 patch
const patch = snabbdom.init([
    snabbdom_class,
    snabbdom_props,
    snabbdom_style,
    snabbdom_eventlisteners
])

// 定义 h
const h = snabbdom.h

const container = document.getElementById('container')

// 生成 vnode
const vnode = h('ul#list', {}, [
    h('li.item', {}, 'Item 1'),
    h('li.item', {}, 'Item 2')
])
patch(container, vnode)

document.getElementById('btn-change').addEventListener('click', () => {
    // 生成 newVnode
    const newVnode = h('ul#list', {}, [
        h('li.item', {}, 'Item 1'),
        h('li.item', {}, 'Item B'),
        h('li.item', {}, 'Item 3')
    ])
    patch(vnode, newVnode)
})
  1. 模版编译,模版是 vue 开发中最常用的部分,即与使用相关联的原理。它不是 html,有指令、插值、JS 表达式,会通过组件渲染和更新过程所去体现出来,如下所示:
  • 前置知识也是 JSwith 语法,能改变 {} 内自由变量的查找方式,vue template complier 将模版编译为 render 函数,执行 render 函数生成 vnode
  • with 语法,改变 {} 内自由变量的查找规则,当做 obj 属性来查找。如果找不到匹配的 obj 属性,就会报错。with 要慎用,它打破了作用域规则,易读性变差
  • 编译模版,模版不是 html,有指令、插值、JS 表达式,能实现判断、循环。html 是标签语言,只有 JS 才能实现判断、循环,图灵完备的。因此,模版一定是转换为某种 JS 代码,即编译模版
  1. 对于 vue 组件中使用 render 代替 template。在有些复杂情况中,不能用 template,可以考虑用 renderReact 一直都用 render,没有模版,和这里一样。所以,vue 组件中使用 render 代替 template,代码如下所示:
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;
// // }

  1. 对于组件的渲染和更新过程,一个组件渲染到页面,修改 data 触发更新,数据驱动视图,对背后原理和流程需要很熟悉。响应式,监听 data 属性 gettersetter,包括数组。模版编译,模版到 render 函数,再到 vnodevdom,patch(elem, vnode)patch(vnode, newVnode)

  2. 对于初次渲染过程,更新过程和异步渲染,也需要很熟悉,如下所示:

  • 初次渲染过程,解析模版为 render 函数或者是在开发环境已完成,vue-loader。触发响应式,监听 data 属性 gettersetter。执行 render 函数,生成 vnodepatch( elem, vnode)
  • 对于更新过程,修改 data,触发 setter,此前 setter 中已被监听。重新执行 render 函数,生成 newVnode,patch(vnode, newVnode)
  • 对于异步渲染,需要用到 $nextTick,汇总 data 的修改,一次性更新视图。减少 DOM 操作次数,提高性能。异步渲染,$nextTickDOM 渲染完再回调。页面渲染时会将 data 的修改做整合,多次 data 修改只会渲染一次。
  1. 对于模版的渲染与隔离过程,需要理清渲染和响应式的关系、渲染和模版编译的关系、渲染和 vdom 关系。初次渲染过程,更新过程和异步渲染,同样需要理清。

  2. 前端路由原理,对于 SPA,都需要路由,vue-router 也是 vue 全家桶的标配之一。vue-router 的路由模式、hashH5 history,如下所示:

  • hash 的特点,如下所示:
    • hash 变化会触发网页跳转,即浏览器的前进和后退
    • hash 变化不会刷新页面,SPA 必须的特点
    • hash 永远不会提交到 server
  • H5 history,用 url 规范的路由,但跳转时不刷新页面,history.pushStatewindow.onpopstate
  • 对于两者的选择,如下所示:
    • to B 的系统推荐使用 hash,简单易用,对 url规范不敏感
    • to C 的系统,可以考虑选择 H5 history,但需要服务端支持
    • 能选择简单的,就别用复杂的,要考虑成本和收益
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章