当前位置:   article > 正文

FE.SRC-Vue实战与原理笔记

FE.SRC-Vue实战与原理笔记

实战 - 插件

form-validate

  1. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
  2. <div id="app">
  3. <form @submit="validate">
  4. <input v-model="text">
  5. <br>
  6. <input v-model="email">
  7. <ul v-if="!$v.valid" style="color:red">
  8. <li v-for="error in $v.errors">
  9. {{ error }}
  10. </li>
  11. </ul>
  12. <input type="submit" :disabled="!$v.valid">
  13. </form>
  14. </div>
  15. <script>
  16. const emailRE = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
  17. const validationPlugin = {
  18. install(Vue) {
  19. // 全局注入的方法
  20. Vue.mixin({
  21. computed: {
  22. $v() {
  23. const rules = this.$options.validations
  24. let valid = true
  25. let errors = []
  26. Object.keys(rules || {}).forEach(key => {
  27. const rule = rules[key]
  28. const value = this[key]
  29. const result = rule.validate(value)
  30. if(!result) {
  31. valid = false
  32. errors.push(
  33. rule.message(key, value)
  34. )
  35. }
  36. })
  37. return {
  38. valid,
  39. errors
  40. }
  41. }
  42. }
  43. })
  44. }
  45. }
  46. Vue.use(validationPlugin)
  47. new Vue({
  48. el: '#app',
  49. data: {
  50. text: 'foo',
  51. email: ''
  52. },
  53. validations: {
  54. text: {
  55. validate: value => value.length >= 5,
  56. message: (key, value) => `${key} should have a min length of 5, but got ${value.length}`
  57. },
  58. email: {
  59. validate: value => emailRE.test(value),
  60. message: key => `${key} must be a valid email`
  61. }
  62. },
  63. methods: {
  64. validate (e) {
  65. if (!this.$v.valid) {
  66. e.preventDefault()
  67. alert('not valid!')
  68. }
  69. }
  70. }
  71. })
  72. </script>

i18n

  1. <script src="../node_modules/vue/dist/vue.js"></script>
  2. <div id="app">
  3. <h1>{{ $t('welcome-message') }}</h1>
  4. <button @click="changeLang('en')">English</button>
  5. <button @click="changeLang('zh')">中文</button>
  6. <button @click="changeLang('nl')">Dutch</button>
  7. </div>
  8. <script>
  9. const i18nPlugin = {
  10. install(Vue,locales){
  11. Vue.prototype.$t=function(id){
  12. return locales[this.$root.lang][id]
  13. }
  14. }
  15. }
  16. Vue.use(i18nPlugin, /* option */ {
  17. en: { 'welcome-message': 'hello' },
  18. zh: { 'welcome-message': '你好' },
  19. nl: { 'welcome-message': 'Hallo' }
  20. })
  21. new Vue({
  22. el: '#app',
  23. data: {
  24. lang: 'en'
  25. },
  26. methods: {
  27. changeLang (lang) {
  28. this.lang = lang
  29. }
  30. }
  31. })
  32. </script>

实战 - 组件

Vue组件=Vue实例=new Vue(options)

  • 属性
    • 自定义属性 props:组件props中声明的属性
    • 原生属性 attrs:没有声明的属性,默认自动挂载到组件根元素上
    • 特殊属性 class,style:挂载到组件根元素上
  • 事件
    • 普通事件 @click,@input,@change,@xxx 通过this.$emit('xxx')触发
    • 修饰符事件 @input.trim @click.stop
  • 插槽
    • v-slot:xxx
    • v-slot:xxx="props"
    • 相同名称的插槽替换

      动态导入/延迟加载组件

    1. <template>
    2. <div>
    3. <lazy-component />
    4. </div>
    5. </template>
    6. <script>
    7. const lazyComponent = () => import('Component.vue')
    8. export default {
    9. components: { lazyComponent }
    10. }
    11. </script>

    基于路由拆分

    1. const routes = [
    2. { path: /foo', component: () => import('./RouteComponent.vue') }
    3. ]

函数式组件

无状态 、实例、this上下文、生命周期

临时变量组件

  1. <TempVar
  2. :var1="`hello ${name}`"
  3. :var2="destroyClock ? 'hello vue' : 'hello world'"
  4. >
  5. <template v-slot="{ var1, var2 }">
  6. {{ var1 }}
  7. {{ var2 }}
  8. </template>
  9. </TempVar>
  10. <script>
  11. export default {
  12. functional:true,
  13. render:(h,ctx)=>{
  14. return ctx.scopedSlots.default && ctx.scopedSlots.default(ctx.props||{})
  15. }
  16. }
  17. </script>

批量渲染标签组件

  1. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
  2. <div id="app">
  3. <function-component :tags="['h1','h2','h3']"></function-component>
  4. </div>
  5. <script>
  6. // 函数组件的渲染函数还会接收一个额外的 context 参数,为没有实例的函数组件提供上下文信息。
  7. // 这里我们用对象解构context
  8. const FunctionComponent = {
  9. functional: true, // 标记组件为 functional
  10. render (h, {props:{tags}}){
  11. return h('div', {attrs: {
  12. class:'function'
  13. }},
  14. tags.map((tag, i) => h(tag, i))
  15. )
  16. }
  17. }
  18. Vue.component('function-component', FunctionComponent)
  19. new Vue({el: '#app'})
  20. </script>

封装应用实例化函数

  1. function createApp ({ el, model, view, actions }) {
  2. const wrappedActions={}
  3. Object.keys(actions).forEach(key=>{
  4. const originalAction=actions[key]
  5. wrappedActions[key]=()=>{
  6. const nextModel=originalAction(model)
  7. vm.model=nextModel
  8. }
  9. })
  10. const vm=new Vue({
  11. el,
  12. data:{model},
  13. render(h){
  14. return view(h,this.model,wrappedActions)
  15. }
  16. })
  17. }
  18. createApp({
  19. el: '#app',
  20. model: {
  21. count: 0
  22. },
  23. actions: {
  24. inc: ({ count }) => ({ count: count + 1 }),
  25. dec: ({ count }) => ({ count: count - 1 })
  26. },
  27. view: (h, model, actions) => h('div', { attrs: { id: 'app' }}, [
  28. model.count, ' ',
  29. h('button', { on: { click: actions.inc }}, '+'),
  30. h('button', { on: { click: actions.dec }}, '-')
  31. ])
  32. })

高阶组件

  1. <script src="https://cdn.jsdelivr.net/npm/vue@2.5.21/dist/vue.js"></script>
  2. <div id="app">
  3. <smart-avatar username="vuejs" id="hello">
  4. <div slot="foo">
  5. 这是一个具名插槽
  6. </div>
  7. </smart-avatar>
  8. </div>
  9. <script>
  10. // mock API
  11. function fetchURL(username, cb) {
  12. setTimeout(() => {
  13. cb('https://avatars3.githubusercontent.com/u/6128107?v=4&s=200')
  14. }, 500)
  15. }
  16. const Avatar = {
  17. props: ['src'],
  18. template: `<img :src="src" />`
  19. }
  20. // 高阶组件withAvatarUrl
  21. function withAvatarUrl(innerComponent, fetchURL) {
  22. return {
  23. props: ['username'],
  24. data() {
  25. return {
  26. url: `http://via.placeholder.com/200*200`
  27. }
  28. },
  29. created() {
  30. fetchURL(this.username, url => {
  31. this.url = url;
  32. })
  33. },
  34. render(h) {
  35. // console.log(this.$slots.default);
  36. // console.log(this.$slots.foo);
  37. return h(innerComponent, {
  38. props: {
  39. src: this.url,
  40. attrs: this.$attrs
  41. }
  42. }, this.$slots.default)
  43. }
  44. }
  45. }
  46. const SmartAvatar = withAvatarUrl(Avatar, fetchURL);
  47. new Vue({
  48. el: '#app',
  49. components: {SmartAvatar}
  50. })
  51. </script>

异步组件

  1. const AsyncComp = () => ({
  2. // 需要加载的组件。应当是一个 Promise
  3. component: import('./MyComp.vue'),
  4. // 加载中应当渲染的组件
  5. loading: LoadingComp,
  6. // 出错时渲染的组件
  7. error: ErrorComp,
  8. // 渲染加载中组件前的等待时间。默认:200ms。
  9. delay: 200,
  10. // 最长等待时间。超出此时间则渲染错误组件。默认:Infinity
  11. timeout: 3000
  12. })
  13. Vue.component('async-example', AsyncComp)

实战 - 组件通信

父传子props

  1. <!-- 子组件 -->
  2. <template>
  3. <ul>
  4. <li v-for="item in dataList">{{item}}</li>
  5. </ul>
  6. </template>
  7. <script>
  8. export default {
  9. props : { dataList : [] }
  10. }
  11. </script>
  12. <!-- 父组件 -->
  13. <template>
  14. <component-child v-bind:data-list="dataList"> </component-child>
  15. <input v-model="dataInput" v-on:keyup.13="addDataItem()" ></input>
  16. </template>
  17. <script>
  18. import ComponentChild from './child.vue'
  19. export default {
  20. data () {
  21. return {
  22. dataInput: "",
  23. dataList : [ 'hello world!','welcome to use vue.js' ]
  24. }
  25. },
  26. components : { ComponentChild },
  27. methods : {
  28. addDataItem () {
  29. let self = this
  30. if( !(self.dataInput && self.dataInput.length > 0) ) { return }
  31. self.dataList.push( self.dataInput )
  32. self.dataInput = ""
  33. }
  34. }
  35. }
  36. </script>

子传父组件$emit, $on, $off

在组件中,可以使用 $emit, $on, $off 分别来分发、监听、取消监听事件

  1. // NewTodoInput ---------------------
  2. // ...
  3. methods: {
  4. addTodo: function () {
  5. eventHub.$emit('add-todo', { text: this.newTodoText })
  6. this.newTodoText = ''
  7. }
  8. }
  9. // DeleteTodoButton ---------------------
  10. // ...
  11. methods: {
  12. deleteTodo: function (id) {
  13. eventHub.$emit('delete-todo', id)
  14. }
  15. }
  16. // Todos ---------------------
  17. // ...
  18. created: function () {
  19. eventHub.$on('add-todo', this.addTodo)
  20. eventHub.$on('delete-todo', this.deleteTodo)
  21. },
  22. // 最好在组件销毁前
  23. // 清除事件监听
  24. beforeDestroy: function () {
  25. eventHub.$off('add-todo', this.addTodo)
  26. eventHub.$off('delete-todo', this.deleteTodo)
  27. },
  28. methods: {
  29. addTodo: function (newTodo) {
  30. this.todos.push(newTodo)
  31. },
  32. deleteTodo: function (todoId) {
  33. this.todos = this.todos.filter(function (todo) {
  34. return todo.id !== todoId
  35. })
  36. }
  37. }

内置$parent、$children、$ref;$attrs和$listeners

$ref ref="xxx"
$parent / $children:访问父 / 子实例

  1. <input
  2. :value="value"
  3. v-bind="$attrs"
  4. v-on="listeners"
  5. >
  6. <script>
  7. computed: {
  8. listeners() {
  9. return {
  10. ...this.$listeners,
  11. input: event =>
  12. this.$emit('input', event.target.value)
  13. }
  14. }
  15. }
  16. </script>

高阶插件/组件库 provide & inject(observable)

  1. // 父级组件提供 'foo'
  2. var Provider = {
  3. provide: {
  4. foo: 'bar'
  5. },
  6. // ...
  7. }
  8. // 子组件注入 'foo'
  9. var Child = {
  10. inject: ['foo'],
  11. created () {
  12. console.log(this.foo) // => "bar"
  13. }
  14. // ...
  15. }
  1. // 父级组件提供 'state'
  2. var Provider = {
  3. provide: {
  4. state = Vue.observable({ count: 0 })
  5. },
  6. // ...
  7. }
  8. // 子组件注入 'foo'
  9. var Child = {
  10. inject: ['state'],
  11. created () {
  12. console.log(this.state) // => { count: 0 }
  13. }
  14. // ...
  15. }

全局对象 Event Bus

  1. const state={count:0}
  2. const Counter = {
  3. data(){return state},
  4. render:h=>h('div',state.count)
  5. }
  6. new Vue({
  7. el: '#app',
  8. components:{Counter},
  9. methods:{
  10. inc(){
  11. state.count++
  12. }
  13. }
  14. })
  1. //中央事件总线
  2. var bus = new Vue();
  3. var app = new Vue({
  4. el: "#app",
  5. template: `
  6. <div>
  7. <brother1></brother1>
  8. <brother2></brother2>
  9. </div>
  10. `
  11. });
  12. // 在组件 brother1 的 methods 方法中触发事件
  13. bus.$emit("say-hello", "world");
  14. // 在组件 brother2 的 created 钩子函数中监听事件
  15. bus.$on("say-hello", function(arg) {
  16. console.log("hello " + arg);
  17. // hello world
  18. });

自实现boradcast和dispatch

$dispatch 和 $broadcast 已经被弃用,使用
Vuex代替

以下自实现参考 iview/emitter.js at 2.0 · iview/iview -github

  1. function broadcast(componentName, eventName, params) {
  2. this.$children.forEach(child => {
  3. const name = child.$options.name;
  4. if (name === componentName) {
  5. child.$emit.apply(child, [eventName].concat(params));
  6. } else {
  7. // todo 如果 params 是空数组,接收到的会是 undefined
  8. broadcast.apply(child, [componentName, eventName].concat([params]));
  9. }
  10. });
  11. }
  12. export default {
  13. methods: {
  14. dispatch(componentName, eventName, params) {
  15. let parent = this.$parent || this.$root;
  16. let name = parent.$options.name;
  17. while (parent && (!name || name !== componentName)) {
  18. parent = parent.$parent;
  19. if (parent) {
  20. name = parent.$options.name;
  21. }
  22. }
  23. if (parent) {
  24. parent.$emit.apply(parent, [eventName].concat(params));
  25. }
  26. },
  27. broadcast(componentName, eventName, params) {
  28. broadcast.call(this, componentName, eventName, params);
  29. }
  30. }
  31. };

原理 - 响应式

单向数据流,双向绑定语法糖

demo

  1. <PersonalInfo
  2. v-model="phoneInfo"
  3. :zip-code.sync="zipCode"
  4. />
  5. <!-- 等同于 -->
  6. <PersonalInfo
  7. :phone-info="phoneInfo"
  8. @change="val=>(phoneInfo=val)"
  9. :zip-code="zipCode"
  10. @update:zipCode="val=>(zipCode=val)"
  11. />

原理

数据劫持+观察订阅模式:

  • 在读取属性的时候依赖收集,在改变属性值的时候触发依赖更新
  • 实现一个observer,劫持对象属性
  • 实现一个全局的订阅器Dep,可以追加订阅者,和通知依赖更新
  • 在读取属性的时候追加当前依赖到Dep中,在设置属性的时候循环触发依赖的更新
  • new Vue(options)创建实例的时候,initData()进行数据劫持
  • 通过Object.defineProperty(obj,key,desc)对data进行数据劫持,即创建get/set函数
  • 这里需要考虑对对象的以及对数组的数据劫持(支持 push,pop,shift,unshift,splice,sort,reverse,不支持 filter,concat,slice)
  • 对象递归调用
  • 数组变异方法的解决办法:代理原型/实例方法
  • 避免依赖重读Observer
  1. // 全局的依赖收集器Dep
  2. window.Dep = class Dep {
  3. constructor() {this.subscribers = new Set()}
  4. depend() {activeUpdate&&this.subscribers.add(activeUpdate)}
  5. notify() {this.subscribers.forEach(sub =>sub())}
  6. }
  7. let activeUpdate
  8. function autorun(update) {
  9. function wrapperUpdate() {
  10. activeUpdate = wrapperUpdate
  11. update()
  12. activeUpdate = null
  13. }
  14. wrapperUpdate()
  15. }
  16. function observer(obj) {
  17. Object.keys(obj).forEach(key => {
  18. var dep = new Dep()
  19. let internalValue = obj[key]
  20. Object.defineProperty(obj, key, {
  21. get() {
  22. // console.log(`getting key "${key}": ${internalValue}`)
  23. // 将当前正在运行的更新函数追加进订阅者列表
  24. activeUpdate&&dep.depend() //收集依赖
  25. return internalValue
  26. },
  27. set(newVal) {
  28. //console.log(`setting key "${key}" to: ${internalValue}`)
  29. // 加个if判断,数据发生变化再触发更新
  30. if(internalValue !== newVal) {
  31. internalValue = newVal
  32. dep.notify() // 触发依赖的更新
  33. }
  34. }
  35. })
  36. })
  37. }
  38. let state = {count:0}
  39. observer(state);
  40. autorun(() => {
  41. console.log('state.count发生变化了', state.count)
  42. })
  43. state.count = state.count + 5;
  44. // state.count发生变化了 0
  45. // state.count发生变化了 5

MVVM实现

实现mvvm-github

Model-View-ViewModel,其核心是提供对View 和 ViewModel 的双向数据绑定,这使得ViewModel 的状态改变可以自动传递给 View

observer

  • proxy 方法遍历 data 的 key,把 data 上的属性代理到 vm 实例上(通过Object.defineProperty 的 getter 和 setter )
  • observe(data, this) 给 data 对象添加 Observer做监听。
    • 创建一个 Observer 对象
      • 创建了一个 Dep 对象实例(观察者模式)
        • 遍历data,convert(defineReactive) 方法使他们有getter、setter
        • getter 和 setter 方法调用时会分别调用 dep.depend 方法和 dep.notify
          • depend:把当前 Dep 的实例添加到当前正在计算的Watcher 的依赖中
          • notify:遍历了所有的订阅 Watcher,调用它们的 update 方法

computed

  • computed初始化被遍历computed,使用Object.defineProperty进行处理,依赖收集到Dep.target
  • 触发data值时会触发Watcher监听函数
  • computed值缓存 watcher.dirty决定了计算属性值是否需要重新计算,默认值为true,即第一次时会调用一次。每次执行之后watcher.dirty会设置为false,只有依赖的data值改变时才会触发

mixin

全局注册的选项,被引用到你的每个组件中
1、Vue.component 注册的 【全局组件】
2、Vue.filter 注册的 【全局过滤器】
3、Vue.directive 注册的 【全局指令】
4、Vue.mixin 注册的 【全局mixin】

合并权重
1、组件选项
2、组件 - mixin
3、组件 - mixin - mixin
4、.....
x、全局 选项
函数合并叠加(data,provide)
数组叠加(created,watch)
原型叠加(components,filters,directives)
两个对象合并的时候,不会相互覆盖,而是 权重小的 被放到 权重大 的 的原型上
覆盖叠加(props,methods,computed,inject)
两个对象合并,如果有重复key,权重大的覆盖权重小的
直接替换(el,template,propData 等)

filter

something | myFilter 被解析成_f('myFilter')( something )

nextTick

Vue.js 在默认情况下,每次触发某个数据的 setter 方法后,对应的 Watcher 对象其实会被 push 进一个队列 queue 中,在下一个 tick 的时候将这个队列 queue 全部拿出来 run( Watcher 对象的一个方法,用来触发 patch 操作) 一遍。

原理 - virtaul DOM

真实DOM操作昂贵,虚拟DOM就是js对象,操作代价小

模板编译&渲染

平时开发写vue文件都是用模板template的方法写html,模板会被编译成render函数,流程如下:

  • 初始化的时候
    • 模板会被编译成render函数
    • render函数返回虚拟DOM
    • 生成真正的DOM
  • 数据更新的时候
    • render函数返回新的virtual Dom
    • 新的virtual Dom和旧的virtual Dom做diff
    • 将差异运用到真实DOM
  • render API

    1. //template, jsx, render本质都是一样的, 都是一种dom和数据状态之间关系的表示
    2. render(h) {
    3. h(tag, data, children)
    4. }
    5. // tag可以是原生的html标签
    6. render(h) {
    7. return h('div', { attrs: {}}, [])
    8. }
    9. // 也可以是一个vue component
    10. import vueComponent from '...'
    11. render(h) {
    12. h(vueComponent, {
    13. props: {}
    14. })
    15. }
    • 偏逻辑用render,偏视图用template

compile

compile 编译可以分成 parse、 optimize 与 generate 三个阶段,最终需要得到 render function。

  • parse:会用正则等方式解析 template 模板中的指令、class、style 等数据,形成 AST。
    • transclude(el, option) 把 template 编译成一段 document fragment
    • compileNode(el, options) 深度遍历DOM,正则解析指令
    • vm.bindDir(descriptor, node, host, scope) 根据 descriptor 实例化不同的 Directive 对象
    • Directive 在初始化时通过 extend(this, def) 扩展 bind 和 update,创建了 Watcher关联update
    • 解析模板字符串生成 AST `const ast = parse(template.trim(), options)
      循环解析 template,利用正则表达式顺序解析模板,当解析到开始标签、闭合标签、文本的时候都会分别执行对应的回调函数,来达到构造 AST 树的目的。 AST 元素节点总共有 3 种类型,type 为 1 表示是普通元素,为 2 表示是表达式,为 3 表示是纯文本。
  • optimize:标记 static 静态节点,而减少了比较的过程 等优化
    • 优化语法树 optimize(ast, options)
      深度遍历这个 AST 树,去检测它的每一颗子树是不是静态节点,如果是静态节点则它们生成 DOM 永远不需要改变(标记静态节点 markStatic(root);标记静态根 markStaticRoots(root, false))
  • generate:是将 AST 转化成 render function 字符串
    • AST转可执行的代码 const code = generate(ast, options)
    • vue模板编译前后:

      1. `<ul :class="bindCls" class="list" v-if="isShow">
      2. <li v-for="(item,index) in data" @click="clickItem(index)">{{item}}:{{index}}</li>
      3. </ul>`
      4. with(this){
      5. return (isShow) ?
      6. _c('ul', {
      7. staticClass: "list",
      8. class: bindCls
      9. },
      10. _l((data), function(item, index) {
      11. return _c('li', {
      12. on: {
      13. "click": function($event) {
      14. clickItem(index)
      15. }
      16. }
      17. },
      18. [_v(_s(item) + ":" + _s(index))])
      19. })
      20. ) : _e()
      21. }

      对比react的jsx编译前后

      1. `<div id="1" class="li-1">
      2. Hello World
      3. <MyComp></MyComp>
      4. </div>`
      5. h('div',{ id: '1', 'class': 'li-1' },'Hello World',
      6. h(MyComp, null)
      7. )

      diff

  • vnode

    1. {
    2. el: div //对真实的节点的引用,本例中就是document.querySelector('#id.classA')
    3. tagName: 'DIV', //节点的标签
    4. sel: 'div#v.classA' //节点的选择器
    5. data: null, // 一个存储节点属性的对象,对应节点的el[prop]属性,例如onclick , style
    6. children: [], //存储子节点的数组,每个子节点也是vnode结构
    7. text: null, //如果是文本节点,对应文本节点的textContent,否则为null
    8. }
  • 核心 patch (oldVnode, vnode)
    • key和sel相同才去比较,否则新替旧
    • patchVnode (oldVnode, vnode)节点比较5种情况
      • if (oldVnode === vnode),他们的引用一致,可以认为没有变化
      • if(oldVnode.text !== null && vnode.text !== null && oldVnode.text !== vnode.text),文本节点的比较,需要修改,则会调用Node.textContent = vnode.text
      • if( oldCh && ch && oldCh !== ch ), 两个节点都有子节点,而且它们不一样,这样我们会调用updateChildren函数比较子节点

      • else if (ch),只有新的节点有子节点,调用createEle(vnode),vnode.el已经引用了老的dom节点,createEle函数会在老dom节点上添加子节点
      • else if (oldCh),新节点没有子节点,老节点有子节点,直接删除老节点
  • 同层比较作用:将一棵树转换成另一棵树的最小操作次数是O(n^3),同层是O(1)
  • key的作用:
    • 为了在数据变化时强制更新组件,以避免“原地复用”带来的副作用。
    • 在交叉对比没有结果(列表数据的重新排序,插,删)的时候会采用key来提高这个diff速度(不设key,newCh和oldCh只会进行头尾两端的相互比较,设key后,除了头尾两端的比较外,还会从用key生成的对象oldKeyToIdx中查找匹配的节点,从而移动dom而不是销毁再创建)

vue&react vdom区别

Vue 很“ 嚣张 ”,它宣称可以更快地计算出Virtual DOM的差异,这是由于它在渲染过程中,由于vue会跟踪每一个组件的依赖收集,通过setter / getter 以及一些函数的劫持,能够精确地知道变化,并在编译过程标记了static静态节点,在接下来新的Virtual DOM 并且和原来旧的 Virtual DOM进行比较时候,跳过static静态节点。所以不需要重新渲染整个组件树。

React默认是通过比较引用的方式进行,当某个组件的状态发生变化时,它会以该组件为根,重新渲染整个组件子树。如果想避免不必要的子组件重新渲染,你需要在所有可能的地方使用PureComponent,或者手动实现shouldComponentUpdate方法。但是Vue中,你可以认定它是默认的优化。

摘自 https://juejin.im/post/5b617801518825615d2fc92c

vdom实现

类vue vdom

snabbdom-github

snabbdom源码阅读分析

Vue 2.0 的 virtual-dom 实现简析

类react vdom

preact-github

preact工作原理

原理 - Router(路由)

vue插件,通过hash /history 2中方式实现可配路由
Hash

  1. push(): 设置新的路由添加历史记录并更新视图,常用情况是直接点击切换视图,调用流程:
  • $router.push() 显式调用方法
  • HashHistory.push() 根据hash模式调用,设置hash并添加到浏览器历史记录(window.location.hash= XXX)
  • History.transitionTo() 开始更新
  • History.updateRoute() 更新路由
  • app._route= route
  • vm.render() 更新视图
  1. replace: 替换当前路由并更新视图,常用情况是地址栏直接输入新地址,流程与push基本一致
    但流程2变为替换当前hash (window.location.replace= XXX)
    3.监听地址栏变化:在setupListeners中监听hash变化(window.onhashchange)并调用replace

History

  1. push:与hash模式类似,只是将window.hash改为history.pushState
  2. replace:与hash模式类似,只是将window.replace改为history.replaceState
  3. 监听地址变化:在HTML5History的构造函数中监听popState(window.onpopstate)

实战

  1. const Foo = {
  2. props: ['id'],
  3. template: `<div>foo with id: {{id}} </div>`
  4. }
  5. const Bar = {
  6. template: `<div>bar</div>`
  7. }
  8. const NotFound = {
  9. template: `<div>not found</div>`
  10. }
  11. const routeTable = {
  12. '/foo/:id': Foo,
  13. '/bar': Bar,
  14. }
  15. const compiledRoutes = [];
  16. Object.keys(routeTable).forEach(path => {
  17. const dynamicSegments = []
  18. const regex = pathToRegexp(path, dynamicSegments)
  19. const component = routeTable[path]
  20. compiledRoutes.push({
  21. component,
  22. regex,
  23. dynamicSegments
  24. })
  25. })
  26. window.addEventListener('hashchange', () => {
  27. app.url = window.location.hash.slice(1);
  28. })
  29. const app = new Vue({
  30. el: '#app',
  31. data() {
  32. return {
  33. url: window.location.hash.slice(1)
  34. }
  35. },
  36. render(h) {
  37. const url = '/' + this.url
  38. let componentToRender
  39. let props = {}
  40. compiledRoutes.some(route => {
  41. const match = route.regex.exec(url)
  42. if (match) {
  43. componentToRender = route.component
  44. route.dynamicSegments.forEach((segment,index) => {
  45. props[segment.name] = match[index+1]
  46. })
  47. }
  48. })
  49. return h('div', [
  50. h('a', { attrs: { href: '#foo/123' } }, 'foo123'),
  51. '|',
  52. h('a', { attrs: { href: '#foo/234' } }, 'foo234'),
  53. '|',
  54. h('a', { attrs: { href: '#bar' } }, 'bar'),
  55. h(componentToRender || NotFound, { props })
  56. ])
  57. }
  58. })

原理 - props(属性)

父组件怎么传值给子组件的 props

  • 父组件的模板 会被解析成一个 模板渲染函数,执行时会绑定 父组件为作用域

    1. (function() {
    2. with(this){
    3. return _c('div',{staticClass:"a"},[
    4. _c('testb',{attrs:{"child-name":parentName}})
    5. ],1)
    6. }
    7. })
  • 所以渲染函数内部所有的变量,都会从父组件对象 上去获取
    组件怎么读取 props
  • 子组件拿到父组件赋值过后的 attr,筛选出 props,然后保存到实例的_props 中,并逐一复制到实例上,响应式的。

父组件数据变化,子组件props如何更新
父组件数据变化,触发set,从而通知依赖收集器的watcher重新渲染

原理 - Vuex

vuex 仅仅是作为 vue 的一个插件而存在,不像 Redux,MobX 等库可以应用于所有框架,vuex 只能使用在 vue 上,很大的程度是因为其高度依赖于 vue 的 computed 依赖检测系统以及其插件系统,

vuex 整体思想诞生于 flux,可其的实现方式完完全全的使用了 vue 自身的响应式设计,依赖监听、依赖收集都属于 vue 对对象 Property set get 方法的代理劫持。vuex 中的 store 本质就是没有 template 的隐藏着的 vue 组件;

state

  • this.$store.state.xxx 取值
  • 提供一个响应式数据
  • vuex 就是一个仓库,仓库里放了很多对象。其中 state 就是数据源存放地,对应于一般 vue 对象里面的 data
  • state 里面存放的数据是响应式的,vue 组件从 store 读取数据,若是 store 中的数据发生改变,依赖这相数据的组件也会发生更新
  • 它通过 mapState 把全局的 state 和 getters 映射到当前组件的 computed 计算属性

getter

  • this.$store.getters.xxx 取值
  • 借助vue计算属性computed实现缓存
  • getter 可以对 state 进行计算操作,它就是 store 的计算属性
    虽然在组件内也可以做计算属性,但是 getters 可以在多给件之间复用
  • 如果一个状态只在一个组件内使用,是可以不用 getters

mutaion

  • this.$store.commit("xxx") 赋值
  • 更改state方法
  • action 类似于 muation, 不同在于:action 提交的是 mutation,而不是直接变更状态
  • action 可以包含任意异步操作

action

  • this.$store.dispatch("xxx") 赋值
  • 触发mutation方法

module

  • Vue.set动态添加state到响应式数据中

简单版Vuex实现

  1. //min-vuex
  2. import Vue from 'vue'
  3. const Store = function Store (options = {}) {
  4. const {state = {}, mutations={}} = options
  5. this._vm = new Vue({
  6. data: {
  7. $$state: state
  8. },
  9. })
  10. this._mutations = mutations
  11. }
  12. Store.prototype.commit = function(type, payload){
  13. if(this._mutations[type]) {
  14. this._mutations[type](this.state, payload)
  15. }
  16. }
  17. Object.defineProperties(Store.prototype, {
  18. state: {
  19. get: function(){
  20. return this._vm._data.$$state
  21. }
  22. }
  23. });
  24. export default {Store}

使用 Vuex 只需执行 Vue.use(Vuex),并在 Vue 的配置中传入一个 store 对象的示例,store 是如何实现注入的?

Vue.use(Vuex) 方法执行的是 install 方法,它实现了 Vue 实例对象的 init 方法封装和注入,使传入的 store 对象被设置到 Vue 上下文环境的store中。因此在VueComponent任意地方都能够通过this.store 访问到该 store。

state 内部支持模块配置和模块嵌套,如何实现的?

在 store 构造方法中有 makeLocalContext 方法,所有 module 都会有一个 local context,根据配置时的 path 进行匹配。所以执行如 dispatch('submitOrder', payload)这类 action 时,默认的拿到都是 module 的 local state,如果要访问最外层或者是其他 module 的 state,只能从 rootState 按照 path 路径逐步进行访问。

在执行 dispatch 触发 action(commit 同理)的时候,只需传入(type, payload),action 执行函数中第一个参数 store 从哪里获取的?

store 初始化时,所有配置的 action 和 mutation 以及 getters 均被封装过。在执行如 dispatch('submitOrder', payload)的时候,actions 中 type 为 submitOrder 的所有处理方法都是被封装后的,其第一个参数为当前的 store 对象,所以能够获取到 { dispatch, commit, state, rootState } 等数据。

Vuex 如何区分 state 是外部直接修改,还是通过 mutation 方法修改的?

Vuex 中修改 state 的唯一渠道就是执行 commit('xx', payload) 方法,其底层通过执行 this._withCommit(fn) 设置_committing 标志变量为 true,然后才能修改 state,修改完毕还需要还原_committing 变量。外部修改虽然能够直接修改 state,但是并没有修改_committing 标志位,所以只要 watch 一下 state,state change 时判断是否_committing 值为 true,即可判断修改的合法性。

调试时的"时空穿梭"功能是如何实现的?

devtoolPlugin 中提供了此功能。因为 dev 模式下所有的 state change 都会被记录下来,'时空穿梭' 功能其实就是将当前的 state 替换为记录中某个时刻的 state 状态,利用 store.replaceState(targetState) 方法将执行 this._vm.state = state 实现。

原理 - SSR

Vue.js 是构建客户端应用程序的框架。默认情况下,可以在浏览器中输出 Vue 组件,进行生成 DOM 和操作 DOM。

然而,也可以将同一个组件渲染为服务器端的 HTML 字符串,将它们直接发送到浏览器,最后将静态标记"混合"为客户端上完全交互的应用程序。

服务器渲染的 Vue.js 应用程序也可以被认为是"同构"或"通用",因为应用程序的大部分代码都可以在服务器和客户端上运行。

服务端渲染的核心就在于:通过vue-server-renderer插件的renderToString()方法,将Vue实例转换为字符串插入到html文件

其他

vue 企业级应用模板-github

VueConf 2018 杭州(第二届Vue开发者大会)

Vue 3.0 进展 - 尤雨溪

参考资料

深入响应式原理 —— Vue.js官网

Advanced Vue.js Features from the Ground Up - 尤雨溪

Vue 源码解析:深入响应式原理 - 黄轶

Vue 源码研究会 - 神仙朱

vue组件之间8种组件通信方式总结 - zhoulu_hp

Vue问得最多的面试题 - yangcheng

剖析 Vue.js 内部运行机制 - 染陌

转载于:https://www.cnblogs.com/seasonley/p/10915608.html

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop】
推荐阅读
相关标签