FrankKai.github.io icon indicating copy to clipboard operation
FrankKai.github.io copied to clipboard

Vue踩坑记录

Open FrankKai opened this issue 6 years ago • 22 comments

主要记录在使用Angular,React,Vue三大框架过程中的微小发现,其中可能会包括一些框架之间的对比。

1.vue和vuex中的ES6 Shorthand method names 2.ng-bind与v-bind的区别是什么? 3.:model与v-model的区别是什么? 4.vue 知识点查漏补缺 5.表单验证 async-validator 6.Vue.extend() 7.Vue实例(单文件组件)详解 8.Vue响应式更新数组--删除组件列表中特定项不符合预期 9.vue 插件 10. vue单元测试 11. 为

FrankKai avatar Apr 19 '18 05:04 FrankKai

1.vue和vuex中的ES6 Shorthand method names

666.png

最近在用vue和vuex开发。 在.vue单文件的生命周期和vuex的actions定义中,有两段代码让人费解: pag.vue

export default {
    //...
    created(){
        this.$store.dispatch('getUsersSize')
    }
    //...
}

action.js中

const actions = {
    getAllUsers({commit},url){
        dataapi.getData(url,(users)=>{
            commit(types.RECEIVE_USERS,{users})
        })
    }
}

抽离出来就是{created(){}}{getAllUsers({commit},url){}} 正常情况下,如果将函数赋值到对象的属性值,简称为方法,应该这样写才对: {created:function(){}}以及{getAllUsers:function({commit},url){}}

所以我很纳闷这是什么鬼东西?

印象中ES6有个概念叫computed property,于是去查MDN。 最后查到其实并不是计算属性,而是shorthand methods names

// Shorthand method names (ES2015)
var o = {
  property([parameters]) {}
};

而计算属性其实是这样的:

// Computed property names (ES2015)
var prop = 'foo';
var o = {
  [prop]: 'hey',
  ['b' + 'ar']: 'there'
};

仔细对比{created(){}}{created:function(){}}。 所以这个ES6 Shorthand method names语法糖其实就是,省略了':function',省略了冒号和'function'。 虽然这个sugar不是很甜,但好歹是个糖,糖多了自己写的bug别人就看不懂了。 而人们往往对于不懂的东西,都会说:666 (逃

FrankKai avatar Apr 19 '18 05:04 FrankKai

2.ng-bind与v-bind的区别是什么?

首先看个问题:angular中,什么时候用ng-bind,什么时候用{{}}模板写法?

{{foo.bar}}更加直观,但是对于index.html,也就是首页展示,应该使用ng-bind="foo.bar"。 因为如果浏览器可能angular对花括号中的语法完成编译之前,就将{{foo.bar}}中的内容展示给用户,从而导致用户看到我们的代码后,再看到自己的数据。 这与浏览器运行机制有关,浏览器先下载好html的dom层,之后才会使用js对dom层中的内容进行处理,本例中是对标签节点的文本节点{{foo.bar}}进行编译,本质上是替换文本节点。 而ng-bind则是以修改标签的属性内容的形式,为标签增加文本节点,而不是替换文本节点。

与vue的v-bind指令不同,v-bind指令是对标签节点的属性节点绑定model中的数据,例如v-bind:src="imgSrc",而angular中则支持属性中编译花括号,直接使用src="{{imgSrc}}",即可实现与controller中的数据绑定。

有几点很重要: 1.ng-bind是对文本节点做更新 2.angular可以直接编译属性节点中的花括号值 3.首页数据,应使用ng-bind;其他页为提高代码可读性,应使用{{}}式写法

FrankKai avatar Apr 19 '18 05:04 FrankKai

3.:model与v-model的区别是什么?

:model 的完整形式是v-bind:model,主要是v-bind,也是双向数据绑定吗,但是针对DOM元素的属性或者特性做绑定,例如src,title原生属性,或者自定义属性等等,我们此处的:model属于iview的Form组件自定义的一个属性。

v-model 则是为了在或者

吐槽:iview用起来有点反人类,Radio用label做区分,Form的表单数据又用model这样的属性做数据绑定(好吧,elememt-ui也是这样做数据绑定)。

关于src,title类似的HTML原生属性,有一点需要特别注意,那就是v-bind的使用。

Mustache 语法不能作用在 HTML 特性上,遇到这种情况应该使用 v-bind 指令: <div v-bind:id="dynamicId"></div> ①

<div id=“dynamicId”> ②

①中的dynamicId是一个变量 ②中的dynamicId仅仅是一个字符串

大多数情况下,我们会使用:id这样的为了让Mustache语法作用于HTML特性上的写法。

FrankKai avatar Apr 19 '18 05:04 FrankKai

4.vue 知识点查漏补缺

  • vue中props和computed的区别是什么? 子组件显式地用props选项声明(从父组件获取的)预期的数据。 Computed 计算属性被混入到vue实例中。所有的getter和setter的this上下文自动地绑定为vue实例。

  • slot是什么? 往具名插槽中插入子组件内容。

  • 具名插槽是什么?

<slot name=“header”></slot>
<h1 slot=“header”> 这里的内容会被分发到header插槽中</h1>
  • getter 和 setter
vm.aPlus   // => 2
vm.aPlus = 3
  • 为什么this.$refs["sendData"].resetFields();可以重置表单?

vm.$refs是一个对象,持有已注册过ref的所有子组件,是一种直接访问到子组件的方式,需要注意是非响应式的。

<Form ref=“sendData”></Form>

this.$refs[“sendData”]或者this.$refs.sendData获取到已注册的子组件,然后调用子组件的resetField()方法.

iview的Form组件源码: form.vue:

resetFields() {
    this.fields.forEach(field => {
        field.resetField();
    });
}

fields数组中的每一项都调用resetField() Form-item.vue:

resetField () {
    this.validateState = '';
    this.validateMessage = '';

    let model = this.form.model;
    let value = this.fieldValue;
    let path = this.prop;
    if (path.indexOf(':') !== -1) {
        path = path.replace(/:/, '.');
    }

    let prop = getPropByPath(model, path);

    if (Array.isArray(value)) {
        this.validateDisabled = true;
        prop.o[prop.k] = [].concat(this.initialValue);
    } else {
        this.validateDisabled = true;
        prop.o[prop.k] = this.initialValue;
    }
},
  • $parent是什么? 父实例。

  • vm.$parent.$options是什么? 自定义属性。

new Vue({
    customOption:’foo’,
    created:function(){
      console.log(this.options.customOption)//=>’foo'  
    }
})
  • props props用于接收来自父组件的数据,props为对象时,可以配置高级选项,如类型检测,自定义校验,设置默认值。

  • mixins是什么? 混入hook。

  • 混入是什么? 目的:分发vue组件的复用功能,可以混入一个对象到组件。 浅合并,组件优先,慎用全局混入。

import Emitter from '../../mixins/emitter';
mixins: [ Emitter ]

iview的emitter混入的是什么? 混入的是methods,一个vuex的diapatch,一个组件自身的broadcast方法。

  • render函数是什么? 比template更接近编译器的render函数。

  • 实例属性API?vue实例都会做代理访问。 vm.$data 观察的数据对象。 vm.$props 当前组件接收到的props。 vm.$el 根DOM元素 vm.$options 自定义属性 vm.$parent 父实例 vm.$root 组件树根vue实例,如果没有父实例,此实例将会是自己。 vm.$children 当前实例的子组件。 vm.$slots

    this.$slots.header vm.$scopedSlots 访问作用域插槽,slot-scope。作用域插槽是一种特殊类型的插槽,用作一个(能被传递数据的)可重用模板,来代替已经渲染好的元素。 vm.$refs 一个对象,持有已注册过ref的所有子组件。 vm.$isServer 判断当前vue实例是否运行于服务器。 vm.$attrs 属性集,正常的html属性集。 vm.$listeners 包含了父作用域中的v-on事件监听器。
  • 插槽是什么? 插槽就是像扩展内存一样,热插拔式扩展。

  • 组件的属性,html的特性,dom的属性有何异同? 组件是vm.$props,html的特性是vm.$attrs,dom属性是domProps。

  • directives是什么? 包含vue实例可用指令的哈希表。

  • filters是什么? <!-- 在双花括号中 --> {{ message | capitalize }}

<!-- 在 v-bind 中 --> <div v-bind:id="rawId | formatId"></div>

FrankKai avatar May 04 '18 02:05 FrankKai

5.表单验证 async-validator

知识点:

rules可以定义冗余,最后会根据实际情况选取验证规则。

原因:

v-if / v-else会移除dom,从而跳过验证。

场景: 通过v-if v-else控制模板渲染时,由于需要根据type进行变换,所以错误认为验证规则需要实时根据当前模板内容进行变换,但实际上验证规则仅需一套。

下面的代码是完全没有必要的!!!

const _commonRules = {
    foo: [{ required: true, message: '请选择foo', trigger: 'blur' }],
};
switch (label) {
    case '0':
        this.moment.rules = Object.assign(_commonRules, {
            'bar.list': [{ type: 'array', required: true, len: { min: 1 }, message: '请至少选择一个' }],
        });
        break;
    case '1':
        this.moment.rules = Object.assign(_commonRules, {
            'baz': [{ required: true, message: '请上传一个', trigger: 'blur' }],
        });
        break;
    case '2':
        this.moment.rules = Object.assign(_commonRules, {
            'bar.list': [{ type: 'array', required: true, len: { min: 1 }, message: '请至少选择一个' }],
            'baz': [{ required: true, message: '请上传一个', trigger: 'blur' }],
        });
        break;
    default:
        this.moment.rules = Object.assign(_commonRules, {
            'bar.list': [{ type: 'array', required: true, len: { min: 1 }, message: '请至少选择一个' }],
        });
}

这样才是正确的打开方式!!!

data() {
    return {
        rules: {
            foo: [{ required: true, message: '请选择foo', trigger: 'blur' }],
            'bar.list': [{ type: 'array', required: true, len: { min: 1 }, message: '请至少选择一个' }],
            'baz': [{ required: true, message: '请上传一个', trigger: 'blur' }],
        },
    },
}

当为Input组件添加number属性后,验证不通过?

原因是此时在对Input的rule中,type类型未指定,其默认值为‘string’,因此需要指明type类型,其它组件也一样。

input:[{type:'number',[...]}]

FrankKai avatar Jul 04 '18 07:07 FrankKai

6. Vue.extend()

关键词:子类 组件选项

<div id="mount-point"></div>
var Profile = Vue.extend({
    template: '<p>{{firstName}}{{lastName}}aka{{alias}}</p>',
    data: function() {
        return {
            firstName: 'foo',
            lastName: 'bar',
            alias: 'baz',
        }
    }
})
new Profile(),$mount('#mount-point')

结果:

<p>Walter White aka Heisenberg</p>

FrankKai avatar Jul 10 '18 06:07 FrankKai

7.Vue实例(单文件组件)详解

  • 实例属性
  • 实例方法
    • 数据
    • 事件
    • 生命周期

实例属性

vm.$data getter setter

  • Vue实例代理data对象上的所有属性,因此vm.a等价于vm.$data.a。
  • 以_,$开头的属性不会被Vue实例代理,需要使用vm.$data._property访问这些属性。
  • data属性不要使用箭头函数,但是可以使用es6的函数简写。
  • data属性为什么要强制要求写成函数类型?因为若是对象,则共同引用同一个对象。而函数类型则是以构造函数的形式,可以创建出一个个独立的数据副本。(引申知识点:引用传递,构造函数)

错误的箭头函数写法

data: () => { return { foo: 1 } }

正确的函数缩写写法

data() {
    return {
        foo: 1
    }
}

vm.$props getter setter 组件接收到的props对象。Vue实例代理对属性的访问。

vm.$el getter Vue实例的根DOM元素。

vm.$options getter 实例初始化选项。用于自定义属性:

new Vue({
    customOption: 'foo',
    created: function () {
        console.log(this.$options.customOption)
    }
})

vm.$parent getter 父实例。

vm.$root getter 组件树的根Vue实例。

vm.$root与vm.$el区别是什么? 以公司项目的某个页面组件为例,挂载虚拟DOM到真实DOM时打印vm.$el,`vm.$root。

  mounted() {
    console.log('vm.$el:', this.$el);
    console.log('vm.$root:', this.$root);
  },

打印结果如下: image image

通过以上2张图,可以这么理解:

vm.$el: 当前组件的真实根DOM。
vm.$root: 当前组件所在组件树的根Vue实例,算是虚拟DOM。

vm.$children 子组件实例数组,无序,非响应式。

vm.$slots slot="foo" 中的内容将会在 vm.$slots.foo 中被找到,是具名插槽分发的内容。

<blog-post>
  <h1 slot="header">
    About Me
  </h1>
</blog-slot>
Vue.component('blog-post', {
  render: function (createElement) {
    var header = this.$slots.header
    return createElement('div', [
      createElement('header', header),
    ])
  }
})

vm.$scopedSlots 访问作用域插槽。

vm.$slotsvm.$scopedSlots区别是什么? vm.$slots:slot静态分发内容。 vm.$scopedSlots:slot分发内容数据;可用于向子组件分发数据。

this.$slots 获取 VNodes 列表中的静态内容:

render: function (createElement) {
  // `<div><slot></slot></div>`
  return createElement('div', this.$slots.default)
}

this.$scopedSlots 获得能用作函数的作用域插槽,这个函数返回 VNodes:

props: ['message'],
render: function (createElement) {
  // `<div><slot :text="message"></slot></div>`
  return createElement('div', [
    this.$scopedSlots.default({
      text: this.message
    })
  ])
}

向子组件中传递作用域插槽,利用 VNode 数据中的 scopedSlots:

render: function (createElement) {
  return createElement('div', [
    createElement('child', {
      // pass `scopedSlots` in the data object
      // in the form of { name: props => VNode | Array<VNode> }
      scopedSlots: {
        default: function (props) {
          return createElement('span', props.text)
        }
      }
    })
  ])
}

vm.$refs getter 一个对象,持有注册过ref特性的所有DOM元素和组件实例。 特别注意:仅仅是当前组件实例中注册过ref属性的DOM元素和组件实例。

<div ref="foo"></div>
<Form ref="modalReplyForm">
        <FormItem label="回复标题" prop="title"></FormItem>
</Form>

image

vm.$isServer getter 简单当前实例是否运行于服务器。

vm.$attrs getter 包含父作用域中不作为prop被识别的特性属性。

vm.listeners getter 包含了父作用域中的v-on事件监听器。

FrankKai avatar Jul 10 '18 06:07 FrankKai

8.Vue响应式更新数组--删除组件列表中特定项不符合预期

情况:[组件实例0,组件实例1,组件实例2]希望删除组件实例1 代码: this.componentArr.splice(1,1); image 结果:看起来像是删除了实例2 image

原因:Vue不能检测数组的setter,类似这样vm.items[1]=1的都不能被检测,更别提数组元素的删除,Vue并不能检测到。(这是个错误观点,实质上会被删除,只不过子组件中数据没有更新,但vm.items[1]=1检测不到是真)。 方法:

  • this.componentArr.splice(1,1,null)
  • Vue.set(this.component, 1, null) 结果: image

上面的方法正确吗? 某些情况下是适用的,但是它实际上造成了内存浪费,因为这样会导致数据没有被完全删除,会变成[组件实例0,null,组件实例2]

什么情况下不适用呢? 当与另外一个数组有映射关系时,也就是说数据与我们当前数组一一对应时,上面的方式就不适用了。例如:[1,2,3]与[实例1,实例2,实例3],删除后理应是[1,3]与[实例1,实例3],但是使用vm.items.splice(1,1,null)后,会导致结果为[1,2]与[实例1,null,实例3]。 这种情况下必须使用vm.items.splice(1,1)

所以最好的方式是**this.componentArr.splice(1,1);+ prop **。

this.componentArr.splice(1,1);:可以将第1项的数据删除,但是不会更新子组件中数据。 prop:会传递更新后的数据到子组件,子组件在watch函数中重新对赋与v-model绑定的值。

  props: {
    data: {
      type: Object,
    },
  },
  watch: {
    data(val, oldVal) {
      const _val = JSON.stringify(val);
      if (_val !== '"{"text":"","sort":"","type":1}"' && this.disabled) {
        this.params = Object.assign({}, val);
      }
      if (_val === undefined) {
        this.params = {
          text: '',
          sort: '',
        };
        this.disabled = false;
      }
    },
  },
<component :is="component" :data="data[i]"></component>

FrankKai avatar Jul 11 '18 14:07 FrankKai

9.vue 插件

写一个插件

  • 插件通常添加 全局函数 给Vue
  • 插件类型包括以下5种
    • 自定义一些全局方法或者属性。vue-custom-element
    • 自定义一个或者多个静态方法:指令/过滤器/过渡等等。
    • 通过全局的mixin添加一个组件的选项。vue-router
    • 通过Vue.prototype添加一个Vue实例方法
    • 为自己提供API的库,于此同时注入一些上面的组合。

一个Vue.js插件应该暴露一个install方法。这个方法将在Vue构造器的第一个实参中被调用,就像下面这样:

MyPlugin.install = function (Vue, options) {
    //1. 添加全局的方法或者属性
    Vue.myGlobalMethod = function () {
        //一些逻辑
    }
    //2. 添加一个全局的asset
    Vue.directive('my-directive', {
        bind(el, binding, vnode, oldVnode) {
            //一些逻辑
        }
    })
    //3. 注入一些组件选项
    Vue.mixin({
        created: function () {
            // something logic...
        }
    })
    //4. 添加一个实例方法
    Vue.prototype.$myMethod = function (methodOptions) {
        //一些逻辑
    }
}

使用一个插件

通过调用Vue.use()使用插件

// 调用MyPlugin.install(Vue)
Vue.use(MyPlugin)

你可以选择性的传递一些选项进去:

Vue.use(MyPlugin, {someOption: true} )

Vue.use自动阻止你多次使用同一个插件,所以在同一个插件上调用多次Vue.use,只会触发一次插件的安装。 在Vue是全局变量的时候,官方插件可以自动调用Vue.use()。 如果在模块化环境中,需要显式地使用Vue.use()。

// 通过Browserify或者Webpack使用CommanJS
var Vue = require('vue');
var VueRouter = require('vue-router');

//不要忘记调用
Vue.use(VueRouter)

一个非常简单的例子

开发插件:foo.js

import bar from 'bar'; //第三方模块

export default {
  install(Vue) {
    Vue.prototype.$foo = function $foo(value) {
        return bar(value);
    };
  },
};

注册插件:main.js

import Vue from 'vue';
import foo from 'foo';
Vue.use(foo);

使用插件:baz.vue

this.$foo('Hello Vue');

FrankKai avatar Jul 13 '18 08:07 FrankKai

10. vue单元测试

  • 最简单的单元测试的例子
  • test runner(Jest,mocha-webpack,VUE TEST UTILS)
  • 测试单文件组件

最简单的单元测试例子

//导入vue测试工具和需要测试的组件
import { mount } from '@vue/test-utils';
import Counter from './counter';

//挂载组件
const wrapper = mount(Counter);

//获取组价的Vue实例
const vm = wrapper.vm;

//打印wrapper获取更加详尽的wrapper信息
console.log(wrapper);

//测试组件输出的已渲染的HTML
describe('Counter',()=>{
  const wrapper = mount(Counter);
  it('renders the correct markup',()=>{
    expect(wrapper.html()).toContain('<span class="count">0</span>')
  })
  //测试按钮是否存在
  it('has a button',()=>{
    expect(wrapper.contains('button')).toBe(true)
  })
  //模拟用户点击按钮
  it('button click should increment the count',()=>{
    expect(wrapper.vm.count).toBe(0);
    const button = wrapper.find('button');
    button.trigger('click');
    expect(wrapper.vm.count).toBe(1);
  })
  //将done注册到Vue的全局错误处理函数
  it('will catch the error using done',(done)=>{
    Vue.config.errorHandler = done;
    Vue.nextTick(()=>{
      expect(true).toBe(false);
      done();
    })
  })
  //返回Promise
  it('will catch the error using a promise',()=>{
    return Vue.nextTick()
      .then(function(){
        expect(true).toBe(false);
      })
  })

Test Runners

  • VUE TEST UTILS依赖于浏览器环境,可以使用虚拟的JSDOM
  • Jest会自动安装JSDOM,其他的需要手动安装JSDOM,可以再引入jsdom-global

测试单文件组件

使用Jest为单文件组件写测试用例

  • 安装Jest和Vue Test Utils
npm install --save-dev jest @vue/test-utils
  • 定义unit script到package.json
{
    "scripts": {
         "test": "jest"
    }
}
  • 在Jest中执行 SFC 安装并配置vue-test,使得Jest如何处理*.vue文件。
npm install --save-dev vue-jest

package.json中创建jest块:

{
    // ...
    "jest": {
        "moduleFileExtensions": [
             "js",
             "json",
             //告诉Jest处理*.vue文件
             "vue"
        ],
        "transform": {
            //用vue-jest处理*.vue文件
            ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest"
        }
    }
}

注意:vue-jest不支持vue-loader和webpack中的code-splitting等等,若想支持,需要使用Mocha去替代。

处理webpack别名

如果用了@替代/src,那么需要使用moduleNameMapper去配置Jest。

{
    "jest": {
        //...
        //映射源代码的时候
       "moduleNameMapper": {
        "^@/(.*)$": "<rootDir>/src/$1"
       }
    }
}

配置Jest的Babel

尽管Node最新的版本已经支持ES2015特性,但是你如果想使用ES 模块语法以及stage-x草案阶段的特性到test时。我们需要安装babel-jest:

npm install --save-dev babel-jest

下一步,我们需要告诉Jest使用babel-jest处理js文件,这需要在package.json中添加jest.transform:

{
    // ...
    "jest": {
        // ...
        // process js with `babel-jest`
       "^.+\\.js$": "<rootDir>/node_modules/babel-jest"
    },
    // ...
}

Node版本选择可以指定。 .babelrc

{
    "presets": [
        ["env", {"modules": false} ]
    ],
    "env": {
         "test": {
              "presets": [
                  ["env", { "targets": {"node": "current"} }]
              ]
          }
    }
}

Snapshot Testing

你可以使用vue-server-renderer去将组件渲染成一个字符串,这样它就可以被保存成一个可以用于Jest snapshot 测试。

vue-server-render渲染结果包括一些SSR特性,而且它可以忽略空格,会导致很难扫描出diff。我们可以使用一个普通的序列化器提升保存的snapshot:

npm install --save-dev jest-serializer-vue

然后在package.json中配置:

{
    //...
    "jest": {
      //...
      // snapshots的序列化器
      "snapshotSerializers": [
          "<rootDir>/node_modules/jest-serializer-vue"
      ]
    }
}

放置测试文件

默认情况下,Jest将在整个项目中,递归地选择所有的.spec.js或者.test.js文件。如果这样不能满足你的需求,可以在package.json中配置testRegex,默认正则是:(/tests/.*|(\.|/)(test|spec))\.jsx?$。

├── __tests__
│   └── component.spec.js # test
│   └── anything # test
├── package.json # not test
├── foo.test.js # test
├── bar.spec.jsx # test
└── component.js # not test

Jest推荐为所有待测试的代码创建一个__tests__目录,但是可以按照你的需求组织测试文件。只要知道Jest将在测试文件附近生成一个__snapshots__目录就行。

覆盖率

Jest可以生成多种格式的覆盖率报告。下面的是最简单的一种: 使用collectCoverage选项扩展你的jest配置(package.json或者jest.config.js),然后在collectCoverageFrom数组中定义覆盖信息需要收集到的文件。

{
    "jest": {
        // ...
        "collectCoverage": true,
        "collectCoverageFrom": [
             "**/*.{js,vue}",
             "!**/node_modules/**"
         ]
    }
}

可以对coverageReporters选项进行修改,从而修改默认的覆盖率报告。

{
    "jest": {
        // ...
        "coverageReporters": ["html", "text-summary"]
    }
}

注意:coverageReporters数组默认值是["json", "lcov", "text"]。这是一组Jest在写测试用例报告时生成的报告名。添加text或者text-summary到数组可以在控制台看到覆盖率总结信息。

{
    "jest": {
        //...
        "coverageReporters": ["html", "text-summary"]
    }
}

例子

如果你使用过Jasmine,那么使用Jest的断言api时,就像是回到家一样:

import { mount } from '@vue/test-utils'
import Component from './component'

describe('Component', () => {
    test('is a Vue instance', () => {
        const wrapper = mount(Component)
        expect(wrapper.isVueInstance()).toBeTruthy()
    })
})

FrankKai avatar Jul 18 '18 02:07 FrankKai

11. 为<style>添加scoped属性有什么效果?

关键词:无值attr CSS属性选择器 CSS局部作用域

为当前组件的DOM元素添加无值attr,CSS再通过属性选择器匹配元素,保证了CSS样式的局部作用域。

若是不加scoped,就会仅仅匹配class名为example的DOM元素,这会错误的匹配到其他组件(父组件,兄弟组件,子组件,自定义组件,第三方组件),从而导致影响到其他组件的样式和布局。

  • <style scoped>包裹的CSS仅仅适用于当前组件的标签
  • 这种方式类似于Shadow DOM中的样式encapsulation(封装)
  • 需要一些说明(caveats),但不需要polyfill
  • 可以使用PostCSS转换
<style scoped>
.example {
    color: red;
}
</style>
<template>
    <div class="example">hi</div>
</template>

经过PostCSS转换后的形式如下:

<style>
.example[data-v-f3f3eg9] {
    color: red;
}
</style>
<template>
    <div class="example" data-v-f3f3eg9>hi</div>
</template>
  • .example[data-v-f3f3eg9] {color: red};是什么选择器? 吃了没文化的亏,基础不够扎实。

CSS Attribute Selector 通过DOM对象的attributes集合中的属性名或者属性值匹配元素。

.example[data-v-f3f3eg9] {
    color: red
};

本质上匹配到了属性名为data-v-f3f3eg9,并且class名为example的DOM元素。

若是不加scoped,就会仅仅匹配class名为example的DOM元素,这会错误的匹配到其他组件(父组件,兄弟组件,子组件,自定义组件,第三方组件),从而导致影响到其他组件的样式和布局。

  • 为div直接添加属性<div data-v-f3f3eg9></div>是什么操作? 一图剩千言! image
<div data-v-42d88036 class="breadcrumb-container"></div>
  • 上面的DOM元素可以通过properties下的div.breadcrumb-container查看到
  • data-v-42d88036和class这两个属性包含在DOM的attributes对象中
  • attributes对象中与属性名有关的key包括:localName,name,nodeName
  • attributes对象中与属性值有关的key包括:nodeValue,textContent,value
  • attributes对象是一NameNodeMap类型

思考:

这个无值的attr data-v-42d88036有什么用?

关键词:DOM分组 CSS局部作用域 可以用来保证组件CSS的局部作用域。 当加上scoped时,它做了一个类似DOM分组的操作,为当前组件的几乎所有DOM对象都加上了无值attr data-v-42d88036。同理,当去掉scoped时,取消添加无值attr。

注意: 为什么不是为所有对象增加无值attr?它仅仅会为最高层级的DOM对象添加无值attr,举个形象的例子就是:对于外部引入的组件,例如iview,element的组件,仅仅会为最高层级添加attr,对于其组件内部的DOM对象,不会去添加。 组件引用:

<Button type="primary">检查更新</Button>

预编译结果:

<button data-v-194ef13e type="button" class="ivu-btn ivu-btn-primary">
    <!----> <!----> <span>检查更新</span>
</button>

至于根据scoped属性生成无值attr,然后添加到一组组件DOM节点上的过程,等后面再做深入理解。

FrankKai avatar Jul 18 '18 02:07 FrankKai

12. 什么是style blocks,custom blocks?

  • styles blocks就是Scoped CSS,第11个comment有讲。
  • custom blocks是什么呢?
    • 加载器可以可以根据block的lang属性,block的tag名,以及webpack中的配置规则。
    • lang属性被指定时,block将被作为一个文件去与lang属性匹配。
    • 匹配到以后,*.vue文件将作为custom块暴露出来的函数的参数。
    • 可以使用resourceQuery去匹配一个无lang属性的块。匹配不到会忽略。例如,想匹配块:
{
    module: {
        rules: [
            resourceQuery: /blockType=foo/,
            loader: 'loader-to-use'
        ]
     }
}

注入自定义标签,类似

module.exports = function (source, map) {
  this.callback(
    null,
    `export default function (Component) {
      Component.options.__docs = ${
        JSON.stringify(source)
      }
    }`,
    map
  )
}

webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        resourceQuery: /blockType=docs/,
        loader: require.resolve('./docs-loader.js')
      }
    ]
  }
}

ComponentB.vue

<!-- ComponentB.vue -->
<template>
  <div>Hello</div>
</template>

<docs>
This is the documentation for component B.
</docs>

ComponentA.vue

<!-- ComponentA.vue -->
<template>
  <div>
    <ComponentB/>
    <p>{{ docs }}</p>
  </div>
</template>

<script>
import ComponentB from './ComponentB.vue';

export default {
  components: { ComponentB },
  data () {
    return {
      docs: ComponentB.__docs
    }
  }
}
</script>

FrankKai avatar Jul 18 '18 04:07 FrankKai

13. Duplicate keys detected: '0'. This may cause an update error.

<div v-for="(item, i) in foos" :key="`foo-${i}`"></div>

<div v-for="(item, i) in bars" :key="`bar-${i}`"></div>

<div v-for="(item, i) in bazs" :key="`baz-${i}`"></div>

FrankKai avatar Jul 18 '18 07:07 FrankKai

14. vue调用生命周期钩子源码

callHook(vm, 'beforeCreate') // 拿不到 props data
callHook(vm, 'created');
callHook(vm, 'beforeMount');
callHook(vm, 'mounted');
callHook(vm, 'updated')
callHook(vm, 'beforeDestroy');
callHook(vm, 'destroyed');
function callHook (vm, hook) {
  // #7573 disable dep collection when invoking lifecycle hooks
  pushTarget();
  var handlers = vm.$options[hook];
  if (handlers) {
    for (var i = 0, j = handlers.length; i < j; i++) {
      try {
        handlers[i].call(vm);
      } catch (e) {
        handleError(e, vm, (hook + " hook"));
      }
    }
  }
  if (vm._hasHookEvent) {
    vm.$emit('hook:' + hook);
  }
  popTarget();
}

FrankKai avatar Aug 06 '18 03:08 FrankKai

15. vue的$emit源码

Vue.prototype.$emit = function (event) {
    var vm = this;
    {
      var lowerCaseEvent = event.toLowerCase();
      if (lowerCaseEvent !== event && vm._events[lowerCaseEvent]) {
        tip(
          "Event \"" + lowerCaseEvent + "\" is emitted in component " +
          (formatComponentName(vm)) + " but the handler is registered for \"" + event + "\". " +
          "Note that HTML attributes are case-insensitive and you cannot use " +
          "v-on to listen to camelCase events when using in-DOM templates. " +
          "You should probably use \"" + (hyphenate(event)) + "\" instead of \"" + event + "\"."
        );
      }
    }
    var cbs = vm._events[event];
    if (cbs) {
      cbs = cbs.length > 1 ? toArray(cbs) : cbs;
      var args = toArray(arguments, 1);
      for (var i = 0, l = cbs.length; i < l; i++) {
        try {
          cbs[i].apply(vm, args);
        } catch (e) {
          handleError(e, vm, ("event handler for \"" + event + "\""));
        }
      }
    }
    return vm
  };

FrankKai avatar Aug 06 '18 03:08 FrankKai

16. vue的mixin什么时候适合用?

Vue.mixin( mixin )

参数:{Object} mixin 用法: 全局注册一个混入,影响注册之后所有创建的每个 Vue 实例。插件作者可以使用混入,向组件注入自定义的行为。不推荐在应用代码中使用。

全局混入

也可以全局注册混入对象。注意使用! 一旦使用全局混入对象,将会影响到 所有 之后创建的 Vue 实例。使用恰当时,可以为自定义对象注入处理逻辑。

// 为自定义的选项 'myOption' 注入一个处理器。
Vue.mixin({
  created: function () {
    var myOption = this.$options.myOption
    if (myOption) {
      console.log(myOption)
    }
  }
})

new Vue({
  myOption: 'hello!'
})
// => "hello!"

谨慎使用全局混入对象,因为会影响到每个单独创建的 Vue 实例 (包括第三方模板)。大多数情况下,只应当应用于自定义选项,就像上面示例一样。也可以将其用作 Plugins 以避免产生重复应用。 foo.vue

<template>
    <Bar nickname='趁你还年轻'></Bar>
</template>
<script>
import Bar from './bar';
export default {
  components: [Bar],
}
</script>

bar.vue

<template>
    <div>
        <p>{{nickname}}</p>
    </div>
</template>
<script>
import mixin from '../mixin';
export default {
  mixins: [mixin],
}
<script>

mixin.js

export default {
  props: {
    nickname: {
      type: String,
      default: '昵称',
    },
  },
};

FrankKai avatar Aug 09 '18 09:08 FrankKai

17. vue中的mixin与plugin区别是什么?

https://medium.com/@denny.headrick/mixins-and-plugins-in-vuejs-ecee9b37d1bd

FrankKai avatar Aug 10 '18 06:08 FrankKai

18.component与vnode的区别是什么?

vnode.js

export default class VNode {
  tag: string | void;
  data: VNodeData | void;
  children: ?Array<VNode>;
  text: string | void;
  elm: Node | void;
  ns: string | void;
  context: Component | void; // rendered in this component's scope
  key: string | number | void;
  componentOptions: VNodeComponentOptions | void;
  componentInstance: Component | void; // component instance
  parent: VNode | void; // component placeholder node

  // strictly internal
  raw: boolean; // contains raw HTML? (server only)
  isStatic: boolean; // hoisted static node
  isRootInsert: boolean; // necessary for enter transition check
  isComment: boolean; // empty comment placeholder?
  isCloned: boolean; // is a cloned node?
  isOnce: boolean; // is a v-once node?
  asyncFactory: Function | void; // async component factory function
  asyncMeta: Object | void;
  isAsyncPlaceholder: boolean;
  ssrContext: Object | void;
  fnContext: Component | void; // real context vm for functional nodes
  fnOptions: ?ComponentOptions; // for SSR caching
  fnScopeId: ?string; // functional scope id support

  constructor (
    tag?: string,
    data?: VNodeData,
    children?: ?Array<VNode>,
    text?: string,
    elm?: Node,
    context?: Component,
    componentOptions?: VNodeComponentOptions,
    asyncFactory?: Function
  ) {
    this.tag = tag
    this.data = data
    this.children = children
    this.text = text
    this.elm = elm
    this.ns = undefined
    this.context = context
    this.fnContext = undefined
    this.fnOptions = undefined
    this.fnScopeId = undefined
    this.key = data && data.key
    this.componentOptions = componentOptions
    this.componentInstance = undefined
    this.parent = undefined
    this.raw = false
    this.isStatic = false
    this.isRootInsert = true
    this.isComment = false
    this.isCloned = false
    this.isOnce = false
    this.asyncFactory = asyncFactory
    this.asyncMeta = undefined
    this.isAsyncPlaceholder = false
  }

  // DEPRECATED: alias for componentInstance for backwards compat.
  /* istanbul ignore next */
  get child (): Component | void {
    return this.componentInstance
  }
}

export const createEmptyVNode = (text: string = '') => {
  const node = new VNode()
  node.text = text
  node.isComment = true
  return node
}

export function createTextVNode (val: string | number) {
  return new VNode(undefined, undefined, undefined, String(val))
}

// optimized shallow clone
// used for static nodes and slot nodes because they may be reused across
// multiple renders, cloning them avoids errors when DOM manipulations rely
// on their elm reference.
export function cloneVNode (vnode: VNode): VNode {
  const cloned = new VNode(
    vnode.tag,
    vnode.data,
    vnode.children,
    vnode.text,
    vnode.elm,
    vnode.context,
    vnode.componentOptions,
    vnode.asyncFactory
  )
  cloned.ns = vnode.ns
  cloned.isStatic = vnode.isStatic
  cloned.key = vnode.key
  cloned.isComment = vnode.isComment
  cloned.fnContext = vnode.fnContext
  cloned.fnOptions = vnode.fnOptions
  cloned.fnScopeId = vnode.fnScopeId
  cloned.asyncMeta = vnode.asyncMeta
  cloned.isCloned = true
  return cloned
}

static nodes and slot nodes? slot是通过slot属性和slot标签挂载的node。例如:

<template slot="foo" >
</template>
<slot name="foo">
</slot>

component上也有tag,data,children,text,elm,context,componentOptions,asyncFactory,ns,isStatic,key,isComment,fnContext,fnOptions,fnScoped,asyncMeta,isCloned这些属性吗? component没有,但是它会有一个$vnode对象。 一个最简单的例子:

 <Button ref="add">新增人数</Button>

image

综上可以发现两点; 1.component继承自Vue类。$vnode继承自VNode类。 2.component包含了$vnode,可以说是父子关系。

FrankKai avatar Nov 20 '18 01:11 FrankKai

19.深入理解slot算法和shadow DOM

阅读完这篇博客你会有以下收获:

  • slot算法是什么?
  • shadow DOM是什么?
  • vue slot机制与w3c web component 规范的 shadow DOM渲染结果有何异同?

image

slot算法

The slotting algorithm assigns nodes of a shadow tree host into slots of that tree.

Input HOST -- a shadow tree host Output All child nodes of HOST are slotted

  1. Let TREE be HOST's shadow tree
  2. Let DEFAULT be an empty list of nodes
  3. For each child node NODE of HOST, in tree order:
  4. Let NAME be NODE's slot name
  5. If NAME is missing, add NODE to DEFAULT
  6. Let SLOT be the slot with slot name NAME for TREE
  7. If SLOT does not exist, discard node
  8. Otherwise, assign NODE to SLOT
  9. Let DEFAULT-SLOT be the the default slot for TREE
  10. If DEFAULT-SLOT does not exist, stop
  11. Otherwise, assign all nodes in DEFAULT to DEFAULT-SLOT.

When each node is assigned to a slot, this slot is also added to the node's destination insertion points list.

这是w3c web components规范的一个提案,地址位于https://github.com/w3c/webcomponents/blob/gh-pages/proposals/Slots-Proposal.md

在这个提案中,我们发现shadow DOM和shadow Tree这两个概念,关于它们的规范,在这里:http://w3c.github.io/webcomponents/spec/shadow/#conformance

mdn上关于shadow DOM的一篇特别好的文章:https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_shadow_DOM

Shadow DOM : attach a hidden separated DOM to an element.

几个shadow DOM的专业术语:

  • Shadow host: shadow DOM要连接的普通DOM节点。
  • Shadow tree: shadow DOM里的DOM树。
  • Shadow boundary: Shadow DOM结束的地方,也是普通DOM开始的地方。
  • Shadow root:shadow tree的根节点。

Shadow DOM知识点:

  • shadow DOM和普通DOM一样可以添加子节点设置属性,但是shadow DOM内部的代码不能影响到外部的任何东西。
  • shadow DOM其实一直都在用,例如
  • 两种模式mode:open,closed。

shadow DOM的作用是什么:增强组件内聚性

An important aspect of web components is encapsulation — being able to keep the markup structure, style, and behavior hidden and separate from other code on the page so that different parts do not clash, and the code can be kept nice and clean.

vue demo: component.vue -> shadow host

<slot></slot>
<slot name="foo"></slot>
<slot name="bar"></slot>

page.vue -> shadow Tree

<span>+</span>
<span slot="foo">-</span>
<span slot="bar">2</span>
<span slot="bar">3</span>

渲染结果: image

渲染结果与slot算法十分契合,但是较为奇怪的是,vue的slot机制,不会生成像w3c web component 的shadow DOM。

web component shadow DOM是下面这样: image

FrankKai avatar Nov 20 '18 06:11 FrankKai

20.v-model与vuex中的state绑定

非严格模式:直接修改 严格模式:直接修改报错 需要特殊处理 1.监听事件提交mutation 2.计算属性里set提交mutation

20.0 为什么非严格模式下可以直接修改?

因为vuex中的state,映射到组件实例上之后,是一个计算属性,同时拥有getter和setter的。 当我们的属性修改时,v-model会被解析为对应的v-bind和事件,在事件中触发属性的setter从而修改值,之后setter通知watcher收集到的依赖,对所有相关的组件更新属性值。

20.1 如何将vuex中的值设置为仅能通过mutation更新?

  • strict设置为true
  • mapState中返回getter的值

20.2 严格模式下,如何对使用了mapState重命名的计算属性做到双向绑定修改值?

  • mapState重命名计算属性
  • 在重命名计算属性的set中提交mutation
export default new Vuex.Store({
  strict: true,
});
computed:{
    ...mapState({
      userId(state) {
        return state.userId;
      }
    }),
    vuser: {
      get() {
        return this.userId;
      },
      set(id) {
        this.$store.commit('updateUserId', id);
      },
    },
}

const mutation =  {
 updateMessage(state, id) {
    state.userId = id;
  },
}

FrankKai avatar Dec 25 '18 09:12 FrankKai

21.vue-router的实现原理

  • 侦听属性:Vue.defineReactive(“_route")
  • 生成新的route对象:push transitionTo Run queue(active,deactive,updated)
  • 更新app的route:updateRoute _rouoe
  • 变更路url:pushState

FrankKai avatar Nov 19 '20 09:11 FrankKai

22.$nextTick的实现原理

获取到数据层导致视图更新后的DOM。

// 修改数据
vm.msg = 'Hello'
// DOM 还没有更新
Vue.nextTick(function () {
  // DOM 更新了
})

源码实现: 通过nextTick包裹的callback,会作为一个任务(v2.5.6按照setImmediate,MessageChannel和setTimeout的优先级;最新版的按照Promise,MutationObserver,setImmediate和setTimeout的优先级)增加到任务队列,等待所有DOM完成更新后执行callback。

// v2.5.6
if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  macroTimerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else if (typeof MessageChannel !== 'undefined' && (
  isNative(MessageChannel) ||
  // PhantomJS
  MessageChannel.toString() === '[object MessageChannelConstructor]'
)) {
  const channel = new MessageChannel()
  const port = channel.port2
  channel.port1.onmessage = flushCallbacks
  macroTimerFunc = () => {
    port.postMessage(1)
  }
} else {
  /* istanbul ignore next */
  macroTimerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}
// vue的最新版
if (typeof Promise !== 'undefined' && isNative(Promise)) {
  const p = Promise.resolve()
  timerFunc = () => {
    p.then(flushCallbacks)
    if (isIOS) setTimeout(noop)
  }
  isUsingMicroTask = true
} else if (!isIE && typeof MutationObserver !== 'undefined' && (
  isNative(MutationObserver) ||
  MutationObserver.toString() === '[object MutationObserverConstructor]'
)) {
  let counter = 1
  const observer = new MutationObserver(flushCallbacks)
  const textNode = document.createTextNode(String(counter))
  observer.observe(textNode, {
    characterData: true
  })
  timerFunc = () => {
    counter = (counter + 1) % 2
    textNode.data = String(counter)
  }
  isUsingMicroTask = true
} else if (typeof setImmediate !== 'undefined' && isNative(setImmediate)) {
  timerFunc = () => {
    setImmediate(flushCallbacks)
  }
} else {
  // Fallback to setTimeout.
  timerFunc = () => {
    setTimeout(flushCallbacks, 0)
  }
}

哪些属于宏任务?

setTimeout
setInterval
setImmediate
requestAnimationFrame
I/O
UI渲染

哪些属于微任务?

Promise
MutationObserver
process.nextTick
queueMicrotask

// 2022年5月7日更新 nextTick,本质上是为了提升虚拟DOM更新的性能,将多次数据导致的DOM更新,处理为1次DOM更新。其实现原理则是将nextTick要执行的内容,会被添加为一个微任务或者宏任务(根据当前状态去选择)去延迟执行,从而保证了可以获取到最新的DOM。

FrankKai avatar Nov 19 '20 09:11 FrankKai