首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Vue.js 2 深入理解

Vue.js 2 深入理解

作者头像
Cellinlab
发布2023-05-17 15:20:52
发布2023-05-17 15:20:52
1.3K0
举报
文章被收录于专栏:Cellinlab's BlogCellinlab's Blog

# 组件化

# 组件通信

组件常用通信方式

  • props
  • EventBus
  • Vuex
  • 自定义事件
  • 其他 parent children root refs provide/inject
  • 非prop特性 attrs listeners

props

代码语言:javascript
复制
// child
props: { msg: String }

// parent
<Helloworld msg="hello world" />

事件总线 任意两个组件之间传值常用事件总线或 Vuex 的方式

代码语言:javascript
复制
// 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,通过它管理数据并通知组件状态变更

自定义事件

子给父传值

代码语言:javascript
复制
// child
this.$emit('add', good);

// parent
<Cart @add="cartAdd($event)"></Cart>

$parent/$root

  • 兄弟组件之间通信可通过共同祖辈搭桥,parent 或 root
代码语言:javascript
复制
// brother1
this.$parent.$on('foo', handle);

// brother2
this.$parent.$emit('foo')

$children

  • 父组件可以通过 $children 访问子组件实现父子通信
代码语言:javascript
复制
// parent
this.$children[0].xxx = 'xxx';

$attrs/$listeners

含了父作用域中不作为 prop 被识别(且获取)的特性绑定(classstyle 除外)

当一个组件没有声明任何 prop 时,这里会包含所有父作用域的绑定(classstyle除外),并且可以通过 v-bind="$attrs" 传入内部组件

这些特性在创建高级别的组件时非常有用

代码语言:javascript
复制
// child: 没有在props中声明foo 
<p>{{$attrs.foo}}</p>

// parent
<Helloworld foo="foo">

$refs

  • 获取子节点引用
代码语言:javascript
复制
// parent
<Helloworld ref="hw">

mounted() {
  this.$refs.hw.xx = 'xxx';
}

provide/inject

  • 能实现祖先和后代之间传值
代码语言:javascript
复制
// ancestor
provide() {
  return { foo: 'foo' }
}

// descendant
inject: ['foo']

# 插槽

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

匿名插槽

代码语言:javascript
复制
// comp1
<div>
  <slot></slot>
</div>

// parent
<comp>hello</comp>

具名插槽

  • 将内容化分发到子组件指定位置
代码语言:javascript
复制
// comp2
<div>
  <slot></slot>
  <slot name="content"></slot>
</div>

// parent
<comp2>
  <!-- 默认插槽用default做参数 -->
  <template v-slot:default>默认插槽</template>
  <!-- 具名插槽用插槽名做参数 -->
  <template v-slot:content>具名插槽</template>
</comp2>

作用域插槽

  • 分发内容要用到子组件中的数据
代码语言:javascript
复制
// 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-bindv-onv-modelv-forv-if
  • 渲染
    • 如何将模板转换为 html
    • 模板 -》 VDOM -> DOM

# 实现

  1. 数据响应式原理
代码语言:javascript
复制
// 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. 和视图关联
代码语言:javascript
复制
<!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. 优化
代码语言:javascript
复制
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. 响应化处理及数据代理
代码语言:javascript
复制
// 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模板特殊语法,初始化视图、更新视图

编译器

代码语言:javascript
复制
// 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];
  }
}

使用编译器

代码语言:javascript
复制
// 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);
  }
}
// ...

测试

代码语言:javascript
复制
<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 并在数据更新时触发 watcherupdate

代码语言:javascript
复制
// 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

代码语言:javascript
复制
// 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. 依赖收集
代码语言:javascript
复制
// 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. 事件绑定
代码语言:javascript
复制
// 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 设定,事件监听
代码语言:javascript
复制
// 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;
  }
}
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2021/1/5,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • # 组件化
    • # 组件通信
    • # 插槽
  • # MVVM
    • # 实现
  • # Vue
    • # 思路
    • # 实现
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档