最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • Vue2源码分析(一)

    正文概述 掘金(可乐1028)   2021-06-29   638

    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.数据代理

    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 {
    
        }
    }
    

    起源地下载网 » Vue2源码分析(一)

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元