Lambda博客
  • 自我介绍
  • SSR
    • 浅谈SSR
  • React
    • 页面路由——React-Router
    • 管好你的状态——React-Redux
    • 一个哪行——React详解
    • 左膀右臂一个不少——React初探
  • 问题记录
    • js-xlsx实现纯前端导出excel表格
    • 前端开发需要了解的东西
    • 打造高性能网站
  • JavaScript学习笔记
    • 语法和API
    • js-垃圾回收
    • 收集的JS使用技巧
    • 执行机制
    • 原型和原型链
    • 执行上下文
    • 事件循环
    • JavaScript手写代码
    • 43道JS面试题
    • 悄悄的带走了你的东西——闭包详解
    • 你是个富二代吗?——JavaScript作用域链
    • 捉摸不透的女生——JavaScript类型转换
    • 这是你的女神!——JavaScript
  • 网络学习笔记
    • 强不强——HTTP-协商缓存VS强缓存
    • 网络缓存
    • 我要飞的更高——计算机网络
    • 快点!再快点!——CDN
    • 喜怒哀乐多状态——HTTP状态码
    • 你会爱上我的(ಥ_ಥ) ——TCP详解
    • 隔壁老王想篡位?门都没有——同源策略
  • 软技能
  • 安全专题
    • 保护好自己——网站安全,预防
  • 浏览器兼容性
    • 我是个好人——浏览器兼容性
  • 多图片加载——懒加载详解
  • 数据结构
  • Node
    • Node初探
  • Typescript
    • JS Plus 真香——初探 TypeScript
  • 设计模式
    • 不要花里胡哨,要一套一套——设计模式(1)
    • 学会“套路”——设计模式(2)
  • Vue
    • 实操实干——vue实例记录
    • 停下来,问问自己——Vue-刨根问底
    • 你有喘息的机会吗?——Vue,逐步了解
    • 你累吗?来来来,安利框架——Vue-初次见面
  • 小程序
    • 今天天气怎么样——记一次小程序开发
  • Webpack
    • 蛋糕分割整合工具——Webpack-前端工程化
  • 你一块,我一块——Web-modules 前端模块化
  • HTML5
    • 你会画小猪佩奇吗?—— canvas
    • 画个矢量图——HTML5-SVG
    • 听说你爱闹腾——HTML5-多媒体
    • 动画神器——HTML5-requestAnimationFrame
    • 留下痕迹——HTML5-客户端存储
    • 你知道指北针吗?——HTML5-DeviceMotionEvent
    • 你在哪啊?我在这啊——HTML5-Geolocation
    • 你到这,你到那——HTML5-拖拽
    • 你从这,到那——HTML5-拖拽上传文件
    • 开启前端之路——HTML 标签
  • 瀑布流(无限滚动)
  • 我是怎么来的?——浏览器渲染原理
  • Css
    • 由大变小,你行吗 —— 移动端
    • 动起来,这样比较炫—— CSS3 动画
    • 请不要拐弯抹角 —— CSS3 选择器
    • 想炫吗?—— CSS3 属性
    • 最后的最后——CSS自问自答
    • 万事开头难?——深入学习前端常见布局
    • 一入前端深似海——BFC剖析
    • 还有哪些好玩的东西——CSS提升
    • 那些好看的页面是怎么形成的——CSS 初识
  • 拿个小本本记下——Cookie
由 GitBook 提供支持
在本页
  • 为什么官方建议数据异步请求在mounted事件进行?
  • 子组件修改父组件传过来的值
  • Vue 双向绑定的原理
在GitHub上编辑
  1. Vue

停下来,问问自己——Vue-刨根问底

为什么官方建议数据异步请求在mounted事件进行?

很多人觉得在created 事件里面把数据请求到,然后一起生成虚拟 DOM,再渲染会更好。实际上呢,请求是需要时间的,而且这个时间具有不稳定性,很可能 vue 的虚拟 DOM 准备好了,你的数据才请求到,然后又得更新一遍虚拟DOM,再渲染,极大地延长了白屏时间,用户体验很不好。而在 mounted 事件请求数据呢,静态页面会先渲染好,等数据好了,再更新部分 DOM 即可。

子组件修改父组件传过来的值

v-model 在使用的时候很像双向绑定的,但是 Vue 是单项数据流,v-model 只是语法糖而已:父组件用v-bind将值传给子组件,子组件通过change/input事件触发修改父组件的值。

<input v-model="inputValue" />
<!-- 等价于 -->
<input :value="inputValue" @change="inputValue" />

v-model 不仅仅能在input上用,在组件上也能使用vue组件间传递数据是单向的,即数据总是由父组件传递到子组件,子组件在其内部可以有自己维护的数据,但它无权修改父组件传递给它的数据,我们也可以参照v-model语法糖进行修改父组件的值,但是每次都这样写太麻烦了,vue 提供了一个修饰符.sync,用法如下:

<child :value.sync="inputValue"></child>

<!-- 子组件 -->
<script>
  export default{
    props:{
      //props可以设置值得类型,默认值,是否必传以及校验函
      value:{
        type: [String, Number],
        required: true
      }
    }
    //用一个变量中转,子组件中就用_value就不会直接修改父组件的值
    computed: {
      _value: {
        get() {
            return this.value;
        },
        set(val) {
            this.$emit('update:value', val);
        },
      }
    }
  }
</script>

Vue 双向绑定的原理

v-model 是 Vue 2.2.0 添加的,在此之前 Vue 只支持单向数据流

Vue 是采用数据劫持结合发布者-订阅者模式的方式,通过Object.defineProperty()来劫持各个属性的setter,getter,在数据变动时发布消息给订阅者,触发相应的监听回调。

双向绑定实现

如果想实现显示文字根据输入 input 变化,实现一个简单版的

<input type="text" id="a" />
<span id="b"></span>

<script>
  const obj = {}
  Object.defineProperty(obj, 'hello', {
    get() {
      console.log(`方法被调用`)
    },
    set(newVal) {
      document.getElementById('a').value = newVal
      document.getElementById('b').innerHTML = newVal
    }
  })
  document.addEventListener('keyup', function(e) {
    obj.hello = e.target.value
  })
</script>

拆解任务,实现 vue 的双向数据绑定

我们最终实现下面 vue 的效果

<div id="app">
  <input type="text" v-model="text" />
</div>
<script>
  const vm = new Vue({
    id: 'app',
    data: {
      text: 'hello world'
    }
  })
</script>
  1. 输入框的文本与文本节点的 data 数据绑定

  2. 输入框的内容发生变化时,data 中的数据也发生变化,实现 view->model 的变化

  3. data 中的数据发生变化时,文本节点的内容同步发生变化,实现 model->view 的变化

要实现 1 的要求,则又涉及到了 dom 的编译,其中有一个 DocumentFragment 的知识点。

DocumentFragment

众所周知,vue 吸收了 react 虚拟 DOM 的优点,使用 DocumentFragment 处理节点,其性能和速度远胜于直接操作 dom。vue 进行编译时,就是将所有挂载在 dom 上的子节点进行劫持到使用 DocumentFragment 处理节点,等到所有操作都执行完毕,将 DocumentFragment 再一模一样返回到挂载的目标上。

先实现一段劫持函数,将要操作的 dom 全部劫持到 DocumentFragment 中,然后再 append 会原位置。

<div id="app">
  <input type="text" v-model="text" />
</div>
<script>
  const app = document.getElementById('app')
  const nodeToFragment = (node) => {
    const flag = document.createDocumentFragment()
    let child
    while ((child = node.firstChild)) {
      flag.appendChild(child) //不断劫持挂载元素下的所有dom节点到创建的DocumentFragment
    }
    return flag
  }
  const dom = nodeToFragment(app)
</script>

数据初始化绑定

当已经获取到所有的 dom 元素之后,则需要对数据进行初始化绑定,这里简单涉及到了模板的编译。

// 编译 HTML 模板
const compile = (node, vm) => {
  const regex = /\{\{(.\*)\}\}/ //为临时正则表达式,为 demo 而生
  //如果节点类型为元素的话
  if (node.nodeType === 1) {
    const attrs = node.attributes
    for (let i = 0; i < attrs.length; i++) {
      let attr = attrs[i]
      if (attr.nodeName === 'v-model') {
        let name = attr.nodeValue
        node.addEventListener('input', function(e) {
          vm.data[name] = e.target.value
        })
        node.value = vm.data[name]
        node.removeAttribute('v-model')
      }
    }
  }
  //如果节点类型为文本的话
  if (node.nodeType === 3) {
    if (regex.test(node.nodeValue)) {
      let name = RegExp.$1 //获取匹配的字符串,又学到了。。。
      name = name.trim()
      node.nodeValue = vm.data[name]
    }
  }
}

//劫持挂载元素到虚拟dom
let nodeToFragment = (node, vm) => {
  const flag = document.createDocumentFragment()
  let child
  while ((child = node.firstChild)) {
    compile(child, vm) //绑定数据,插入到虚拟DOM中
    flag.appendChild(child)
  }
  return flag
}

//初始化
class Vue {
  constructor(option) {
    this.data = option.data
    let id = option.el
    let dom = nodeToFragment(document.getElementById(id), this)
    document.getElementById(id).appendChild(dom)
  }
}

const vm = new Vue({
  el: 'app',
  data: {
    text: 'hello world'
  }
})

通过以上代码先实现了第一个要求,文本框和文本节点已经出现了 hello world 了

响应式的数据绑定

接下来我们要实现数据双向绑定的第一步,即 view->model 的绑定。根据之前那个简单的例子看到,我们可以通过 input 事件实时获取 input 中的值,接着将通过 Object.defineProperty 这个方法将 data 中的 text 设置为 vm 的访问器属性。当我们将获取到的 input 值设置 vm.data.text 时,通过 set 方法,实现了数据层的绑定。在这一步,set 中要做的操作是更新属性的值。

let defineReactive = (obj,key,val) => {
   Object.defineProperty(obj,key,{
      get(val){
         return val;
      }
      set(newVal,oldVal){
         if(newVal === oldVal) return;
         val = newVal;
         console.log(`我获取到了新${val},并成功设置`);
      }
   })
};

//监听所有 data 传递进来的数据,将他们绑定到原型 vm 上面
let observe = (obj,vm) => {
   Object.keys(obj).forEach((key)=>{
      defineReactive(vm.data,key,obj[key]);
   })
};

订阅/发布模式(subscribe&publish)

text 属性变化了,set 方法触发了,可以通过 view 层的改变实时改变数据,可是并没有改变文本节点的数据。一个新的知识点:订阅发布模式。

订阅发布模式定义了一种一对多的关系,让多个观察者同时监听一个主题对象,这个主体对象的改变会通知所有观察者对象。

发布者发出通知=>主题对象收到通知并推送给订阅者=>订阅者执行操作

// 三个订阅者
let sub1 = {
  update() {
    console.log(1)
  }
}
let sub2 = {
  update() {
    console.log(2)
  }
}
let sub3 = {
  update() {
    console.log(3)
  }
}

// 一个主题发布器
class Dep {
  constructor() {
    this.subs = [sub1, sub2, sub3]
  }
  notify() {
    subs.forEach((sub) => {
      sub.update()
    })
  }
}
const dep = new Dep()
// 一个发布者
const pub = {
  publish() {
    dep.notify()
  }
}
pub.publish()

上图为一个简单实例,发布者执行发布命令,所有这个主题的订阅者执行更新操作。接下去我们要做的就是,当 set 方法触发后,input 作为发布者,改变了 text 属性;而文本节点作为订阅者,在收到消息后执行更新操作。

双向绑定的实现

  1. 每次 new 一个新的 vue 对象时,主要是做了两件事,一件是监听数据:observer(监听数据),第二个是编译 HTML,HTML,nodeToFragment(id)。

  2. 在监听数据的过程中,会为 data 中的每一个属性生成一个主题对象。

  3. 而在编译 HTML 的过程中,会为每个与数据绑定的相关节点生成一个订阅者 watcher,订阅者 watcher 会将自己订阅到相应属性的 dep 中。

  4. 在前面的方法中已经实现了:修改输入框内容=>再时间回调中修改属性值=>触发属性的 set 方法。

  5. 接下来要做的是发出通知 dep.notify=>发出订阅者的 update 方法=>更新视图。

那么如何将 watcher 添加到关联属性的 dep 中呢。

编译 HTML 过程中,为每一个与 data 关联的节点生成一个 watcher,那么 watcher 中又发生了什么?

// 每一个属性节点的 watcher
class Watcher{
   constructor(vm,node,name){
      Dep.target = this;
      this.name = name;
      this.node = node;
      this.vm = vm;
      this.update();
      Dep.target = null;
   }
   update(){
      //获得最新值,然后更新视图
      this.get();
      this.node.nodeValue = this.value;
   }
   get(){
      this.value = this.vm.data[this.name];
   }
}
在编译 HTML 的过程中,生成 watcher

let compile = (node,vm){
   // ......
   //如果节点类型为文本的话
   if(node.nodeType === 3){
      if(regex.test(node.nodeValue)){
         let name = RegExp.$1;
         name = name.trim();
         node.nodeValue = vm.data[name];
         //在编译过程中,每发现一个属性,则新建一个 watcher
         new Watcher(vm,node,name);//在此处添加订阅者
      }
   }
}
  • 首先将自己赋给了一个全局变量 Dep.target;然后执行了 update 方法,进而执行了 get 方法,读取了 vm 的访问器属性,从而触发了访问器属性的 get 方法,get 方法将相应的 watcher 添加到对应访问器属性的 dep 中

  • 再次,获取属性的值,然后更新视图

  • 最后将 dep.target 设置为空,是因为这是个全局变量也是 watcher 与 dep 之间唯一的桥梁,任何时间都只能保证只有一个值。

// 一个主题发布器
class Dep(){
   constructor(){
      this.subs = [];
   }
   notify(){
      this.subs.forEach((sub) => {
         sub.update();
      }
   }
   addSub(sub){
      this.subs.push(sub);
      }
}
let defineReactive = (obj,key,val) => {
   let dep = new Dep();
   Object.defineProperty(obk,key,{
      get(){
         //在此处将所有的监测器 watcher 添加进发布器,每一个属性都有自己的发布器
         if(dep.target) dep.addSub(dep.target);
      }
      set(newVal,oldVal){
         if(newVal === oldVal) return;
         val = newVal;
         dep.notify();
      }
   })
}

至此,hello world 双向绑定就基本实现了。文本内容会随输入框内容同步变化,在控制器中修改 vm.text 的值,会同步反映到文本内容中。

上一页实操实干——vue实例记录下一页你有喘息的机会吗?——Vue,逐步了解

最后更新于3年前

QdCN9I.png