Vue2源码分析(一)

654 阅读4分钟

1.rollup项目搭建

1.1.依赖

  • rollup:打包工具
  • @babel/core:babel核心模块
  • @babel/preset-env:es6->es5
  • rollup-plugin-babel:rollup和babel之间的桥梁
yarn add rollup @babel/core @babel/preset-env rollup-plugin-babel

1.2.rollup.config.js

import babel from 'rollup-plugin-babel';
export default {
    input:'./src/index.js',
    output:{
        format:'umd',//支持amd和commonjs
        file:'dist/vue.js',
        sourcemap:true,//es5->es6映射文件
        name:'Vue'
    },
    plugins:[
        babel({//使用babel进行转化,排除node_moduels
            exclude:'node_modules/**'
        })
    ]
}

1.3..babelrc

{
    "presets": ["@babel/preset-env"]
}

1.4.package.json

{
    "scripts":{
        "serve":"rollup -c -w"
    }
}

2.响应式数据

测试

<body>
    <div id="app"></div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                name:'zhangsan',
                family:{
                    father:'李四',
                    mather:'王武'
                }
            }
        })
        vm._data.family={
            father:'章六'
        }
        console.log(vm)
    </script>
</body>

2.1.index.js

import { initMixin } from "./init";

/**
 * 
 * @param {*} options 用户传入的选项
 */
function Vue(options){
    //初始化操作
    this._init(options);
}

//扩展原型方法
initMixin(Vue);

export default Vue;

2.2.init.js

import { initState } from "./state";
/**
 * 初始化操作
 * @param {*} Vue 类
 */
export function initMixin(Vue){
    Vue.prototype._init=function(options){
        const vm=this;
        vm.$options=options;
        //对数据进行初始化
        initState(vm);
    }
}

2.3.state.js

import { observe } from "./observer/index";
import {
    isFunction
} from "./utils";

/**
 * 初始化状态
 * @param {*} vm 实例
 */
export function initState(vm) {
    const opt = vm.$options;
    if (opt.data) {
        initData(vm);
    }
}

/**
 * 初始化Data数据
 * @param {*} vm 实例
 */
function initData(vm) {
    let data = vm.$options.data;
    /**
     * //TODO
     * 1.如果data是方法,需要执行,并不this依然是Vue实例
     * 2.需要通过_data将劫持到的数据关联起来
     */
    data = vm._data = isFunction(data) ? data.call(vm) : data;

    //对数据进行劫持
    observe(data);
}

2.4.observer/index.js

import {
    isObject
} from "../utils";

class Observer {
    constructor(data) {
        this.walk(data);
    }

    /**
     * 对象数据劫持
     * @param {*} data 数据
     */
    walk(data) {
        Object.keys(data).forEach(key => {
            defineReactive(data, key, data[key]);
        })
    }
}

/**
 * TODO:Vue2为什么性能不好,主要原因就是数据的劫持的全量劫持
 * @param {*} data 原数据
 * @param {*} key key
 * @param {*} value 值
 */
function defineReactive(data, key, value) {
    observe(value); //TODO:如果value是一个对象,需要对value进行深层次的劫持操作
    Object.defineProperty(data, key, {
        get() {
            return value;
        },
        set(newVal) {
            if (newVal === value) return;
            observe(newVal); //TODO:重新设置的值可能是一个对象,这个时候需要重新对其进行劫持处理
            value = newVal;
        }
    })
}

export function observe(data) {
    //TODO:data必须是一个对象,默认最外层必须是一个对象
    if (!isObject(data)) return;

    return new Observer(data);
}

2.5.utils.js

export function isFunction(val) {
    return typeof val === 'function';
}

export function isObject(val) {
    return typeof val === 'object' && val !== null;
}

3.数据代理

为了方便用户取到data的数据,比如:vm._data.name,可以通过vm.name来取值

state.js

/**
 * 初始化Data数据
 * @param {*} vm 实例
 */
function initData(vm) {
    let data = vm.$options.data;
    /**
     * //TODO
     * 1.如果data是方法,需要执行,并不this依然是Vue实例
     * 2.需要通过_data将劫持到的数据关联起来
     */
    data = vm._data = isFunction(data) ? data.call(vm) : data;

    //对vm._data上的数据进行代理,方便用户后续的取值和设值
    for (let key in data) {
        proxy(vm, '_data', key)
    }
    //对数据进行劫持
    observe(data);
}

/**
 * 对数据进行一层代理,方便用户对数据取值和设值,「vm._data.name='李四',可以直接用vm.name='李四'」
 * @param {*} vm 
 * @param {*} source _data
 * @param {*} key 
 */
function proxy(vm, source, key) {
    Object.defineProperty(vm, key, {
        get() {
            return vm[source][key];
        },
        set(newVal) {
            vm[source][key] = newVal;
        }
    })
}

4.数组响应式

4.1.测试

  • 1.数组里面是对象类型的需要被劫持
  • 2.数组新增的是对象类型需要被劫持
<body>
    <div id="app"></div>
    <script src="./dist/vue.js"></script>
    <script>
        const vm=new Vue({
            el:'#app',
            data:{
                arr:[1,2,{name:'zhangsan'}]
            }
        })
        vm.arr.push({age:18});
        console.log(vm.arr)
    </script>
</body>

4.2.observe/index.js

import {
    arrayMethods
} from "./array";

class Observer {
    constructor(data) {
        //给data上添加__ob__属性,值为Observer实例,并且不可枚举,不然死循环
        Object.defineProperty(data, '__ob__', {
            value: this,
            enumerable: false
        })
        if (Array.isArray(data)) {
            //TODO:数组劫持,数组原来方法的重写
            data.__proto__ = arrayMethods;
            //TODO:如果数组中的数据也可能是对象类型
            this.observeArray(data);
        } else {
            this.walk(data);
        }
    }

    /**
     * TODO:
     * 1.对数组中的数据进行观察,如果是对象需要继续进行劫持
     * 2.新增的数据可能是对象,也需要进行劫持
     * @param {*} data 数组
     */
    observeArray(data) {
        data.forEach(item => observe(item));
    }
}

export function observe(data) {
    //TODO:data必须是一个对象,默认最外层必须是一个对象
    if (!isObject(data)) return;
    //如果观察的数据已经有了__ob__属性,说明这个数据已经被劫持过了,不用再劫持
    if (data.__ob__) return;
    return new Observer(data);
}

4.3.observer/array.js

//原始Array的原型
const oldArrayPrototype = Array.prototype;
//创建一个新的数组原型,arrayMethods.__proto__=Array.propotype
export const arrayMethods = Object.create(oldArrayPrototype);
//需要重写的方法 7个
const methods = [
    'push',
    'unshift',
    'shift',
    'pop',
    'splice',
    'sort',
    'reverse'
];

methods.forEach(method => {
    //用户调用的如果是上面的7种方法,会先走自己重新的方法
    arrayMethods[method] = function (...args) {
        //原始的数组方法调用
        oldArrayPrototype[method].call(this, ...args);
        let inserted;
        switch (method) {
            case 'push':
            case 'unshift':
                inserted = args; //新增内容
                break;
            case 'splice':
                inserted = args.slice(2);
                break;
        }
        //新增的数据需要对其进行劫持 「this.__ob__是Observer实例」
        if (inserted) this.__ob__.observeArray(inserted);
    }
})

5.生成编译模板

5.1.init.js

import { compileToFunction } from "./compiler/index";
import {
    initState
} from "./state";
/**
 * 初始化操作
 * @param {*} Vue 类
 */
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;
        vm.$options = options;
        //对数据进行初始化
        initState(vm);

        if (options.el) {
            /**
             * TODO:将数据挂载到模板上
             * 用户挂载可以通过两种方式,
             * 1.一种自动挂载,new Vue({el:'#app'})
             * 2.手动挂载,vm.$mount('#app')
             */
            vm.$mount(options.el);
        }
    }

    Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options;
        el = document.querySelector(el);
        /**
         * TODO:
         * 1.把模板字符串转化成对应的渲染函数
         * 2.渲染函数执行生成虚拟DOM
         * 3.diff算法,更新虚拟DOM
         * 4.产生真是节点,更新
         */
        if (!options.render) {
            let template = options.template;
            if (!template && el) {
                //获取模版字符串 <div id="app"></div>
                template = el.outerHTML;
                options.render = compileToFunction(template);
            }
        }

    }
}

6.模板解析「词法解析」

6.1.compiler/index.js

import { parserHTML } from "./parser";

/**
 * 模板编译
 * @param {*} template 模板
 */
export function compileToFunction(template){
    parserHTML(template);
}

6.2.compiler/parser.js

const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`; // 标签名 
const qnameCapture = `((?:${ncname}\\:)?${ncname})`; //  用来获取的标签名的 match后的索引为1的
const startTagOpen = new RegExp(`^<${qnameCapture}`); // 匹配开始标签的 <div
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); // 匹配闭合标签的 </div>
//           aa  =   "  xxx "  | '  xxxx '  | xxx
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/; // a=b  a="b"  a='b'
const startTagClose = /^\s*(\/?)>/; //   >  /> 
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{aaaaa}}

/**
 * 词法解析
 * @param {*} html 
 */
export function parserHTML(html) { //<div id="app">{{name}}</div>
    while (html) {
        //<当前的位置
        const textEnd = html.indexOf('<');
        if (textEnd === 0) { //开始位置
            const startTagMatch = parseStartTag(); //解析开始标签
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs);
                continue;
            }
            // ["</div>", "div", index: 0, input: "</div>", groups: undefined]
            const endTagMatch = html.match(endTag);
            if (endTagMatch) {
                end(endTagMatch[1]);
                advance(endTagMatch[0].length);
                continue;
            }
        }
        let text;
        if (textEnd > 0) {
            text = html.substring(0, textEnd);
        }
        if (text) {
            chars(text);
            advance(text.length); //</div>
        }
    }

    function parseStartTag() {
        //<div", "div", index: 0, input: "<div id=\"app\">{{name}}</div>", groups: undefined]
        const start = html.match(startTagOpen);
        if (start) {
            const match = {
                tagName: start[1],
                attrs: []
            }
            advance(start[0].length); // id="app">{{name}}</div>
            let end, attr;
            //不是结束标签,并且有属性
            while (!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                //attr= [" id=\"app\"", "id", "=", "app", undefined, undefined, index: 0, input: " id=\"app\">{{name}}</div>", groups: undefined]
                match.attrs.push({
                    name: attr[1],
                    value: attr[3] || attr[4] || attr[5]
                })
                advance(attr[0].length); // >{{name}}</div>
            }
            if (end) {
                advance(end[0].length); // {{name}}</div>
            }
            return match;
        }
    }

    function advance(len) {
        html = html.substring(len)
    }
}

function start(tagName, attrs) {
    console.log('start', tagName, attrs)
}

function chars(text) {
    console.log('chars', text)
}

function end(tagName) {
    console.log('end', tagName)
}

7.构建AST树

7.1.compiler/parser.js

/构建AST树,栈型结构
let root = null,
    stack = [];

function start(tagName, attrs) {
    //获取父节点
    const parent = stack[stack.length - 1];
    //创建节点
    const element = createAstElement(tagName, attrs);
    if (!root) { //树里还没用东西
        root = element;
    }
    if (parent) { //与父亲建立关联
        element.parent = parent;
        parent.children.push(element);
    }
    stack.push(element);
}

function chars(text) {
    //去除空格
    text = text.replace(/\s/g, '');
    if (text) {
        const parent = stack[stack.length - 1];
        parent.children.push({
            type: 3,
            text
        })
    }
}

function end(tagName) {
    const last = stack.pop();
    if (last.tag !== tagName) throw new Error('标签错误');
}

/**
 * 创建节点
 * @param {*} tagName 标签名称
 * @param {*} attrs 属性
 */
function createAstElement(tagName, attrs) {
    return {
        tag: tagName,
        type: 1, //TODO:标签是1,文本是3
        attrs,
        parent: null,
        children: []
    }
}

8.codeGen生成

8.1.compiler/index.js

import { generate } from "./generate";
import { parserHTML } from "./parser";

/**
 * 模板编译
 * @param {*} template 模板
 */
export function compileToFunction(template){
    const root=parserHTML(template);
    generate(root);
}

8.2.compiler/generate.js

const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g; // {{aaaaa}}
/**
 * 属性处理
 * @param {*} attrs 属性对象 [{name:'id',value:'app'},{name: "style", value: "color:red;background:green"}]
 */
function genProps(attrs) {
    let str = '';
    for (let i = 0; i < attrs.length; i++) {
        const attr = attrs[i];
        if (attr.name === 'style') { //{name: "style", value: "color:red;background:green"}
            let styleObj = {};
            attr.value.replace(/([^;:]+)\:([^;:]+)/g, function () {
                styleObj[arguments[1]] = arguments[2];
            })
            attr.value = styleObj;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;
    }
    return `{${str.slice(0,-1)}}`
}

/**
 * // hello {{name}} world 转化为=> _v("hell"+_s(name)+"world")
 * @param {*} el 
 * @returns 
 */
function gen(el) {
    if (el.type === 1) { //标签
        return generate(el);
    } else { //文本
        const text = el.text;
        if (!defaultTagRE.test(text)) { //不是{{}}包裹的
            return `_v('${text}')`
        } else {
            const tokens = [];
            let match;
            //TODO:defaultTagRE.lastIndex需要制为零,exec会改变下标,每次进入时需要现重新拨回0的位置
            let lastIndex = defaultTagRE.lastIndex = 0;
            while (match = defaultTagRE.exec(text)) {
                let index = match.index; //开始索引
                if (index > lastIndex) {
                    tokens.push(JSON.stringify(text.slice(lastIndex, index))); //hello
                }
                tokens.push(`_s(${match[1].trim()})`); //{{name}}
                lastIndex = index + match[0].length;
            }
            if (lastIndex < text.length) { //
                tokens.push(JSON.stringify(text.slice(lastIndex))); //world
            }
            return `_v(${tokens.join('+')})`;
        }
    }
}

/**
 * 处理孩子
 * @param {*} el =[{"type":3,"text":"{{name}}"}]
 */
function genChildren(el) {
    const children = el.children;
    if (children) {
        return children.map(c => gen(c)).join(',');
    }
    return false;
}

/**
{
    "tag":"div",
    "type":1,
    "attrs":[
        {
            "name":"id",
            "value":"app"
        },
        {
            "name":"style",
            "value":"color:red;background:green"
        }
    ],
    "parent":null,
    "children":[
        {
            "type":3,
            "text":"hell{{name}}world"
        }
    ]
}
 */
export function generate(el) {
    console.log(JSON.stringify(el))
    const children = genChildren(el);
    const code = `_c('${el.tag}',${el.attrs.length?genProps(el.attrs):'undefined'}${children?`,${children}`:''})`;
    console.log(code)
}

9.虚拟DOM实现

9.1.生成render函数

compiler/index.js

import {
    generate
} from "./generate";
import {
    parserHTML
} from "./parser";

/**
 * 模板编译,生成render函数
 * @param {*} template 模板
 */
export function compileToFunction(template) {
    //生成AST树
    const root = parserHTML(template);
    //通过AST树构建codegen 「_('div',{'id':'#app'},'hello')」
    const code = generate(root);
    console.log(code)
    //Function +with构建方式 this=vm
    let render = new Function(`with(this){return ${code}}`);
    return render;
}

9.2.挂载

init.js

Vue.prototype.$mount = function (el) {
    const vm = this;
    const options = vm.$options;
    el = document.querySelector(el);
    /**
     * TODO:
     * 1.把模板字符串转化成对应的渲染函数
     * 2.渲染函数执行生成虚拟DOM
     * 3.diff算法,更新虚拟DOM
     * 4.产生真是节点,更新
     */
    if (!options.render) {
        let template = options.template;
        if (!template && el) {
            //获取模版字符串 <div id="app"></div>
            template = el.outerHTML;
            options.render = compileToFunction(template);
        }
    }
    //组件挂载流程
    mountComponent(vm, el);
}

lifecycle.js

export function lifecycleMixin(Vue){
    Vue.prototype._update=function(vnode){
        const vm=this;
        console.log('vnode',vnode);
    }
}
/**
 * 组件挂载
 * @param {*} vm 
 * @param {*} el <div id='app'></div>
 */
export function mountComponent(vm,el){
    /**
     * TODO:更新函数
     * 1.调用_render生成vdom
     * 2.调用_update进行更新操作
     */
    const updateComponent=()=>{
        vm._update(vm._render());
    }
    updateComponent();
}

9.3.初始化

index.js

//扩展原型方法
initMixin(Vue);
renderMixin(Vue);//_render
lifecycleMixin(Vue);//_update

9.4.生成虚拟DOM

render.js

import {
    createElement,
    createTextElement
} from "./vdom/index";

export function renderMixin(Vue) {
    //处理元素
    Vue.prototype._c = function () {
        const vm = this;
        return createElement(vm, ...arguments);
    }
    //处理文本
    Vue.prototype._v = function (text) {
        const vm = this;
        return createTextElement(vm, text);
    }
    //处理{{}}
    Vue.prototype._s = function (val) {
        if (typeof val === 'object') return JSON.stringify(val);
        return val;
    }

    //render函数,返回虚拟节点
    Vue.prototype._render = function () {
        const vm = this;
        const render = vm.$options.render;
        const vnode = render.call(vm);
        return vnode;
    }
}

vdom/index.js

export function createElement(vm, tag, data = {}, ...children) {
    return vnode(vm, tag, data, data.key, children, undefined);
}

export function createTextElement(vm, text) {
    return vnode(vm, undefined, undefined, undefined, undefined, text);
}
//创建虚拟节点
function vnode(vm, tag, data, key, children, text) {
    return {
        vm,
        tag,
        data,
        key,
        children,
        text,
        //...TODO
    }
}

10.vdom创建真实dom

10.1.将el绑定到vm上

init.js

Vue.prototype.$mount = function (el) {
        const vm = this;
        const options = vm.$options;
        el = document.querySelector(el);
        vm.$el = el;
}        

10.2.虚拟DOM转为真实DOM

lifecycle.js

import { patch } from "./vdom/patch";

export function lifecycleMixin(Vue){
    Vue.prototype._update=function(vnode){
        const vm=this;
        patch(vm.$el,vnode);
    }
}

vdom/patch.js

/**
 * 根据虚拟DOM创建真实DOM
 * @param {*} vnode 虚拟DOM
 */
function createElem(vnode) {
    const {
        tag,
        children,
        text
    } = vnode;
    if (typeof tag === 'string') { //元素
        //vdom会添加一个el属性,对应真实节点
        vnode.el = document.createElement(tag);
        children.forEach(child => {
            vnode.el.appendChild(createElem(child));
        })
    } else { //文本
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

/**
 * 添加新的虚拟DOM,删除老得DOM
 * @param {*} el 可能是真是dom,也可能是虚拟DOM
 * @param {*} vnode 新的虚拟DOM
 */
export function patch(el, vnode) {
    if (el.nodeType === 1) { //是真是DOM
        const parentNode = el.parentNode;
        const elem = createElem(vnode);
        //添加
        parentNode.insertBefore(elem, el.nextSibling);
        //删除老得
        parentNode.removeChild(el);
        return elem;
    } else {

    }
}