前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >18 vue 实例及其双向绑定的实现原理

18 vue 实例及其双向绑定的实现原理

作者头像
LIYI
发布2020-02-13 11:56:20
5530
发布2020-02-13 11:56:20
举报
文章被收录于专栏:艺述论专栏艺述论专栏
代码语言:javascript
复制
目录

一个vue实例
生命周期钩子函数
set/get 访问器属性的实现
v-model属性与{{text}}在模板中是如何被解析的?
观察者模式
源码

一个vue实例

一个典型的vue实例:

代码语言:javascript
复制
<div id="app">
 <input type="text" v-model="text">
 {{ text }}
</div>
...
let vm = new Vue({
  el: '#app',
  data:()=>({
      text:'hi'
  }),
  created () {
    console.log(this.text)
  }
})

当Vue选项对象中有render渲染函数时,Vue构造函数将直接使用渲染函数渲染DOM树,否则Vue构造函数将尝试通过template模板编译生成渲染函数,然后再渲染DOM树,如果template也没有,则会通过el属性获取挂载元素的outerHTML来作为模板,并编译生成渲染函数。

简言之,优先级render>template>el。

运行效果:

这里有几个问题值得思考:

  1. data对象中的text是怎么绑定到template中{{text}}之上的?
  2. v-model是如何实现双向绑定的,当用户输入文本时,是如何更新data.text的?
  3. 在运行时,当text有更新时,模板中的{{text}}是如何更新的?
  4. created函数中,为什么可以通过this.text访问data对象中text属性?

生命周期钩子函数

created是vue实例的生命周期钩子函数之一。

如上所示,所有生命周期钩子函数依次有:beforeCrate->created->beforeMount->mounted->beforeDestroy->destroyed。

这些钩子函数给了开发者在vue生命不同周期阶段执行自己代码的机会。大多数情况下业务逻辑都是在放在created函数内,若干事件监听的移除、资源销毁等放在destroyed函数内。

在上面的示例中,为什么在created中可以用this.text访问data对象中的text属性呢?

在vue实例中,vm.$data指代data,通过this.text访问与通过this.$data.text访问有没有区别?

set/get 访问器属性的实现

vue源码略过复杂,有开发者依照其原理,写了一个基本的声明式渲染双向绑定的例子,可以在源码/simple-vue-project/public/index-vue-compile.html文件中查看。

通过学习这个文件,可以了解vue是如何实现的。

在vue实例初始化时,会对data做一些分析,将data的属性依次循环在vm实例上做一个访问器属性代理,主要涉及的代码类似于:

代码语言:javascript
复制
observe(data, this);
// 循环设置每一个属性
function observe (obj, vm) {
  Object.keys(obj).forEach(function (key) {
    defineReactive(vm, key, obj[key]);
  })
}
// data对象中的属性,被解析并定义在了vm上
// 此处obj = vm
function defineReactive (obj, key, val) {
  // 此时监听器是属于单个属性的,一个属性一个监听器
  var dep = new Dep();

  Object.defineProperty(obj, key, {
    get: function () {
      // 添加订阅者 watcher 到主题对象 Dep
      if (Dep.target) dep.addSub(Dep.target);
      return val
    },
    set: function (newVal) {
      if (newVal === val) return
      val = newVal;
      // 作为发布者发出通知
      // 当用户在input处输入内容后,触发data变量的set更新
      // 进而触发Watcher.update调用
      dep.notify();
    }
  });
}

在当template或created中访问this时,指代的是vm实例对象。在上文代码中,defineReactive函数给每一个data中的属性,例如text,在vm上通过Object.defineProperty注册了set/get一对访问器属性。

当访问this.text时,执行的是get;当调用this.text = 'xx'时,执行的是set。

由于第三个参数val是一个引用,在上下文中实际指代obj[key],所以val = newVal这行代码才是起作用的。

这就是第4个问题,created函数中,为什么可以通过this.text访问data对象中text属性的答案。

vm.$data.text与this.text指向的是同一块内存区域。但是直接修改vm.$data.text,并没有触发set中的诸如dep.notify之类的视图更新操作,所以两者还是有区别的。

Vue选项对象中的data,只能在实例化前指定;如果运行时想添加被监察变化的属性,可以使用this.$set方法。

v-model属性与{{text}}在模板中是如何被解析的?

代码语言:javascript
复制
var dom = nodeToFragment(document.getElementById(id), this);
// 编译完成后,将 dom 返回到 app 中
document.getElementById(id).appendChild(dom); 
...
function nodeToFragment (node, vm) {
  var flag = document.createDocumentFragment();
  var child;
  while (child = node.firstChild) {
    // 转化是为了调用 compile,解析template内容
    compile(child, vm);
    flag.appendChild(child); // 将子节点劫持到文档片段中
  }
  return flag
}
...
function compile (node, vm) {
  var reg = /\{\{(.*)\}\}/;
  // 节点类型为元素
  if (node.nodeType === 1) {
    var attr = node.attributes;
    // 解析属性
    for (var i = 0; i < attr.length; i++) {
      if (attr[i].nodeName == 'v-model') {
        var name = attr[i].nodeValue; // 获取 v-model 绑定的属性名
        node.addEventListener('input', function (e) {
          // 给相应的 data 属性赋值,进而触发该属性的 set 方法
          //
          vm[name] = e.target.value;
        });
        // 单就本示例而言,这行代码与下面的Watcher做的工作是重复的
        // node.value = vm[name]; // 将 data 的值赋给该 node
        node.removeAttribute('v-model');
      }
    };
    // 传递input、text类型,是为了区别更新的属性名称
    new Watcher(vm, node, name, 'input');
  }
  // 节点类型为 text
  if (node.nodeType === 3) {
    if (reg.test(node.nodeValue)) {
      var name = RegExp.$1; // 获取匹配到的字符串
      name = name.trim();

      new Watcher(vm, node, name, 'text');
    }
  }
}

使用DocumentFragment API,是为了解析html。核心逻辑都在compile函数内。

当nodeType为1时,看看没有v-mode属性,如果有,把它删除;在删除之前,对node添加一个input事件监听,当输入文本有变化时,调用vm上的set。在这里是vm.text。

当nodeType为3时,这是一个模板变量,在这里就是{{text}}。这也是一个node对象。通过正则匹配出变量名称,并注册变量的监听器,当变量变化时更新这个node的nodeValue。

这就是双向绑定,是第1、2、3问题的答案。

观察者模式

Watcher是什么?

可以看作是一个观察者模式对象,每个属性都有一个自己的Watcher。

代码语言:javascript
复制
function Watcher (vm, node, name, nodeType) {
  // 只有在编译阶段,Dep.target才不为null
  // this等于Watcher实例
  Dep.target = this;
  this.name = name;
  this.node = node;
  this.vm = vm;
  this.nodeType = nodeType;
  // update间接调用了vm的访问器get,在那里注册了data变量变化的监听
  this.update();
  // target设置为null,只有编译时添加监听一次
  Dep.target = null;
}
Watcher.prototype = {
  // 数据变化时,仍会调用
  update: function () {
    this.get();
    if (this.nodeType == 'text') {
      // nodeValue是{{xxx}}节点文本值
      this.node.nodeValue = this.value;
    }
    if (this.nodeType == 'input') {
      // value指input的value
      // 这里仅处理示例中的有限指令
      this.node.value = this.value;
    }
  },
  // 获取 data 中的属性值
  get: function () {
    this.value = this.vm[this.name]; // 触发相应属性的 get
  }
}

Watcher的update方法,在变量发生变化时会被调用。而data变量更新的根源,还在于对原生html dom元素的事件监听。

源码

https://git.code.tencent.com/shiqiaomarong/vue-go-rapiddev-example/tags/v20200114

参考链接

  • https://cn.vuejs.org/v2/api/#vm-data
  • https://blog.csdn.net/hxy19971101/article/details/79948074
  • https://www.cnblogs.com/kidney/p/6052935.html
  • https://github.com/DDFE/DDFE-blog/issues/7
本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-01-14,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 艺述论 微信公众号,前往查看

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

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

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一个vue实例
  • 生命周期钩子函数
  • set/get 访问器属性的实现
  • v-model属性与{{text}}在模板中是如何被解析的?
  • 观察者模式
  • 源码
  • 参考链接
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档