Vue1.0.25源码分析,及Zue模拟实现(一)

265 阅读2分钟

这是我参与11月更文挑战的第6天,活动详情查看:2021最后一次更文挑战

前言

之前断断续续地在研究vue原理,但还未动手实战过。本文从最简单的开始,vue如何将{{}}data联系起来,并渲染出来,比如{{msg}}data(){ return {msg:333} },那么{{msg}}将渲染成333,现在让我们开始吧。

实战

为了实现该功能,我动手写了一个Zue,已将其放在github上,其链接地址为github.com/zwf193071/z… 。下面我将为大家详细地说明原理。

index.html的代码如下所示:

	<div id="app">{{msg}}</div>
    <script type="module">
        import Zue from './src/zue.js'

        {
            new Zue({
                el: '#app',
                data() {
                    return {
                        msg: 666
                    }
                }
            })
        }
    </script>

Zue首先执行init初始化操作,代码如下所示:

init(options) {
        const el = query(options.el);
        this.$options = options;
        this._initState();
        this._compile(el);
 }

query通过document.querySelector(el)获取到dom元素,_initStatedata(){ return {msg:666} }函数里返回的对象挂载在Zue对象上,_initState的源码如下所示:

	Object.defineProperty(Zue.prototype, '$data', {
        get () {
            return this._data;
        },
        set (newData) {
            if (newData !== this._data) {
                // this._setData(newData);
            }
        }
    });
    
    Zue.prototype._initState = function () {
        this._initData();
    }

    Zue.prototype._initData = function () {
        var dataFn = this.$options.data;
        var data = this._data = dataFn ? ( typeof dataFn == 'function' ? dataFn() : dataFn ) : {}
        // proxy data on instance
        var keys = Object.keys(data);
        var i, key;
        i = keys.length;
        while (i--) {
            key = keys[i];
            this._proxy(key);
        }
    }
    Zue.prototype._proxy = function (key) {
        var self = this;
        Object.defineProperty(self, key, {
            configurable: true,
            enumerable: true,
            get: function proxyGetter() {
                return self._data[key];
            },
            set: function proxySetter(val) {
                self._data[key] = val;
            }
        });
    }

_compile只做一件事,那便是将我们的{{msg}}转换成666,其代码如下所示:

Zue.prototype._compile = function (el) {
    var vm = this;
    el.childNodes.forEach(node => {
      if (node.nodeType === 3 && /\{\{(.*)\}\}/.test(node.textContent)) {
        node.textContent = vm[RegExp.$1]
      }
    });
}

其中RegExp.$1是捕获正则表达式/\{\{(.*)\}\}/第一个匹配的结果msg,通过前面的_initData,我们已经将msg挂载在Zue上,Zue["msg"]值为666,将666赋值给node.textContent,即可实现值替换

若更改index.html的代码,需要对子元素内部的{{}}实现值替换

	<div id="app">{{msg}}<span>{{text}}</span></div>
    <script type="module">
        import Zue from './src/zue.js'

        {
            new Zue({
                el: '#app',
                data() {
                    return {
                        msg: 666,
                        text: "lucy"
                    }
                }
            })
        }
    </script>

我们需要修改__compile里的源码

import { 
  toArray,
  isElementNode,
  isTextNode
} from './utils.js'
export default function lifecycleMixin(Zue) {
  Zue.prototype._compile = function (el) {
    var vm = this;
    var childNodes = toArray(el.childNodes);
    childNodes.forEach(node => {
      var text = node.textContent;
      var reg = /\{\{(.*)\}\}/;
      if (isElementNode(node)) {
        
      } else if (isTextNode(node) && reg.test(text)) {
        vm.compileText(node, RegExp.$1.trim());
      }
      if (node.childNodes && node.childNodes.length) {
        vm._compile(node); // 迭代替换子元素内部的{{}}
      }
    });

  }
  Zue.prototype.compileText = function (node, exp) {
    node.textContent = this[exp]
  }
}

utils.js的代码如下所示:

function toArray (list, start) {
    start = start || 0
    var i = list.length - start
    var ret = new Array(i)
    while (i--) {
      ret[i] = list[i + start]
    }
    return ret
}
function isElementNode (node) {
  return node.nodeType == 1;
}
function isTextNode (node) {
  return node.nodeType == 3;
}

且慢,有同学可能会问,若修改index.html如下所示,那么This is a test{{msg}}会被覆盖为msg的值吧

	<div id="app">This is a test{{msg}}<span>{{text}}</span></div>
    <script type="module">
        import Zue from './src/zue.js'

        {
            new Zue({
                el: '#app',
                data() {
                    return {
                        msg: 666,
                        text: "lucy"
                    }
                }
            })
        }
    </script>

是的,所以我们需要修改_compile方法,如下所示:

function compileTextNode(node, vm) {
  const reg = /\{\{(.+?)\}\}/g;;
  let txt = node.wholeText;
  txt = txt.replace(reg, (_, gl) => {
    let key = gl.trim();
    let value = vm[key];
    return value;
  });
  node.textContent = txt;
}
export default function lifecycleMixin(Zue) {
  Zue.prototype._compile = function (el) {
    const vm = this;
    const childNodes = toArray(el.childNodes);
    childNodes.forEach(node => {
      const text = node.textContent;
      
      if (isElementNode(node)) {
        
      } else if (isTextNode(node)) {
        compileTextNode(node, vm);
      }
      if (node.childNodes && node.childNodes.length) {
        vm._compile(node);
      }
    });
  }
}

循环遍历,并替换{{}}里面的文本值,即可实现值替换

上面还有一个明显的问题,直接操作dom元素,对textContent进行文本替换,需创建Document Fragment,将el元素拷贝到Document Fragment中,对Document Fragment文本值替换完毕后,再将其appendChildel元素内,代码如下所示:

function compileTextNode(node, vm) {
  const reg = /\{\{(.+?)\}\}/g;;
  let txt = node.wholeText;
  txt = txt.replace(reg, (_, gl) => {
    let key = gl.trim();
    let value = vm[key];
    return value;
  });
  node.textContent = txt;
}
function compileNode(frag, vm) {
  const childNodes = toArray(frag.childNodes);
  childNodes.forEach(node => {
    
    if (isElementNode(node)) {
      
    } else if (isTextNode(node)) {
      compileTextNode(node, vm);
    }
    if (node.childNodes && node.childNodes.length) {
      vm._compile(node);
    }
  });
}
function node2Fragment(el) {
  let fragment = document.createDocumentFragment(), child;

  // 将原生节点拷贝到fragment
  while (child = el.firstChild) {
    fragment.appendChild(child);
  }
  return fragment;
}
export default function lifecycleMixin(Zue) {
  Zue.prototype._compile = function (el) {
    const frag = node2Fragment(el);
    compileNode(frag, this);
    el.appendChild(frag);
  }
}