深入理解 Vue2 中核心要点

简介: 深入理解 Vue2 中核心要点

组件化

组件通信

  • 组件常用通信方式

    • props
    • EventBus
    • Vuex
    • 自定义事件
    • 其他

      • $parent
      • $children
      • $root
      • $refs
      • provide/inject
    • 非prop特性

      • $attrs
      • $listeners
  • props

    // child
    props: { msg: String }
    
    // parent
    <Helloworld msg="hello world" />
  • 事件总线
    任意两个组件之间传值常用事件总线或vuex的方式

    // Bus 事件派发、监听和回调管理,实际使用中会用Vue代替Bus
    class Bus {
      constructor() {
        this.callbacks = {};
      }
      $on(name, fn) {
        this.callbacks[name] = this.callbacks[name] || [];
        this.callbacks[name].push(fn);
      }
      $emit(name, args) {
        if (this.callbacks[name]) {
          this.callbacks[name].forEach((cb) => cb(args));
        }
      }
    }
    // main.js
    Vue.prototype.$bus = new Bus();
    
    // child1
    this.$bus.$on('foo', handle);
    // child2
    this.$bus.$emit('foo')
  • vuex
    通过创建唯一的全局数据管理者store,通过它管理数据并通知组件状态变更
  • 自定义事件
    子给父传值

    // child
    this.$emit('add', good);
    
    // parent
    <Cart @add="cartAdd($event)"></Cart>
  • $parent/$root
    兄弟组件之间通信可通过共同祖辈搭桥,$parent或$root

    // brother1
    this.$parent.$on('foo', handle);
    
    // brother2
    this.$parent.$emit('foo')
  • $children
    父组件可以通过$children访问子组件实现父子通信

    // parent
    this.$children[0].xxx = 'xxx';
  • $attrs/$listeners
    包含了父作用域中不作为prop被识别(且获取)的特性绑定(class和style除外)。当一个组件没有声明任何prop时,这里会包含所有父作用域的绑定(class和style除外),并且可以通过v-bind="$attrs"传入内部组件。这些特性在创建高级别的组件时非常有用。

    // child: 没有在props中声明foo 
    <p>{{$attrs.foo}}</p>
    
    // parent
    <Helloworld foo="foo">
  • $refs
    获取子节点引用

    // parent
    <Helloworld ref="hw">
    
    mounted() {
      this.$refs.hw.xx = 'xxx';
    }
  • provide/inject
    能实现祖先和后代之间传值

    // ancestor
    provide() {
      return { foo: 'foo' }
    }
    
    // descendant
    inject: ['foo']

插槽

插槽语法是Vue实现的内容分发API,用于复合组件开发。

  • 匿名插槽

    // comp1
    <div>
      <slot></slot>
    </div>
    
    // parent
    <comp>hello</comp>
  • 具名插槽
    将内容化分发到子组件指定位置

    // comp2
    <div>
      <slot></slot>
      <slot name="content"></slot>
    </div>
    
    // parent
    <comp2>
      <!-- 默认插槽用default做参数 -->
      <template v-slot:default>默认插槽</template>
      <!-- 具名插槽用插槽名做参数 -->
      <template v-slot:content>具名插槽</template>
    </comp2>
  • 作用域插槽
    分发内容要用到子组件中的数据

    // comp3
    <div>
      <slot :foo="foo"></slot>
    </div>
    
    // parent
    <comp3>
      <!--把v-slot的值指定为作用域上下文对象-->
      <template v-slot:default="slotProps">
        来自子组件数据:{{slotProps.foo}}
      </template>
    </comp3>

MVVM

MVVM 框架的三要素:数据响应式、模板引擎和渲染
数据响应式:监听数据变化并在视图中更新

  • Object.defineProperty()
  • Proxy

模板引擎:提供描述视图的模板语法

  • 插值: {{}}
  • 指令:v-bind,v-on,v-model,v-for,v-if

渲染:如何将模板转换为html

  • 模板=》vdom=》dom

实现

  1. 数据响应式原理
// defProp.js
// 响应式
const obj = {};

function defineReactive(obj, key, val) {
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get' + key);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        val = newVal;
      }
    }
  });
}

defineReactive(obj, 'foo', 'foo');
obj.foo;
obj.foo = 'foooo';
  1. 和视图关联
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Document</title>
</head>
<body>
  <div id="app"></div>
  <script>
    const obj = {};
    function defineReactive(obj, key, val) {
      // 对传入obj进行访问拦截
      Object.defineProperty(obj, key, {
        get() {
          console.log('get' + key);
          return val;
        },
        set(newVal) {
          if (newVal !== val) {
            console.log('set ' + key + ':' + newVal);
            val = newVal;
            // 更新函数
            update();
          }
        }
      });
    }
    function update() {
      app.innerText = obj.foo;
    }
    defineReactive(obj, 'foo', '');
    obj.foo = new Date().toLocaleTimeString();
    setInterval(() => {
      obj.foo = new Date().toLocaleTimeString();
    }, 1000);
  </script>
</body>
</html>
  1. 优化
function defineReactive(obj, key, val) {
  // 递归
  observe(val);
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ' + key);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        // 如果传入的newVal依然是obj,需要做响应化处理
        observe(newVal);
        val = newVal;
      }
    }
  });
}
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return;
  }
  Object.keys(obj).forEach(key => {
    defineReactive(obj, key, obj[key]);
  })
}
function set(obj, key, val) {
  defineReactive(obj, key, val);
}
const obj = { foo: 'foo', bar: 'bar', baz: { a: 1 } };
// defineReactive(obj, 'foo', 'foo');
oba=observe(obj);
obj.foo;
obj.foo = 'foooo';
obj.bar;
obj.bar = 'barrrr';
obj.baz.a;
obj.baz.a = 3;
obj.baz = {a:1,b:2}
obj.baz.b = 99

set(obj, 'dong', 'dong');
obj.dong;

Vue

思路

  1. new Vue()首先执行初始化,对data执行响应化处理,这个过程发生在Observer中
  2. 同时对模板执行编译,找到其中动态绑定的数据,从data中获取并初始化视图,这个过程发生在compile中
  3. 同时定义一个更新函数和Watcher,将来对应数据变化时Watcher会调用更新函数
  4. 由于data的某个key在视图中可以出现多次,所以每个key都需要一个管家Dep来管理多个Watcher
  5. 将来data中的数据一旦发生变化,会首先找到对应的Dep,通知所有Watcher执行更新函数

职责划分

  • CVue:框架构造函数
  • Observer:执行数据响应化(分辨数据是对象还是数组)
  • Compile:编译模板,初始化视图,收集依赖(更新函数、watcher创建)
  • Watcher:执行更新函数(更新dom)
  • Dep:管理多个Watcher,批量更新

实现

  1. 响应化处理及数据代理
// cvue.js
class CVue {
  constructor(options) {
    // 保存选项
    this.$options = options;
    this.$data = options.data;
    // 响应化处理
    observe(this.$data);

    // 代理
    Proxy(this, '$data')
  }
}
// 代理函数,提供对$data中数据的直接访问
function Proxy(vm, sourceKey) {
  Object.keys(vm[sourceKey]).forEach(key => {
    Object.defineProperty(vm, key, {
      get() {
        return vm[sourceKey][key];
      },
      set(newVal) {
        vm[sourceKey][key] = newVal;
      }
    })
  })
}
function defineReactive(obj, key, val) {
  // 递归
  observe(val);
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ' + key);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        // 如果传入的newVal依然是obj,需要做响应化处理
        observe(newVal);
        val = newVal;
      }
    }
  });
}
function observe(obj) {
  if (typeof obj !== 'object' || obj == null) {
    return;
  }
  // 创建Observer实例
  new Observer(obj);
}
// 根据对象类型决定如何做响应化
class Observer {
  constructor(value) {
    this.value = value;

    // 判断其类型
    if (typeof value === 'object') {
      this.walk(value);
    }
  }
  // 对象数据响应化
  walk(obj) {
    Object.keys(obj).forEach(key => {
      defineReactive(obj, key, obj[key]);
    })
  }
  // TODO 数组数据响应化
}
  1. 编译

编译模板中vue模板特殊语法,初始化视图、更新视图

编译器

// compile.js
class Compiler {
  /**
   * @param  el 宿主元素
   * @param  vm CVue实例
   */
  constructor(el, vm) {
    this.$vm = vm;
    this.$el = document.querySelector(el);

    if (this.$el) {
      // 执行编译
      this.compile(this.$el);
    }
  }
  compile(el) {
    // 递归遍历DOM
    const childNodes = el.childNodes;
    Array.from(childNodes).forEach((node) => {
      // 判断节点类型
      //  如果是元素,则遍历其属性判断是否是指令或事件,然后递归子元素
      if (this.isElement(node)) {
        // console.log('编译元素' + node.nodeName);
        this.compileElement(node);
      } else if(this.isInter(node)) { // 如果是文本,则判断是否插值绑定
        // console.log('编译插值绑定' + node.textContent);
        this.compileText(node);
      }
      // 递归子节点
      if (node.childNodes && node.childNodes.length > 0) {
        this.compile(node);
      }
    });
  }
  isElement(node) {
    return node.nodeType === 1;
  }
  isInter(node) {
    // 文本标签且内容为{{xx}}
    return node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent);
  }
  compileElement(node) {
    // 遍历属性列表
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach(attr => {
      // 约定指令格式 c-xx="yy"
      const attrName = attr.name; // c-xx
      const exp = attr.value; // yy
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2); // xx
        // 执行指令
        this[dir] && this[dir](node, exp);
      }
    })
  }
  isDirective(attr) {
    return attr.indexOf('c-') === 0;
  }
  // k-text
  text(node, exp) {
    node.textContent = this.$vm[exp];
  }
  // k-html
  html(node, exp) {
    node.innerHTML = this.$vm[exp];
  }
  compileText(node) {
    node.textContent = this.$vm[RegExp.$1];
  }
}

使用编译器

// cvue.js
class CVue {
  constructor(options) {
    // 保存选项
    this.$options = options;
    this.$data = options.data;
    // 响应化处理
    observe(this.$data);

    // 代理
    Proxy(this, '$data');
    // 创建编译器
    new Compiler(this.$options.el, this);
  }
}
// ...

测试

<div id="app">
  <!-- 插值 -->
  <p>{{counter}}</p>
  <!-- 指令 -->
  <p c-text="counter"></p>
  <p c-html="desc"></p>
</div>
<script src="cvue.js"></script>
<script src="compile.js"></script>
<script>
  const app = new CVue({
    el: '#app',
    data: {
      counter: 1,
      desc: '<span style="color:blue;">hello cvue</span>',
    },
  });
  setInterval(() => {
    app.counter++;
  }, 1000);
</script>
  1. 数据监听

视图中会用到data中某key,叫依赖。同一个key可能出现多次,每次都需要收集出来用一个watcher来维护它,这个过程为依赖收集。多个watcher需要一个Dep来管理,需要更新时由Dep统一通知。

定义watcher并在数据更新时触发watcher的update

// cvue.js
// 观察者:保存更新函数,值发生变化调用更新函数
const watchers = [];
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;
    watchers.push(this);
  }
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}
function defineReactive(obj, key, val) {
  // 递归
  observe(val);
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ' + key);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        // 如果传入的newVal依然是obj,需要做响应化处理
        observe(newVal);
        val = newVal;

        // 执行更新函数
        watchers.forEach(w => w.update());
      }
    }
  });
}

编译时绑定watcher

// compile.js
// 编译器
class Compiler {
  // ...
  compileText(node) {
    // node.textContent = this.$vm[RegExp.$1];
    this.update(node, RegExp.$1, 'text');
  }
  textUpdater(node, value) {
    node.textContent = value;
  }
  // k-text
  text(node, exp) {
    // node.textContent = this.$vm[exp];
    this.update(node, exp, 'text');
  }
  // k-html
  html(node, exp) {
    // node.innerHTML = this.$vm[exp];
    this.update(node, exp, 'html');
  }
  htmlUpdater(node, value) {
    node.innerHTML = value;
  }
  update(node, exp, dir) {
    // 初始化
    // 指令对应的更新函数xxUpdater
    const fn = this[dir + 'Updater'];
    fn && fn(node, this.$vm[exp]);

    // 更新 封装一个更新函数,可以更新对应dom元素
    new Watcher(this.$vm, exp, function(val) {
      fn && fn(node, val);
    });
  }
}
  1. 依赖收集
// cvue.js
function defineReactive(obj, key, val) {
  // 递归
  observe(val);
  // 创建一个Dep和当前的key一一对应
  const dep = new Dep();
  // 对传入obj进行访问拦截
  Object.defineProperty(obj, key, {
    get() {
      console.log('get ' + key);
      // 依赖收集
      Dep.target && dep.addDep(Dep.target);
      return val;
    },
    set(newVal) {
      if (newVal !== val) {
        console.log('set ' + key + ':' + newVal);
        // 如果传入的newVal依然是obj,需要做响应化处理
        observe(newVal);
        val = newVal;

        // 执行更新函数
        // watchers.forEach(w => w.update());
        dep.notify();
      }
    }
  });
}
class Watcher {
  constructor(vm, key, updateFn) {
    this.vm = vm;
    this.key = key;
    this.updateFn = updateFn;

    // Dep.target静态属性上设置为当前watcher实例
    Dep.target = this;
    this.vm[this.key]; // 读取触发getter
    Dep.target = null; // 收集完就置空
  }
  update() {
    this.updateFn.call(this.vm, this.vm[this.key]);
  }
}

// Dep 依赖,管理某个key相关所有的watcher实例
class Dep {
  constructor() {
    this.deps = [];
  }
  addDep(dep) {
    this.deps.push(dep);
  }
  notify() {
    this.deps.forEach(dep => dep.update());
  }
}
  1. 事件绑定
// compile.js
// 编译器
class Compiler {
  // ...
  compileElement(node) {
    // 遍历属性列表
    const nodeAttrs = node.attributes;
    Array.from(nodeAttrs).forEach(attr => {
      // 约定指令格式 c-xx="yy"
      const attrName = attr.name; // c-xx
      const exp = attr.value; // yy
      if (this.isDirective(attrName)) {
        const dir = attrName.substring(2); // xx
        // 执行指令
        this[dir] && this[dir](node, exp);
      }
      // 事件处理
      if (this.isEvent(attrName)) {
        // @click="onClick"
        const dir = attrName.substring(1) // "click"
        // 事件监听
        this.eventHandler(node, exp, dir);
      }
    })
  }
  isEvent(dir) {
    return dir.indexOf('@') === 0;
  }
  eventHandler(node, exp, dir) {
    // 在实例中 methods: { onClick: function(){} }
    const fn = this.$vm.$options.methods && this.$vm.$options.methods[exp];
    node.addEventListener(dir, fn.bind(this.$vm));
  }
  // ...
}
  1. c-model:语法糖,value设定,事件监听
// compile.js
// 编译器
class Compiler {
  // ...
  // c-model="xx"
  model(node, exp) {
    // update方法
    this.update(node, exp, 'model');
    // 事件监听
    node.addEventListener('input', e => {
      // 新的值赋值给数据
      this.$vm[exp] = e.target.value;
    });
  }
  modelUpdater(node, value) {
    // 表达元素赋值
    node.value = value;
  }
}
相关文章
|
6月前
|
JavaScript 前端开发
【查漏补缺你的Vue基础】Vue数据监听深度解析
【查漏补缺你的Vue基础】Vue数据监听深度解析
|
1天前
|
存储 JavaScript 前端开发
介绍一下Vue的核心功能
介绍一下Vue的核心功能
|
6月前
|
JavaScript 前端开发 API
vue3基础流程
vue3基础流程
73 0
|
JavaScript 开发者
Vue2向Vue3过度核心技术指令补充
Vue2向Vue3过度核心技术指令补充
54 0
|
存储 缓存 JavaScript
一文梳理Vue3核心原理
一文梳理Vue3核心原理 前言 本篇文章更多是以梳理的视角进行讲述,将各个原理细节串在一起,方便查漏补缺,而非为了讲懂某个原理,当然也会大致讲解。所以如果某个原理不太清楚,请自行查阅其他文章,我也会尽量给出相关的阅读推荐。 ==本文阅读需要你有一定的vue应用程序开发经验并了解一些原理== 接下来先废话一下,关注知识点的可以直接跳过前言部分 首先,我们先回到最初的起点是为什么要使用Vue框架,它为我们做了什么工作: 能开发出一个应用? 性能好、构建产物轻量? 对用户友好,声明式代码心智负担小? 可组件化开发? 社区活跃,生态丰富? ...
1015 1
|
存储 JavaScript 前端开发
Vue2的核心原理剖析
Vue2的核心原理剖析
|
JavaScript 前端开发
简单了解一下Vue3的整体运行机制
Vue3是如何运行的 这章作为Vue3原理的开头,将整体介绍JS框架的作用,以及Vue的总体运行流程,整体架构分为哪些等。 致谢Vue Mastery非常好的课程,可以转载,但请声明源链接:文章源链接justin3go.com(有些latex公式某些平台不能渲染可查看这个网站)
168 0
简单了解一下Vue3的整体运行机制
|
JavaScript 程序员 开发者
Vue —— 基础(七)(组件化编程【非单文件组件】)
Vue —— 基础(七)(组件化编程【非单文件组件】)
129 0
|
JavaScript
【Vue 开发实战】基础篇 # 10:生命周期的应用场景和函数式组件
【Vue 开发实战】基础篇 # 10:生命周期的应用场景和函数式组件
152 0
【Vue 开发实战】基础篇 # 10:生命周期的应用场景和函数式组件
|
JavaScript 前端开发 API
vue3 源码学习,实现一个 mini-vue(十一):组件的设计原理与渲染方案
在实现了 `ELEMENT`、`COMMENT`、`TEXT` 节点的挂载后,我们最后再来实现一下组件的挂载与更新
vue3 源码学习,实现一个 mini-vue(十一):组件的设计原理与渲染方案
下一篇
无影云桌面