Spiga

前端提升5:手撸vue2响应式+渲染

2025-03-29 16:26:46

阅读vue(2.6)的源码,data响应式大概有如下4个过程:

  1. 数据代理:core/instance/state.js--->vue函数--->initMixin--->initState--->数据遍历--->observer/index.js--->Observer---> defineReactive--->defineProperty
  2. 数据装载:$mount--->装载template--->compileToFunctions--->mountComponet--->创建【渲染Watcher】--->Watcher.get()--->Watcher.get()依赖收集--->Dep.target就是当前的Watcher(data中的属性—dep<----->watcher 形成双向引用)
  3. 数据相应式:需要修改data属性--->触发setter回调执行--->通知dep上的watcher更新dep.notify()--->遍历所有的watcher并调用watcher的update方法--->调用run方法--->调用get()方法实现页面渲染
  4. 数据渲染:vm.__patch__--->createPatchFunction--->patchVnode--->双方都有孩子节点updateChildrendiff代理

以下我们参考vue的源码,记录实现data响应式的全过程

一、数据劫持

  1. 收先我们混入一个方法,给Vue对象添加一个内部_init方法,我们在这个内部方法中去实现需要的功能

    //index.js
    import { initMixin } from "./init";
    
    function Vue(options){ 			// vue2 配置项
        console.log("my vue 6666")
        this._init(options);     	// vue 内部约定 私有的方法或属性  一般是以 $ _ 开头
    }
    
    initMixin(Vue);   				// 一执行,Vue就有_init方法
    
    export default Vue
    
    //init.js 初始化
    import { initState } from "./state";
    
    // 混入 类似于 对象语言的继承
    // 在不改变这个函数的前提下,给这个函数增加方法  增强功能
    // 原理是在prototype上添加方法
    export function initMixin(Vue) {
        Vue.prototype._init = function (options) {
            const vm = this;
    
            vm.$options = options;  // 将配置项暂存到vm实例上
    
            // 初始化 数据 
            initState(vm);
        }
    }
    
    //state.js 初始化数据
    import { observe } from "./observer/index";
    
    export function initState(vm) {
        const opts = vm.$options;
    
        if (opts.props) {
            // 处理父传子的属性
        }
        if (opts.methods) {
            // 处理方法
        }
        if (opts.data) {
            initData(vm);
        }
    }
    
    // 初始化 数据
    function initData(vm) {
        // 拿到数据
        let data = vm.$options.data;
        // 数据可能是对象  也可能是函数
        data = vm._data = typeof data === 'function' ? data.call(vm) : data;
    
        // 监听处理
        observe(data);
    }
    
  2. 进行监听处理

    // 过程代码,之后还会改造
    class Observer {
        constructor(data) {
            this.walk(data);
        }
        // 遍历属性
        walk(data) {
            Object.keys(data).forEach(key => {
                // 需要对每个属性做响应式处理
                console.log(key);
                defineReactive(data, key, data[key]);
            })
        }
    }
    
    // 数据响应式处理
    function defineReactive(data, key, value) {
        // 如果传入的是一个对象,则递归监听
        observe(value);
    
        Object.defineProperty(data, key, {
            get() {
                console.log("触发get", key);
                return value;
            },
            set(newValue) {
                console.log("触发set", key);
                // 如果值没有变化,则忽略
                if (value === newValue) return;
    
                // 设置属性可能也是一个对象,需要监听处理
                observe(newValue)
    
                value = newValue;
            }
        })
    }
    
    // 监听函数可以监听 只能监听对象
    export function observe(data) {
        return new Observer(data);
    }
    
  3. 监听数组

    由于我们不能修改数组的原生方法,为了实现原生方法的监听,我们可以为数组的方法创建同名的新方法,在同名方法里面实现我们的监听逻辑

    //observer/array.js 
    // 数组监听
    // 数组的旧原型对象
    let oldArrayMethods = Array.prototype;
    
    // 基于array原型创建一个新的对象,可以找到数组原型上的方法,而且修改对象不影响原生数组方法
    export let arrayMethods = Object.create(oldArrayMethods);
    
    // 在新的对象上实现自己的数组方法
    let methods = [
        "push",          // 插入到最后
        "pop",
        "shift",
        "unshift",       // 插入到最前
        "sort",
        "reverse",
        "splice"         // 在某个位置上  先删除  后添加
    ]
    
    methods.forEach(method => {
        // 方法的劫持
        arrayMethods[method] = function(...args) {
            // 执行原生方法
            let result = oldArrayMethods[method].apply(this, args);
    
            // 拿到Observer
            const ob = this.__ob__;
    
            // push unshift splice 都可以新增, 可能是对象, 如果是对象就要监听处理
            let inserted;
            switch(method) {
                case 'push':
                case 'unshift':
                    inserted = args;
                    break;
                case 'splice':  // 如何取到要添加数据? 从第2个参数开始截取
                    inserted = args.slice(2);
                    break;
                default:
                    break;
            }
    
            // 新添加的数据可能是对象, 需要做响应式处理
            inserted && ob.observeArray(inserted);
    
    
            return result;
        }
    })
    
  4. 避免重复监听

    目前我们已经完成了data的监听了,但是还有一个问题,已经监听过的对象,如何避免再次被监听呢?

    解决办法: 在监听时,给当前对象添加一个标识属性 ob 引用的是Observe实例对象

    我们修改Observer类,如下是最终的实现

    //observer/index.js 
    // 数据监听处理
    import {isObject} from "../utils.js"
    import { arrayMethods } from "./array.js";
    
    class Observer {
        constructor(data) {
    
            // 给每个对象添加__ob__ 引用当前的Observer实例
            Object.defineProperty(data, "__ob__", {
                enumerable: false,
                configurable: false,
                value: this
            })
    
            if (Array.isArray(data)) {
    
                // data设置新的代理对象
                data.__proto__ = arrayMethods;
    
                this.observeArray(data);
            } else {
                this.walk(data);
            }
        }
        // 遍历属性
        walk(data) {
            Object.keys(data).forEach(key => {
                // 需要对每个属性做响应式处理
                console.log(key);
                defineReactive(data, key, data[key]);
            })
        }
        // 监听数组中的每一项
        observeArray(data) {
            for (let i=0; i<data.length; i++) {
                observe(data[i]);
            }
        }
    }
    
    // 数据响应式处理
    function defineReactive(data, key, value) {
    
        // 如果传入的是一个对象,则递归监听
        observe(value);
    
        Object.defineProperty(data, key, {
            get() {
                console.log("触发get", key);
                return value;
            },
            set(newValue) {
                console.log("触发set", key);
                // 如果值没有变化,则忽略
                if (value === newValue) return;
    
                // 设置属性可能也是一个对象,需要监听处理
                observe(newValue)
    
                value = newValue;
            }
        })
    }
    
    // 监听函数可以监听 只能监听对象
    export function observe(data) {
        // 如果不对象 则返回
        if (!isObject(data)) {
            return;
        }
    
        // 避免二次监听
        if (data.__ob__ instanceof Observer) {
            return;
        }
    
        // 对对象属性进行监听处理(defineProperty)
        return new Observer(data);
    }
    
    //utils.js 
    // 工具类
    /**
     * 判断data是否是一个对象
     * @param {} data 
     * @returns 
     */
    export function isObject(data) {
        return typeof data === 'object' && data !== null;
    }
    

通过以上的过程,我们已经可以实现data的数据监听了。

但我们的任务还没有完,当数据发生变化时,如何在页面上进行渲染呢?

在vue中,需要实现响应式的数据格式如下,接下来我们实现把数据装载到vue对象中

<div><p>{{user.name}}</p></div>

二、模版渲染

1. 模版编译原理

首选创建一个方法来完成模版编译

//compiler/index.js
// 模板编译
import { generator } from "./generator";
import { parseHTML } from "./parser";

// 生成虚拟dom
export function compileToFunction(template) {
    // 实现模板编译

    // 1. 先把html转化为ast语法树
    // 2. 标识静态树, 树的遍历
    // 3. 通过ast产生的语法树, 生成渲染后的代码
    let ast = parseHTML(template);
    console.log("ast", ast)
}

//compiler/parser.js
// 标签名正则
// * 匹配前面的子表达式0次或多次
// 分2部分:前面可以为字母或下划线, 后面可以为字母,数字,下划线等
const ncname = `[a-zA-Z_][\\-\\.0-9_a-zA-Z]*`;
// ?: 匹配不捕获 div  im:div
const qnameCapture = `((?:${ncname}\\:)?${ncname})`;
// 开始标签正则 以<开头标签  用来匹配开始标签
const startTagOpen = new RegExp(`^<${qnameCapture}`); 
// 结束标签正则 [^>]不能有>标签, 如 </div>
const endTag = new RegExp(`^<\\/${qnameCapture}[^>]*>`); 
// 用来匹配属性标签
// \s* 匹配任何空白字符 + 匹配前面的子表达式一次或多次
// 第一个分组是限制一些非法字符
// 第二个分组是捕获等号2边内容,等号右边内容分3种情况
// 1. 捕获用双引号包括内容, 里面不能再有双引号
// 2. 捕获用单引号包括内容
// 3. 捕获任何内容,但排除特定符号
const attribute = /^\s*([^\s"'<>\/=]+)(?:\s*(=)\s*(?:"([^"]*)"+|'([^']*)'+|([^\s"'=<>`]+)))?/;
// 标签关闭正则 <div>  <div />
const startTagClose = /^\s*(\/?)>/; 
// 模板内容 {{msg}}
// win \r\n linux \n  mac \r
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

// 切香肠
export function parseHTML(html) {
    // ast语法树
    let root; // 根
    let currentParent; // 父节点
    let stack = [];  // 用来判断标签是否正常闭合 <div>  </div>

    function createASTElement(tagName, attrs) {
        return {
            tag: tagName,
            attrs,
            children: [],
            parent: null,
            type: 1   // 1--普通元素  3-文本 <div>  sss  </div>
        }
    }

    // 开始标签处理
    function start(tagName, attrs) {
        let element = createASTElement(tagName, attrs);
        if (!root) {
            root = element; // 根节点
        }
        currentParent = element; // 指定父节点
        stack.push(element);   // 维护父子关系
    }

    function end(tagName) {
        let element = stack.pop(); // 取当前的元素
        currentParent = stack[stack.length - 1];  // 取当前元素上一级元素
        if (currentParent) {
            element.parent = currentParent;
            currentParent.children.push(element);
        }
    }

    // 遍历html
    while(html) {
        let textEnd = html.indexOf("<");
        if (textEnd === 0) {
            // 正常处理  开始捕获开始标签
            const startTagMatch = parseStartTag();
            console.log("开始标签:", startTagMatch)
            if (startTagMatch) {
                start(startTagMatch.tagName, startTagMatch.attrs);
            }
            // 结束标签
            const endTagMatch = html.match(endTag)
            if (endTagMatch) {
                // 结束标签 不用处理,直接删除
                advance(endTagMatch[0].length);
                end(endTagMatch[1]);
            }
        }

        // 如果不是0, 说明是文本或换行符,需要清除
        let text;
        if (textEnd > 0) {
            text = html.substring(0, textEnd); // 将标签前面的内容截取
            // 过滤空格换行符,将文本保存下来
            chars(text);
        }

        // 如果没有文本 只有换行符 空格
        if (text) {
            advance(text.length);
        }
    }

    // 保存文本
    function chars(text) {
        // 去掉空格
        text = text.replace(/\s/g, '');
        if (text) {
            currentParent.children.push({
                text,
                type: 3
            })
        }
    }

    // 将已经匹配的内容切除
    function advance(len){
        html = html.substring(len);
    }

    // 匹配开始标签
    function parseStartTag() {
        const start = html.match(startTagOpen);// 匹配开始标签
        if (start) {
            const match = {
                tagName: start[1], // 匹配的标签名
                attrs: []
            }
            // 切香肠
            advance(start[0].length);
            // 继续后面的匹配
            let end, attr; // 匹配结束标签 属性
            while(!(end = html.match(startTagClose)) && (attr = html.match(attribute))) {
                // 存储标签的属性和属性值
                match.attrs.push({name: attr[1], value: attr[3] || attr[4] || attr[5]})
                // 切香肠
                advance(attr[0].length)
            }
            // 匹配关闭标签 
            if (end) {
                advance(end[0].length);
                return match;
            }
        }
    }

    return root;
}

在init中调用模版编译函数

//init.js
// 初始化
import { compileToFunction } from "./compiler/index";
import { initState } from "./state";

// 混入 类似于 对象语言的继承
// 在不改变这个函数的前提下,给这个函数增加方法  增强功能
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;

        vm.$options = options;  // 将配置项暂存到vm实例上
        
        // 初始化 数据 
        initState(vm);

        // 需要通过模板渲染页面
        if (vm.$options.el) {
            vm.$mount(vm.$options.el)
        }
    }

    Vue.prototype.$mount = function(el) {
        const vm = this;
        // el 可能是字符串 也可能是dom对象
        el = document.querySelector(el);

        const opts = vm.$options;

        // 如果没有render函数,要创建一个render函数, 使用id=app模板
        if (!opts.render) {
            let template = opts.template;
            // 如果没有在vue中配置模板, 那就取el为模板
            if (!template && el) {
                template = el.outerHTML;
            }
            // console.log("template:", template)
            // 模板编译
            compileToFunction(template);
        }
    }
}

2. 模板渲染函数

模版编译完成时候,接下来我们添加一个模版渲染函数。

我们需要渲染的数据格式是这样的{{xxx.bb.cc}},因此我们的模版渲染函数需要能从对象中找的元素属性

//compiler/generator.js
// 构建模板渲染函数 {{xxx.bb.cc}} 应该要从对象中查找
const defaultTagRE = /\{\{((?:.|\r?\n)+?)\}\}/g

// 处理子元素
function gen(node) {
    // 如果是元素则递归处理
    if (node.type === 1) {//  元素
        return generator(node);
    } else {
        // 处理文本  hello {{msg}}---{{msg}}
        let text = node.text;
        if (!defaultTagRE.test(text)) {
            // 单纯文本  就可以直接dom写入  _v()表示写文本函数
            return `_v(${JSON.stringify(text)})`     // _v('hello')
        } else {
            // {{}} 
            let tokens = [];  // 保存一点点截取的字符串,最后拼接起来
            // 最后索引
            let lastIndex = defaultTagRE.lastIndex = 0;
            let match, index;
            while(match = defaultTagRE.exec(text)) {
                // 匹配第一个{{}}的索引位置
                index = match.index;
                // 截取表达式{{}}左边内容
                tokens.push(JSON.stringify(text.slice(lastIndex,index)));
                // 取出{{}}表达式的内容 拼函数 _s()
                tokens.push(`_s(_data.${match[1].trim()})`)
                // 调整lastIndex的位置  下一次匹配的开始位置
                lastIndex = index + match[0].length;
            }
            if (lastIndex < text.length) {
                // 截取{{}}右边的内容
                tokens.push(JSON.stringify(text.slice(lastIndex)));
            }
            // 拼接所有内容
            return `_v(${tokens.join('+')})`;
        }
    }
}
//   hello {{msg}}---{{msg}}
// 1.    hello 切香肠
// 2. {{msg}} _s(msg) 切香肠
// 3. --- 切掉
// 4. {{msg}} _s(msg)

// 判断有没有子元素
function genChildren(el) {
    const children = el.children;
    if (children) {
        return children.map(c => gen(c)).join(",")
    } else {
        return false;
    }
}

// 模板的处理: _c("xxx", "yyy", {xx:yy})
// js 字符串表达式: function fun(){}  fun(x,y,z)   执行: eval(js字符串表达式)
export function generator(el) {
    // 遍历所有的子元素
    let children = genChildren(el);
    // console.log("children:", children)
    // _c()表示:生成元素函数 参数:元素名,元素属性,元素子集
    let code = `_c("${el.tag}", ${
        el.attrs.length ? `${genProps(el.attrs)}` : undefined
    } ${
        children ? `, ${children}` : ""
    })`
    return code;
}

// 处理元素的属性  {}
function genProps(attrs) {
    let str = "";
    for (let i=0; i<attrs.length; i++) {
        let attr = attrs[i]; // 取第一个属性
        if (attr.name === 'style') {
            // style  color:red; font-size: 30;
            let obj = {}
            attr.value.split(';').forEach(item => {
                let [key, value] = item.split(":");
                obj[key] = value;
            })
            // 将原来的属性value变成obj
            attr.value = obj;
        }
        str += `${attr.name}:${JSON.stringify(attr.value)},`;  // += "class: test, style:{color:red,xxx:yyy}"
    }
    // 最后的属性,需要去掉逗号
    return `{${str.slice(0, -1)}}`
}

// 字符串代码  可以传参  jsonp  eval  with(this)
// _c("div", {id:"app"}, 
//   _c("div", {class:"test",style:{"color":"red"," font-size":" 30"}}, 
//     _c("span", undefined, _v("hello")),))


// "with(this){ 
//     return _c("div", {id:"app"} , _c("div", {class:"test",style:{"color":"red"," fontSize":" 30"}} , 
//     _c("span", undefined , _v("hello")),_v(""+_s(_data.msg)))) 
//     }"

在init中调用模版渲染函数

//compiler/index.js
// 模板编译
import { generator } from "./generator";
import { parseHTML } from "./parser";

// 生成虚拟dom
export function compileToFunction(template) {
    // 实现模板编译

    // 1. 先把html转化为ast语法树
    // 2. 标识静态树, 树的遍历
    // 3. 通过ast产生的语法树, 生成渲染后的代码
    let ast = parseHTML(template);
    console.log("ast", ast)

    // 构建渲染函数
    let code = generator(ast);
    // console.log("code:", code)

    // 绑定对象作用域   this==当前的组件对象Component
    code = `with(this){ \r\n return ${code} \r\n }`
    
    // 将js字符串表达式转为函数
    let render = new Function(code);
    console.log("code:", render)
    return render;
}

这里特别注意一下with方法,with方法在平时一般不建议使用,它在数据量大的时候存在性能问题。作者这里使用with方法应该是权衡了很多实现之后,最终还是选择使用with来实现的。

三、虚拟DOM

1. 挂载组件监听变化

首先我们在index中混入一个_update方法

//index.js
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";

function Vue(options){ // vue2 配置项
    console.log("my vue 6666")
    this._init(options);     // vue 内部约定 私有的方法或属性  一般是以 $ _ 开头
}

initMixin(Vue);   // 一执行,Vue就有_init方法
lifeCycleMixin(Vue);  // 混入_udpate方法

export default Vue

接下来我们来创建一个生命周期过程,完成组件的挂载和监听

//init.js
import { mountComponent } from "./lifecycle";
export function initMixin(Vue) {
    // 其他逻辑

    Vue.prototype.$mount = function(el) {
        const vm = this;
        // el 可能是字符串 也可能是dom对象
        el = document.querySelector(el);

        // 其他逻辑
        
        // 组件挂载
        mountComponent(vm, el)
    }
}

//lifecycle.js
import Watcher from "./observer/watcher"

// 混入mixin 继承
export function lifeCycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        console.log("vnode:", vnode)
        // 如果有子对象要对比更新 就要用diff算法
    }
}

// 挂载组件
export function mountComponent(vm, el) {
    // vue渲染过程中,会创建一个 “渲染Watcher”
    // 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
    // 不能够在修改属性后渲染页面,需要Watcher监听

    // 渲染函数
    const updateComponent = ()=>{
        let vnode = "";
        // 这里需要拿到虚拟dom, 在下一步实现
        vm._update(vnode)
    }

    let cb = ()=>{}
    new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)
}

// 渲染watcher
class Watcher {
    /**
     * 参数
     * @param {*} vm vue实例
     * @param {*} exprOrFn 表达式 或 函数
     * @param {*} cb  回调函数
     * @param {*} options 配置项
     */
    constructor(vm, exprOrFn, cb, options) {
        exprOrFn();
    }
}

export default Watcher

2. 虚拟dom生成与渲染

我们继续在index中混入一个_render方法

//index.js
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";

function Vue(options){ // vue2 配置项
    console.log("my vue 6666")
    this._init(options);     // vue 内部约定 私有的方法或属性  一般是以 $ _ 开头
}

initMixin(Vue);   // 一执行,Vue就有_init方法
lifeCycleMixin(Vue);  // 混入_udpate方法
renderMixin(Vue);  // 混入 _render方法

export default Vue

生成虚拟dom

//render.js
// 生成虚拟dom
// 需要实现_c:创建虚拟节点 _v:创建文本虚拟节点 _s:可能是对象
import { createElement, createTextVNode } from "./vdom/create-element"

// 将vue对象中混入 _render 方法
export function renderMixin(Vue) {
    Vue.prototype._c = function(){
        // 创建虚拟节点  arguments:函数的所有形参和实参集合
        console.log("创建虚拟节点")
        return createElement(...arguments);
    }

    Vue.prototype._v = function(text){
        // 创建文本虚拟节点
        console.log("创建文本虚拟节点")
        return createTextVNode(text)
    }

    Vue.prototype._s = function(val){
        // {{msg}} {{obj.name}} {{json}}
        return val == undefined ? "" : (typeof val === 'object' ? JSON.stringify(val) : val);
    }

    // 渲染
    Vue.prototype._render = function() {
        // 调用compileToFunction生成的render函数
        const vm = this;
        const { render } = vm.$options;

        let vnode = render.call(vm);

        return vnode;
    }
}

//vdom/create-element.js
// 构建虚拟dom对象  就是用js对象描述的一个对象
// 创建文本节点
export function createTextVNode(text) {
    return vnode(undefined, undefined, undefined, undefined, text);
}

// 创建虚拟dom对象
export function createElement(tag, data = {}, ...children) {
    return vnode(tag, data, data.key, children)
}

// 创建虚拟节点对象,用于描述dom结构
export function vnode(tag, data, key, children, text) {
    return {
        tag,
        data,
        key,
        children,
        text
    }
}


/* 
    <ul id="my-id">
        <li v-for="(i,item) in list" key="i">{{item}}</li>
    </ul>

    编译后的渲染函数
    createElement(
        "ul",
        // 标签上的属性 用对象存储键值对
        {
            attr: {
                id: 'my-id'
            }
        },
        [
            createElement("li", 1)
            createElement("li", 2)
            createElement("li", 3)
        ]
    )

    执行渲染函数结果  虚拟dom

    vnode  用js对象形式表示
    tag: ul li
    data: array[]
    children: [] 
*/

然后我们在lifecycle中将没有写完的逻辑补上

//lefecycle.js
import Watcher from "./observer/watcher"
import { patch } from "./vdom/patch";

// 混入mixin 继承
export function lifeCycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        console.log("vnode:", vnode)
        // 如果有子对象要对比更新 就要用diff算法
        // 渲染真实的dom
        const vm = this;
        vm.$el = patch(vm.$el, vnode);
    }
}

// 挂载组件
export function mountComponent(vm, el) {
    // vue渲染过程中,会创建一个 “渲染Watcher”
    // 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
    // 不能够在修改属性后渲染页面,需要Watcher监听

    // 渲染函数
    const updateComponent = ()=>{
        let vnode = "";
        // 先拿到虚拟dom,  vue2引入虚拟dom
        vm._update(vm._render())
    }

    let cb = ()=>{}
    new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)
}

// 主流 chrome 内核 webkit 处理dom用 dom模块
// js操作dom成本高,  提高效率  dom模块批量处理

//vdom/patch.js
/**
 * 给虚拟dom打补丁
 * @param {*} oldVnode 旧的el对象
 * @param {*} newVnode 新的虚拟dom对象
 */
export function patch(oldVnode, newVnode){
    // 根据节点的类型 判断是否是真实元素  el-1  文本-3
    const isRealElement = oldVnode.nodeType;
    if (isRealElement) {
        // 是真实的dom
        const oldElm = oldVnode;
        const parentElm = oldElm.parentNode;

        // 创建元素 操作dom
        let el = createElm(newVnode);
        // 将新生成的元素插入到旧元素后面
        parentElm.insertBefore(el, oldElm.nextSibling);
        // 将旧的真实元素删除
        parentElm.removeChild(oldElm);
    } else {
        // 虚拟dom diff
    }
}

// 创建元素
export function createElm(vnode) {
    let {tag, children, data, key, text} = vnode;
    if (tag) {
        // 元素
        vnode.el = document.createElement(tag);   // dom操作--浏览器内核--dom模块
        // 处理元素属性
        updateProperties(vnode);
        children.forEach(child => {               // 在浏览器内存中处理
            // 递归创建dom元素,并添加到父元素中
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 文本
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

// 元素属性更新
function updateProperties(vnode) {
    let el = vnode.el;
    // 属性
    let newProps = vnode.data || {}

    for (let key in newProps) {
        if (key === 'style') {
            // 当作对象处理
            for (let styleName in newProps.style) {
                // style="color:red; font-size: 30;"
                // 添加一个style对象
                el.style[styleName] = newProps.style[styleName];
            }
        } else {
            // 单一属性处理
            el.setAttribute(key, newProps[key]);
        }
    }
}

我们再添加一个数据代理

// state.js
// 初始化数据
import { observe } from "./observer/index";

export function initState(vm) {
    const opts = vm.$options;

    if (opts.props) {
        // 处理父传子的属性
    }
    if (opts.methods) {
        // 处理方法
    }
    if (opts.data) {
        initData(vm);
    }
}

// 增加代理属性访问, vm._data.msg  => vm.msg  this.msg
// target---代理对象   property--代理对象属性  key---属性的属性
function proxy(target, property, key) {
    Object.defineProperty(target, key, {
        get(){// vm[_data][msg]
            return target[property][key];
        },
        set(newValue) {
            // vm[_data][msg] = 'test'
            target[property][key] = newValue;
        }
    })
}

// 初始化 数据
function initData(vm) {
    // 拿到数据
    let data = vm.$options.data;
    // 数据可能是对象  也可能是函数
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;

    // 加一层数据访问代理
    for (let key in data) {
        proxy(vm, "_data", key);
    }

    // 监听处理
    observe(data);
}

四、生命周期合并

我们先在工具类中完善常见的生命周期工具方法

//utils.js
// 工具类
/**
 * 判断data是否是一个对象
 * @param {} data 
 * @returns 
 */
export function isObject(data) {
    return typeof data === 'object' && data !== null;
}
/**
 * 判断是否为函数
 * @param {*} data 
 * @returns 
 */
export function isFunction(data) {
    return typeof data === 'function' && data !== undefined;
}

// 常见生命周期
const LIFECYCLE_HOOKS = [
    'beforeCreate',
    'created',
    'beforeMount',
    'mounted',
    'beforeUpdate',
    'updated'
]

// 生命周期钩子函数,返回[]
function mergeHook(parentVal, childVal) {
    if (childVal) {
        // 如果有子生命周期
        if (parentVal) { // 是一个数组
            // 第二次mixin就会走这
            return parentVal.concat(childVal);
        } else {
            return [childVal];
        }
    } else {
        return parentVal;
    }
}

// 初始化生命周期对象,{name: fn} fn负责合并相同的生命周期fn
let lifecycles = {}
// 生命周期钩子初始化
LIFECYCLE_HOOKS.forEach(hook => {
    lifecycles[hook] = mergeHook;
})

/**
 * 合并配置项
 * @param {*} parentVal 父
 * @param {*} childVal 子
 */
export function mergeOptions(parentVal, childVal) {
    const options = {};

    // 合并父中的options
    for (let key in parentVal) {
        // 第一次为空  第二次是有数据的
        mergeField(key);
    }

    // 合并子中的options
    for (let key in childVal) {
        if (!parentVal.hasOwnProperty(key)) {
            mergeField(key);
        }
    }

    function mergeField(key) {
        // 合并生全周期
        
        if (lifecycles[key]) {
            options[key] = lifecycles[key](parentVal[key], childVal[key]);
        } else if (isObject(parentVal[key]) && isObject(childVal[key])) {
            // 父子都是对象
            // 用子对象覆盖父对象
            options[key] = Object.assign(parentVal[key], childVal[key]);
        } else if (parentVal[key] && isFunction(parentVal[key]) || childVal[key] && isFunction(childVal[key]) ){
            // 判断data 必须是一个函数
            // 拿到data的数据, 然后用子覆盖父的属性
            let _parentVal = {}, _childVal = {};
            if (parentVal[key]) {
                // 如果父的data()存在
                _parentVal = isFunction(parentVal[key]) ? parentVal[key].call() : parentVal[key];
            }
            if (childVal[key]) {
                _childVal = isFunction(childVal[key]) ? childVal[key].call() : childVal[key];
            }
            // 用子的数据将父覆盖
            options[key] = Object.assign(_parentVal, _childVal);
        } else {
            if (childVal[key] == null) {// 没有子属性
                options[key] = parentVal[key];
            } else {
                options[key] = childVal[key]; // 子替换父
            }
        }
    }

    return options;
}

接着我们在index中添加初始化一个全局api,调用工具类中的mergeOptions函数,完成生命周期合并

//index.js
import { initGlobalAPI } from "./global-api/index";
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";

function Vue(options){ // vue2 配置项
    console.log("my vue 6666")
    this._init(options);     // vue 内部约定 私有的方法或属性  一般是以 $ _ 开头
}

initMixin(Vue);   // 一执行,Vue就有_init方法
lifeCycleMixin(Vue);  // 混入_udpate方法
renderMixin(Vue);  // 混入 _render方法

// 初始化全局api
initGlobalAPI(Vue);

export default Vue

//global-api/index.js
// 全局api初始化
import { mergeOptions } from "../utils";

export function initGlobalAPI(Vue) {
    Vue.options = {};

    Vue.mixin = function(mixin) {
        // 将一个option合并
        // 生命周期合并 数组
        // data 当前的为准
        this.options = mergeOptions(this.options, mixin);
        console.log("options:", this.options)
    }
}

生命周期的加载

//lifecycle.js
import Watcher from "./observer/watcher"
import { patch } from "./vdom/patch";

// 混入mixin 继承
export function lifeCycleMixin(Vue) {
    Vue.prototype._update = function(vnode) {
        console.log("vnode:", vnode)
        // 如果有子对象要对比更新 就要用diff算法
        // 渲染真实的dom
        const vm = this;
        vm.$el = patch(vm.$el, vnode);
    }
}

// 挂载组件
export function mountComponent(vm, el) {
    // vue渲染过程中,会创建一个 “渲染Watcher”
    // 数据响应式, Object.defineProperty 仅在属性中添加getter/setter
    // 不能够在修改属性后渲染页面,需要Watcher监听

    // 渲染前钩子
    callHook(vm, 'beforeMount')

    // 渲染函数
    const updateComponent = ()=>{
        let vnode = "";
        // 先拿到虚拟dom,  vue2引入虚拟dom
        vm._update(vm._render())// 调用渲染,要读取属性,触发getter
    }

    let cb = ()=>{}
    new Watcher(vm, updateComponent, cb, true /* 渲染Watcher */)

    // 渲染后钩子
    callHook(vm, 'mounted');
}

// 主流 chrome 内核 webkit 处理dom用 dom模块
// js操作dom成本高,  提高效率  dom模块批量处理

// 生命周期的回调方法
// 在vue实例的创建---使用---销毁过程中 添加对应的事件钩子, 你只需要注入对应的钩子函数即可
// webpack--tapable
export function callHook(vm, hook) {
    let handlers = vm.$options[hook]; // 生命周期函数 created---[fn, fn]
    if(handlers) {
        // 如果你配置了钩子函数
        for (let i=0; i<handlers.length; i++) {
            handlers[i].call(vm);
        }
    }
}

//init.js
// 初始化
import { compileToFunction } from "./compiler/index";
import { callHook, mountComponent } from "./lifecycle";
import { initState } from "./state";
import { mergeOptions } from "./utils";

// 混入 类似于 对象语言的继承
// 在不改变这个函数的前提下,给这个函数增加方法  增强功能
export function initMixin(Vue) {
    Vue.prototype._init = function (options) {
        const vm = this;

        // vm.$options = options;  // 将配置项暂存到vm实例上
        vm.$options = mergeOptions(vm.constructor.options, options);
        
        // 定义加载前事件
        callHook(vm, 'beforeCreate');
        // 初始化 数据 
        initState(vm);

        // 定义已创建事件
        callHook(vm, 'created');

        // 需要通过模板渲染页面
        if (vm.$options.el) {
            vm.$mount(vm.$options.el)
        }
    }

    Vue.prototype.$mount = function(el) {
        const vm = this;
        // el 可能是字符串 也可能是dom对象
        el = vm.$el = document.querySelector(el);

        const opts = vm.$options;

        // 如果没有render函数,要创建一个render函数, 使用id=app模板
        if (!opts.render) {
            let template = opts.template;
            // 如果没有在vue中配置模板, 那就取el为模板
            if (!template && el) {
                template = el.outerHTML;
            }
            // console.log("template:", template)
            // 模板编译
            const render = compileToFunction(template);
            // 全局函数 将render添加到全局配置对象中
            opts.render = render;
        }
        // 组件挂载
        mountComponent(vm, el)
    }
}

五、响应式处理

1. 依赖收集

数据已经装载了,生命周期也有了,接下来我们如何去收集呢?

这时我们需要一个全局变量Dep.target,

  1. 打开页面, vue实例化,模板解析,生成渲染函数_render, 挂载页面
  2. 先执行 new Watcher(), 把watcher实例保存到全局 Dep.target.
  3. 接着执行vm._render(), 触发getter回调,在回调中就可以拿到当前Watcher(dep.target)
//observer/dep.js
// 属性的依赖
let id = 0;  // 每个属性的dep实例编号

// Dep <===> Watcher 双向依赖
class Dep {
    constructor() {
        this.id = id++;
        this.subs = []; // 用于存储 渲染Watcher
    }

    depend(){
        // 调用watcher的方法引用dep
        Dep.target.addDep(this);
    }
    addSub(watcher){
        // dep关联watcher
        this.subs.push(watcher);
    }

    // 修改属性时, 触发setter回调, 需要通知deps上面所有的watcher更新渲染页面
    notify(){
        this.subs.forEach(watcher => watcher.update())
    }
}

// 全局变量
Dep.target = null;
const stack = [];

// 压入watcher
export function pushTarget(watcher) {
    stack.push(watcher);
    // 设置全局变量,便于在getter中访问
    Dep.target = watcher;
}
// 抛出watcher
export function popTarget() {
    // 抛出栈最上面的watcher
    stack.pop();
    // 修改dep.target全局变量的引用
    Dep.target = stack[stack.length - 1];
}

export default Dep;

响应式处理,形成双向依赖关系

//observer/index.js
// 数据监听处理
import {isObject} from "../utils.js"
import { arrayMethods } from "./array.js";
import Dep from './dep.js'

class Observer {
    constructor(data) {

        // 给每个对象添加__ob__ 引用当前的Observer实例
        Object.defineProperty(data, "__ob__", {
            enumerable: false,
            configurable: false,
            value: this
        })
        
        if (Array.isArray(data)) {

            // data设置新的代理对象
            data.__proto__ = arrayMethods;

            this.observeArray(data);
        } else {
            this.walk(data);
        }
    }
    // 遍历属性
    walk(data) {
        Object.keys(data).forEach(key => {
            // 需要对每个属性做响应式处理
            console.log(key);
            defineReactive(data, key, data[key]);
        })
    }
    // 监听数组中的每一项
    observeArray(data) {
        for (let i=0; i<data.length; i++) {
            observe(data[i]);
        }
    }
}

// 数据响应式处理
function defineReactive(data, key, value) {

    // 如果传入的是一个对象,则递归监听
    observe(value);

    // 属性--dep依赖
    let dep = new Dep();

    Object.defineProperty(data, key, {
        get() {
            if (Dep.target) {
                // 属性添加dep依赖,依赖渲染watcher
                dep.depend();   // 依赖收集
            }
            console.log("触发get", key, dep);
            return value;
        },
        set(newValue) {
            console.log("触发set", key, dep);
            // 如果值没有变化,则忽略
            if (value === newValue) return;

            // 设置属性可能也是一个对象,需要监听处理
            observe(newValue)

            value = newValue;

            // 通知deps--watcher更新
            dep.notify();
        }
    })
}

// 监听函数可以监听 只能监听对象
export function observe(data) {
    // 如果不对象 则返回
    if (!isObject(data)) {
        return;
    }

    // 避免二次监听
    if (data.__ob__ instanceof Observer) {
        return;
    }

    // 对对象属性进行监听处理(defineProperty)
    return new Observer(data);
}

//observer/watcher.js
// 本节后面还要在该类添加计算属性
// 渲染watcher
import { popTarget, pushTarget } from "./dep";

// watcher的编号
let id = 0;

class Watcher {
    /**
     * 参数
     * @param {*} vm vue实例
     * @param {*} exprOrFn 表达式 或 函数
     * @param {*} cb  回调函数
     * @param {*} options 配置项
     */
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;

        if (typeof exprOrFn === 'function') {
            this.getter = exprOrFn;
        }
        this.id = id++;

        // watcher需要关联dep
        this.deps = [];
        this.depsId = new Set();  // 数据结构  唯一,不重复

        // 实例化时,就调用get();
        this.get();
    }

    // watcher压栈---读属性---dep收集依赖---watcher出栈
    get() {
        // 初始化
        // 保存watcher对象到Dep.target上为全局变量
        pushTarget(this);
        // 依赖收集
        this.getter();
        // 清除当前的watchar对象
        popTarget();
    }
    // 在渲染watcher中关联 dep 
    addDep(dep) {
        let id = dep.id;
        if (!this.depsId.has(id)) {// 通过dep-id避免重复的收集
            this.depsId.add(id);
            this.deps.push(dep);
            // 双向引用  dep <===> watcher
            dep.addSub(this);
        }
    }

    update(){
        // 更新渲染页面
        this.get();
    }
}

export default Watcher

2. 异步队列

//observer/scheduler.js
// 异步任务队列
let has = {};  // 检查渲染watcher是否存在
let queue = [];   // 用于保存渲染watcher

// 将队列中所有的watcher执行一遍
function flushSchedulerQueue(){
    for (let i=0; i<queue.length; i++) {
        queue[i].run();
    }

    // 清空
    queue = [];
    has = {};
}

export function queueWatcher(watcher) {
    const id = watcher.id;
    if (!has[id]) {
        has[id] = true;
        queue.push(watcher);
        // 会创建多个 异步任务
        // let n = setTimeout(flushSchedulerQueue)
        // console.log("task:", n)
        nextTick(flushSchedulerQueue);
    }
}

// nexttick
let callbacks = [];   // 存储多个异步函数 [flushSchedulerQueue, flushSchedulerQueue]
let pending = false;   // 判断是否需要创建定时任务setTimeout

function flushCallbackQueue(){
    // 执行所有的渲染watcher, 即所有的dom渲染完成后
    callbacks.forEach(fn => fn());

    pending = false;
}

export function nextTick(fn) {
    callbacks.push(fn);
    if (!pending) {// 判断是否创建定时任务
        setTimeout(()=>{
            // 执行所有异步任务
            flushCallbackQueue();
        }, 0)
        pending = true;
    }
}

3. 计算属性

//state.js
// 初始化数据
import Dep from "./observer/dep";
import { observe } from "./observer/index";
import Watcher from "./observer/watcher";
import Vue from './index'

export function initState(vm) {
    const opts = vm.$options;

    if (opts.props) {
        // 处理父传子的属性
    }
    if (opts.methods) {
        // 处理方法
    }
    if (opts.data) {
        initData(vm);
    }
    if (opts.computed) {
        initComputed(vm);
    }
    if (opts.watch) {
        initWatch(vm);
    }
}

// 前面数据劫持: data----dep-----渲染watcher
// 计算属性: key-----计算watcher
function initComputed(vm) {
    // 用户定义计算属性
    const computed = vm.$options.computed;
    // 计算属性保存 key--value
    const watchers = vm._computedWatcher = {}
    
    // 遍历拿到所有的计算属性
    for (let k in computed) {
        // 计算属性值
        const userDef = computed[k];
        // 拿到计算属性的getter
        const getter = typeof userDef === 'function' ? userDef : userDef.get;
        // 将所有的计算属性保存,  计算属性-----计算watcher   渲染watcher
        watchers[k] = new Watcher(vm, getter, ()=>{}, {lazy: true})

        // 在vm实例上动态定义计算属性
        defineComputed(vm, k, userDef);
    }
}

// 定义一个普通的对象来劫持计算属性
const sharedPropertyDefinition = {
    configurable: true,
    enumerable: true,
    get: ()=>{},
    set: ()=>{}
}

// 动态定义计算属性
function defineComputed(target, key, userDef) {
    if (typeof userDef === 'function') {
        sharedPropertyDefinition.get = createComputedGetter(key);
    } else {
        sharedPropertyDefinition.get = createComputedGetter(key);
        sharedPropertyDefinition.set = userDef.set;
    }

    // 给vm实例动态绑定计算属性
    Object.defineProperty(target, key, sharedPropertyDefinition);
}

// 重写计算属性get方法,判断是否需要重新计算
function createComputedGetter(key) {
    return function(){
        // 拿到计算属性对应的 watcher
        const watcher = this._computedWatcher[key];
        if (watcher) {
            // 判断是否需要计算
            if (watcher.dirty) {
                // 计算属性取值
                watcher.evaluate();
                // 判断当前的Watcher
                if (Dep.target) {
                    watcher.depend();
                }
            }
            return watcher.value;
        }
    }
}

// 实现watch侦听器
function initWatch(vm) {
    let watch = vm.$options.watch;
    for (let key in watch) {
        // 回调函数
        const handlerObj = watch[key];
        if (Array.isArray(handlerObj)) {
            // 遍历handler数组
            handlerObj.forEach(handler => {
                // 创建用户watcher
                createWatcher(vm, key, handler)
            })
        } else {
            createWatcher(vm, key, handlerObj);
        }
    }
}

// 创建 用户watcher 用于侦听watche
function createWatcher(vm, exprOrFn, handler, options={}) {
    // 如果回调是对象, 那就要取hanlder函数
    if (typeof handler === 'object') {
        options = handler;          // 保存用户传入的参数
        handler = handler.handler;  // 取出watch侦听回调
    }
    // 如果回调是一个字符串, 那一定是vm上
    if (typeof handler === 'string') {
        handler = vm[handler];  // 从vue实例上取侦听回调
    }

    // 回调是一个函数
    return vm.$watch(exprOrFn, handler, options)
}

// 在vue原型上添加$watch,  需要在配置项上增加一个额外的标识 user:true
Vue.prototype.$watch = function(exprOrFn, cb, options) {
    // console.log("watch:",exprOrFn, cb, options)
    const vm = this;
    // 用户watcher
    let watcher = new Watcher(vm, exprOrFn, cb, {...options, user:true})
    // 立即执行
    if (options.immediate) {
        cb();
    }
}

// 增加代理属性访问, vm._data.msg  => vm.msg  this.msg
// target---代理对象   property--代理对象属性  key---属性的属性
function proxy(target, property, key) {
    Object.defineProperty(target, key, {
        get(){// vm[_data][msg]
            return target[property][key];
        },
        set(newValue) {
            // vm[_data][msg] = 'test'
            target[property][key] = newValue;
        }
    })
}

// 初始化 数据
function initData(vm) {
    // 拿到数据
    let data = vm.$options.data;
    // 数据可能是对象  也可能是函数
    data = vm._data = typeof data === 'function' ? data.call(vm) : data;

    // 加一层数据访问代理
    for (let key in data) {
        proxy(vm, "_data", key);
    }

    // 监听处理
    observe(data);
}

//observer/watcher.js
// 渲染watcher   支持计算watcher   支持用户watcher
import { isObject } from "../utils";
import { popTarget, pushTarget } from "./dep";
import { queueWatcher } from "./scheduler";

// watcher的编号
let id = 0;

class Watcher {
    /**
     * 参数
     * @param {*} vm vue实例
     * @param {*} exprOrFn 表达式 或 函数
     * @param {*} cb  回调函数
     * @param {*} options 配置项
     */
    constructor(vm, exprOrFn, cb, options) {
        this.vm = vm;
        this.exprOrFn = exprOrFn;
        this.cb = cb;
        this.options = options;

        if (typeof exprOrFn === 'function') {
            this.getter = exprOrFn;
        } else {
            // for 用户watcher 中 getter 即为读取属性的值
            this.getter = function(){
                // 用户watcher传递是一个字符串
                let obj = vm;
                let path = exprOrFn.split('.');
                for (let i = 0; i<path.length; i++) {
                    // 一级级取对象的值 aa.bb.cc  vm[aa][bb][cc]
                    obj = obj[path[i]];
                }
                return obj;
            }
        }
        this.id = id++;

        // watcher需要关联dep
        this.deps = [];
        this.depsId = new Set();  // 数据结构  唯一,不重复

        // 用户watcher
        this.user = options.user;

        this.lazy = options.lazy;// 计算属性标识
        this.dirty = this.lazy;  // 脏值判断, 如果计算属性有修改需要重新计算则为脏
        // 若为计算watcher则不处理, 为渲染watcher则执行get
        this.value = this.lazy ? undefined : this.get();
    }
    // watcher压栈---读属性---dep收集依赖---watcher出栈
    get() {
        // 初始化
        // 保存watcher对象到Dep.target上为全局变量
        pushTarget(this);
        // 依赖收集
        const res = this.getter.call(this.vm);
        // 清除当前的watchar对象
        popTarget();
        return res;
    }
    // 在渲染watcher中关联 dep 
    addDep(dep) {
        let id = dep.id;
        if (!this.depsId.has(id)) {// 通过dep-id避免重复的收集
            this.depsId.add(id);
            this.deps.push(dep);
            // 双向引用  dep <===> watcher
            dep.addSub(this);
        }
    }

    update(){
        // 更新渲染页面  同步
        if (this.lazy) {
            // 表示云计算属性有变化, 需要重新计算
            this.dirty = true;
        } else {
            queueWatcher(this);
        }
    }

    run(){
        // 异步队列调用
        // 先要拿侦听的属性的值
        const oldValue = this.value;
        const newValue = this.get();
        // console.log("watcher:", oldValue, newValue)
        this.value = newValue;
        if (this.user) {
            // 是用户侦听watcher
            if (newValue !== oldValue || isObject(newValue)) {
                // 执行侦听回调函数
                this.cb.call(this.vm, newValue, oldValue);
            }
        } else {
            // 渲染watcher
            this.cb.call(this.vm);
        }
    }

    evaluate() {
        // 计算新的计算属性结果
        this.value = this.get();
        // 当计算属性执行完毕后,Dep.target指向的是渲染watcher
        // 改变脏值状态
        this.dirty = false;
    }

    // 让所有的计算属性都有渲染watcher
    depend(){
        let i = this.deps.length;
        while(i--) {
            this.deps[i].depend();  // 绑定当前的渲染watcher
        }
    }
}

export default Watcher

六、diff原理

虚拟DOM就像是一个轻量级的"内存中的UI模型",Vue用它来高效地更新真实DOM。Diff算法就是比较新旧虚拟DOM,找出最小变更集的算法。

diff算法

  1. 根节点对比, 如果不相同,直接替换, 如果相同, 检查属性是否相同。
  2. 老的有子节点,新的没有子节点, 直接删除老的子节点
  3. 老的无子节点,新的有子节点,直接插入新的子节点
  4. 老的和新的都有子节点,则使用diff比较

diff比较

  1. 从头开始比较, 从左到右, 老abcd, 新abcde, 直接插入多余的e节点
  2. 从右到左比较, 老abcd, 新eabcd.
  3. 头尾都不相同,有可能旧头与新尾相同,旧尾与新头相同 abcd dcba
  4. 乱序比较, 用新头节点与旧的所有的节点比较,若有相同的则移动元素, 若无相同则插入新的节点
abcd   cdme
用新头与旧的所有匹配, 可以匹配到c, 移动c, abcd---> cab_d
用新d与旧的所有匹配, 可以匹配到d, 移动d, cab_d---> cdab__
新m与旧的所有匹配, 无匹配, 直接插入d后面, cdmab__
新e与旧的所有匹配, 无匹配, cdmeab__, 删除ab__

为什么要key?

// 老元素
<ul>
     <li key=0>aaaa</li>
     <li key=1>bbbb</li>
</ul>
// 新元素
<ul>
     <li key=0>cccc</li>
     <li key=1>aaaa</li>
</ul>
// diff比较会先比较key, 会使用元素内容替换,会替换2次

// 最好的情况
// 老元素
<ul>
     <li key=aaaa>aaaa</li>
     <li key=bbbb>bbbb</li>
 </ul>
// 新元素
 <ul>
     <li key=ccccc>cccc</li>
     <li key=aaaa>aaaa</li>
 </ul>
// 只会插入一次,移动一次

代码实现

//vdom/patch.js
/**
 * 给虚拟dom打补丁
 * @param {*} oldVnode 旧的el对象
 * @param {*} newVnode 新的虚拟dom对象
 */
export function patch(oldVnode, newVnode){
    // 根据节点的类型 判断是否是真实元素  el-1  文本-3
    
    const isRealElement = oldVnode.nodeType;
    if (isRealElement) {
        // 是真实的dom
        const oldElm = oldVnode;
        const parentElm = oldElm.parentNode;

        // 创建元素 操作dom
        let el = createElm(newVnode);
        // 将新生成的元素插入到旧元素后面
        parentElm.insertBefore(el, oldElm.nextSibling);
        // 将旧的真实元素删除
        parentElm.removeChild(oldElm);
        return el;
    } else {
        // 虚拟dom diff
        
        // 比较标签是否相同
        if (oldVnode.tag !== newVnode.tag) {
            // 直接替换旧元素
            oldVnode.el.parentNode.replaceChild(createElm(newVnode), oldVnode.el)
        }
        // 标签相同,如果是文本变化, 直接用新的文本替换老的文本
        if (!oldVnode.tag) {
            // 比较内容
            if (oldVnode.text !== newVnode.text) {
                oldVnode.el.textContent = newVnode.text;
            }
        }

        // 标签相同, 比较属性
        let el = newVnode.el = oldVnode.el;
        // 更新根节点属性
        updateProperties(newVnode, oldVnode.data)

        // 情况1,从左到右比较
        let oldChildren = oldVnode.children || {}
        let newChildren = newVnode.children || {}
        if (oldChildren.length > 0 && newChildren.length > 0) {
            // diff
            updateChildren(el, oldChildren, newChildren);
        } else if (oldChildren.length > 0) {
            // 老的有子节点,新的没有,则直接删除
            el.innerHTML = "";
        } else if (newChildren.length > 0) {
            // 老的没有子节点,新的有,则直接添加
            for (let i=0; i<newChildren.length; i++) {
                let child = newChildren[i];
                el.appendChild(createElm(child));
            }
        }
    }
}

// 判断节点是否相同, 通过key和标签比较
function isSameVnode(oldVnode, newVnode) {
    return (oldVnode.key === newVnode.key) && (oldVnode.tag === newVnode.tag);
}

/**
 * diff核心方法
 * @param {*} parent 
 * @param {*} oldChildren 
 * @param {*} newChildren 
 */
function updateChildren(parent, oldChildren, newChildren) {
    let oldStartIndex = 0;                      // 老的开始索引
    let oldStartVnode = oldChildren[0];         // 老的开始节点
    let oldEndIndex = oldChildren.length - 1;   // 老的尾部索引
    let oldEndVnode = oldChildren[oldEndIndex]; // 老的尾部节点

    let newStartIndex = 0;                      // 新的开始索引
    let newStartVnode = newChildren[0];         // 新的开始节点
    let newEndIndex = newChildren.length - 1;   // 新的尾部索引
    let newEndVnode = newChildren[newEndIndex]; // 新的尾部节点

    // 创建key--索引映射表  a-0 b-1
    function makeIndexByKey(children) {
        let map = {};
        children.forEach((item, index)=>{
            map[item.key] = index;
        })
        return map;
    }
    // 生成老节点的映射表
    let map = makeIndexByKey(oldChildren);
    
    // 开始比较
    while(oldStartIndex <= oldEndIndex && newStartIndex <= newEndIndex) {
        // 指针移动, 为空元素
        if (!oldStartVnode) {
            // 左 --- 右
            oldStartVnode = oldChildren[++oldStartIndex];
        } else if (!oldEndVnode) {
            // 右 --- 左
            oldEndVnode = oldChildren[--oldEndIndex];
        } else 
        // 情况1--从左到右比较
        if (isSameVnode(oldStartVnode, newStartVnode)) {
            // 比较节点的标签和属性
            patch(oldStartVnode, newStartVnode);
            // 向右移动指针
            oldStartVnode = oldChildren[++oldStartIndex];
            newStartVnode = newChildren[++newStartIndex];
        } else if (isSameVnode(oldEndVnode, newEndVnode)) {
            // 比较节点的标签和属性 情况2
            patch(oldEndVnode, newEndVnode);
            // 向左移动指针
            oldEndVnode = oldChildren[--oldEndIndex];
            newEndVnode = newChildren[--newEndIndex];
        } else if (isSameVnode(oldStartVnode, newEndVnode)) {
            // 情况3 旧头与新尾
            patch(oldStartVnode, newEndVnode)
            // 移动元素 将旧头节点移动到旧的尾节点后面
            parent.insertBefore(oldStartVnode.el, oldEndVnode.el.nextSibling)
            // 指针调整 旧的向右  新的向左
            oldStartVnode = oldChildren[++oldStartIndex];
            newEndVnode = newChildren[--newEndIndex];
        } else if (isSameVnode(oldEndVnode, newStartVnode)) {
            // 旧尾节点与新的头节点
            patch(oldEndVnode, newStartVnode);
            // 移动元素 将旧尾节点移动到旧头节点前面
            parent.insertBefore(oldEndVnode.el, oldStartVnode.el)
            // 指针调整,旧的向左, 新的向右
            oldEndVnode = oldChildren[--oldEndIndex];
            newStartVnode = newChildren[++newStartIndex];
        } else {
            // 乱序比较
            // 根据新头的key查找是否存在对应索引
            let oldMoveIndex = map[newStartVnode.key];
            if (oldMoveIndex == undefined) {
                // 未匹配到,说明是新元素,直接插入
                parent.insertBefore(createElm(newStartVnode), oldStartVnode.el);
            } else {
                // 匹配到cd 找到旧cd并清除原占位置
                let oldMoveVnode = oldChildren[oldMoveIndex];
                oldChildren[oldMoveIndex] = null;
                // 将c移动到旧头前面
                parent.insertBefore(oldMoveVnode.el, oldStartVnode.el);
                // 比较节点内容
                patch(oldMoveVnode, newStartVnode);
            }
            // 指针调整 新节点
            newStartVnode = newChildren[++newStartIndex];
        }
    }

    // 情况1--从左到右,后面多余的元素直接插入
    if (newStartIndex <= newEndIndex) {
        for(let i=newStartIndex; i <= newEndIndex; i++) {
            
            // 新节点结束指针后面的元素
            let el2 = newChildren[newEndIndex + 1] == null ? null : newChildren[newEndIndex + 1].el;
            parent.insertBefore(createElm(newChildren[i]), el2);
        }
    }

    // diff比较完毕, 删除多余的旧节点
    if (oldStartIndex <= oldEndIndex) {
        for (let i = oldStartIndex; i<= oldEndIndex; i++) {
            let child = oldChildren[i];
            if (child) {
                parent.removeChild(child.el);
            }
        }
    }
}


// 创建元素
export function createElm(vnode) {
    let {tag, children, data, key, text} = vnode;
    if (tag) {
        // 元素
        vnode.el = document.createElement(tag);   // dom操作--浏览器内核--dom模块
        // 处理元素属性
        updateProperties(vnode);
        children.forEach(child => {               // 在浏览器内存中处理
            // 递归创建dom元素,并添加到父元素中
            vnode.el.appendChild(createElm(child));
        })
    } else {
        // 文本
        vnode.el = document.createTextNode(text);
    }
    return vnode.el;
}

// 元素属性更新
function updateProperties(vnode, oldProps = {}) {
    let el = vnode.el;
    // 属性
    let newProps = vnode.data || {}

    // diff 属性更新
    // 获取旧的样式和新的样式的差异, 如果新的没有旧的属性,则删除
    let newStyle = newProps.style || {}
    let oldStyle = oldProps.style || {}

    for (let key in oldStyle) {
        // 新的样式中不包括旧的样式  则删除
        if (!newStyle[key]) {
            el.style[key] = "";
        }
    }

    // 判断属性
    for (let key in oldProps) {
        // 新的属性中不存在旧的属性, 则删除
        if (!newProps[key]) {
            el.removeAttribute(key);
        }
    }

    for (let key in newProps) {
        if (key === 'style') {
            // 当作对象处理
            for (let styleName in newProps.style) {
                // style="color:red; font-size: 30;"
                // 添加一个style对象
                el.style[styleName] = newProps.style[styleName];
            }
        } else {
            // 单一属性处理
            el.setAttribute(key, newProps[key]);
        }
    }
}

测试

//index.js
import { initGlobalAPI } from "./global-api/index";
import { initMixin } from "./init";
import { lifeCycleMixin } from "./lifecycle";
import { renderMixin } from "./render";

function Vue(options){ // vue2 配置项
    console.log("my vue 6666")
    this._init(options);     // vue 内部约定 私有的方法或属性  一般是以 $ _ 开头
}

initMixin(Vue);   // 一执行,Vue就有_init方法
lifeCycleMixin(Vue);  // 混入_udpate方法
renderMixin(Vue);  // 混入 _render方法

// 初始化全局api
initGlobalAPI(Vue);

// diff  真元素  虚拟元素 虚拟元素

import {compileToFunction} from './compiler/index'
import {createElm, patch} from './vdom/patch'
let vm1 = new Vue({data: {name: "test1"}})
let vm2 = new Vue({data: {name: "test2"}})

// 生成虚拟dom
let render1 = compileToFunction(`<div style="color:red;background-color:#333" id=2>{{name}}</div>`)
let oldVnode = render1.call(vm1);
console.log("oldVnode:", oldVnode)

// 把虚拟dom生成为真实dom
let realElement = createElm(oldVnode);
document.body.appendChild(realElement)

// 再创建一个虚拟dom
let render2 = compileToFunction(`<div style="color: blue;">{{name}}</div>`)
let newVnode = render2.call(vm2);

// diff比较
setTimeout(()=>{
    // 真实元素与虚拟dom比较: 先插入新元素,再删除旧元素
    // patch(realElement, newVnode);

    // 虚拟dom比较
    patch(oldVnode, newVnode);
}, 1000)


export default Vue
//public/index
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>vue</title>
</head>
<body>
    <script src="../dist/vue.js"></script>
    <div id=app>
        <div class="test" style="color:red; fontSize: 30;">
            <div>hello</div>
            --{{msg}}----{{name.aa}}----{{myInfo}}----{{addr}}
        </div>
    </div>
    <!-- <div id=app2>
        <div class="test" style="color:red; fontSize: 30;">
            <div>hello</div>
            --{{msg2}}----{{name.aa}}
        </div>
    </div> -->
    <script>
        // 渲染watcher 计算watcher
        // 依赖收集,在getter中收集依赖,收集的是渲染watcher, 在setter中触发更新
        // 依赖收集结果:data---属性---dep---渲染watcher
        // 更新:data---属性---触发更新---setter---找到对应属性上的dep---遍历执行所有关联的 渲染watcher
        // 计算属性,用户自定义的属性, 在vm上动态添加一个属性,同时添加劫持,计算属性----计算watcher
        // 计算watcher 与 渲染watcher 区别: 
        // 1. 计算watcher有一个 lazy=true 标识  2. 计算watcher只是改变dirty状态
        // 如果修改计算属性,页面没刷新的原因是: 计算属性-----(计算watcher,渲染watcher)

        // watch侦听器的原理:也需要添加一个watcher,  用户Watcher, 用户主观去监听某个属性变化
        // 1. 取侦听属性名, 和 侦听回调函数
        // 2. 创建一个用户wathcer, user:true
        // 3. 取属性的值,判断,如果有变化,就执行 用户watcher的回调函数
        const vm = new Vue({
            el: "#app",
            data() {
                return {
                    msg: "abcd",
                    name: "asfdsafdsadf",
                    age: 25,
                    aa: {
                        bb: 'cc'
                    },
                    addr: "广东深圳",
                    arr: ['1','2']
                }
            },
            watch: {
                addr(newValue, oldValue){
                    console.log("侦听器:", newValue, oldValue)
                },
                "aa.bb": {
                    handler(newValue, oldValue) {
                        console.log("侦听器2:", newValue, oldValue)
                    }
                },
                age: [
                    function handler1(newValue, oldValue) {
                        console.log("侦听器3:", newValue, oldValue)
                    },
                    function handler2(newValue, oldValue) {
                        console.log("侦听器4:", newValue, oldValue)
                    }
                ]
            },
            computed: {
                myInfo() {
                    return this.name + "===" + this.age;
                },
                myInfo2: {
                    get: function(){
                        return this.name + "=====" + this.age;
                    },
                    set: function(newValue){ // 只修改name
                        this.name = newValue;
                    }
                }
            }
        })
        
        // console.log(vm)
        console.log(vm._data.msg, vm.msg)   // 读取属性
        // vm._data.msg = {value: "bbbb"}       // 设置属性
        // console.log(vm._data.msg.value)

        // const vm2 = new Vue({
        //     el: "#app2",
        //     data() {
        //         return {
        //             msg2: "abcd22222",
        //             name: {
        //                 aa: 'bb'
        //             },
        //             arr: ['1','2']
        //         }
        //     },
        //     data2: null,
        //     data3: {test: 4},
        //     created(){
        //         console.log('create 3')
        //     },
        //     mounted(){
        //         console.log("数据已装载完成")
        //     },
        //     beforeCreate(){
        //         console.log('beforeCreate 3')
        //     }
        // })

        setTimeout(()=>{
            vm.msg="test1";
            vm.age=26;
            vm.aa.bb="test3";
            vm.addr="北京"

        }, 1000)
        // setTimeout(()=>{
        //     vm.myInfo2 = "333333333333333"
        //     console.log("修改计算属性")
        // }, 1000)
    </script>
</body>
</html>