vue双向绑定原理及其简单实现

也许很多人写了很久的vue但是实际对于其双向绑定和其渲染还是存在一个模糊的概念,仅知道是通过defineProperty来达到,但是具体详细怎么实现,及其分多少个模块,并不是非常清除。

模块划分

大体分三个模块:
observer观察者(dep容器)
compile解释器
watcher订阅者

原理.png

下面用简单实现一个v-model及{{name}}的简单示例来描述此三个模块。
1.先来看一下初始化


  
{{name}}
{{xiaxia}}

入口文件(index.js)

class Vue {
  constructor(options) {
    this.$el = document.querySelector(options.el);
    this.$data = options.data;
    Object.keys(this.$data).forEach(key => {
      this.proxyData(key);
    });
    this.init(this.$data);
  }
  init() {
    // 加入观察者
    observer(this.$data);
    // 编译器
    new Compile(this);
  }
  // 数据劫持 重写get set 让数据直接读取写入
  proxyData(key) {
    Object.defineProperty(this, key, {
      get(){
        return this.$data[key]
      },
      set(value){
        this.$data[key] = value;
      }
    });
  }
}

定义一个构造函数
在constructor传入了所挂载的节点以及其data对象(此处为对象,实则vue中data为一个方法,暂不做深入分析,往后会提到)
遍历data对象中的元素修改其get和set以此来达到this.name可直接访问及修改其属性的效果。
并在初始化中引入observer观察者及compile编译器

observer观察者

function observer(data) {
  if (!data || typeof data !== "object") {
    return;
  }
  for (let key in data) {
    defineReactive(data, key, data[key]);
  }
}

function defineReactive(data, key, value) {
  //递归调用,监听所有属性
  observer(value);
  const dep = new Dep();
  Object.defineProperty(data, key, {
    get(){
      if (Dep.target) {
        dep.addSub(Dep.target);
      }
      return value;
    },
    set(newVal){
      if (value !== newVal) {
        value = newVal;
        dep.notify(); //通知订阅器
      }
    }
  });
}

监听每一个属性(此处存在一定的性能问题在vue3.0已优化,目前哪怕没有用到的属性也会去监听浪费性能,暂不展开)
get中addSub为把watcher加入到容器中管理
set则是通过对不比其值来通知watcher订阅器

// 容器
class Dep {
  constructor() {
    this.subs = [];
  }
  addSub(sub) {
    this.subs.push(sub);
  }
  notify() {
    this.subs.forEach(sub => {
      sub.update();
    })
  }
}
// es6 class只能以此添加公共方法属性
Dep.prototype.target=null

其中update为watcher中方法

watcher订阅者

class Watcher{
  constructor(vm, prop, callback){
    this.vm = vm;
    this.prop = prop;
    this.callback = callback;
    this.value = this.get();
  }
  update(){
    const value = this.vm.$data[this.prop];
    const oldVal = this.value;
    if (value !== oldVal) {
      this.value = value;
      this.callback(value);
    }
  }
  get(){
    Dep.target = this; //储存订阅器
    const value = this.vm.$data[this.prop]; //因为属性被监听,这一步会执行监听器里的 get方法
    Dep.target = null;
    return value;
  }
}

watcher中get方法之所以设置Dep.target = this后对this.vm也就是data赋值是为了触发observer中的get属性,故赋值成功后则应当置空。
update方法则含一个回掉函数,watcher在compile中创建

compile解释器

class Compile {
  constructor(vm) {
    this.vm = vm;
    this.el = vm.$el;
    this.init()
  }
  init() {
    this.fragment = this.nodeFragment(this.el);
    this.compileNode(this.fragment);
    this.el.appendChild(this.fragment);
  }
  nodeFragment(el) {
    // 创建内存中的DOM
    const fragment = document.createDocumentFragment();
    let child = el.firstChild;
    //将子节点,全部移动文档片段里
    while (child) {
      fragment.appendChild(child);
      child = el.firstChild;
    }
    return fragment;
  }
  // 编译节点
  compileNode(fragment) {
    const childNodes = fragment.childNodes;
    [...childNodes].forEach(node => {
      // 如果节点是元素节点,则 nodeType 属性将返回 1。
      // 如果节点是属性节点,则 nodeType 属性将返回 2。
      // ......
      // 参照https://www.w3school.com.cn/jsref/prop_node_nodetype.asp
      if (this.isElementNode(node)) {
        this.compile(node);
      }

      const reg = /\{\{(.*)\}\}/;
      const text = node.textContent;
      if (reg.test(text)) {
        const prop = reg.exec(text)[1];
        this.compileText(node, prop); //替换模板
      }

      if (node.childNodes && node.childNodes.length) {
        this.compileNode(node);
      }
    })
  }
  compile(node) {
    // 编译vue的指令
    let nodeAttrs = node.attributes;
    [...nodeAttrs].forEach(attr => {
      let { name, value } = attr;
      if (name === "v-model") {
        this.compileModel(node, value);
      }
    });
  }
  compileModel(node, prop) {
    let val = this.vm.$data[prop];
    // 初始化值
    this.updateModel(node, val);
    // 添加观察者
    new Watcher(this.vm, prop, (value) => {
      // 回调函数
      this.updateModel(node, value);
    });

    node.addEventListener('input', e => {
      let newValue = e.target.value;
      if (val === newValue) {
        return;
      }
      this.vm.$data[prop] = newValue;
    });
  }
  compileText(node,prop){
    let text = this.vm.$data[prop];
    this.updateView(node, text);
    new Watcher(this.vm, prop, (value) => {
      this.updateView(node, value);
    });
  }
  updateModel(node,value){
    node.value = typeof value === 'undefined' ? '' : value;
  }
  updateView(node,value){
    node.textContent = typeof value === 'undefined' ? '' : value;
  }
  isElementNode(node) {
    return node.nodeType === 1;
  }
}

init为初始化方法,也就是vue第一次默认渲染的数据。
从onst fragment = document.createDocumentFragment();中可以看到其创建了一个文档片段,其实也就是所谓的VirtualDOM虚拟DOM
compileNode 遍历其子节点通过正则找到{{name}}来替换其内容以及通过nodeType来定位到元素找到v-model来添加watcher观察者和给input添加监听来修改其值(之前定义好的get,set)
通过Watcher的get来把watcher添加到dep及通过dep的notify来调用watcher的更新方法产生的值来更新数据(有点绕。。。。。。)

Finally

主要还是得读代码,三者存在循环调用密不可分。
附上代码https://github.com/Xyifeng/vue-core-demo

你可能感兴趣的:(vue双向绑定原理及其简单实现)